From ba80dc254d2b6355bbdcef44ae1f76f577db63d1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 13:56:21 -0500 Subject: [PATCH 001/273] Iterate classes with object-literal iterators and close them on early exits --- .../builtin-apis/synchronous-iteration.mdx | 2 +- .../language-features/async-iteration.mdx | 1 + .../lowering/class-dynamic-dispatch.ts | 10 +- .../src/frontend/lowering/dispatch-sites.ts | 1 + .../lowering/expressions/array-literals.ts | 8 + .../src/frontend/lowering/lower-classes.ts | 126 +++++++++++++-- .../src/frontend/lowering/lower-stmts.ts | 146 +++++++++++++++--- .../src/frontend/lowering/lower-stream.ts | 2 +- .../compiler/src/frontend/lowering/lowerer.ts | 10 +- .../6928-class-iterator-protocol.ts.json | 6 + .../6929-class-iterator-protocol.mjs.json | 6 + ...6935-class-async-iterator-protocol.ts.json | 6 + ...936-class-async-iterator-protocol.mjs.json | 6 + packages/runtime/src/scr_dyn_for_await.c | 8 +- tests/corpus/6928-class-iterator-protocol.ts | 93 +++++++++++ tests/corpus/6929-class-iterator-protocol.mjs | 78 ++++++++++ .../6935-class-async-iterator-protocol.ts | 60 +++++++ .../6936-class-async-iterator-protocol.mjs | 60 +++++++ .../6928-class-iterator-protocol.ts.json | 1 + .../6929-class-iterator-protocol.mjs.json | 1 + ...6935-class-async-iterator-protocol.ts.json | 1 + ...936-class-async-iterator-protocol.mjs.json | 1 + 22 files changed, 582 insertions(+), 51 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6928-class-iterator-protocol.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6929-class-iterator-protocol.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-class-async-iterator-protocol.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-class-async-iterator-protocol.mjs.json create mode 100644 tests/corpus/6928-class-iterator-protocol.ts create mode 100644 tests/corpus/6929-class-iterator-protocol.mjs create mode 100644 tests/corpus/6935-class-async-iterator-protocol.ts create mode 100644 tests/corpus/6936-class-async-iterator-protocol.mjs create mode 100644 tests/harness/corpus-costs/6928-class-iterator-protocol.ts.json create mode 100644 tests/harness/corpus-costs/6929-class-iterator-protocol.mjs.json create mode 100644 tests/harness/corpus-costs/6935-class-async-iterator-protocol.ts.json create mode 100644 tests/harness/corpus-costs/6936-class-async-iterator-protocol.mjs.json diff --git a/docs/content/limitations/builtin-apis/synchronous-iteration.mdx b/docs/content/limitations/builtin-apis/synchronous-iteration.mdx index 564f9af45..38cc9c475 100644 --- a/docs/content/limitations/builtin-apis/synchronous-iteration.mdx +++ b/docs/content/limitations/builtin-apis/synchronous-iteration.mdx @@ -1,4 +1,4 @@ - **Synchronous iteration:** Checked `for...of` follows live iterator methods, including compiled class methods and abrupt-completion cleanup. Mapper-less checked `Array.from` accepts those iterators and plain array-like objects. Sparse checked arrays and general borrowed iterator factories are unsupported. - Direct statically typed class loops also restrict iterator `return` and `throw` methods. + Direct statically typed class loops accept iterators that are class instances or object literals; a loop that exits early calls the iterator's zero-parameter `return()`, as Node's IteratorClose does. diff --git a/docs/content/limitations/language-features/async-iteration.mdx b/docs/content/limitations/language-features/async-iteration.mdx index 863c9be90..b15dca7fc 100644 --- a/docs/content/limitations/language-features/async-iteration.mdx +++ b/docs/content/limitations/language-features/async-iteration.mdx @@ -1,6 +1,7 @@ - **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported. Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record. + A class instance whose `[Symbol.asyncIterator]()` returns an object literal, or that has only `[Symbol.iterator]()`, iterates through the checked-dynamic async protocol, and an early exit awaits the iterator's `return()`. Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options. The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options. `Array.fromAsync` accepts arrays of values or of promises and typed async generators, with an optional mapping function; a `thisArg` and arrays that mix values and promises are unsupported. diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index af3212ef0..86ad05386 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -2367,15 +2367,19 @@ export class ClassDynamicDispatch { } const iterator = expr.kind === "libCall" && - (expr.fn === "dyn.iterator" || expr.fn === "dyn.arrayFromIterator") + (expr.fn === "dyn.iterator" || + expr.fn === "dyn.arrayFromIterator" || + expr.fn === "dyn.asyncIteratorOpen") ? expr : null; + // `for await` opens a class instance's [Symbol.asyncIterator](). + const asyncOpen = iterator?.fn === "dyn.asyncIteratorOpen"; const invoke: Invoke | null = iterator ? { kind: "dynInvoke", recv: iterator.args[0]!, - method: "sym:iterator", - calleeName: "value[Symbol.iterator]", + method: asyncOpen ? "sym:asyncIterator" : "sym:iterator", + calleeName: asyncOpen ? "value[Symbol.asyncIterator]" : "value[Symbol.iterator]", args: [], type: DYN, loc: iterator.loc, diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index 2618dacf9..49d0bd243 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -59,6 +59,7 @@ const DISPATCH_LIB_CALLS = new Set([ "dyn.keySetComputed", "dyn.iterator", "dyn.arrayFromIterator", + "dyn.asyncIteratorOpen", "dyn.toString", "dyn.stringConstructor", "dyn.forInKeys", diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 8f119504c..6153b87f2 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -425,6 +425,14 @@ export function lowerArrayLiteral( if (src.type.kind === "object") { const drained = lowerer.classIteratorDrainCall(src, locOf(el), type.elem); if (drained) src = drained; + // In JavaScript an iterable class whose inferred iterator shape has + // no static protocol spreads through the runtime's iterator walk. + else if ( + isJsSourceFile(ts.getSourceFileOfNode(el)) && + lowerer.classes.get(src.type.className) && + lowerer.findMethodOn(lowerer.classes.get(src.type.className)!, "sym:iterator") + ) + src = lowerer.coerceInto(el.expression, src, DYN); } // `[...s]` on a STRING spreads its code-point characters (the // string iterator's walk — astral chars whole) through the same diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d8ccc15b7..9d994b76b 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -7923,31 +7923,83 @@ export function classMemberNameOf(lowerer: Lowerer, name: ts.PropertyName): stri export interface ClassIteratorInfo { /** The `[Symbol.iterator]()` call's receiver class + its declarer. */ className: string; - /** The iterator object's type (the method's return). */ - iterT: IrType & { kind: "object" }; + /** The iterator's type (the method's return): a class instance, or a + * record (an object literal) whose `next` field is a function. */ + iterT: IrType & ({ kind: "object" } | { kind: "record" }); /** next()'s result record type. */ resultT: IrType & { kind: "record" }; valueT: IrType; /** False: no done field — the protocol never terminates. */ hasDone: boolean; + /** The iterator declares a zero-parameter return(), which a consumer + * that stops before the end calls (IteratorClose). Only consumers that + * ask for `close` see such iterators. */ + hasReturn: boolean; } -export function classIteratorOf(lowerer: Lowerer, t: IrType): ClassIteratorInfo | null { +/** A return() result type that is always an object: IteratorClose after a + * break throws a TypeError for any other result, which these desugars do + * not model. */ +function objectResult(t: IrType): boolean { + return t.kind === "record" || t.kind === "object"; +} + +/** What a class iterable's consumer handles: `records` accepts an + * object-literal iterator; `close` accepts an iterator with return(), + * which the consumer calls when it stops early (or never stops early). */ +export interface ClassIteratorOptions { + records?: boolean; + close?: boolean; +} +export function classIteratorOf( + lowerer: Lowerer, + t: IrType, + options: ClassIteratorOptions = {}, +): ClassIteratorInfo | null { if (t.kind !== "object") return null; const info = lowerer.classes.get(t.className); if (!info) return null; const iter = findMethodOn(lowerer, info, "sym:iterator"); if (!iter || iter.sig.params.length !== 0 || iter.sig.abstract === true) return null; const iterT = iter.sig.ret; - if (iterT.kind !== "object") return null; - const itInfo = lowerer.classes.get(iterT.className); - if (!itInfo) return null; - // IteratorClose honesty: a declared return()/throw() would be called - // by JS on abrupt completion; these desugars never close. - if (findMethodOn(lowerer, itInfo, "return") || findMethodOn(lowerer, itInfo, "throw")) - return null; - const next = findMethodOn(lowerer, itInfo, "next"); - if (!next || next.sig.params.length !== 0 || next.sig.abstract === true) return null; - const resultT = next.sig.ret; + let resultT: IrType; + let hasReturn: boolean; + if (iterT.kind === "object") { + const itInfo = lowerer.classes.get(iterT.className); + if (!itInfo) return null; + // IteratorClose honesty: a declared return() would be called by JS on + // abrupt completion, so only a consumer that closes takes it; for-of + // never calls throw(), the other consumers refuse it. + const close = findMethodOn(lowerer, itInfo, "return"); + if ( + close && + (!options.close || + close.sig.abstract === true || + close.sig.params.length !== 0 || + !objectResult(close.sig.ret)) + ) + return null; + if (findMethodOn(lowerer, itInfo, "throw") && !options.close) return null; + const next = findMethodOn(lowerer, itInfo, "next"); + if (!next || next.sig.params.length !== 0 || next.sig.abstract === true) return null; + resultT = next.sig.ret; + hasReturn = !!close; + } else if (iterT.kind === "record" && options.records) { + const fields = lowerer.shapes.get(iterT.shapeId)?.fields; + const next = fields?.find((f) => f.name === "next")?.type; + if (!next || next.kind !== "func" || next.params.length !== 0 || next.rest) return null; + const close = fields?.find((f) => f.name === "return")?.type; + if ( + close && + (!options.close || + close.kind !== "func" || + close.params.length !== 0 || + close.rest || + !objectResult(close.ret)) + ) + return null; + resultT = next.ret; + hasReturn = close !== undefined; + } else return null; if (resultT.kind !== "record") return null; const shape = lowerer.shapes.get(resultT.shapeId); const value = shape?.fields.find((f) => f.name === "value"); @@ -7960,6 +8012,7 @@ export function classIteratorOf(lowerer: Lowerer, t: IrType): ClassIteratorInfo resultT, valueT: value.type, hasDone: done !== undefined, + hasReturn, }; } @@ -7971,7 +8024,43 @@ export function classIteratorNextCall( itRef: IrExpr, loc: SrcLoc, ): IrExpr { - return accessorCall(lowerer, cit.iterT.className, "next", itRef, [], cit.resultT, loc); + return classIteratorMethodCall(lowerer, cit, "next", itRef, loc); +} + +/** `it.next()` or `it.return()` of a class iterator: a (possibly virtual) + * method call of an iterator class, or the call of an object-literal + * iterator's function field. */ +export function classIteratorMethodCall( + lowerer: Lowerer, + cit: ClassIteratorInfo, + method: "next" | "return", + itRef: IrExpr, + loc: SrcLoc, +): IrExpr { + const iterT = cit.iterT; + if (iterT.kind === "object") { + const ret = + method === "next" + ? cit.resultT + : findMethodOn(lowerer, lowerer.classes.get(iterT.className)!, "return")!.sig.ret; + return accessorCall(lowerer, iterT.className, method, itRef, [], ret, loc); + } + const field = lowerer.shapes.get(iterT.shapeId)!.fields.find((f) => f.name === method)!; + const fnT = field.type as IrType & { kind: "func" }; + return { + kind: "callValue", + callee: { + kind: "recordGet", + obj: itRef, + shapeId: iterT.shapeId, + field: method, + type: fnT, + loc, + }, + args: [], + type: fnT.ret, + loc, + }; } /** `recv[Symbol.iterator]()` as an ordinary method call. */ @@ -7999,12 +8088,15 @@ export function classIteratorDrainCall( loc: SrcLoc, elemT?: IrType, ): IrExpr | null { - const cit = classIteratorOf(lowerer, src.type); + // A spread takes every value, so it never closes the iterator. + const cit = classIteratorOf(lowerer, src.type, { records: true, close: true }); if (!cit) return null; const outElem = elemT ?? cit.valueT; // Probe the element coercion purely: identical types, or an arm of a // union destination (the wrap coerceToExpected applies below). - if (!typeEquals(outElem, cit.valueT)) { + // A checked-dynamic value (`IteratorResult`'s value is `T | any`) + // converts to the destination element with a check. + if (!typeEquals(outElem, cit.valueT) && cit.valueT.kind !== "dyn") { if (outElem.kind !== "union" || lowerer.armTag(outElem.unionId, cit.valueT) < 0) return null; } const outT = arrayOf(outElem); @@ -8122,7 +8214,7 @@ export function classIteratorRestDrainCall( loc: SrcLoc, ): IrExpr { const outT = arrayOf(cit.valueT); - const key = `it:${cit.iterT.className}`; + const key = `it:${cit.iterT.kind === "object" ? cit.iterT.className : typeKey(cit.iterT)}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { name = keyedHelperName(lowerer, "%iter.drain", key); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 213abfec6..bee887e0f 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -9467,8 +9467,23 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { // iterator are awaited as the async-from-sync iterator awaits them, and // a value that is not iterable throws Node's TypeError. const headT = lowerer.mapTypeOf(lowerer.typeOf(stmt.expression)); + // A class instance whose [Symbol.asyncIterator]() (or + // [Symbol.iterator]()) returns an iterator object takes the same + // checked-dynamic protocol over the boxed instance. + const headClass = headT?.kind === "object" ? lowerer.classes.get(headT.className) : undefined; + const classAsync = + headClass !== undefined && lowerer.findMethodOn(headClass, "sym:asyncIterator") !== null; + // Without [Symbol.asyncIterator], the loop opens [Symbol.iterator]() and + // awaits its values, as the async-from-sync iterator does. + const classSync = + headClass !== undefined && + !classAsync && + lowerer.findMethodOn(headClass, "sym:iterator") !== null; + const classIterable = classAsync || classSync; const syncGenerator = - (headT?.kind === "generator" && headT.async !== true) || isJsSourceFile(stmt.getSourceFile()); + (headT?.kind === "generator" && headT.async !== true) || + classIterable || + isJsSourceFile(stmt.getSourceFile()); if (lowerer.ctx.isAsync && (syncGenerator || isUntypedIterable(lowerer, stmt.expression))) { const iterable = lowerer.lowerExpr(stmt.expression); if (iterable.type.kind === "generator" && iterable.type.async) @@ -9480,10 +9495,19 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { ); if (iterable.type.kind === "dyn") return lowerForOfDyn(lowerer, stmt, iterable, labels, true); if ( - (iterable.type.kind === "generator" || isJsSourceFile(stmt.getSourceFile())) && + (iterable.type.kind === "generator" || + classIterable || + isJsSourceFile(stmt.getSourceFile())) && lowerer.dynConvertible(iterable.type) ) - return lowerForOfDyn(lowerer, stmt, lowerer.coerceToExpected(iterable, DYN), labels, true); + return lowerForOfDyn( + lowerer, + stmt, + lowerer.coerceToExpected(iterable, DYN), + labels, + true, + classSync && iterable.type.kind === "object", + ); } // A NodeJS.ReadableStream iterates as the general Readable. requestGeneralStreamSurface(lowerer, stmt.expression); @@ -9732,17 +9756,30 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { ); // Class iterables ([Symbol.iterator]() + next() — classIteratorOf) // drive the same protocol with ordinary method calls. - const cit = lowerer.classIteratorOf(iterable.type); + const cit = lowerer.classIteratorOf(iterable.type, { records: true, close: true }); if (cit) return lowerForOfClassIterator(lowerer, stmt, iterable, cit, labels); // A declared [Symbol.iterator] whose shape classIteratorOf refused // (an `any`-typed iterator or result, a declared return()/throw()): // name the protocol, not the class. const info = lowerer.classes.get(iterable.type.className); + // In JavaScript the inferred shape is often a union of result + // objects: the loop runs the iterator protocol over the boxed value. + if ( + info && + isJsSourceFile(ts.getSourceFileOfNode(stmt)) && + lowerer.findMethodOn(info, "sym:iterator") + ) + return lowerForOfDyn( + lowerer, + stmt, + lowerer.coerceInto(stmt.expression, iterable, DYN), + labels, + ); if (info && lowerer.findMethodOn(info, "sym:iterator")) { lowerer.unsupported( "SC1090", stmt.expression, - `for-of over this [Symbol.iterator] shape (the method must take no parameters and return an object whose zero-parameter next() returns a { value, done? } record with a boolean done; iterator classes declaring return()/throw() stay out — IteratorClose has no lowering)`, + `for-of over this [Symbol.iterator] shape (the method must take no parameters and return an object whose zero-parameter next() returns a { value, done? } record with a boolean done, and whose return(), if any, takes no parameters and returns an object)`, ); } } @@ -10641,11 +10678,13 @@ function lowerForOfMatchAll( * } } * * A result record with NO done field never terminates — exactly JS - * (undefined is falsy forever; Node loops forever too). No - * IteratorClose: classIteratorOf already refused iterator classes - * declaring return()/throw(), so a break exits with nothing to close. - * `done: true` results bind nothing (the loop breaks before the value - * read), matching JS's ignore-the-final-value rule. */ + * (undefined is falsy forever; Node loops forever too). An iterator with + * return() is closed when the loop leaves after a value arrived (break, + * return, or a throw from the body), as IteratorClose does: a done flag + * set around each step guards the call in a finally, and after a throw + * from the body an error from return() is dropped. A throw from next() + * does not close. `done: true` results bind nothing (the loop breaks + * before the value read), matching JS's ignore-the-final-value rule. */ function lowerForOfClassIterator( lowerer: Lowerer, stmt: ts.ForOfStatement, @@ -10688,6 +10727,19 @@ function lowerForOfClassIterator( const recvRef: IrExpr = { kind: "varRef", localId: recv.id, type: iterable.type, loc }; const itRef = (): IrExpr => ({ kind: "varRef", localId: it.id, type: cit.iterT, loc }); const rRef = (): IrExpr => ({ kind: "varRef", localId: r.id, type: cit.resultT, loc }); + const done = cit.hasReturn ? lowerer.declareHiddenLocal("%cid", BOOL) : null; + if (done) done.mutable = true; + const setDone = (value: boolean): IrStmt[] => + done + ? [ + { + kind: "assign", + localId: done.id, + value: { kind: "boolLit", value, type: BOOL, loc }, + loc, + }, + ] + : []; const varTarget = exprTarget ?? (decl ? forOfVarTarget(lowerer, decl) : null); const x = varTarget ? lowerer.declareHiddenLocal("%vof", cit.valueT) @@ -10700,6 +10752,7 @@ function lowerForOfClassIterator( const xRef: IrExpr = { kind: "varRef", localId: x.id, type: cit.valueT, loc }; const blame: ts.Node = decl?.name ?? exprTargetNode ?? stmt.initializer; const head: IrStmt[] = [ + ...setDone(true), { kind: "varDecl", localId: r.id, @@ -10747,8 +10800,16 @@ function lowerForOfClassIterator( } satisfies IrStmt, ] : []), + ...setDone(false), ]; const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement), labels); + const loop: IrStmt = { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [...head, ...body], + ...(labels && { labels }), + loc, + }; return { kind: "block", body: [ @@ -10759,13 +10820,45 @@ function lowerForOfClassIterator( init: lowerer.classIteratorOpenCall(cit, recvRef, loc), loc, }, - { - kind: "while", - cond: { kind: "boolLit", value: true, type: BOOL, loc }, - body: [...head, ...body], - ...(labels && { labels }), - loc, - }, + ...(done + ? [ + { + kind: "varDecl", + localId: done.id, + init: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + } satisfies IrStmt, + { + kind: "tryCatch", + tryBody: [loop], + catchBody: null, + catchLocalId: null, + dropFinallyErrors: true, + finallyBody: [ + { + kind: "if", + cond: { + kind: "unary", + op: "!", + operand: { kind: "varRef", localId: done.id, type: BOOL, loc }, + type: BOOL, + loc, + }, + then: [ + { + kind: "exprStmt", + expr: lowerer.classIteratorReturnCall(cit, itRef(), loc), + loc, + }, + ], + else_: null, + loc, + }, + ], + loc, + } satisfies IrStmt, + ] + : [loop]), ], loc, }; @@ -10783,6 +10876,7 @@ function lowerForOfDyn( iterable: IrExpr, labels?: string[], asyncLoop = false, + syncOpen = false, ): IrStmt { const loc = locOf(stmt); const spell = notIterableSpelling(stmt.expression, asyncLoop ? "async iterable" : "iterable"); @@ -11010,7 +11104,7 @@ function lowerForOfDyn( localId: iterator.id, init: check({ kind: "libCall", - fn: asyncLoop ? "dyn.asyncIteratorOpen" : "dyn.iterator", + fn: asyncLoop && !syncOpen ? "dyn.asyncIteratorOpen" : "dyn.iterator", args: [iterable, { kind: "strLit", value: spell, type: STRING, loc }], type: DYN, loc, @@ -11022,13 +11116,15 @@ function lowerForOfDyn( { kind: "varDecl", localId: syncIterator.id, - init: { - kind: "libCall", - fn: "dyn.asyncIteratorWasSync", - args: [], - type: BOOL, - loc, - }, + init: syncOpen + ? { kind: "boolLit", value: true, type: BOOL, loc } + : { + kind: "libCall", + fn: "dyn.asyncIteratorWasSync", + args: [], + type: BOOL, + loc, + }, loc, } satisfies IrStmt, ] diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 6540b3a99..90bdf917e 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -1954,7 +1954,7 @@ function classIteratorObject( iter: IrExpr, loc: SrcLoc, ): IrExpr { - const key = `streamFromIter:${cit.iterT.className}`; + const key = `streamFromIter:${cit.iterT.kind === "object" ? cit.iterT.className : typeKey(cit.iterT)}`; let name = lowerer.valueHelpers.get(key); if (!name) { name = keyedHelperName(lowerer, "%stream.fromIter", key); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 747e396af..ea503e1f1 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -348,6 +348,7 @@ import { type ClassInfo, type ClassMethodSignature, type ClassIteratorInfo, + type ClassIteratorOptions, type GenericClassInfo, registerBuiltinErrorClasses, defineClassValueConstruct, @@ -359,6 +360,7 @@ import { builtinStreamInfoOf, analyzeClassDecoration, classIteratorDrainCall, + classIteratorMethodCall, classIteratorNextCall, classIteratorOf, classIteratorOpenCall, @@ -13049,8 +13051,12 @@ export class Lowerer { return accessorCall(this, className, member, obj, extraArgs, ret, loc); } - classIteratorOf(t: IrType): ClassIteratorInfo | null { - return classIteratorOf(this, t); + classIteratorOf(t: IrType, options?: ClassIteratorOptions): ClassIteratorInfo | null { + return classIteratorOf(this, t, options); + } + + classIteratorReturnCall(cit: ClassIteratorInfo, it: IrExpr, loc: SrcLoc): IrExpr { + return classIteratorMethodCall(this, cit, "return", it, loc); } classIteratorOpenCall(cit: ClassIteratorInfo, recv: IrExpr, loc: SrcLoc): IrExpr { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6928-class-iterator-protocol.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6928-class-iterator-protocol.ts.json new file mode 100644 index 000000000..2efd8521a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6928-class-iterator-protocol.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6928-class-iterator-protocol.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6929-class-iterator-protocol.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6929-class-iterator-protocol.mjs.json new file mode 100644 index 000000000..49e5e4304 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6929-class-iterator-protocol.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6929-class-iterator-protocol.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-class-async-iterator-protocol.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-class-async-iterator-protocol.ts.json new file mode 100644 index 000000000..93775b96b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-class-async-iterator-protocol.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6935-class-async-iterator-protocol.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-class-async-iterator-protocol.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-class-async-iterator-protocol.mjs.json new file mode 100644 index 000000000..378f7d18b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-class-async-iterator-protocol.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6936-class-async-iterator-protocol.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn_for_await.c b/packages/runtime/src/scr_dyn_for_await.c index 25183c4ee..2d39885c0 100644 --- a/packages/runtime/src/scr_dyn_for_await.c +++ b/packages/runtime/src/scr_dyn_for_await.c @@ -39,7 +39,11 @@ ScrDyn *scr_dyn_async_iterator_open(ScrDyn *value, ScrStr *spell) { } /* Whether the iterator the last scr_dyn_async_iterator_open returned is a - * synchronous one. Read immediately after the open. */ + * synchronous one. Read immediately after the open; the read clears it, so + * an iterator a class's [Symbol.asyncIterator]() returned (opened without + * the runtime) reads false. */ bool scr_dyn_async_iterator_was_sync(void) { - return scr_for_await_sync; + bool sync = scr_for_await_sync; + scr_for_await_sync = false; + return sync; } diff --git a/tests/corpus/6928-class-iterator-protocol.ts b/tests/corpus/6928-class-iterator-protocol.ts new file mode 100644 index 000000000..f097368de --- /dev/null +++ b/tests/corpus/6928-class-iterator-protocol.ts @@ -0,0 +1,93 @@ +// for-of and spread over classes whose [Symbol.iterator]() returns an iterator object, +// with return() called on early exits as Node's IteratorClose does. +class Bag { + readonly items: string[]; + constructor(items: string[]) { this.items = items; } + [Symbol.iterator]() { + let i = 0; + const items = this.items; + return { + next(): IteratorResult { + return i < items.length ? { value: items[i++]!, done: false } : { value: undefined, done: true }; + }, + return(): IteratorResult { + console.log("return at", i); + return { value: undefined, done: true }; + }, + }; + } +} +const bag = new Bag(["a", "b", "c", "d"]); +function firstLong(): string | undefined { + for (const s of bag) if (s >= "b") return s; + return undefined; +} +console.log(firstLong()); +try { + for (const s of bag) if (s === "c") throw new Error("stop at " + s); +} catch (e) { + console.log((e as Error).message); +} +outer: for (const n of [1, 2]) { + for (const s of bag) { + if (s === "b") continue outer; + console.log(n, s); + } +} +let all = ""; +for (const s of bag) all += s; +console.log(all); + +class Counter { + n = 0; + [Symbol.iterator](): CounterIterator { return new CounterIterator(3); } +} +class CounterIterator { + i = 0; + readonly limit: number; + constructor(limit: number) { this.limit = limit; } + next(): { value: number; done: boolean } { + return this.i < this.limit ? { value: this.i++, done: false } : { value: -1, done: true }; + } + return(): { value: number; done: boolean } { + console.log("class return", this.i); + return { value: -1, done: true }; + } +} +for (const v of new Counter()) { if (v === 1) break; console.log("v", v); } +console.log([...new Counter()].join()); + +class Fussy { + [Symbol.iterator]() { + let i = 0; + return { + next(): IteratorResult { return { value: i++, done: i > 5 }; }, + return(): IteratorResult { throw new Error("return failed"); }, + }; + } +} +try { + for (const v of new Fussy()) if (v === 1) break; +} catch (e) { + console.log("break:", (e as Error).message); +} +try { + for (const v of new Fussy()) if (v === 1) throw new Error("body failed"); +} catch (e) { + console.log("throw:", (e as Error).message); +} + +class Range { + readonly n: number; + constructor(n: number) { this.n = n; } + [Symbol.iterator]() { + let i = 0; + const n = this.n; + return { + next(): IteratorResult { return i < n ? { value: i++, done: false } : { value: undefined, done: true }; }, + }; + } +} +let total = 0; +for (const v of new Range(4)) total += v; +console.log(total, [...new Range(3)].join()); diff --git a/tests/corpus/6929-class-iterator-protocol.mjs b/tests/corpus/6929-class-iterator-protocol.mjs new file mode 100644 index 000000000..b5255ba35 --- /dev/null +++ b/tests/corpus/6929-class-iterator-protocol.mjs @@ -0,0 +1,78 @@ +// for-of and spread in JavaScript over classes whose [Symbol.iterator]() returns an +// iterator object, with return() called on early exits as Node's IteratorClose does. +class Bag { + + constructor(items) { this.items = items; } + [Symbol.iterator]() { + let i = 0; + const items = this.items; + return { + next() { + return i < items.length ? { value: items[i++], done: false } : { value: undefined, done: true }; + }, + return() { + console.log("return at", i); + return { value: undefined, done: true }; + }, + }; + } +} +const bag = new Bag(["a", "b", "c", "d"]); +function firstLong() { + for (const s of bag) if (s >= "b") return s; + return undefined; +} +console.log(firstLong()); +try { + for (const s of bag) if (s === "c") throw new Error("stop at " + s); +} catch (e) { + console.log(e.message); +} +outer: for (const n of [1, 2]) { + for (const s of bag) { + if (s === "b") continue outer; + console.log(n, s); + } +} +let all = ""; +for (const s of bag) all += s; +console.log(all); + +class Counter { + n = 0; + [Symbol.iterator]() { return new CounterIterator(3); } +} +class CounterIterator { + i = 0; + + constructor(limit) { this.limit = limit; } + next() { + return this.i < this.limit ? { value: this.i++, done: false } : { value: -1, done: true }; + } + return() { + console.log("class return", this.i); + return { value: -1, done: true }; + } +} +for (const v of new Counter()) { if (v === 1) break; console.log("v", v); } +console.log([...new Counter()].join()); + +class Fussy { + [Symbol.iterator]() { + let i = 0; + return { + next() { return { value: i++, done: i > 5 }; }, + return() { if (i >= 0) throw new Error("return failed"); return { value: undefined, done: true }; }, + }; + } +} +try { + for (const v of new Fussy()) if (v === 1) break; +} catch (e) { + console.log("break:", e.message); +} +try { + for (const v of new Fussy()) if (v === 1) throw new Error("body failed"); +} catch (e) { + console.log("throw:", e.message); +} diff --git a/tests/corpus/6935-class-async-iterator-protocol.ts b/tests/corpus/6935-class-async-iterator-protocol.ts new file mode 100644 index 000000000..9ec5a61a5 --- /dev/null +++ b/tests/corpus/6935-class-async-iterator-protocol.ts @@ -0,0 +1,60 @@ +// for await over classes whose [Symbol.asyncIterator]() or [Symbol.iterator]() +// returns an iterator object: early exits await return(), as in Node. +class Ticks { + readonly n: number; + constructor(n: number) { this.n = n; } + [Symbol.asyncIterator]() { + let i = 0; + const n = this.n; + return { + async next(): Promise> { + return i < n ? { value: i++, done: false } : { value: undefined, done: true }; + }, + async return(): Promise> { + console.log("closed at", i); + return { value: undefined, done: true }; + }, + }; + } +} +class Pages { + [Symbol.asyncIterator](): PageCursor { return new PageCursor(); } +} +class PageCursor { + page = 0; + async next(): Promise<{ value: string; done: boolean }> { + this.page++; + return this.page <= 3 ? { value: "page " + this.page, done: false } : { value: "", done: true }; + } +} +class Letters { + [Symbol.iterator]() { + const letters = ["x", "y", "z"]; + let i = 0; + return { + next(): IteratorResult { return i < letters.length ? { value: letters[i++]!, done: false } : { value: undefined, done: true }; }, + }; + } +} +async function main(): Promise { + let sum = 0; + for await (const v of new Ticks(4)) sum += v * 2; + console.log("sum", sum); + for await (const v of new Ticks(5)) { + if (v === 2) break; + console.log("early", v); + } + try { + for await (const v of new Ticks(5)) if (v === 1) throw new Error("body " + v); + } catch (e) { + console.log("caught", (e as Error).message); + } + async function find(): Promise { + for await (const v of new Ticks(9)) if (v > 2) return v; + return -1; + } + console.log("found", await find()); + for await (const p of new Pages()) console.log(p); + for await (const l of new Letters()) console.log("letter", l); +} +main(); diff --git a/tests/corpus/6936-class-async-iterator-protocol.mjs b/tests/corpus/6936-class-async-iterator-protocol.mjs new file mode 100644 index 000000000..cb7b1e68a --- /dev/null +++ b/tests/corpus/6936-class-async-iterator-protocol.mjs @@ -0,0 +1,60 @@ +// for await in JavaScript over classes whose [Symbol.asyncIterator]() or [Symbol.iterator]() +// returns an iterator object: early exits await return(), as in Node. +class Ticks { + + constructor(n) { this.n = n; } + [Symbol.asyncIterator]() { + let i = 0; + const n = this.n; + return { + async next() { + return i < n ? { value: i++, done: false } : { value: undefined, done: true }; + }, + async return() { + console.log("closed at", i); + return { value: undefined, done: true }; + }, + }; + } +} +class Pages { + [Symbol.asyncIterator]() { return new PageCursor(); } +} +class PageCursor { + page = 0; + async next() { + this.page++; + return this.page <= 3 ? { value: "page " + this.page, done: false } : { value: "", done: true }; + } +} +class Letters { + [Symbol.iterator]() { + const letters = ["x", "y", "z"]; + let i = 0; + return { + next() { return i < letters.length ? { value: letters[i++], done: false } : { value: undefined, done: true }; }, + }; + } +} +async function main() { + let sum = 0; + for await (const v of new Ticks(4)) sum += v * 2; + console.log("sum", sum); + for await (const v of new Ticks(5)) { + if (v === 2) break; + console.log("early", v); + } + try { + for await (const v of new Ticks(5)) if (v === 1) throw new Error("body " + v); + } catch (e) { + console.log("caught", e.message); + } + async function find() { + for await (const v of new Ticks(9)) if (v > 2) return v; + return -1; + } + console.log("found", await find()); + for await (const p of new Pages()) console.log(p); + for await (const l of new Letters()) console.log("letter", l); +} +main(); diff --git a/tests/harness/corpus-costs/6928-class-iterator-protocol.ts.json b/tests/harness/corpus-costs/6928-class-iterator-protocol.ts.json new file mode 100644 index 000000000..577adfbd9 --- /dev/null +++ b/tests/harness/corpus-costs/6928-class-iterator-protocol.ts.json @@ -0,0 +1 @@ +{ "plain": 3913, "san": 3785 } diff --git a/tests/harness/corpus-costs/6929-class-iterator-protocol.mjs.json b/tests/harness/corpus-costs/6929-class-iterator-protocol.mjs.json new file mode 100644 index 000000000..a653f7709 --- /dev/null +++ b/tests/harness/corpus-costs/6929-class-iterator-protocol.mjs.json @@ -0,0 +1 @@ +{ "plain": 3934, "san": 3488 } diff --git a/tests/harness/corpus-costs/6935-class-async-iterator-protocol.ts.json b/tests/harness/corpus-costs/6935-class-async-iterator-protocol.ts.json new file mode 100644 index 000000000..facd86f6d --- /dev/null +++ b/tests/harness/corpus-costs/6935-class-async-iterator-protocol.ts.json @@ -0,0 +1 @@ +{ "plain": 3179, "san": 2683 } diff --git a/tests/harness/corpus-costs/6936-class-async-iterator-protocol.mjs.json b/tests/harness/corpus-costs/6936-class-async-iterator-protocol.mjs.json new file mode 100644 index 000000000..cbe2cb7d2 --- /dev/null +++ b/tests/harness/corpus-costs/6936-class-async-iterator-protocol.mjs.json @@ -0,0 +1 @@ +{ "plain": 3547, "san": 2898 } From c271aa85aa4ed98cae5602ccc17393fdbc53b882 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 16:45:21 -0500 Subject: [PATCH 002/273] Count holes written through dynamic views of typed arrays --- .../compiler/src/frontend/lowering/lowerer.ts | 103 ++++++++++++++++++ .../lowering/runtime-optional-elements.ts | 62 +++++++++-- .../test/runtime-optional-precision.test.ts | 25 +++++ .../6067-view-holes-in-typed-arrays.ts.json | 6 + .../corpus/6067-view-holes-in-typed-arrays.ts | 57 ++++++++++ .../6067-view-holes-in-typed-arrays.ts.json | 1 + 6 files changed, 247 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6067-view-holes-in-typed-arrays.ts.json create mode 100644 tests/corpus/6067-view-holes-in-typed-arrays.ts create mode 100644 tests/harness/corpus-costs/6067-view-holes-in-typed-arrays.ts.json diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ae91a87d5..e737eca64 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -6309,7 +6309,110 @@ export class Lowerer { }); return keys; }; + // Element ABIs of the native arrays a value converted to a dynamic type + // can reach: the conversion keeps each such array as a live view, so + // stores through the dynamic value write the native storage. + const collectNativeViews = (): Set => { + const keys = new Set(); + const seen = new Set(); + const collect = (type: IrType | null): void => { + if (!type) return; + const id = typeKey(type); + if (seen.has(id)) return; + seen.add(id); + if (type.kind === "array") { + if (type.elem.kind !== "dyn") keys.add(typeKey(type.elem)); + collect(type.elem); + } else if (type.kind === "union") { + for (const arm of this.unions.get(type.unionId)?.arms ?? []) collect(arm); + } else if (type.kind === "record") { + for (const field of this.shapes.get(type.shapeId)?.fields ?? []) collect(field.type); + } else if (type.kind === "object") { + for (const field of this.classes.get(type.className)?.fields.values() ?? []) + collect(field); + } else if (type.kind === "map") { + collect(type.key); + collect(type.value); + } else if (type.kind === "set") collect(type.elem); + else if (type.kind === "promise") collect(type.inner); + }; + const holdsDynamic = (type: IrType | null, depth: number): boolean => { + if (!type || depth > 4) return false; + if (type.kind === "dyn") return true; + if (type.kind === "array" || type.kind === "set") return holdsDynamic(type.elem, depth + 1); + if (type.kind === "map") return holdsDynamic(type.value, depth + 1); + if (type.kind === "promise") return holdsDynamic(type.inner, depth + 1); + if (type.kind === "union") + return (this.unions.get(type.unionId)?.arms ?? []).some((arm) => + holdsDynamic(arm, depth + 1), + ); + if (type.kind === "record") + return (this.shapes.get(type.shapeId)?.fields ?? []).some((field) => + holdsDynamic(field.type, depth + 1), + ); + return false; + }; + const mapped = (type: ts.Type | null | undefined): IrType | null => + type ? panicSafe(() => this.mapTypeOf(type), null) : null; + const flow = (source: ts.Expression, target: ts.Type | null | undefined): void => { + if (target === undefined) return; + const into = target === null ? null : mapped(target); + // An unanswerable target may be dynamic. + if (target !== null && into !== null && !holdsDynamic(into, 0)) return; + const from = typeOrNull(source); + if (from === null) keys.add("*"); + else collect(mapped(from)); + }; + const visit = (node: ts.Node): void => { + if (ts.isAsExpression(node) || ts.isTypeAssertion(node)) + flow(node.expression, typeOrNull(node)); + else if (ts.isVariableDeclaration(node) && node.type && node.initializer) + flow(node.initializer, typeOrNull(node.name)); + else if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) + flow(node.right, typeOrNull(node.left)); + else if (ts.isReturnStatement(node) && node.expression) + flow(node.expression, contextualOrNull(node.expression)); + else if (ts.isCallExpression(node) || ts.isNewExpression(node)) { + for (const arg of node.arguments?.nodes ?? []) + flow(ts.isSpreadElement(arg) ? arg.expression : arg, contextualOrNull(arg)); + } else if (ts.isPropertyAssignment(node)) + flow(node.initializer, contextualOrNull(node.initializer)); + else if (ts.isShorthandPropertyAssignment(node)) { + const name = node.name as ts.Expression; + flow(name, contextualOrNull(name)); + } else if (ts.isArrayLiteralExpression(node)) { + for (const element of node.elements.nodes) + if (!ts.isSpreadElement(element) && !ts.isOmittedExpression(element)) + flow(element, contextualOrNull(element)); + } else if (ts.isArrowFunction(node) && !ts.isBlock(node.body)) + flow(node.body as ts.Expression, contextualOrNull(node.body as ts.Expression)); + }; + for (const sf of sourceFiles) + ts.walkPreorder(sf, (node) => { + // A checker panic hides this conversion: any array. + if (!panicSafe(() => (visit(node), true), false)) keys.add("*"); + return undefined; + }); + return keys; + }; + const dynamicType = (type: ts.Type): boolean => { + if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return true; + if (this.checker.isArrayType(type) && !this.checker.isTupleType(type)) { + const element = this.checker.getTypeArguments(type as ts.TypeReference)[0]; + if (element && (element.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) + return true; + } + return panicSafe(() => this.mapTypeOf(type)?.kind === "dyn", false); + }; const elementStates = new ArrayElementStates({ + isDynamicArray: (node) => { + const type = typeOrNull(node); + return type === null || dynamicType(type); + }, + nativeViewKeys: collectNativeViews, elementAdmitsUndefined: (key) => { siteDependencies?.read(key); return optionalElementKeys.has(key); diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts index 96d20d2c4..8fc0c4a95 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts @@ -37,6 +37,13 @@ export interface ElementStateHost { ownerOf(node: ts.Expression): ts.Symbol | null; /** The owning slot a newly built array is stored into. */ allocationOwner(node: ts.Expression): ts.Symbol | null; + /** `node` is a dynamic value (`any`, `unknown`, `unknown[]`): when it is + * a live view of a native array, a store through it writes that array's + * storage. */ + isDynamicArray(node: ts.Expression): boolean; + /** Element keys of the native arrays a value converted to a dynamic type + * can reach, as live views ("*" for any array). */ + nativeViewKeys(): ReadonlySet; /** Debug provenance for a new fact. */ note: ((fact: "undefined" | "holes", key: string, site: ts.Node | undefined) => void) | null; /** The scan read, or added to, the facts of an element key, of an owning @@ -48,6 +55,8 @@ export interface ElementStateHost { /** The fact name of the facts shared by every array. */ const ANY_ARRAY = "*"; +/** The fact name of holes stored through dynamic views. */ +const VIEW_HOLES = "%view"; /** What an array value can contain besides present values. */ interface Contents { @@ -81,6 +90,10 @@ export class ArrayElementStates { * whose element ABI is decided per generic instantiation. */ private anyUndefined = false; private anyHoley = false; + /** A store through a dynamic value can leave holes; they land in every + * native array such a value views. */ + private viewHoles = false; + private viewKeys: ReadonlySet | null = null; /** Facts of arrays owned by one storage slot. */ private readonly undefinedOwners = new Set(); private readonly holeyOwners = new Set(); @@ -330,14 +343,32 @@ export class ArrayElementStates { this.host.read?.(ANY_ARRAY); if (keys === null) return null; for (const key of keys) this.host.read?.(key); + this.host.read?.(VIEW_HOLES); return { undefined: this.anyUndefined || keys.some((key) => this.undefinedKeys.has(key) || this.host.elementAdmitsUndefined(key)), - holes: this.anyHoley || keys.some((key) => this.holeyKeys.has(key)), + holes: this.anyHoley || keys.some((key) => this.holeyKeys.has(key) || this.viewed(key)), }; } + /** Holes stored through a dynamic view can be in arrays of this key. */ + private viewed(key: string): boolean { + if (!this.viewHoles) return false; + this.viewKeys ??= this.host.nativeViewKeys(); + return this.viewKeys.has(key) || this.viewKeys.has(ANY_ARRAY); + } + + /** A store through `receiver`, which has no native element key, can + * leave holes. Only a dynamic value can be a view of a native array. */ + private addViewHoles(receiver: ts.Expression): boolean { + if (this.viewHoles || !this.host.isDynamicArray(receiver)) return false; + this.viewHoles = true; + this.host.changed?.(VIEW_HOLES); + this.host.note?.("holes", VIEW_HOLES, this.site); + return true; + } + /** Reading every index of `node` (a spread, an iteration, `Array.from`) * can produce `undefined`. Only native arrays can: other iterables reject * an undefined value when it is stored. */ @@ -427,7 +458,11 @@ export class ArrayElementStates { /** A store through `receiver` can leave holes. */ private addHolesTo(receiver: ts.Expression): boolean { const owner = this.host.ownerOf(receiver); - if (owner === null) return this.addHoles(this.host.arrayKeys(receiver)); + if (owner === null) { + const keys = this.host.arrayKeys(receiver); + if (keys !== null && keys.length === 0) return this.addViewHoles(receiver); + return this.addHoles(keys); + } if (this.holeyOwners.has(owner)) return false; this.holeyOwners.add(owner); this.host.changed?.(owner); @@ -490,7 +525,8 @@ export class ArrayElementStates { const undefinedValue = storesValue && this.host.mayBeUndefined(node.right); if (!holes && !undefinedValue) return false; const keys = this.host.arrayKeys(target.expression); - if (keys !== null && keys.length === 0) return false; + if (keys !== null && keys.length === 0) + return holes && !namesProperty(target) && this.addViewHoles(target.expression); let changed = false; if (holes && this.addHolesTo(target.expression)) changed = true; if (undefinedValue && this.addUndefinedTo(target.expression)) changed = true; @@ -501,8 +537,6 @@ export class ArrayElementStates { target.name.text === "length" && !isShrinkingLength(node, target) ) { - const keys = this.host.arrayKeys(target.expression); - if (keys !== null && keys.length === 0) return false; return this.addHolesTo(target.expression); } return false; @@ -574,10 +608,16 @@ export class ArrayElementStates { )); if (!reflective) return false; let changed = false; - for (const arg of args) { + // The written object: `Reflect.apply`'s receiver argument, otherwise + // the first argument. + const written = this.host.isGlobal(owner, "Reflect") && method === "apply" ? 1 : 0; + for (const [index, arg] of args.entries()) { const target = ts.isSpreadElement(arg) ? arg.expression : arg; const keys = this.host.arrayKeys(target); - if (keys !== null && keys.length === 0) continue; + if (keys !== null && keys.length === 0) { + if (index === written && this.addViewHoles(target)) changed = true; + continue; + } if (this.addUndefinedTo(target)) changed = true; if (this.addHolesTo(target)) changed = true; } @@ -761,6 +801,14 @@ function isAppendIndex(target: ts.ElementAccessExpression): boolean { ); } +/** `o["name"]`: a string literal key that is not an array index writes a + * named property, never an element. */ +function namesProperty(target: ts.ElementAccessExpression): boolean { + const key = peel(target.argumentExpression); + if (!ts.isStringLiteral(key) && !ts.isNoSubstitutionTemplateLiteral(key)) return false; + return !/^(0|[1-9][0-9]*)$/.test(key.text) || Number(key.text) >= 2 ** 32 - 1; +} + /** `xs.length = 0`, `xs.length = xs.length - k` and `xs.length -= k` * truncate; truncation never leaves holes. */ function isShrinkingLength( diff --git a/packages/compiler/test/runtime-optional-precision.test.ts b/packages/compiler/test/runtime-optional-precision.test.ts index 7840b0c62..3a3251b5a 100644 --- a/packages/compiler/test/runtime-optional-precision.test.ts +++ b/packages/compiler/test/runtime-optional-precision.test.ts @@ -102,6 +102,31 @@ console.log(copy.map((item) => weigh(item)).join(",")); expect(signature(spread, "weigh")).toBe("weigh(Item|undefined): f64"); }); +test("holes stored through a dynamic view widen callbacks over the arrays it views", async () => { + const viewed = await lower(`${ITEM} +const items: Item[] = [new Item(1)]; +const view: unknown[] = items; +view[3] = new Item(2); +console.log(items.findIndex((item) => weigh(item) > 1)); +`); + expect(signature(viewed, "weigh")).toBe("weigh(Item|undefined): f64"); + // A dynamic store cannot reach an element type no dynamic value views. + const other = await lower(`${ITEM} +const items: Item[] = [new Item(1)]; +const values: unknown[] = [1]; +values[3] = 2; +console.log(items.findIndex((item) => weigh(item) > 1)); +`); + expect(signature(other, "weigh")).toBe("weigh(Item): f64"); + // A view without stores that leave holes keeps the parameters. + const read = await lower(`${ITEM} +const items: Item[] = [new Item(1)]; +const view: unknown[] = items; +console.log(view.length, items.findIndex((item) => weigh(item) > 1)); +`); + expect(signature(read, "weigh")).toBe("weigh(Item): f64"); +}); + test("holes that only their owning slot can observe stay out of callbacks", async () => { const module = await lower(`${ITEM} class Cache { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6067-view-holes-in-typed-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6067-view-holes-in-typed-arrays.ts.json new file mode 100644 index 000000000..9d257fd49 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6067-view-holes-in-typed-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6067-view-holes-in-typed-arrays.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6067-view-holes-in-typed-arrays.ts b/tests/corpus/6067-view-holes-in-typed-arrays.ts new file mode 100644 index 000000000..b59437646 --- /dev/null +++ b/tests/corpus/6067-view-holes-in-typed-arrays.ts @@ -0,0 +1,57 @@ +// Holes written through an `unknown[]` or `any` view of a typed array are +// holes of the typed array: for...of visits them as undefined and the find +// family passes them to its callback, as in Node. The views reach the array +// through a binding, an `any` parameter, a generic instantiated with +// `unknown`, a record field, a class field and a nested array. + +const numbers: number[] = [1, 2]; +const numbersView: unknown[] = numbers; +numbersView[3] = 5; +for (const value of numbers) console.log("numbers", value, typeof value); +console.log(numbers.findIndex((value) => value === undefined)); +console.log(numbers.findLastIndex((value) => value === undefined)); +console.log(numbers.find((value) => value !== undefined && value > 1)); + +const grown: string[] = ["a"]; +const grownView: unknown[] = grown; +grownView.length = 3; +for (const value of grown) console.log("grown", value); + +function growAny(values: any): void { + values[2] = 9; +} +const passed: number[] = [7]; +growAny(passed); +for (const value of passed) console.log("passed", value); + +function keep(value: T): T { + return value; +} +const kept: number[] = [3]; +(keep(kept) as unknown[])[2] = 4; +for (const value of kept) console.log("kept", value); + +const boxed: number[] = [1]; +const box: unknown = { values: boxed }; +(box as { values: unknown[] }).values[2] = 3; +for (const value of boxed) console.log("boxed", value); + +class Holder { + values: number[] = [1, 2]; +} +const holder = new Holder(); +const holderView: unknown = holder; +(holderView as { values: unknown[] }).values[3] = 4; +for (const value of holder.values) console.log("holder", value); + +const nested: number[][] = [[1]]; +const nestedView: unknown[][] = nested; +nestedView[0]![2] = 3; +for (const value of nested[0]!) console.log("nested", value); + +// A named property written through a view is not an element. +const named: number[] = [1, 2]; +const bag = named as unknown as Record; +bag["label"] = "x"; +for (const value of named) console.log("named", value); +console.log(named.length, bag["label"]); diff --git a/tests/harness/corpus-costs/6067-view-holes-in-typed-arrays.ts.json b/tests/harness/corpus-costs/6067-view-holes-in-typed-arrays.ts.json new file mode 100644 index 000000000..b8b740bf8 --- /dev/null +++ b/tests/harness/corpus-costs/6067-view-holes-in-typed-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 3967, "san": 4623 } From 3bbb88c43c8c5e0957047c6035c0a9ff42c66aba Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 16:51:25 -0500 Subject: [PATCH 003/273] Construct through arrays of class values that mix a class and its subclasses `const kinds = [Dog, Cat, Animal]; new kinds[0]()` now works as in Node: - TypeScript: the checker types the element as the common base (`typeof Animal`), and an element read keeps the `undefined` an index past the end reads. The construction now narrows the class value out of that union, and an index past the end throws Node's TypeError ("kinds[5] is not a constructor"). Before, it refused with SC2001. - JavaScript: such a literal is a checked-dynamic array stored in a slot of the base's class values, and the check accepted only the base's own class value, so storing it threw "expected class constructor". A class value of a subclass now checks into its base class's slot, as a subclass instance already checks into a base-class instance slot. Classes without subclasses keep the exact check. --- packages/compiler/src/backend/llvm/dyn.ts | 18 ++++ .../src/frontend/lowering/lower-classes.ts | 93 ++++++++++++++++++- .../corpus/6937-class-value-arrays.mjs.json | 6 ++ .../corpus/6938-class-value-arrays.ts.json | 6 ++ tests/corpus/6937-class-value-arrays.mjs | 30 ++++++ tests/corpus/6938-class-value-arrays.ts | 28 ++++++ .../6937-class-value-arrays.mjs.json | 1 + .../6938-class-value-arrays.ts.json | 1 + 8 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6937-class-value-arrays.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6938-class-value-arrays.ts.json create mode 100644 tests/corpus/6937-class-value-arrays.mjs create mode 100644 tests/corpus/6938-class-value-arrays.ts create mode 100644 tests/harness/corpus-costs/6937-class-value-arrays.mjs.json create mode 100644 tests/harness/corpus-costs/6938-class-value-arrays.ts.json diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 5c9d8e60f..f0278bce6 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -1530,6 +1530,24 @@ export class LlDyn { const B = new BlockBuilder(); if (t.kind === "classval") { host.declare(`declare ptr @scr_dyn_class_check(ptr, ptr, ptr)`); + // A subclass's class value checks into the base class's slot. + const subclasses = host.classSubtypes(t.className).filter((name) => name !== t.className); + if (subclasses.length > 0) { + host.declare(`declare zeroext i1 @scr_dyn_class_is(ptr, ptr)`); + for (const className of subclasses) { + const subKey = host.cstr(typeKey({ kind: "classval", className })); + const matched = B.tmp(); + B.line(`${matched} = call zeroext i1 @scr_dyn_class_is(ptr %d, ptr ${subKey})`); + const yes = B.newLabel("sub.yes"); + const no = B.newLabel("sub.no"); + B.condBr(matched, yes, no); + B.startBlock(yes); + const sub = B.tmp(); + B.line(`${sub} = call ptr @scr_dyn_class_check(ptr %d, ptr ${subKey}, ptr %path)`); + B.terminate(`ret ptr ${sub}`); + B.startBlock(no); + } + } const checked = B.tmp(); B.line( `${checked} = call ptr @scr_dyn_class_check(ptr %d, ptr ${host.cstr(key)}, ptr %path)`, diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d8ccc15b7..34631cf53 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -10769,8 +10769,99 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { "constructing through a class value of an uninstantiated generic type (annotate the slot with a concrete instantiation — e.g. 'new (v: number) => Box')", ); } - const callee = lowerer.lowerExpr(expr.expression); + let callee = lowerer.lowerExpr(expr.expression); if (callee.type.kind === "dyn") return lowerCheckedConstructor(lowerer, expr, callee); + // An array element read (`new T[0]()` over `[Dog, Animal]`, whose + // checker type is the common base) keeps the undefined an index past + // the end reads: the class value is narrowed out with a check, which + // throws a TypeError for undefined. + if (callee.type.kind === "union") { + const arms = lowerer.unions.get(callee.type.unionId)?.arms ?? []; + const classArms = arms.filter((arm) => arm.kind === "classval"); + if ( + classArms.length === 1 && + classArms[0]!.kind === "classval" && + classArms[0]!.className === calleeT.className && + arms.every((arm) => arm.kind === "classval" || arm.kind === "undefinedT") + ) { + const unionT = callee.type; + const helper = lowerer.narrowedArmHelper(unionT.unionId, calleeT, callee.loc); + if (helper) { + const loc = callee.loc; + const name = `%new.callee:${unionT.unionId}`; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const undefinedTag = lowerer.armTag(unionT.unionId, UNDEFINED_T); + const value: IrExpr = { kind: "varRef", localId: "value", type: unionT, loc }; + lowerer.liftedFns.push({ + name, + params: [ + { localId: "value", name: "value", type: unionT }, + { localId: "message", name: "message", type: STRING }, + ], + locals: [ + { id: "value", name: "value", type: unionT, mutable: false }, + { id: "message", name: "message", type: STRING, mutable: false }, + ], + returnType: calleeT, + body: [ + ...(undefinedTag >= 0 + ? [ + { + kind: "if", + cond: { + kind: "unionIsTag", + unionId: unionT.unionId, + tag: undefinedTag, + negated: false, + value, + type: BOOL, + loc, + }, + then: [ + { + kind: "throw", + value: { + kind: "libCall", + fn: "error.new", + args: [{ kind: "varRef", localId: "message", type: STRING, loc }], + type: { kind: "object", className: "%TypeError" }, + loc, + }, + loc, + }, + ], + else_: null, + loc, + } satisfies IrStmt, + ] + : []), + { + kind: "return", + value: lowerer.checkedNarrowCall(helper, value, calleeT, loc), + loc, + }, + ], + loc, + }); + } + callee = { + kind: "call", + callee: name, + args: [ + callee, + { + kind: "strLit", + value: `${expr.expression.getText()} is not a constructor`, + type: STRING, + loc, + }, + ], + type: calleeT, + loc, + }; + } + } + } if (callee.type.kind !== "classval") lowerer.badType(expr.expression, lowerer.typeOf(expr.expression)); lowerer.noteEdge(classValueConstructor(info.def).fn); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6937-class-value-arrays.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6937-class-value-arrays.mjs.json new file mode 100644 index 000000000..eefa82916 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6937-class-value-arrays.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6937-class-value-arrays.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6938-class-value-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6938-class-value-arrays.ts.json new file mode 100644 index 000000000..ff2a51546 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6938-class-value-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6938-class-value-arrays.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6937-class-value-arrays.mjs b/tests/corpus/6937-class-value-arrays.mjs new file mode 100644 index 000000000..a563d7514 --- /dev/null +++ b/tests/corpus/6937-class-value-arrays.mjs @@ -0,0 +1,30 @@ +// Arrays of class values that mix a base class and its subclasses: the +// elements construct, compare and name like the classes themselves. +class Animal { + speak() { + return "..."; + } +} +class Dog extends Animal { + speak() { + return "woof"; + } +} +class Cat extends Animal { + speak() { + return "meow"; + } +} +const kinds = [Dog, Cat, Animal]; +console.log(kinds.length, kinds.map((C) => C.name).join()); +console.log(new kinds[0]().speak(), new kinds[1]().speak(), new kinds[2]().speak()); +console.log(kinds[0] === Dog, kinds.indexOf(Cat), kinds.includes(Animal)); +const made = kinds.map((C) => new C()); +console.log(made.map((a) => a.speak()).join(), made[0] instanceof Animal, made[1] instanceof Dog); +try { + new kinds[5](); +} catch (e) { + console.log(e.name, e.message); +} +const byName = { dog: Dog, cat: Cat }; +console.log(new byName.dog().speak(), new byName["cat"]().speak()); diff --git a/tests/corpus/6938-class-value-arrays.ts b/tests/corpus/6938-class-value-arrays.ts new file mode 100644 index 000000000..672700cbf --- /dev/null +++ b/tests/corpus/6938-class-value-arrays.ts @@ -0,0 +1,28 @@ +// Arrays of class values that mix a base class and its subclasses, with the +// element type the checker infers (the base class): elements construct and +// dispatch to the subclass, and an index past the end throws Node's TypeError. +class Shape { + area(): number { + return 0; + } +} +class Square extends Shape { + area(): number { + return 4; + } +} +class Circle extends Shape { + area(): number { + return 3; + } +} +const shapes = [Square, Circle, Shape]; +console.log(shapes.length, new shapes[0]().area(), new shapes[1]().area(), new shapes[2]().area()); +let total = 0; +for (const S of shapes) total += new S().area(); +console.log(total, shapes[1] === Circle, shapes.map((S) => S.name).join()); +try { + new shapes[3](); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} diff --git a/tests/harness/corpus-costs/6937-class-value-arrays.mjs.json b/tests/harness/corpus-costs/6937-class-value-arrays.mjs.json new file mode 100644 index 000000000..7e905d65a --- /dev/null +++ b/tests/harness/corpus-costs/6937-class-value-arrays.mjs.json @@ -0,0 +1 @@ +{ "plain": 4211, "san": 6383 } diff --git a/tests/harness/corpus-costs/6938-class-value-arrays.ts.json b/tests/harness/corpus-costs/6938-class-value-arrays.ts.json new file mode 100644 index 000000000..76098ce8d --- /dev/null +++ b/tests/harness/corpus-costs/6938-class-value-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 2399, "san": 4080 } From aaca1f1e2b838e0ef15d91743b06bfd5803d6dc0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 18:17:48 -0500 Subject: [PATCH 004/273] Count the arguments a function receives when it is called through a value `arguments.length` in a typed function folds to a constant. In a function declaration it was the declared parameter count even when the function is called through a value with more arguments: Node prints 4 for `[1].map(f)` with `function f(v) { return v + arguments.length; }`, the compiled program printed 2. A top-level function declaration that reads `arguments` and that the program uses as a value now gets a count slot. Its value is an adapter that stores the parameter count of the function type the value is passed as (3 for map, 4 for reduce, 2 for a sort comparator) and forwards; the function reads the slot on entry and clears it, so a direct call sees the declared count. Function expressions and declarations passed where the expected function type has optional or rest parameters (a replace callback) are called with a varying count and are refused with SC1090 instead of folding to a wrong count, as are methods, nested functions, and arrows inside a function used as a value. --- .../src/frontend/lowering/argument-counts.ts | 230 ++++++++++++++++++ .../lowering/expressions/identifiers.ts | 13 +- .../lowering/expressions/property-access.ts | 50 ++-- .../src/frontend/lowering/function-context.ts | 3 + .../src/frontend/lowering/lower-functions.ts | 2 + .../compiler/src/frontend/lowering/lowerer.ts | 3 + ...81-function-value-arguments-length.ts.json | 6 + .../function-value-arguments-length.ts.json | 6 + .../7381-function-value-arguments-length.ts | 27 ++ .../function-value-arguments-length.ts | 36 +++ .../function-value-arguments-length.ts.txt | 43 ++++ ...81-function-value-arguments-length.ts.json | 1 + 12 files changed, 398 insertions(+), 22 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/argument-counts.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-function-value-arguments-length.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-arguments-length.ts.json create mode 100644 tests/corpus/7381-function-value-arguments-length.ts create mode 100644 tests/diagnostics/function-value-arguments-length.ts create mode 100644 tests/harness/__snapshots__/function-value-arguments-length.ts.txt create mode 100644 tests/harness/corpus-costs/7381-function-value-arguments-length.ts.json diff --git a/packages/compiler/src/frontend/lowering/argument-counts.ts b/packages/compiler/src/frontend/lowering/argument-counts.ts new file mode 100644 index 000000000..ec1753160 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/argument-counts.ts @@ -0,0 +1,230 @@ +import * as ts from "../ts7/adapter.js"; +import { bodyReadsArguments } from "../arguments-usage.js"; +import { isJsSourceFile, locOf } from "../program.js"; +import { BOOL, F64, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; +import { varRef } from "../../ir/build.js"; +import { typeKey } from "../type-mapper.js"; +import type { Lowerer } from "./lowerer.js"; +import { isThisParameter } from "./call-signatures.js"; +import { buildFunctionAdapter } from "./coercions/builders.js"; +import { keyedHelperName } from "./stable-names.js"; + +/** `arguments.length` in a top-level function declaration that the program + * also uses as a value. A direct call passes the declared parameters (tsc + * checks the arity), while a call through the value passes the parameter + * count of the function type the value was passed as (`[1].map(f)` calls f + * with 3). The value is an adapter that stores that count in a global slot + * before forwarding; the function reads the slot on entry and clears it, + * so a direct call (the cleared slot) sees the declared count. */ + +/** The count slot of `decl`, or null when its `arguments.length` folds to + * the declared count (or the declaration does not qualify). */ +export function argumentCountSlot(lowerer: Lowerer, decl: ts.FunctionDeclaration): string | null { + const cached = lowerer.argumentCountSlots.get(decl); + if (cached !== undefined) return cached; + let slot: string | null = null; + if ( + decl.body !== undefined && + decl.name !== undefined && + decl.asteriskToken === undefined && + !decl.typeParameters?.nodes.length && + lowerer.isTopLevelFnSymbol(decl.name) && + decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) !== true && + !isJsSourceFile(ts.getSourceFileOfNode(decl)) && + fixedArity(decl) && + bodyReadsArguments(decl) && + usedAsValue(lowerer, decl) + ) { + const sig = lowerer.fnSigOf(decl.name); + if (sig) { + slot = `%g.argc.${sig.name}`; + lowerer.globalsList.push({ id: slot, name: "%argc", type: F64, mutable: true }); + } + } + lowerer.argumentCountSlots.set(decl, slot); + return slot; +} + +/** Reads `decl`'s count slot into a hidden local on entry and clears it; the + * local holds the declared count when the slot was clear. */ +export function declareArgumentCount( + lowerer: Lowerer, + decl: ts.FunctionDeclaration, + prologue: IrStmt[], +): void { + const slot = argumentCountSlot(lowerer, decl); + if (slot === null) return; + const loc = locOf(decl); + const local = lowerer.declareHiddenLocal("%argc", F64); + const stored = varRef(slot, F64, loc); + prologue.push( + { + kind: "varDecl", + localId: local.id, + init: { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: stored, + right: { kind: "numLit", value: 0, type: F64, loc }, + type: BOOL, + loc, + }, + then: { kind: "numLit", value: declaredCount(decl), type: F64, loc }, + else_: stored, + type: F64, + loc, + }, + loc, + }, + { kind: "assign", localId: slot, value: { kind: "numLit", value: 0, type: F64, loc }, loc }, + ); + lowerer.ctx.argumentCount = local; +} + +/** `decl` as a value at `reference`: the plain closure when every call + * through the expected function type passes the declared count, otherwise + * an adapter that stores the expected type's count in the slot first. */ +export function argumentCountValue( + lowerer: Lowerer, + decl: ts.FunctionDeclaration, + reference: ts.Expression, + closure: IrExpr & { type: IrType & { kind: "func" } }, +): IrExpr { + const slot = argumentCountSlot(lowerer, decl); + if (slot === null) return closure; + const count = contextualCallCount(lowerer, reference); + if (count === null || count === "varies") { + lowerer.unsupported( + "SC1090", + reference, + count === null + ? "functions that read 'arguments.length' used as values where no function type is expected (the count each call passes is not known)" + : "functions that read 'arguments.length' used as values where the expected function type has optional or rest parameters (the count varies per call)", + ); + } + const declared = declaredCount(decl); + if (count <= declared) return closure; + const loc = locOf(reference); + const type = closure.type; + const key = `argc:${slot}:${count}:${typeKey(type)}`; + let name = lowerer.arrHofHelpers.get(key); + if (name === undefined) { + name = keyedHelperName(lowerer, "%argc.adapter", key); + lowerer.arrHofHelpers.set(key, name); + const params = type.params.map((t, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: t })); + lowerer.liftedFns.push( + ...buildFunctionAdapter( + name, + type, + type, + params, + [], + [ + { + kind: "assign", + localId: slot, + value: { kind: "numLit", value: count, type: F64, loc }, + loc, + }, + { + kind: "return", + value: { + kind: "callValue", + callee: varRef("f.0", type, loc), + args: params.map((p) => varRef(p.localId, p.type, loc)), + type: type.ret, + loc, + }, + loc, + }, + ], + loc, + ), + ); + } + return { kind: "call", callee: name, args: [closure], type, loc }; +} + +/** The argument count every call through the function type `node` is + * expected as passes: that type's parameter count when it has one call + * signature with a fixed parameter list (a `this` parameter is type-only), + * "varies" when it has optional or rest parameters, null when there is no + * such type. */ +export function contextualCallCount( + lowerer: Lowerer, + node: ts.Expression, +): number | "varies" | null { + const contextual = lowerer.checker.getContextualType(node); + 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 "varies"; + count++; + } + return count; +} + +/** Whether a program reference to the function declaration or method `fn` + * reads it as a value (`xs.map(f)`, `const g = f`, `xs.map(C.parse)`) rather + * than calling it. Export specifiers and default exports only name a + * function; the importing files' references resolve to it and are checked + * the same way. */ +export function usedAsValue( + lowerer: Lowerer, + fn: ts.FunctionDeclaration | ts.MethodDeclaration, +): boolean { + const name = fn.name; + if (!name || !(ts.isIdentifier(name) || ts.isPrivateIdentifier(name))) return false; + const method = ts.isMethodDeclaration(fn); + const symbolAt = (n: ts.Node): ts.Symbol | null | undefined => + method + ? lowerer.checker.getSymbolAtLocation(n) + : lowerer.resolveValueSymbol(n as ts.Identifier); + const symbol = symbolAt(name); + if (!symbol) return false; + let found = false; + for (const sf of lowerer.program.getSourceFiles()) { + if (sf.isDeclarationFile) continue; + ts.walkPreorder(sf, (n) => { + if (found) return "stop"; + if (ts.isTypeNode(n)) return "skip"; + if (n === name || !(ts.isIdentifier(n) || ts.isPrivateIdentifier(n))) return undefined; + if (n.text !== name.text || !n.parent) return undefined; + const parent = n.parent; + // A method is read through its member name (`C.parse`, `this.#m`). + const read = ts.isPropertyAccessExpression(parent) && parent.name === n ? parent : n; + if (method && read === n && !ts.isBindingElement(parent)) return undefined; + const use = read.parent; + if (use && ts.isCallExpression(use) && use.expression === read) return undefined; + if (ts.isExportSpecifier(parent) || ts.isExportAssignment(parent)) return undefined; + if (ts.isImportSpecifier(parent) || ts.isImportClause(parent)) return undefined; + if (symbolAt(n) === symbol) found = true; + return undefined; + }); + if (found) break; + } + return found; +} + +function fixedArity(fn: ts.FunctionLikeDeclaration): boolean { + return fn.parameters.nodes.every( + (p) => + p.questionToken === undefined && + p.initializer === undefined && + p.dotDotDotToken === undefined, + ); +} + +function declaredCount(fn: ts.FunctionLikeDeclaration): number { + return fn.parameters.nodes.filter((p) => !isThisParameter(p)).length; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 63175d50a..ff6f65f26 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -25,6 +25,7 @@ import { lowerNodeModuleIdentifier } from "../lower-node-module.js"; import { lowerCjsExportsObjectValue } from "../cjs-exports-object.js"; import { captureContextArguments } from "../function-context.js"; import { argumentsOwner, typedArgumentsCandidate } from "../typed-arguments.js"; +import { argumentCountValue } from "../argument-counts.js"; import { isCjsExportTableLiteral, isCjsJsFile, @@ -424,7 +425,17 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // semantics); the rest marker carries the trailing dyn-array ABI. const funcType: IrType = funcTypeFromParamShapes(sig.params, sig.returnType); lowerer.requireExactArityValue(expr, expr, sig.params, funcType); - return { kind: "closure", fnName: sig.name, captures: [], type: funcType, loc }; + const closure = { + kind: "closure" as const, + fnName: sig.name, + captures: [], + type: funcType, + loc, + }; + const decl = lowerer.resolveValueSymbol(expr)?.valueDeclaration; + return decl && ts.isFunctionDeclaration(decl) && funcType.kind === "func" + ? argumentCountValue(lowerer, decl, expr, { ...closure, type: funcType }) + : closure; } { // A generic function as a VALUE: monomorphized by flow — the diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 5df3c7fe0..5f1683ff7 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -6,6 +6,7 @@ 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 { argumentCountSlot, contextualCallCount, usedAsValue } from "../argument-counts.js"; import { checkedPrivateValue, localPrivateBrandClass, @@ -669,7 +670,31 @@ export function lowerPropertyAccessExpr( const declared = (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter( (p) => !isThisParameter(p), ).length; - const count = Math.max(declared, contextualArgumentCount(lowerer, fn) ?? 0); + const contextual = contextualArgumentCount(lowerer, fn); + if (contextual === "varies") { + lowerer.unsupported( + "SC1090", + expr, + "'arguments.length' in function expressions passed where the expected function type has optional or rest parameters (the count varies per call)", + ); + } + // A top-level declaration also used as a value reads the count its + // caller stored (argument-counts.ts). + if (ts.isFunctionDeclaration(fn) && lowerer.ctx.argumentCount !== undefined) + return varRef(lowerer.ctx.argumentCount.id, F64, loc); + if ( + (ts.isFunctionDeclaration(fn) || ts.isMethodDeclaration(fn)) && + usedAsValue(lowerer, fn) + ) { + lowerer.unsupported( + "SC1090", + expr, + ts.isFunctionDeclaration(fn) && argumentCountSlot(lowerer, fn) !== null + ? "'arguments.length' in an arrow function inside a function that is also used as a value" + : "'arguments.length' in methods, nested, async and generator functions used as values (a caller of the value can pass more arguments than the function declares)", + ); + } + const count = Math.max(declared, contextual ?? 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)) { @@ -4469,24 +4494,7 @@ function referencesOwnName(lowerer: Lowerer, fn: ts.Node): boolean { } /** 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; + * value flows into a function type (argument-counts.ts contextualCallCount). */ +function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | "varies" | null { + return ts.isFunctionExpression(fn) ? contextualCallCount(lowerer, fn) : null; } diff --git a/packages/compiler/src/frontend/lowering/function-context.ts b/packages/compiler/src/frontend/lowering/function-context.ts index 92042b7d7..702574b63 100644 --- a/packages/compiler/src/frontend/lowering/function-context.ts +++ b/packages/compiler/src/frontend/lowering/function-context.ts @@ -46,6 +46,9 @@ export interface FnCtx { * rest param): the synthetic trailing dyn-array param `arguments` * reads resolve to. */ argumentsLocal?: IrLocal | null; + /** The count `arguments.length` reads in a top-level function declaration + * that is also used as a value (argument-counts.ts). */ + argumentCount?: IrLocal; /** Arrows inherit the nearest ordinary function's arguments binding. */ inheritsArguments?: boolean; /** A function lowered on demand on top of an unrelated caller's frames diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index c1cca9c64..c1682f7c8 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -1,5 +1,6 @@ import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; import { declareTypedArguments } from "./typed-arguments.js"; +import { declareArgumentCount } from "./argument-counts.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; import { bindingNeverReassigned } from "./binding-analysis.js"; @@ -551,6 +552,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I (sig.params[sig.params.length - 1]!.mode === "dynRest" || sig.params[sig.params.length - 1]!.mode === "arguments"), ); + declareArgumentCount(lowerer, decl, prologue); const bodyBlock = blockBodyOf(decl); if (!bodyBlock) { lowerer.unsupported( diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 4add3193c..4b60d9e1b 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2335,6 +2335,9 @@ export class Lowerer { * every name destructured from a present source optional. */ readonly runtimeOptionalPatternTypes = new Map(); readonly runtimeOptionalReduceTypes = new Map(); + /** Each function declaration's `arguments.length` count slot, or null + * (argument-counts.ts argumentCountSlot). */ + readonly argumentCountSlots = new Map(); /** Non-null assertions whose value is forwarded into a slot promoted to * hold undefined. `x!` has no runtime effect, so these keep the stored * undefined state instead of trapping before any dereference. */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-function-value-arguments-length.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-function-value-arguments-length.ts.json new file mode 100644 index 000000000..b9e5eacd1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-function-value-arguments-length.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7381-function-value-arguments-length.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-arguments-length.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-arguments-length.ts.json new file mode 100644 index 000000000..6aa894918 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-arguments-length.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/function-value-arguments-length.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7381-function-value-arguments-length.ts b/tests/corpus/7381-function-value-arguments-length.ts new file mode 100644 index 000000000..059ff1487 --- /dev/null +++ b/tests/corpus/7381-function-value-arguments-length.ts @@ -0,0 +1,27 @@ +// `arguments.length` in a function declaration counts the arguments its +// caller passes: a direct call passes the declared parameters, and a call +// through the function as a value passes the parameters of the function +// type it was passed as (3 for map and filter, 4 for reduce, 2 for sort). +function scaled(value: number): number { + return value * 10 + arguments.length; +} +function total(acc: number, value: number): number { + return acc + value + arguments.length; +} +function compare(a: number, b: number): number { + return arguments.length === 2 ? a - b : 0; +} +console.log([1, 2].map(scaled).join(), scaled(3)); +console.log([5, 6].filter(scaled).length, [1, 2].reduce(total, 0), total(1, 2)); +console.log([3, 1, 2].sort(compare).join()); + +function apply(f: (a: number, b: number, c: number) => number): number { + return f(1, 2, 3); +} +console.log(apply(scaled)); + +// A direct call inside a call through the value sees its own count. +function depth(n: number): number { + return n <= 0 ? arguments.length : depth(n - 1) + arguments.length; +} +console.log([2].map(depth).join(), depth(2)); diff --git a/tests/diagnostics/function-value-arguments-length.ts b/tests/diagnostics/function-value-arguments-length.ts new file mode 100644 index 000000000..ab0b53f82 --- /dev/null +++ b/tests/diagnostics/function-value-arguments-length.ts @@ -0,0 +1,36 @@ +// `arguments.length` has no single value when the calls pass varying +// counts: a function expression or declaration passed where the expected +// function type has optional or rest parameters. Methods and nested +// functions used as values, and arrows inside a function used as a value, +// keep the fence. +const replaced = "a-b".replace(/-/, function (match) { + return String(arguments.length) + match; +}); +function call(f: (a: number, b?: number) => number): number { + return f(1, 2); +} +const counted = call(function (a) { + return arguments.length + a; +}); +function offset(value: number): number { + return value + arguments.length; +} +const offsets = call(offset); +class Scale { + static by(value: number): number { + return value * arguments.length; + } +} +const scaled = [2].map(Scale.by); +function outer(): number[] { + function inner(value: number): number { + return value + arguments.length; + } + return [1].map(inner); +} +function deferred(value: number): number { + const read = (): number => arguments.length; + return value + read(); +} +const late = [1].map(deferred); +export { replaced, counted, offsets, scaled, outer, late }; diff --git a/tests/harness/__snapshots__/function-value-arguments-length.ts.txt b/tests/harness/__snapshots__/function-value-arguments-length.ts.txt new file mode 100644 index 000000000..adcb48d2a --- /dev/null +++ b/tests/harness/__snapshots__/function-value-arguments-length.ts.txt @@ -0,0 +1,43 @@ +function-value-arguments-length.ts:7:17 - error SC1090: 'arguments.length' in function expressions passed where the expected function type has optional or rest parameters (the count varies per call) are not supported yet + + 6 | const replaced = "a-b".replace(/-/, function (match) { + 7 | return String(arguments.length) + match; + | ^~~~~~~~~~~~~~~~ + 8 | }); + +function-value-arguments-length.ts:13:10 - error SC1090: 'arguments.length' in function expressions passed where the expected function type has optional or rest parameters (the count varies per call) are not supported yet + + 12 | const counted = call(function (a) { + 13 | return arguments.length + a; + | ^~~~~~~~~~~~~~~~ + 14 | }); + +function-value-arguments-length.ts:18:22 - error SC1090: functions that read 'arguments.length' used as values where the expected function type has optional or rest parameters (the count varies per call) are not supported yet + + 17 | } + 18 | const offsets = call(offset); + | ^~~~~~ + 19 | class Scale { + +function-value-arguments-length.ts:21:20 - error SC1090: 'arguments.length' in methods, nested, async and generator functions used as values (a caller of the value can pass more arguments than the function declares) are not supported yet + + 20 | static by(value: number): number { + 21 | return value * arguments.length; + | ^~~~~~~~~~~~~~~~ + 22 | } + +function-value-arguments-length.ts:32:30 - error SC1090: 'arguments.length' in an arrow function inside a function that is also used as a value is not supported yet + + 31 | function deferred(value: number): number { + 32 | const read = (): number => arguments.length; + | ^~~~~~~~~~~~~~~~ + 33 | return value + read(); + +function-value-arguments-length.ts:33:18 - error SC2004: uses of 'read' inherit the blocker on its declaration + + 32 | const read = (): number => arguments.length; + 33 | return value + read(); + | ^~~~ + 34 | } + + hint: the declaration of 'read' 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/7381-function-value-arguments-length.ts.json b/tests/harness/corpus-costs/7381-function-value-arguments-length.ts.json new file mode 100644 index 000000000..b4659b9a0 --- /dev/null +++ b/tests/harness/corpus-costs/7381-function-value-arguments-length.ts.json @@ -0,0 +1 @@ +{ "plain": 3400, "san": 3300 } From 7cbb6d3b371003b188201fdff61a6145dad7ed5a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 18:28:44 -0500 Subject: [PATCH 005/273] Count only stores and flows that can reach a typed array's view --- .../compiler/src/frontend/lowering/lowerer.ts | 41 +++++++++++++++---- .../test/runtime-optional-precision.test.ts | 18 ++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index e737eca64..ccb91291c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -6312,6 +6312,27 @@ export class Lowerer { // Element ABIs of the native arrays a value converted to a dynamic type // can reach: the conversion keeps each such array as a live view, so // stores through the dynamic value write the native storage. + // A standard library function only reads an array argument it takes as + // `any` (console.log, JSON.stringify, String), except the reflective + // writes: Object.assign and defineProperty, Reflect.set and the like, + // and Array.prototype methods through call or apply. + const reflectiveWriters = new Set([ + "assign", + "defineProperty", + "defineProperties", + "setPrototypeOf", + "set", + "apply", + "call", + ]); + const standardLibraryReader = (node: ts.CallExpression | ts.NewExpression): boolean => + panicSafe(() => { + const callee = peel(node.expression); + if (ts.isPropertyAccessExpression(callee) && reflectiveWriters.has(callee.name.text)) + return false; + const declaration = this.checker.getResolvedSignature(node)?.declaration; + return declaration !== undefined && this.isStdlibFile(ts.getSourceFileOfNode(declaration)); + }, false); const collectNativeViews = (): Set => { const keys = new Set(); const seen = new Set(); @@ -6376,8 +6397,9 @@ export class Lowerer { else if (ts.isReturnStatement(node) && node.expression) flow(node.expression, contextualOrNull(node.expression)); else if (ts.isCallExpression(node) || ts.isNewExpression(node)) { - for (const arg of node.arguments?.nodes ?? []) - flow(ts.isSpreadElement(arg) ? arg.expression : arg, contextualOrNull(arg)); + if (!standardLibraryReader(node)) + for (const arg of node.arguments?.nodes ?? []) + flow(ts.isSpreadElement(arg) ? arg.expression : arg, contextualOrNull(arg)); } else if (ts.isPropertyAssignment(node)) flow(node.initializer, contextualOrNull(node.initializer)); else if (ts.isShorthandPropertyAssignment(node)) { @@ -6398,14 +6420,17 @@ export class Lowerer { }); return keys; }; + // A receiver typed `any`, `unknown` or an array of either can be a live + // view of a native array. A receiver of another type (`Worker[]`, + // `Record`) is one only after a lying assertion, even + // when its values are dynamic. const dynamicType = (type: ts.Type): boolean => { if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return true; - if (this.checker.isArrayType(type) && !this.checker.isTupleType(type)) { - const element = this.checker.getTypeArguments(type as ts.TypeReference)[0]; - if (element && (element.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) - return true; - } - return panicSafe(() => this.mapTypeOf(type)?.kind === "dyn", false); + if (!this.checker.isArrayType(type) || this.checker.isTupleType(type)) return false; + const element = this.checker.getTypeArguments(type as ts.TypeReference)[0]; + return ( + element !== undefined && (element.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 + ); }; const elementStates = new ArrayElementStates({ isDynamicArray: (node) => { diff --git a/packages/compiler/test/runtime-optional-precision.test.ts b/packages/compiler/test/runtime-optional-precision.test.ts index 3a3251b5a..64f0bd86e 100644 --- a/packages/compiler/test/runtime-optional-precision.test.ts +++ b/packages/compiler/test/runtime-optional-precision.test.ts @@ -125,6 +125,24 @@ const view: unknown[] = items; console.log(view.length, items.findIndex((item) => weigh(item) > 1)); `); expect(signature(read, "weigh")).toBe("weigh(Item): f64"); + // Standard library functions that take arrays as `any` only read them. + const readers = await lower(`${ITEM} +const items: Item[] = [new Item(1)]; +console.log(items.length, JSON.stringify(items)); +const values: unknown[] = [1]; +values[3] = 2; +console.log(items.findIndex((item) => weigh(item) > 1)); +`); + expect(signature(readers, "weigh")).toBe("weigh(Item): f64"); + // A store through a Record type is not a store through a view. + const record = await lower(`${ITEM} +const items: Item[] = [new Item(1)]; +const view: unknown = items; +const bag: Record = {}; +bag["3"] = view; +console.log(items.findIndex((item) => weigh(item) > 1)); +`); + expect(signature(record, "weigh")).toBe("weigh(Item): f64"); }); test("holes that only their owning slot can observe stay out of callbacks", async () => { From a3b9cfb1466649db971d5562405a8a01f028c048 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 18:51:14 -0500 Subject: [PATCH 006/273] Find checker reply element ends in C and list dynamic imports from module references --- packages/compiler/native/ts7-json.test.c | 33 +++++ packages/compiler/native/ts7-process.c | 42 +++++++ packages/compiler/native/ts7-process.ffi.json | 3 +- packages/compiler/native/ts7-process.h | 4 + .../src/frontend/lowering/lower-modules.ts | 30 +++-- .../src/frontend/ts7/native-process.ts | 6 + .../src/frontend/ts7/semantic-json.ts | 26 +++- .../test/native-json-value-end.test.ts | 118 ++++++++++++++++++ 8 files changed, 243 insertions(+), 19 deletions(-) create mode 100644 packages/compiler/native/ts7-json.test.c create mode 100644 packages/compiler/test/native-json-value-end.test.ts diff --git a/packages/compiler/native/ts7-json.test.c b/packages/compiler/native/ts7-json.test.c new file mode 100644 index 000000000..b696d77c2 --- /dev/null +++ b/packages/compiler/native/ts7-json.test.c @@ -0,0 +1,33 @@ +/* Reads cases from standard input, each a little-endian u32 length, a u32 + * start and the bytes, and prints scriptc_ts7_json_value_end for each. */ +#include "ts7-process.h" +#include +#include +#ifdef _WIN32 +#include +#include +#endif + +static int read_u32(uint32_t *value) { + unsigned char bytes[4]; + if (fread(bytes, 1, 4, stdin) != 4) return 0; + *value = (uint32_t)bytes[0] | (uint32_t)bytes[1] << 8 | (uint32_t)bytes[2] << 16 | + (uint32_t)bytes[3] << 24; + return 1; +} + +int main(void) { +#ifdef _WIN32 + /* The cases are bytes: text mode would rewrite line endings. */ + _setmode(_fileno(stdin), _O_BINARY); +#endif + uint32_t length, start; + while (read_u32(&length)) { + if (!read_u32(&start)) return 1; + uint8_t *bytes = malloc(length == 0 ? 1 : length); + if (bytes == NULL || fread(bytes, 1, length, stdin) != length) return 1; + printf("%d\n", (int)scriptc_ts7_json_value_end(bytes, length, start)); + free(bytes); + } + return 0; +} diff --git a/packages/compiler/native/ts7-process.c b/packages/compiler/native/ts7-process.c index ea451345d..3333cf4e8 100644 --- a/packages/compiler/native/ts7-process.c +++ b/packages/compiler/native/ts7-process.c @@ -405,3 +405,45 @@ void scriptc_ts7_close(uint32_t handle) { void scriptc_ts7_close_all(void) { while (processes != NULL) scriptc_ts7_close(processes->id); } + +static int json_space(uint8_t byte) { + return byte == ' ' || byte == '\n' || byte == '\r' || byte == '\t'; +} + +int32_t scriptc_ts7_json_value_end(const uint8_t *bytes, size_t size, uint32_t start) { + if (size > INT32_MAX) return -1; + size_t depth = 0; + size_t index = start; + while (index < size) { + uint8_t byte = bytes[index]; + if (byte == '"') { + index++; + for (;;) { + if (index >= size) return -1; + const uint8_t *quote = memchr(bytes + index, '"', size - index); + if (quote == NULL) return -1; + const uint8_t *escape = memchr(bytes + index, '\\', (size_t)(quote - (bytes + index))); + if (escape == NULL) { + index = (size_t)(quote - bytes); + break; + } + index = (size_t)(escape - bytes) + 2; + } + index++; + if (depth == 0) return (int32_t)index; + continue; + } + if (byte == '{' || byte == '[') { + depth++; + } else if (byte == '}' || byte == ']') { + if (depth == 0) return (int32_t)index; + depth--; + if (depth == 0) return (int32_t)(index + 1); + } else if (depth == 0 && (byte == ',' || json_space(byte))) { + return (int32_t)index; + } + index++; + } + if (depth != 0) return -1; + return (int32_t)index; +} diff --git a/packages/compiler/native/ts7-process.ffi.json b/packages/compiler/native/ts7-process.ffi.json index 86f3cb4d2..9a9e39526 100644 --- a/packages/compiler/native/ts7-process.ffi.json +++ b/packages/compiler/native/ts7-process.ffi.json @@ -7,6 +7,7 @@ { "name": "ts7NativePid", "symbol": "scriptc_ts7_pid", "params": ["u32"], "returns": "u32" }, { "name": "ts7NativeClose", "symbol": "scriptc_ts7_close", "params": ["u32"], "returns": "void" }, { "name": "ts7NativeCloseAll", "symbol": "scriptc_ts7_close_all", "params": [], "returns": "void" }, - { "name": "ts7NativeError", "symbol": "scriptc_ts7_error", "params": ["mutable-bytes"], "returns": "i32" } + { "name": "ts7NativeError", "symbol": "scriptc_ts7_error", "params": ["mutable-bytes"], "returns": "i32" }, + { "name": "ts7NativeJsonValueEnd", "symbol": "scriptc_ts7_json_value_end", "params": ["bytes", "u32"], "returns": "i32" } ] } diff --git a/packages/compiler/native/ts7-process.h b/packages/compiler/native/ts7-process.h index 3d2a0ba05..5bc0d11c4 100644 --- a/packages/compiler/native/ts7-process.h +++ b/packages/compiler/native/ts7-process.h @@ -21,5 +21,9 @@ uint32_t scriptc_ts7_pid(uint32_t handle); void scriptc_ts7_close(uint32_t handle); void scriptc_ts7_close_all(void); int32_t scriptc_ts7_error(uint8_t *bytes, size_t size); +/* The index after the JSON value that starts at `start` in a checker reply, + * as jsonValueEnd in semantic-json.ts computes it, or -1 when the reply is + * malformed there. */ +int32_t scriptc_ts7_json_value_end(const uint8_t *bytes, size_t size, uint32_t start); #endif diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 5238333ad..1301a152f 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -346,24 +346,28 @@ function dynamicImportProgramTargetOf( } /** Every distinct program module a file's `import("literal")` sites name - * (source order, deduped). */ + * (source order, deduped). The parser lists the specifier of every such call + * in the file's module references, in source order after the static imports, + * so the file's nodes need no walk. */ function dynamicProgramImportsOf(program: ts.Program, sf: ts.SourceFile): ts.SourceFile[] { const out: ts.SourceFile[] = []; const seen = new Set(); - ts.walkPreorder(sf, (node) => { + for (const specifier of sf.imports) { + const call = specifier.parent; if ( - ts.isCallExpression(node) && - node.expression.kind === ts.SyntaxKind.ImportKeyword && - node.arguments.nodes[0] !== undefined && - ts.isStringLiteralLike(node.arguments.nodes[0]) - ) { - const dep = dynamicImportProgramTargetOf(program, sf, node.arguments.nodes[0].text); - if (dep && !seen.has(dep)) { - seen.add(dep); - out.push(dep); - } + call === undefined || + !ts.isCallExpression(call) || + call.expression.kind !== ts.SyntaxKind.ImportKeyword || + call.arguments.nodes[0] !== specifier || + !ts.isStringLiteralLike(specifier) + ) + continue; + const dep = dynamicImportProgramTargetOf(program, sf, specifier.text); + if (dep && !seen.has(dep)) { + seen.add(dep); + out.push(dep); } - }); + } return out; } diff --git a/packages/compiler/src/frontend/ts7/native-process.ts b/packages/compiler/src/frontend/ts7/native-process.ts index 7bb523d74..b36189e64 100644 --- a/packages/compiler/src/frontend/ts7/native-process.ts +++ b/packages/compiler/src/frontend/ts7/native-process.ts @@ -1,5 +1,6 @@ import { Ts7Wire, type Ts7WireIo } from "./rpc-wire.js"; import { TS7_FILE_SYSTEM_CALLBACKS } from "./rpc-filesystem.js"; +import { useNativeJsonValueEnd } from "./semantic-json.js"; // Bound by native/ts7-process.ffi.json. This module is a native entry point; // the Node host continues to use rpc-process.ts's public spawn boundary. @@ -25,6 +26,11 @@ declare function ts7NativePid(handle: number): number; declare function ts7NativeClose(handle: number): void; declare function ts7NativeCloseAll(): void; declare function ts7NativeError(bytes: Uint8Array): number; +declare function ts7NativeJsonValueEnd(bytes: Uint8Array, start: number): number; + +// Reply decoding finds element ends with the C scanner. FFI functions can +// only be called, not passed as values, so an arrow function calls it. +useNativeJsonValueEnd((bytes, start) => ts7NativeJsonValueEnd(bytes, start)); // process.exit skips C atexit handlers. Its ordinary exit listener closes // servers too, while the C atexit sweep covers normal native termination. diff --git a/packages/compiler/src/frontend/ts7/semantic-json.ts b/packages/compiler/src/frontend/ts7/semantic-json.ts index 3a04c741f..1a4d93bd6 100644 --- a/packages/compiler/src/frontend/ts7/semantic-json.ts +++ b/packages/compiler/src/frontend/ts7/semantic-json.ts @@ -76,8 +76,8 @@ function malformedReply(): never { /** 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 { + * so brackets inside them do not count. Throws when the value is unclosed. */ +export function jsonValueEnd(bytes: Uint8Array, start: number): number { let depth = 0; let index = start; while (index < bytes.length) { @@ -103,6 +103,22 @@ function jsonValueEnd(bytes: Uint8Array, start: number): number { return index; } +/** The scanner the decoders find value ends with: jsonValueEnd, or the + * native compiler's scanner (useNativeJsonValueEnd). */ +let valueEnd: (bytes: Uint8Array, start: number) => number = jsonValueEnd; + +/** Has the decoders find value ends with `scan`, which returns what + * jsonValueEnd returns, or -1 where jsonValueEnd throws. The native compiler + * installs its C scanner (native/ts7-process.c), which skips the strings that + * make up most reply bytes with memchr. */ +export function useNativeJsonValueEnd(scan: (bytes: Uint8Array, start: number) => number): void { + valueEnd = (bytes, start) => { + const end = scan(bytes, start); + if (end < 0) malformedReply(); + return end; + }; +} + /** The id an object reply starts with (`{"id":N`), or -1 when it starts * otherwise. */ function leadingId(bytes: Uint8Array, start: number, end: number): number { @@ -158,7 +174,7 @@ export function decodeSemanticObject( ): R | null { const start = skipJsonSpace(bytes, 0); if (start === bytes.length) return null; - const end = jsonValueEnd(bytes, start); + const end = valueEnd(bytes, start); if (skipJsonSpace(bytes, end) !== bytes.length) malformedReply(); return replyElement(bytes, start, end, cached, build); } @@ -174,7 +190,7 @@ export function decodeSemanticObjects( let index = skipJsonSpace(bytes, 0); if (index === bytes.length) return null; if (bytes[index] !== OPEN_BRACKET) { - const end = jsonValueEnd(bytes, index); + const end = valueEnd(bytes, index); if ( skipJsonSpace(bytes, end) !== bytes.length || parseJsonSlice(bytes, index, end) !== null @@ -189,7 +205,7 @@ export function decodeSemanticObjects( return result; } for (;;) { - const end = jsonValueEnd(bytes, index); + const end = valueEnd(bytes, index); if (end === index) malformedReply(); result.push(replyElement(bytes, index, end, cached, build)); index = skipJsonSpace(bytes, end); diff --git a/packages/compiler/test/native-json-value-end.test.ts b/packages/compiler/test/native-json-value-end.test.ts new file mode 100644 index 000000000..9d9cc3950 --- /dev/null +++ b/packages/compiler/test/native-json-value-end.test.ts @@ -0,0 +1,118 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { jsonValueEnd } from "../src/frontend/ts7/semantic-json.js"; + +const sources = join(import.meta.dirname, "../native"); +const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; + +/** jsonValueEnd's result, or -1 where it throws. */ +function expectedEnd(bytes: Uint8Array, start: number): number { + try { + return jsonValueEnd(bytes, start); + } catch { + return -1; + } +} + +/** A deterministic generator, so a failing case can be reproduced. */ +function random(seed: number): () => number { + let state = seed; + return () => { + state = (state * 1103515245 + 12345) % 2147483648; + return state / 2147483648; + }; +} + +const ALPHABET = ["{", "}", "[", "]", '"', "\\", ",", " ", "\n", "\t", "a", "1", "n", ":", "é"]; + +function cases(): { bytes: Uint8Array; start: number }[] { + const next = random(1); + const out: { bytes: Uint8Array; start: number }[] = []; + const texts = [ + "", + '""', + '"unclosed', + '"\\"', + '"\\\\"', + '"a\\"b"', + '"\\\\\\""', + "null", + "null,", + "123 ", + '{"id":1}', + '{"id":1,"name":"a}b"}', + '[{"id":1},null,{"id":2,"declarations":["1.2./a.ts","3.4./b \\"c\\".ts"]}]', + '{"id":1,"nested":{"list":[1,{"x":"]"}]}}, next', + "{", + "[[]", + "}", + "]", + ",", + " ", + ]; + for (const text of texts) { + const bytes = Buffer.from(text, "utf8"); + for (let start = 0; start <= bytes.length; start++) out.push({ bytes, start }); + } + for (let n = 0; n < 4000; n++) { + let text = ""; + const length = Math.floor(next() * 40); + for (let k = 0; k < length; k++) text += ALPHABET[Math.floor(next() * ALPHABET.length)]; + const bytes = Buffer.from(text, "utf8"); + out.push({ bytes, start: Math.floor(next() * (bytes.length + 1)) }); + } + const reply = JSON.stringify( + Array.from({ length: 200 }, (_, id) => + id % 7 === 0 + ? null + : { id, name: `name ${id} "q" \\ {[`, declarations: [`${id}.80./src/a ${id}.ts`] }, + ), + ); + const bytes = Buffer.from(reply, "utf8"); + for (let start = 0; start < bytes.length; start += 3) out.push({ bytes, start }); + return out; +} + +test("the native scanner finds the value ends jsonValueEnd finds", () => { + const dir = mkdtempSync(join(tempRoot, "scriptc-ts7-json-")); + try { + const harness = join(dir, "scan" + (process.platform === "win32" ? ".exe" : "")); + execFileSync("clang", [ + "-std=c11", + "-Wall", + "-Wextra", + "-Werror", + ...(process.env["SCRIPTC_SAN"] === "1" ? ["-fsanitize=address,undefined"] : []), + join(sources, "ts7-json.test.c"), + join(sources, "ts7-process.c"), + "-o", + harness, + ]); + const all = cases(); + const chunks: Buffer[] = []; + for (const { bytes, start } of all) { + const header = Buffer.alloc(8); + header.writeUInt32LE(bytes.length, 0); + header.writeUInt32LE(start, 4); + chunks.push(header, Buffer.from(bytes)); + } + const output = execFileSync(harness, { input: Buffer.concat(chunks), encoding: "utf8" }); + const actual = output.trim().split(/\r?\n/).map(Number); + expect(actual.length).toBe(all.length); + for (let k = 0; k < all.length; k++) { + const { bytes, start } = all[k]!; + const expected = expectedEnd(bytes, start); + if (actual[k] !== expected) + expect({ text: Buffer.from(bytes).toString("utf8"), start, end: actual[k] }).toEqual({ + text: Buffer.from(bytes).toString("utf8"), + start, + end: expected, + }); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); From 0d016b7554ebdeb9c537ee768a5a89c5fd482704 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 19:57:46 -0500 Subject: [PATCH 007/273] util: runtime format strings, inspect.custom and colors, untyped promisify --- .../limitations/builtin-function-values.mdx | 2 +- .../console-and-process-streams.mdx | 7 + .../src/generated/node-v24-compatibility.json | 12 +- .../generated/node-v24-internal.json | 35 +- internal/compatibility/static-support.json | 8 +- .../ambient/scriptc-node-fallback.d.ts | 57 +- .../compiler/src/backend/llvm/lib-shared.ts | 16 + .../lowering/builtins/promisify-callback.ts | 26 +- .../src/frontend/lowering/call-arguments.ts | 159 ++- .../lowering/expressions/object-literals.ts | 20 +- .../lowering/expressions/property-access.ts | 11 + .../src/frontend/lowering/lower-calls.ts | 30 + .../src/frontend/lowering/lower-classes.ts | 13 + .../src/frontend/lowering/lower-console.ts | 38 +- .../src/frontend/lowering/lower-inspect.ts | 779 +++++++++++- .../src/frontend/lowering/lower-modules.ts | 13 +- .../compiler/src/frontend/lowering/lowerer.ts | 4 + .../src/frontend/lowering/surfaces.ts | 3 +- .../src/frontend/lowering/symbol-fields.ts | 29 + packages/compiler/src/frontend/type-mapper.ts | 24 + packages/compiler/src/ir/builtin-effects.ts | 11 + .../compiler/src/ir/builtin-signatures.ts | 37 +- .../corpus/7520-util-format-runtime.ts.json | 6 + .../corpus/7521-util-format-runtime.cjs.json | 6 + .../corpus/7522-promisify-untyped.cjs.json | 6 + .../corpus/7523-promisify-untyped.ts.json | 6 + .../tests/corpus/7524-inspect-custom.ts.json | 6 + .../tests/corpus/7525-inspect-custom.cjs.json | 6 + .../7526-inspect-options-runtime.ts.json | 6 + .../diagnostics/inspect-custom-fences.ts.json | 6 + packages/runtime/src/scr_console_native.c | 6 +- packages/runtime/src/scr_dyn.h | 4 + packages/runtime/src/scr_inspect.c | 1123 ++++++++++++++++- packages/runtime/src/scr_inspect.h | 71 +- packages/runtime/src/scr_json.c | 18 + packages/runtime/src/scr_symbol.c | 5 + packages/runtime/src/scr_util_callable.c | 166 +-- tests/corpus/7520-util-format-runtime.ts | 110 ++ tests/corpus/7521-util-format-runtime.cjs | 64 + tests/corpus/7522-promisify-untyped.cjs | 64 + tests/corpus/7523-promisify-untyped.ts | 38 + tests/corpus/7524-inspect-custom.ts | 91 ++ tests/corpus/7525-inspect-custom.cjs | 75 ++ tests/corpus/7526-inspect-options-runtime.ts | 62 + tests/diagnostics/inspect-custom-fences.ts | 44 + .../inspect-custom-fences.ts.txt | 49 + .../7520-util-format-runtime.ts.json | 1 + .../7521-util-format-runtime.cjs.json | 1 + .../7522-promisify-untyped.cjs.json | 1 + .../7523-promisify-untyped.ts.json | 1 + .../corpus-costs/7524-inspect-custom.ts.json | 1 + .../corpus-costs/7525-inspect-custom.cjs.json | 1 + .../7526-inspect-options-runtime.ts.json | 1 + tests/harness/errors.test.ts | 47 + 54 files changed, 3101 insertions(+), 325 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7520-util-format-runtime.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7521-util-format-runtime.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7522-promisify-untyped.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7523-promisify-untyped.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7524-inspect-custom.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7525-inspect-custom.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7526-inspect-options-runtime.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inspect-custom-fences.ts.json create mode 100644 tests/corpus/7520-util-format-runtime.ts create mode 100644 tests/corpus/7521-util-format-runtime.cjs create mode 100644 tests/corpus/7522-promisify-untyped.cjs create mode 100644 tests/corpus/7523-promisify-untyped.ts create mode 100644 tests/corpus/7524-inspect-custom.ts create mode 100644 tests/corpus/7525-inspect-custom.cjs create mode 100644 tests/corpus/7526-inspect-options-runtime.ts create mode 100644 tests/diagnostics/inspect-custom-fences.ts create mode 100644 tests/harness/__snapshots__/inspect-custom-fences.ts.txt create mode 100644 tests/harness/corpus-costs/7520-util-format-runtime.ts.json create mode 100644 tests/harness/corpus-costs/7521-util-format-runtime.cjs.json create mode 100644 tests/harness/corpus-costs/7522-promisify-untyped.cjs.json create mode 100644 tests/harness/corpus-costs/7523-promisify-untyped.ts.json create mode 100644 tests/harness/corpus-costs/7524-inspect-custom.ts.json create mode 100644 tests/harness/corpus-costs/7525-inspect-custom.cjs.json create mode 100644 tests/harness/corpus-costs/7526-inspect-options-runtime.ts.json diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index 0fa62e558..efd45af2f 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -14,7 +14,7 @@ A class can extend `require("events")` directly. Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere. `util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call. -`util.promisify` over an untyped (`any`) function returns a native wrapper that follows Node's callback rules and carries the original's `name` and `length`. +`util.promisify` over an untyped function (a value typed `any`, a function whose parameters are untyped, or any other target in JavaScript) returns a native wrapper that follows Node's callback rules and carries the original's `name` and `length`, and throws Node's `ERR_INVALID_ARG_TYPE` for values that are not functions. Such a function with other own properties, `util.promisify.custom` among them, throws `SC2020` where `util.promisify` runs, and the wrapper does not report Node's DEP0174 warning when the original returns a promise. `util.styleText` supports standard formats and aliases, `#RGB` and `#RRGGBB` hex colors, arrays of formats, nested ANSI resets, and explicit color enabling with `{ validateStream: false }`. diff --git a/docs/content/limitations/console-and-process-streams.mdx b/docs/content/limitations/console-and-process-streams.mdx index c62b82f0c..946cb8061 100644 --- a/docs/content/limitations/console-and-process-streams.mdx +++ b/docs/content/limitations/console-and-process-streams.mdx @@ -1,6 +1,13 @@ `console.log`, `info`, and `debug` write to stdout; `error` and `warn` write to stderr. They accept `unknown`, printing strings verbatim and other values through native `util.inspect`. Direct calls support global `console` and `node:console` imports. +Console output, `util.format`, and `util.formatWithOptions` with a format string known only at run time, spread arguments, or an options object format their arguments at run time. +There, `%s` of a function or of a class instance whose class defines `toString` and `%o` of an object throw `SC2020`. +`util.inspect` with `colors`, with option values known only at run time, or in the `inspect(value, showHidden, depth, colors)` form renders the value's untyped view in the runtime with Node's styles. +Colored output of errors, promises, regular expressions, `DataView`, `ArrayBuffer`, weak collections, iterators, and other native objects throws `SC2020`. +Inspect options other than `depth`, `colors`, `customInspect`, `compact` (a number or `false`), `breakLength`, and `sorted` (a boolean) throw `SC2020` when they are set to a value other than Node's default. +`util.inspect` calls `[util.inspect.custom]` methods of classes and objects with Node's depth, options, and `util.inspect` arguments, and `console.dir` and `customInspect: false` skip them. +A class instance with such a method that is held as an untyped value throws `SC2020` when it is inspected, and a class whose subclasses declare the method without declaring it itself is refused. Stored global console values and detached output methods retain identity and accept ordinary arguments. Stored methods do not support format specifiers with substitution arguments. Other stored console APIs, custom `Console` instances, and console mutation are unsupported. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index c341da852..b94ac47be 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -73027,9 +73027,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" } }, { @@ -73050,9 +73050,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 9393fcaa8..f95cf3aa7 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -101265,6 +101265,13 @@ "tests/corpus/5835-util-format-integer-specifiers.ts", "tests/corpus/5884-inspect-layout-options.ts", "tests/corpus/6428-promisify-callback-functions.ts", + "tests/corpus/7520-util-format-runtime.ts", + "tests/corpus/7521-util-format-runtime.cjs", + "tests/corpus/7522-promisify-untyped.cjs", + "tests/corpus/7523-promisify-untyped.ts", + "tests/corpus/7524-inspect-custom.ts", + "tests/corpus/7525-inspect-custom.cjs", + "tests/corpus/7526-inspect-options-runtime.ts", "tests/corpus/node24-deep-equality-properties.js", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality.ts", @@ -102369,7 +102376,9 @@ "tests": [ "tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", - "tests/corpus/5835-util-format-integer-specifiers.ts" + "tests/corpus/5835-util-format-integer-specifiers.ts", + "tests/corpus/7520-util-format-runtime.ts", + "tests/corpus/7521-util-format-runtime.cjs" ] }, "anchorSource": "exact" @@ -102397,7 +102406,9 @@ "tests": [ "tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", - "tests/corpus/5835-util-format-integer-specifiers.ts" + "tests/corpus/5835-util-format-integer-specifiers.ts", + "tests/corpus/7520-util-format-runtime.ts", + "tests/corpus/7521-util-format-runtime.cjs" ] }, "anchorSource": "exact" @@ -102588,7 +102599,8 @@ "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", - "tests/corpus/5884-inspect-layout-options.ts" + "tests/corpus/5884-inspect-layout-options.ts", + "tests/corpus/7526-inspect-options-runtime.ts" ] }, "anchorSource": "exact" @@ -102611,8 +102623,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.inspect", + "tests": [ + "tests/corpus/7526-inspect-options-runtime.ts" + ] }, "anchorSource": "exact" }, @@ -102634,8 +102649,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.custom", + "tests": [ + "tests/corpus/7524-inspect-custom.ts", + "tests/corpus/7525-inspect-custom.cjs" + ] }, "anchorSource": "ancestor" }, @@ -102952,6 +102971,8 @@ "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", + "tests/corpus/7522-promisify-untyped.cjs", + "tests/corpus/7523-promisify-untyped.ts", "tests/harness/project-config.test.ts" ] }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 06aad26cf..b5211c967 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -244,9 +244,11 @@ {"chapter": "worker_threads", "symbols": ["resourceLimits"], "status": "partial", "evidence": ["tests/corpus/5806-worker-resource-limits.ts"]}, {"chapter": "worker_threads", "symbols": ["BroadcastChannel extends EventTarget", "broadcastChannel.close", "broadcastChannel.postMessage", "broadcastChannel.ref", "broadcastChannel.unref", "onmessage"], "status": "partial", "evidence": ["tests/corpus/5800-broadcast-channel.ts", "tests/corpus/5801-broadcast-channel.cjs", "tests/corpus/5802-broadcast-channel-workers.ts"]}, {"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject"], "status": "partial", "evidence": ["tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]}, - {"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.ts"]}, + {"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.ts", "tests/corpus/7520-util-format-runtime.ts", "tests/corpus/7521-util-format-runtime.cjs"]}, {"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject", "util.types.isDate", "util.types.isSharedArrayBuffer", "util.types.isMapIterator", "util.types.isSetIterator", "util.types.isKeyObject", "util.types.isNumberObject", "util.types.isStringObject", "util.types.isBooleanObject", "util.types.isBigIntObject", "util.types.isSymbolObject", "util.types.isBoxedPrimitive", "util.types.isBigInt64Array", "util.types.isBigUint64Array", "util.types.isFloat16Array", "util.types.isCryptoKey", "util.types.isExternal"], "status": "partial", "evidence": ["tests/corpus/5831-util-types-brands.ts", "tests/corpus/5832-util-types-brands-dynamic.js", "tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]}, - { "chapter": "util", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.ts"] }, + { "chapter": "util", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.ts", "tests/corpus/7526-inspect-options-runtime.ts"] }, + { "chapter": "util", "signatures": ["`util.inspect(object[, showHidden[, depth[, colors]]])`"], "status": "partial", "evidence": ["tests/corpus/7526-inspect-options-runtime.ts"] }, + { "chapter": "util", "signatures": ["`custom` Type: {symbol} that can be used to declare custom inspect functions."], "status": "partial", "evidence": ["tests/corpus/7524-inspect-custom.ts", "tests/corpus/7525-inspect-custom.cjs"] }, { "chapter": "util", "symbols": ["util.styleText"], "status": "partial", "evidence": ["tests/corpus/node24-style-text.ts", "tests/corpus/node24-style-text-validation.js", "tests/corpus/node24-style-text-environment.js", "tests/corpus/node24-style-text.cjs", "tests/fixtures/node-types/style-text.ts"] }, { "chapter": "util", "symbols": ["util.isDeepStrictEqual"], "status": "partial", "evidence": ["tests/corpus/node24-deep-equality.ts", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality-properties.js", "tests/fixtures/node-types/deep-equality.ts", "packages/compiler/test/deep-equality.test.ts"] }, { "chapter": "deprecations", "symbols": ["util.toUSVString"], "status": "not-applicable" }, @@ -400,7 +402,7 @@ { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "symbols": ["getTestContext"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/get-test-context.ts", "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] }, - { "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts"] }, + { "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/corpus/7522-promisify-untyped.cjs", "tests/corpus/7523-promisify-untyped.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] }, { "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] }, { "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index b195c3fbb..80054b4fa 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3838,7 +3838,13 @@ declare module "util" { export type TextDecoder = globalThis.TextDecoder; export var TextEncoder: typeof globalThis.TextEncoder; export var TextDecoder: typeof globalThis.TextDecoder; - /* stream.pipeline's overload comes first: its __promisify__ member keeps + /* An untyped function: an `any` value, or a function whose parameters + * are untyped (JavaScript's `(value, callback) => ...`), the only + * parameters that accept this object type. The promisified function is + * untyped too. It comes first because such functions match every other + * overload. */ + export function promisify(fn: (...args: { readonly "scriptc:untyped": true }[]) => unknown): any; + /* stream.pipeline's overload comes next: its __promisify__ member keeps * every other target out of it. */ export function promisify( fn: typeof import("node:stream").pipeline, @@ -3959,15 +3965,56 @@ declare module "util" { ): (address: string, port: number) => Promise<{ hostname: string; service: string }>; /* Declared surface without full lowering (the type-level tolerance * story: guarded/diagnostic-path code must TYPECHECK; a reached use - * without a lowering fences at its site). inspect's options carry the - * documented knobs through an index signature. */ + * without a lowering fences at its site). */ + export interface InspectOptions { + showHidden?: boolean | undefined; + depth?: number | null | undefined; + colors?: boolean | undefined; + customInspect?: boolean | undefined; + showProxy?: boolean | undefined; + maxArrayLength?: number | null | undefined; + maxStringLength?: number | null | undefined; + breakLength?: number | undefined; + compact?: boolean | number | undefined; + sorted?: boolean | ((a: string, b: string) => number) | undefined; + getters?: "get" | "set" | boolean | undefined; + numericSeparator?: boolean | undefined; + } + export type Style = + | "special" + | "number" + | "bigint" + | "boolean" + | "undefined" + | "null" + | "string" + | "symbol" + | "date" + | "regexp" + | "module"; + export interface InspectOptionsStylized extends InspectOptions { + stylize(text: string, styleType: Style): string; + } + export type CustomInspectFunction = ( + depth: number, + options: InspectOptionsStylized, + inspect: typeof import("node:util").inspect, + ) => any; export function inspect( object: unknown, - options?: { depth?: number | null; colors?: boolean; [k: string]: unknown }, + showHidden?: boolean, + depth?: number | null, + color?: boolean, ): string; + export function inspect(object: unknown, options?: InspectOptions): string; + export namespace inspect { + /* The key of an object's custom inspect function: + * Symbol.for('nodejs.util.inspect.custom'). */ + const custom: unique symbol; + } export function format(format?: unknown, ...args: unknown[]): string; export function formatWithOptions( - inspectOptions: { depth?: number | null; colors?: boolean; [k: string]: unknown }, + inspectOptions: InspectOptions, format?: unknown, ...args: unknown[] ): string; diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 0b6041034..b02cf44f3 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1034,6 +1034,22 @@ export const LIB_FN_SYMS: Record = { "dyn.generatorThis": "scr_gen_receiver", "insp.dyn": "scr_insp_dyn", "insp.dynS": "scr_insp_dyn_s", + "insp.format": "scr_insp_format_pack", + "insp.formatPrimitives": "scr_insp_format_primitives", + "insp.formatWithOptions": "scr_insp_format_with_options", + "insp.inspect": "scr_insp_inspect", + "insp.customEnabled": "scr_insp_custom_enabled", + "insp.registerCustom": "scr_insp_register_custom", + "insp.customInspect": "scr_insp_custom_inspect", + "insp.depthKind": "scr_insp_depth_kind", + "insp.customDepth": "scr_insp_custom_depth", + "insp.customOptions": "scr_insp_custom_options", + "insp.inspectFunction": "scr_insp_inspect_function", + "insp.customText": "scr_insp_custom_text", + "insp.customIsSelf": "scr_insp_custom_is_self", + "insp.customResult": "scr_insp_custom_result", + "insp.formatSpecifies": "scr_insp_format_specifies", + "insp.formatSpecifiesDyn": "scr_insp_format_specifies_dyn", "fs.readFileSyncDyn": "scr_fs_read_file_sync_dyn", "fs.readFileEncoded": "scr_fs_read_file_encoded", "fs.readFdEncoded": "scr_fs_read_fd_encoded", diff --git a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts index b4dbbf513..d3378c8ec 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts @@ -1,7 +1,8 @@ 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"; +import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; +import { isJsSourceFile } from "../../program.js"; import { BOOL, DYN, @@ -92,12 +93,22 @@ function callbackShape( return { fnT, resultT, args, callbackT, callbackParamT: callbackParamT!, inner, error }; } -/** Whether `promisify(fn)`'s one argument is typed `any`: the function is - * only known at run time, so the promisified function is built there. */ +/** Whether `promisify(fn)`'s one argument is only known at run time, so + * the promisified function is built there: a value typed `any`, a function + * with untyped parameters (the declarations type that call `any`), or any + * argument in JavaScript, whose inferred types do not describe the + * function Node receives (it validates the argument and reads its own + * properties). */ export function untypedPromisifyArgument(lowerer: Lowerer, call: ts.CallExpression): boolean { - if (call.questionDotToken || call.arguments.nodes.length !== 1) return false; - const argNode = call.arguments.nodes[0]!; - return !ts.isSpreadElement(argNode) && (lowerer.typeOf(argNode).flags & ts.TypeFlags.Any) !== 0; + if (call.questionDotToken || call.arguments.nodes.length > 1) return false; + const argNode = call.arguments.nodes[0]; + if (argNode !== undefined && ts.isSpreadElement(argNode)) return false; + if (isJsSourceFile(ts.getSourceFileOfNode(call))) return true; + if (argNode === undefined) return false; + return ( + (lowerer.typeOf(argNode).flags & ts.TypeFlags.Any) !== 0 || + (lowerer.typeOf(call).flags & ts.TypeFlags.Any) !== 0 + ); } /** The callback shape of a `promisify(fn)` call, or null. */ @@ -144,10 +155,11 @@ export function lowerCallbackPromisify( if (shape === null) { // An untyped (`any`) function: Node's promisify at run time. if (!untypedPromisifyArgument(lowerer, call)) return null; + const argNode = call.arguments.nodes[0]; return { kind: "libCall", fn: "util.promisifyDyn", - args: [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)], + args: [argNode ? lowerer.lowerExprExpecting(argNode, DYN) : dynUndefinedExpr(loc)], type: DYN, loc, }; diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index 060ffe9d2..7dc302365 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -56,6 +56,95 @@ function emptyTupleSpread(lowerer: Lowerer, node: ts.Expression): boolean { return restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx)?.length === 0; } +/** A call's arguments as one checked-dynamic array, built in source order + * into a fresh hidden local: each argument converts to dyn, and a spread + * contributes the values its iteration yields (holes as undefined). + * `lowerSpread` lowers a spread's operand; the default lowers it here. */ +export function dynArgumentPack( + lowerer: Lowerer, + argNodes: readonly ts.Expression[], + loc: SrcLoc, + options: { + /** Pre-lowered values pushed before the arguments. */ + leading?: readonly IrExpr[] | undefined; + blame?: ts.Node | undefined; + lowerSpread?: (arg: ts.SpreadElement) => IrExpr; + } = {}, +): { stmts: IrStmt[]; pack: () => IrExpr } { + const blame = options.blame ?? argNodes[0]!; + const lowerSpread = + options.lowerSpread ?? ((arg: ts.SpreadElement) => lowerer.lowerExpr(arg.expression)); + const pack = lowerer.declareHiddenLocal("%spreadArgs", DYN); + const ref = (): IrExpr => ({ kind: "varRef", localId: pack.id, type: DYN, loc }); + const stmts: IrStmt[] = [ + { + kind: "varDecl", + localId: pack.id, + init: { kind: "dynArrLit", elems: [], type: DYN, loc }, + loc, + }, + ]; + const push = (value: IrExpr, fn: "dyn.packPush" | "dyn.packPushSpreadIter") => { + stmts.push({ + kind: "exprStmt", + expr: { kind: "libCall", fn, args: [ref(), value], type: VOID, loc: value.loc }, + loc: value.loc, + }); + }; + for (const value of options.leading ?? []) + push(lowerer.coerceInto(blame, value, DYN), "dyn.packPush"); + const spreads = argNodes.filter(ts.isSpreadElement).length; + argNodes.forEach((arg, index) => { + if (!ts.isSpreadElement(arg)) { + push(lowerer.lowerExprExpecting(arg, DYN), "dyn.packPush"); + return; + } + let value = lowerSpread(arg); + if (value.type.kind === "set") { + value = { + kind: "setIntrinsic", + method: "toArray", + receiver: value, + args: [], + type: arrayOf(value.type.elem), + loc: value.loc, + }; + } else if (value.type.kind === "object") { + value = lowerer.classIteratorDrainCall(value, locOf(arg)) ?? value; + } + if (value.type.kind === "array") { + // Argument iteration materializes holes as present undefined + // before conversion to the dense checked-dynamic argument pack. + value = { + kind: "arrayLit", + elems: [value], + spreads: [0], + type: value.type, + loc: value.loc, + }; + } + value = checkedIterableSpread(lowerer, value, ts.getTextOfNode(arg.expression), loc); + if (spreads === 1 && index === argNodes.length - 1) { + stmts.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.packPushSpread", + args: [ + ref(), + value, + { kind: "strLit", value: ts.getTextOfNode(arg.expression), type: STRING, loc }, + ], + type: VOID, + loc, + }, + loc, + }); + } else push(value, "dyn.packPushSpreadIter"); + }); + return { stmts, pack: ref }; +} + export function completeArgs( lowerer: Lowerer, argNodes: readonly ts.Expression[], @@ -96,72 +185,10 @@ export function completeArgs( // Runtime-length spreads determine the complete argument list before // any parameter default runs. Build it once in source order, then // extract the fixed native ABI slots; missing elements are undefined. - const pack = lowerer.declareHiddenLocal("%spreadArgs", DYN); - const ref = (): IrExpr => ({ kind: "varRef", localId: pack.id, type: DYN, loc }); - const stmts: IrStmt[] = [ - { - kind: "varDecl", - localId: pack.id, - init: { kind: "dynArrLit", elems: [], type: DYN, loc }, - loc, - }, - ]; - const push = (value: IrExpr, fn: "dyn.packPush" | "dyn.packPushSpreadIter") => { - stmts.push({ - kind: "exprStmt", - expr: { kind: "libCall", fn, args: [ref(), value], type: VOID, loc: value.loc }, - loc: value.loc, - }); - }; - for (const value of leading ?? []) push(lowerer.coerceInto(blame, value, DYN), "dyn.packPush"); - const spreads = argNodes.filter(ts.isSpreadElement).length; - argNodes.forEach((arg, index) => { - if (!ts.isSpreadElement(arg)) { - push(lowerer.lowerExprExpecting(arg, DYN), "dyn.packPush"); - return; - } - let value = lowerSpread(arg); - if (value.type.kind === "set") { - value = { - kind: "setIntrinsic", - method: "toArray", - receiver: value, - args: [], - type: arrayOf(value.type.elem), - loc: value.loc, - }; - } else if (value.type.kind === "object") { - value = lowerer.classIteratorDrainCall(value, locOf(arg)) ?? value; - } - if (value.type.kind === "array") { - // Argument iteration materializes holes as present undefined - // before conversion to the dense checked-dynamic argument pack. - value = { - kind: "arrayLit", - elems: [value], - spreads: [0], - type: value.type, - loc: value.loc, - }; - } - value = checkedIterableSpread(lowerer, value, ts.getTextOfNode(arg.expression), loc); - if (spreads === 1 && index === argNodes.length - 1) { - stmts.push({ - kind: "exprStmt", - expr: { - kind: "libCall", - fn: "dyn.packPushSpread", - args: [ - ref(), - value, - { kind: "strLit", value: ts.getTextOfNode(arg.expression), type: STRING, loc }, - ], - type: VOID, - loc, - }, - loc, - }); - } else push(value, "dyn.packPushSpreadIter"); + const { stmts, pack: ref } = dynArgumentPack(lowerer, argNodes, loc, { + leading, + blame, + lowerSpread, }); const args = shapes.map((shape, index): IrExpr => { if (shape.mode === "arguments") return ref(); diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 4d04be30a..43322246c 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -33,7 +33,11 @@ import { } from "../../type-mapper.js"; import { isSafeToRepeat } from "./evaluation-safety.js"; import { tryLowerExpression } from "./try-lower-expression.js"; -import { fenceSymbolFieldCopy } from "../symbol-fields.js"; +import { + classSymbolKeyOf, + fenceSymbolFieldCopy, + symbolFieldDisplayName, +} from "../symbol-fields.js"; import { planUnionRetag } from "../union-retag.js"; import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js"; import { builtinGlobalValue } from "./property-access.js"; @@ -194,7 +198,19 @@ function propNameText(lowerer: Lowerer, name: ts.PropertyName): string { * (NamedEvaluation): an identifier, string or numeric key, or a computed * key that folds to one string. Null for other keys. */ function propertyFunctionName(lowerer: Lowerer, name: ts.PropertyName): string | null { - if (ts.isComputedPropertyName(name)) return literalComputedKey(lowerer, name); + if (ts.isComputedPropertyName(name)) { + const literal = literalComputedKey(lowerer, name); + if (literal !== null) return literal; + // A symbol key names the function `[description]`. + let key = name.expression; + while (ts.isParenthesizedExpression(key)) key = key.expression; + const symbol = classSymbolKeyOf(lowerer, key); + if (symbol) return `[${symbolFieldDisplayName(symbol.fieldName).slice("Symbol(".length, -1)}]`; + const wellKnown = ts.isPropertyAccessExpression(key) + ? lowerer.stdlibGlobalMember(key, "Symbol") + : null; + return wellKnown ? `[Symbol.${wellKnown}]` : null; + } if (ts.isNumericLiteral(name)) return String(Number(name.text)); if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text; return null; diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 5df3c7fe0..9157c551d 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -5,6 +5,7 @@ import * as ts from "../../ts7/adapter.js"; import { lowerHistogramProperty } from "../builtins/performance.js"; import { staticFunctionNameRead } from "../static-function-names.js"; import { typedArgumentsArity } from "../typed-arguments.js"; +import { INSPECT_CUSTOM_DESCRIPTION, isInspectCustomAccess } from "../symbol-fields.js"; import { isThisParameter } from "../call-signatures.js"; import { checkedPrivateValue, @@ -310,6 +311,16 @@ export function lowerPropertyAccessExpr( const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); if (navigator) return navigator; } + // util.inspect.custom: Symbol.for('nodejs.util.inspect.custom'). + if (isInspectCustomAccess(lowerer, expr)) { + return { + kind: "libCall", + fn: "sym.for", + args: [{ kind: "strLit", value: INSPECT_CUSTOM_DESCRIPTION, type: STRING, loc }], + type: SYMBOL_T, + loc, + }; + } // A dgram socket's method read as a value in JavaScript (`emit: // socket.emit.bind(socket)`): the socket crosses as its checked-dynamic // handle, whose methods are function values that dispatch on their diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index c0406e77b..711cd7379 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -267,6 +267,28 @@ import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generati const ZLIB_STREAM_METHODS: ReadonlySet = new Set(["flush", "params", "close", "reset"]); +/** The node:util member a `(0, member)` callee names, or null. */ +function indirectUtilCalleeOf( + lowerer: Lowerer, + callee: ts.Expression, +): { module: string; member: string } | null { + let e = callee; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if ( + !ts.isBinaryExpression(e) || + e.operatorToken.kind !== ts.SyntaxKind.CommaToken || + !ts.isNumericLiteral(e.left) + ) + return null; + const target = e.right; + const bi = ts.isIdentifier(target) + ? lowerer.builtinImportOf(target) + : ts.isPropertyAccessExpression(target) && !target.questionDotToken + ? lowerer.builtinMemberOf(target) + : null; + return bi?.module === "util" ? bi : null; +} + export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const loc = locOf(expr); // Object's checker return type is `any`. Claim known object inputs @@ -295,6 +317,14 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const codeGeneration = codeGenerationCalleeOf(lowerer, expr.expression); if (codeGeneration !== null) return lowerCodeGenerationCall(lowerer, expr, codeGeneration); } + // `(0, util_1.inspect)(value)`: TypeScript's CommonJS output calls a + // named import through a comma expression, which leaves the call's + // receiver undefined. node:util's functions do not read their receiver, + // so the call lowers as the direct one. + { + const util = indirectUtilCalleeOf(lowerer, expr.expression); + if (util) return lowerBuiltinCall(lowerer, expr, util, loc); + } // A package shim's runtime intrinsic (a bodiless `declare function` // named scriptcUnixDgram* in a shipped shim): a direct runtime call. if (ts.isIdentifier(expr.expression) && isShimIntrinsicName(expr.expression.text)) { diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index b23697cab..d4875c9df 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -155,6 +155,7 @@ import { } from "./lower-event-emitter.js"; import { declSymbolOf } from "./lower-modules.js"; import { + INSPECT_CUSTOM_MEMBER, classSymbolKeyOf, classSymbolKeyOfSymbol, symbolFieldDisplayName, @@ -3208,6 +3209,15 @@ export function collectClassShapeInner( symbolMethods.set(key.identity, mName); } } + // Error values held as untyped values render through their error + // encoding, which keeps no compiled methods to call. + if (mName === INSPECT_CUSTOM_MEMBER && errorRooted) { + lowerer.unsupported( + "SC1090", + member.name, + "[util.inspect.custom] methods on Error subclasses", + ); + } // Generator calls use their spawn wrapper. Public methods are // direct calls only when no subclass overrides the method; // override chains involving generators fence below. @@ -4422,6 +4432,9 @@ export function collectClassShapeInner( ...(methods.size > 0 ? { methods: [...methods.keys()] } : {}), ...((): { prototypeNames?: string[] } => { const names = classPrototypeNames(decl); + // A [util.inspect.custom] method, by its reserved slot name: the + // checked-dynamic inspect walk refuses instances that have one. + if (methods.has(INSPECT_CUSTOM_MEMBER)) names.push(INSPECT_CUSTOM_MEMBER); return names.length > 0 ? { prototypeNames: names } : {}; })(), ...(abstractClass ? { abstract: true as const } : {}), diff --git a/packages/compiler/src/frontend/lowering/lower-console.ts b/packages/compiler/src/frontend/lowering/lower-console.ts index eec308342..db4d39e70 100644 --- a/packages/compiler/src/frontend/lowering/lower-console.ts +++ b/packages/compiler/src/frontend/lowering/lower-console.ts @@ -14,7 +14,14 @@ import { boolLit, dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/buil import { locOf } from "../program.js"; import { type ConsoleMethodMember } from "./builtins/console.js"; import { jsFuncNameOf, type Lowerer } from "./lowerer.js"; -import { lowerConsoleInspectArg, lowerFormatCall, lowerInspectValue } from "./lower-inspect.js"; +import { + heldFormatArgs, + lowerConsoleInspectArg, + lowerFormatCall, + lowerInspectValue, + lowerRuntimeFormat, + runtimeFormatString, +} from "./lower-inspect.js"; import { streamSidesOf } from "./lower-stream.js"; /** node:console Console instances: a native handle (the checked-dynamic @@ -357,9 +364,32 @@ export function lowerConsoleOutput( ) { return consoleWrite(stdout, [lowerFormatCall(lowerer, expr, loc, false)], loc); } - const args = expr.arguments.nodes.map((a) => - lowerConsoleArg(lowerer, a, lowerer.lowerExpr(a), surface, loc), - ); + const nodes = expr.arguments.nodes; + // Spread arguments and format strings that may hold a substitution at + // run time go through the runtime formatter. A string first argument + // without a '%' keeps the space-joined write below. + if (nodes.some(ts.isSpreadElement)) + return consoleWrite(stdout, [lowerRuntimeFormat(lowerer, nodes, loc)], loc); + const runtime = + nodes.length > 1 && !isStringLiteralLike(first) ? runtimeFormatString(lowerer, first!) : null; + if (runtime === "other") + return consoleWrite(stdout, [lowerRuntimeFormat(lowerer, nodes, loc)], loc); + if (runtime !== null) { + const held = heldFormatArgs(lowerer, nodes, loc); + if (!held.joinable) return seq(held.stmts, consoleWrite(stdout, [held.format()], loc), loc); + const joined = held.args.map((arg) => + lowerConsoleArg(lowerer, arg.node, arg.ref(), surface, loc), + ); + const write: IrStmt = { + kind: "if", + cond: held.specifies, + then: [{ kind: "exprStmt", expr: consoleWrite(stdout, [held.format()], loc), loc }], + else_: [{ kind: "exprStmt", expr: consoleWrite(stdout, joined, loc), loc }], + loc, + }; + return seq([...held.stmts, write], noop(loc), loc); + } + const args = nodes.map((a) => lowerConsoleArg(lowerer, a, lowerer.lowerExpr(a), surface, loc)); return consoleWrite(stdout, args, loc); } diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 47f490c7b..dd950a2a2 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -14,13 +14,15 @@ import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; import { unsignedHex } from "../../format-integer.js"; import type { Lowerer } from "./lowerer.js"; -import { isJsSourceFile } from "../program.js"; +import { isJsSourceFile, locOf } from "../program.js"; +import { noLoweringDiag } from "../../diagnostics/diagnostic.js"; import { BOOL, DYN, F64, type IrExpr, type IrFunction, + type IrLocal, type IrStmt, type IrStrIntrinsicMethod, type IrType, @@ -32,6 +34,7 @@ import { type SrcLoc, canConvertToDyn, canDynCheckTo, + funcOf, CLASS_FIELD_ATTRIBUTES_PREFIX, classDynViewSupported, DYN_CLASS_PROPERTIES, @@ -42,15 +45,16 @@ import { shapeHasAccessorSlots, typeKey, } from "../../ir/ir.js"; -import type { ClassInfo } from "./lower-classes.js"; +import { accessorCall, type ClassInfo } from "./lower-classes.js"; import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js"; -import { symbolFieldDisplayName } from "./symbol-fields.js"; +import { INSPECT_CUSTOM_MEMBER, 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"; import { nullProtoRecordShape, recordNullProtoTest } from "./null-prototype.js"; +import { dynArgumentPack } from "./call-arguments.js"; /* ── IR construction shorthand ───────────────────────────────────────── */ function concatAll(parts: IrExpr[], loc: SrcLoc): IrExpr { @@ -805,6 +809,8 @@ function inspectSupport( case "object": { const info = lowerer.classes.get(t.className); if (!info) return `class '${t.className}' has no inspect lowering`; + const custom = customInspectReason(lowerer, info); + if (custom !== null) return custom; // EventTarget and Event subclasses render through Node's inspectors // for those classes (the runtime's capsule view), and so do classes // declared `extends Date`. @@ -900,6 +906,8 @@ export function inspectExpr( recurse: IrExpr, depth: IrExpr, loc: SrcLoc, + /** The instance's own rendering, without its custom inspect method. */ + ownRendering = false, ): IrExpr { // A Dirent row renders through its dyn view: Node's `Dirent { ... }` // with the Symbol(type) key. @@ -960,6 +968,15 @@ export function inspectExpr( loc, }; case "object": + if (!ownRendering && customInspectMethodOf(lowerer, t.className)) { + return { + kind: "call", + callee: customInspectHelper(lowerer, t, loc), + args: [value, recurse, depth], + type: STRING, + loc, + }; + } if ( eventTargetRootOf(lowerer.classes.get(t.className)) !== null || dateRootOf(lowerer.classes.get(t.className)) !== null @@ -1139,6 +1156,258 @@ function classInspectsAddedProperties(lowerer: Lowerer, info: ClassInfo): boolea ); } +/* ── custom inspect methods ([util.inspect.custom]) ──────────────────── */ + +/** The `[util.inspect.custom]` method a class's instances have, declared + * on the class or a base; null when they have none. */ +function customInspectMethodOf(lowerer: Lowerer, className: string) { + const info = lowerer.classes.get(className); + return info ? (lowerer.findMethodOn(info, INSPECT_CUSTOM_MEMBER) ?? null) : null; +} + +/** Why instances of a class with a custom inspect method have no static + * rendering, or null. Node calls the method with the remaining depth, the + * options and util.inspect; the method's parameters receive them as + * checked-dynamic values, and its result renders in the instance's place. */ +function customInspectReason(lowerer: Lowerer, info: ClassInfo): string | null { + const found = lowerer.findMethodOn(info, INSPECT_CUSTOM_MEMBER); + if (!found) { + const below = (c: ClassInfo): boolean => + c.subclasses.some((s) => s.methods.has(INSPECT_CUSTOM_MEMBER) || below(s)); + return below(info) + ? "classes whose subclasses declare [util.inspect.custom] have no inspect lowering yet (declare the method on the class itself)" + : null; + } + if (found.sig.params.length > 3) + return "[util.inspect.custom] methods with more than three parameters have no inspect lowering"; + for (const param of found.sig.params) { + if ( + param.type.kind !== "dyn" && + !canDynCheckTo( + param.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) + return `[util.inspect.custom] methods with a '${lowerer.fmt(param.type)}' parameter have no inspect lowering`; + } + const ret = found.sig.ret; + if (ret.kind === "object") + return "[util.inspect.custom] methods that return a class instance have no inspect lowering"; + if ( + ret.kind !== "string" && + ret.kind !== "dyn" && + ret.kind !== "void" && + inspectSupport(lowerer, ret, new Set(), { recursive: false }) !== null + ) + return `[util.inspect.custom] methods returning '${lowerer.fmt(ret)}' have no inspect lowering`; + return null; +} + +/** Whether values of `t` can hold a checked-dynamic value, whose own custom + * inspect functions the runtime walk calls. */ +function typeReachesDyn(lowerer: Lowerer, t: IrType): boolean { + const seen = new Set(); + const walk = (u: IrType): boolean => { + if (u.kind === "dyn") return true; + const k = typeKey(u); + if (seen.has(k)) return false; + seen.add(k); + return typeConstituents(lowerer, u).some(walk); + }; + return walk(t); +} + +/** Whether values of `t` can hold an instance of a class with a custom + * inspect method. */ +export function typeReachesCustomInspect(lowerer: Lowerer, t: IrType): boolean { + const seen = new Set(); + const walk = (u: IrType): boolean => { + const k = typeKey(u); + if (seen.has(k)) return false; + seen.add(k); + if (u.kind === "object" && customInspectMethodOf(lowerer, u.className)) return true; + return typeConstituents(lowerer, u).some(walk); + }; + return walk(t); +} + +/** `%util.inspCustom.(v, r, d): string` — formatValue's custom hook + * over a class whose instances have a `[util.inspect.custom]` method: + * with ctx.customInspect set, the method's result (a string re-indented + * at the current level, the instance itself rendered normally, any other + * value rendered in its place); otherwise the instance's own rendering. */ +function customInspectHelper( + lowerer: Lowerer, + t: IrType & { kind: "object" }, + loc: SrcLoc, +): string { + const key = `inspCustom:${typeKey(t)}`; + const existing = lowerer.inspectHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, "%util.inspCustom", key); + lowerer.inspectHelpers.set(key, name); + const found = customInspectMethodOf(lowerer, t.className)!; + const v = (): IrExpr => varRef("v.0", t, loc); + const r = (): IrExpr => varRef("r.0", F64, loc); + const d = (): IrExpr => varRef("d.0", F64, loc); + const ret = (value: IrExpr): IrStmt => ({ kind: "return", value, loc }); + const normal = (): IrExpr => inspectExpr(lowerer, t, v(), r(), d(), loc, true); + const runtimeArgs: IrExpr[] = [ + { kind: "libCall", fn: "insp.customDepth", args: [r(), d()], type: DYN, loc }, + { kind: "libCall", fn: "insp.customOptions", args: [d()], type: DYN, loc }, + { kind: "libCall", fn: "insp.inspectFunction", args: [], type: DYN, loc }, + ]; + const args = found.sig.params.map((param, i) => + lowerer.coerceToExpected(runtimeArgs[i]!, param.type), + ); + const resultT = found.sig.ret; + const call = accessorCall(lowerer, t.className, INSPECT_CUSTOM_MEMBER, v(), args, resultT, loc); + const locals: IrLocal[] = [ + { id: "v.0", name: "v", type: t, mutable: false }, + { id: "r.0", name: "r", type: F64, mutable: false }, + { id: "d.0", name: "d", type: F64, mutable: false }, + ]; + const render: IrStmt[] = []; + if (resultT.kind === "string") { + render.push(ret({ kind: "libCall", fn: "insp.customText", args: [call], type: STRING, loc })); + } else if (resultT.kind === "dyn") { + locals.push({ id: "res.0", name: "res", type: DYN, mutable: false }); + const res = (): IrExpr => varRef("res.0", DYN, loc); + render.push( + { kind: "varDecl", localId: "res.0", init: call, loc }, + { + kind: "if", + cond: { + kind: "libCall", + fn: "insp.customIsSelf", + args: [res(), { kind: "dynFrom", value: v(), liveRef: true, type: DYN, loc }], + type: BOOL, + loc, + }, + then: [ret(normal())], + else_: null, + loc, + }, + ret({ kind: "libCall", fn: "insp.customResult", args: [res(), r(), d()], type: STRING, loc }), + ); + } else if (resultT.kind === "void") { + render.push({ kind: "exprStmt", expr: call, loc }, ret(strLit("undefined", loc))); + } else { + render.push(ret(inspectExpr(lowerer, resultT, call, r(), d(), loc))); + } + lowerer.liftedFns.push({ + name, + params: [ + { localId: "v.0", name: "v", type: t }, + { localId: "r.0", name: "r", type: F64 }, + { localId: "d.0", name: "d", type: F64 }, + ], + returnType: STRING, + locals, + body: [ + { + kind: "if", + cond: { kind: "libCall", fn: "insp.customEnabled", args: [], type: BOOL, loc }, + then: render, + else_: null, + loc, + }, + ret(normal()), + ], + loc, + }); + return name; +} + +/** `%util.inspCustomDyn.(self, depth, options, inspect): dyn` — a + * class's custom inspect method for its instances held as checked-dynamic + * values: the runtime's inspect walk calls it with the instance's typed + * ref and the method's three arguments. */ +function customInspectDynHelper( + lowerer: Lowerer, + t: IrType & { kind: "object" }, + loc: SrcLoc, +): string { + const name = keyedHelperName(lowerer, "%util.inspCustomDyn", typeKey(t)); + const found = customInspectMethodOf(lowerer, t.className)!; + const param = (id: string): IrExpr => varRef(id, DYN, loc); + const ids = ["depth.0", "options.0", "inspect.0"]; + const call = accessorCall( + lowerer, + t.className, + INSPECT_CUSTOM_MEMBER, + varRef("v.0", t, loc), + found.sig.params.map((p, i) => lowerer.coerceToExpected(param(ids[i]!), p.type)), + found.sig.ret, + loc, + ); + const body: IrStmt[] = [ + { kind: "varDecl", localId: "v.0", init: lowerer.coerceToExpected(param("self.0"), t), loc }, + ]; + if (found.sig.ret.kind === "void") { + body.push( + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: dynUndefinedExpr(loc), loc }, + ); + } else { + body.push({ kind: "return", value: lowerer.coerceToExpected(call, DYN), loc }); + } + const params = ["self.0", ...ids]; + lowerer.liftedFns.push({ + name, + params: params.map((id) => ({ localId: id, name: id.slice(0, -2), type: DYN })), + returnType: DYN, + locals: [ + ...params.map((id) => ({ id, name: id.slice(0, -2), type: DYN, mutable: false })), + { id: "v.0", name: "v", type: t, mutable: false }, + ], + body, + loc, + }); + return name; +} + +/** The statements %main runs first, in every thread: each class whose + * instances have a custom inspect method registers its compiled call + * under its instances' typed-ref type key (scr_insp_register_custom). */ +export function customInspectRegistrations(lowerer: Lowerer, loc: SrcLoc): IrStmt[] { + const stmts: IrStmt[] = []; + for (const info of lowerer.classes.values()) { + if (info.def.runtime || info.def.abstract || isErrorClass(lowerer, info.def.name)) continue; + if (!lowerer.findMethodOn(info, INSPECT_CUSTOM_MEMBER)) continue; + if (customInspectReason(lowerer, info) !== null) continue; + const t = { kind: "object" as const, className: info.def.name }; + const fn = customInspectDynHelper(lowerer, t, loc); + stmts.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "insp.registerCustom", + args: [ + strLit(typeKey(t), loc), + { + kind: "dynFrom", + value: { + kind: "closure", + fnName: fn, + captures: [], + type: funcOf([DYN, DYN, DYN, DYN], DYN), + loc, + }, + type: DYN, + loc, + }, + ], + type: VOID, + loc, + }, + loc, + }); + } + return stmts; +} + /* ── the interned per-type helper ────────────────────────────────────── */ /** `%util.insp.(v, r, d): string` — formatValue over the static type: @@ -2213,13 +2482,22 @@ function isDataViewTypeName(tname: string): boolean { /* ── the callsite: options, direct function/class names ──────────────── */ /** The parsed options literal: the resolved depth (numeric; Infinity for - * `null`), breakLength (numeric; default 80) and compact (numeric, 0 for + * `null`), breakLength (numeric; default 80) and compact (numeric, -1 for * false; default 3), with every other non-default knob fenced by name. */ interface InspectOptions { depth: number; breakLength: number; compact: number; sorted: boolean; + /** Set by the `atRunTime` parse: colors or a computed knob value, which + * the runtime reads. */ + runtime: boolean; + /** How the depth option was spelled, for custom inspect methods: 0 a + * number (also when absent), 1 null, 2 undefined. */ + depthKind: number; + /** customInspect: false skips [util.inspect.custom] methods (console.dir's + * default). */ + customInspect: boolean; } function parseInspectOptions( @@ -2227,9 +2505,18 @@ function parseInspectOptions( node: ts.Expression | undefined, recursive: boolean, dir = false, + atRunTime = false, ): InspectOptions { const surface = dir ? "console.dir" : "util.inspect"; - const options: InspectOptions = { depth: 2, breakLength: 80, compact: 3, sorted: false }; + const options: InspectOptions = { + depth: 2, + breakLength: 80, + compact: 3, + sorted: false, + runtime: false, + depthKind: 0, + customInspect: !dir, + }; if (!node || (ts.isIdentifier(node) && node.text === "undefined")) return options; if (!ts.isObjectLiteralExpression(node)) { lowerer.noLowering( @@ -2240,6 +2527,11 @@ function parseInspectOptions( } for (const prop of node.properties.nodes) { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) { + // `{ depth }` and `{ ...base }` hold values known at run time. + if (atRunTime && (ts.isShorthandPropertyAssignment(prop) || ts.isSpreadAssignment(prop))) { + options.runtime = true; + continue; + } lowerer.noLowering( `${surface} options in this form`, prop, @@ -2269,8 +2561,17 @@ function parseInspectOptions( // bounded by the (finite) object graph — exactly Node. void recursive; options.depth = Infinity; - } else if (!(ts.isIdentifier(value) && value.text === "undefined")) { + options.depthKind = value.kind === ts.SyntaxKind.NullKeyword ? 1 : 0; + } else if (ts.isIdentifier(value) && value.text === "undefined") { + // Node's ctx.depth is undefined: no depth limit. + options.depth = Infinity; + options.depthKind = 2; + } else { const n = numeric(); + if (n === null && atRunTime) { + options.runtime = true; + continue; + } if (n === null) { lowerer.noLowering( `${surface} with a non-literal depth option`, @@ -2281,7 +2582,8 @@ function parseInspectOptions( options.depth = n; } } else if (key === "colors") { - if (value.kind !== ts.SyntaxKind.FalseKeyword) { + if (value.kind !== ts.SyntaxKind.FalseKeyword && atRunTime) options.runtime = true; + else if (value.kind !== ts.SyntaxKind.FalseKeyword) { lowerer.noLowering( `${surface} with colors: true`, value, @@ -2293,6 +2595,10 @@ function parseInspectOptions( // compact of at least 1; false and smaller numbers break every // composite onto its own lines. compact: true is the older layout. const n = value.kind === ts.SyntaxKind.FalseKeyword ? 0 : numeric(); + if (n === null && atRunTime && value.kind !== ts.SyntaxKind.TrueKeyword) { + options.runtime = true; + continue; + } if (n === null || Number.isNaN(n)) { lowerer.noLowering( `${surface} with this compact option`, @@ -2300,9 +2606,22 @@ function parseInspectOptions( "compact must be false or a numeric literal (compact: true has no lowering)", ); } - options.compact = Math.max(n, 0); + // false is -1: the same layout as 0, and custom inspect functions + // read it back as false. + options.compact = value.kind === ts.SyntaxKind.FalseKeyword ? -1 : Math.max(n, 0); } else if (key === "sorted") { - if (value.kind !== ts.SyntaxKind.TrueKeyword && value.kind !== ts.SyntaxKind.FalseKeyword) { + const sortedLiteral = + value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword; + if ( + !sortedLiteral && + atRunTime && + !ts.isArrowFunction(value) && + !ts.isFunctionExpression(value) + ) { + options.runtime = true; + continue; + } + if (!sortedLiteral) { lowerer.noLowering( `${surface} with this sorted option`, value, @@ -2312,6 +2631,10 @@ function parseInspectOptions( options.sorted = value.kind === ts.SyntaxKind.TrueKeyword; } else if (key === "breakLength") { const n = ts.isIdentifier(value) && value.text === "Infinity" ? Infinity : numeric(); + if (n === null && atRunTime) { + options.runtime = true; + continue; + } if (n === null || Number.isNaN(n)) { lowerer.noLowering( `${surface} with a non-literal breakLength option`, @@ -2320,6 +2643,12 @@ function parseInspectOptions( ); } options.breakLength = n; + } else if ( + !dir && + key === "customInspect" && + (value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword) + ) { + options.customInspect = value.kind === ts.SyntaxKind.TrueKeyword; } else if (dir && key === "customInspect") { // console.dir's own default; only that value is lowered. if (value.kind !== ts.SyntaxKind.FalseKeyword) { @@ -2417,16 +2746,86 @@ function directCallableInspect(lowerer: Lowerer, node: ts.Expression, loc: SrcLo return null; } -/** util.inspect(value[, options]) — the spoke's inspect entry. */ +/** util.inspect(value[, options]) and inspect(value, showHidden, depth, + * colors) — the spoke's inspect entry. Options the compiler resolves + * render through the static helpers; colors, option values known only at + * run time and the legacy forms render the value's checked-dynamic view + * in the runtime (insp.inspect). */ function lowerInspectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { + const args = expr.arguments.nodes; + if (args.length < 1 || args.length > 4 || args.some(ts.isSpreadElement)) { lowerer.noLowering( - `util.inspect with ${expr.arguments.nodes.length} arguments`, + `util.inspect with ${args.some(ts.isSpreadElement) ? "spread" : args.length} arguments`, expr, - "the supported forms are inspect(value) and inspect(value, options)", + "the supported forms are inspect(value), inspect(value, options) and inspect(value, showHidden, depth, colors)", ); } - return lowerInspectValue(lowerer, expr.arguments.nodes[0]!, expr.arguments.nodes[1], loc, false); + if (args.length <= 2 && !inspectOptionsAtRunTime(lowerer, args[1])) + return lowerInspectValue(lowerer, args[0]!, args[1], loc, false); + return lowerRuntimeInspect(lowerer, args, loc); +} + +/** Whether an options argument needs the runtime reader: an options value + * that is not an object literal, or a literal with colors or a computed + * depth, compact, breakLength or sorted value. Literal options otherwise + * keep the static parse, which also reports the literal options neither + * path lowers. */ +function inspectOptionsAtRunTime(lowerer: Lowerer, node: ts.Expression | undefined): boolean { + if (!node || (ts.isIdentifier(node) && node.text === "undefined")) return false; + if (!ts.isObjectLiteralExpression(node)) return true; + return parseInspectOptions(lowerer, node, false, false, true).runtime; +} + +/** util.inspect over the value's checked-dynamic view, with every argument + * evaluated in order and read in the runtime as Node's inspect reads it. */ +function lowerRuntimeInspect( + lowerer: Lowerer, + args: readonly ts.Expression[], + loc: SrcLoc, +): IrExpr { + const valueNode = args[0]!; + if ( + directCallableInspect(lowerer, valueNode, loc) !== null || + ts.isArrowFunction(valueNode) || + ts.isFunctionExpression(valueNode) + ) { + lowerer.noLowering( + "util.inspect of a function or class with these options", + valueNode, + "pass the options as a literal without colors", + ); + } + const value = lowerer.lowerExpr(valueNode); + if (value.type.kind !== "dyn") { + const reason = inspectSupport(lowerer, value.type, new Set(), { recursive: false }); + if (reason !== null) + lowerer.noLowering(`util.inspect of '${lowerer.fmt(value.type)}' values`, valueNode, reason); + if (!lowerer.dynConvertible(value.type)) + lowerer.noLowering( + `util.inspect with these options of '${lowerer.fmt(value.type)}' values`, + valueNode, + "the options are read at run time, where the value renders through its checked-dynamic view", + ); + } + const stmts: IrStmt[] = []; + const held = (init: IrExpr): IrExpr => { + const local = lowerer.declareHiddenLocal("%inspectArg", DYN); + stmts.push({ kind: "varDecl", localId: local.id, init, loc }); + return varRef(local.id, DYN, loc); + }; + const operands: IrExpr[] = [held(lowerer.coerceToExpected(value, DYN))]; + for (let i = 1; i < 4; i++) { + const node = args[i]; + operands.push(node ? held(lowerer.lowerExprExpecting(node, DYN)) : dynUndefinedExpr(loc)); + } + operands.push(numLit(args.length, loc)); + return { + kind: "seqExpr", + stmts, + result: { kind: "libCall", fn: "insp.inspect", args: operands, type: STRING, loc }, + type: STRING, + loc, + }; } /** `inspect(value, options)` over one value node: the shared body of @@ -2512,6 +2911,46 @@ export function lowerInspectValue( } const options = parseInspectOptions(lowerer, optionsNode, walk.recursive, dir); const depth = options.depth; + // Custom inspect functions read the options as Node spelled them: an + // unbounded depth given as Infinity or undefined, and customInspect: + // false (console.dir's default), apply around the render when the value + // can reach a custom method; the previous settings return afterwards. + const reachesCustom = + typeReachesCustomInspect(lowerer, value.type) || typeReachesDyn(lowerer, value.type); + const depthContext = reachesCustom && depth === Infinity && options.depthKind !== 1; + const customContext = reachesCustom && !options.customInspect; + if (depthContext || customContext) { + const stmts: IrStmt[] = []; + const restore: IrStmt[] = []; + const setting = (fn: "insp.customInspect" | "insp.depthKind", arg: IrExpr, type: IrType) => { + const previous = lowerer.declareHiddenLocal("%inspectContext", type); + const call = (a: IrExpr): IrExpr => ({ kind: "libCall", fn, args: [a], type, loc }); + stmts.push({ kind: "varDecl", localId: previous.id, init: call(arg), loc }); + restore.push({ kind: "exprStmt", expr: call(varRef(previous.id, type, loc)), loc }); + }; + if (customContext) setting("insp.customInspect", boolLit(false, loc), BOOL); + if (depthContext) setting("insp.depthKind", numLit(options.depthKind, loc), F64); + const text = lowerer.declareHiddenLocal("%inspectText", STRING); + stmts.push({ + kind: "varDecl", + localId: text.id, + init: withInspectLayout( + lowerer, + options, + value, + (input) => inspectExpr(lowerer, value.type, input, numLit(0, loc), numLit(depth, loc), loc), + loc, + ), + loc, + }); + return { + kind: "seqExpr", + stmts: [...stmts, ...restore.reverse()], + result: varRef(text.id, STRING, loc), + type: STRING, + loc, + }; + } if (dir && value.type.kind === "dyn") { return withInspectLayout( lowerer, @@ -2542,7 +2981,17 @@ export function lowerInspectValue( * formatNumber, strings verbatim, objects through inspect at the given * depth). Null = fence with the given reason. */ function formatSArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: SrcLoc): IrExpr { - const value = lowerer.lowerExpr(node); + return formatSValue(lowerer, node, lowerer.lowerExpr(node), depth, loc); +} + +/** formatSArg over the already-lowered value of `node`. */ +function formatSValue( + lowerer: Lowerer, + node: ts.Expression, + value: IrExpr, + depth: number, + loc: SrcLoc, +): IrExpr { const t = value.type; if (t.kind === "string") return value; if (t.kind === "f64") @@ -2626,6 +3075,13 @@ function formatOArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S node, reason, ); + if (depth === 4 && typeReachesCustomInspect(lowerer, value.type)) { + lowerer.noLowering( + "util.format %o over values with [util.inspect.custom] methods", + node, + "%o passes showHidden to the methods — use %O", + ); + } if (depth === 4 && typeTreeHasArray(lowerer, value.type, new Set())) { lowerer.noLowering( "util.format %o over arrays", @@ -2708,8 +3164,9 @@ function formatIntegerFixed(value: IrExpr, spec: "%d" | "%i", loc: SrcLoc): IrEx /** util.format(...) / util.formatWithOptions({}, ...): the compile-time * %-substitution over a literal format string (formatWithOptionsInternal * ported exactly — the arg cursor, the args-exhausted guard, %c's - * consume-and-drop), with per-static-type conversions. Non-literal - * format strings lower only in the substitution-free shapes. */ + * consume-and-drop), with per-static-type conversions. Format strings + * known only at run time, spread arguments and non-empty options lower to + * the runtime formatter (lowerRuntimeFormat, heldFormatArgs). */ export function lowerFormatCall( lowerer: Lowerer, expr: ts.CallExpression, @@ -2721,30 +3178,60 @@ export function lowerFormatCall( const argNodes = [...(argNodesOverride ?? expr.arguments.nodes)]; if (withOptions) { const opts = argNodes.shift(); + // The empty literal {} is the defaults: exactly util.format. if (!opts || !ts.isObjectLiteralExpression(opts) || opts.properties.nodes.length > 0) { - lowerer.noLowering( - "util.formatWithOptions with a non-empty options literal", - opts ?? expr, - "only the empty literal {} (the defaults — exactly util.format) is lowered", - ); + if (opts && ts.isSpreadElement(opts)) { + lowerer.noLowering( + "util.formatWithOptions with a spread options argument", + opts, + "pass the options object as the first argument", + ); + } + return lowerRuntimeFormat(lowerer, argNodes, loc, opts ?? null); } } if (argNodes.length === 0) return strLit("", loc); const first = argNodes[0]!; const firstIsLiteral = ts.isStringLiteral(first) || ts.isNoSubstitutionTemplateLiteral(first); + if (argNodes.some(ts.isSpreadElement)) { + if (literalPrefix !== "" && !firstIsLiteral) + throw new InternalCompilerError("a format prefix needs a literal format string"); + return lowerRuntimeFormat(lowerer, argNodes, loc, undefined, literalPrefix); + } /** The rest-args tail: ` ` + (strings verbatim, everything else * inspected at the default depth). */ const restArg = (node: ts.Expression): IrExpr => formatSArg(lowerer, node, 2, loc); if (!firstIsLiteral) { - const firstT = lowerer.mapTypeOf(lowerer.typeOf(first)); - if (firstT?.kind === "string" && argNodes.length > 1) { - lowerer.noLowering( - "util.format with a runtime format string and further arguments", - first, - "the %-substitution positions are runtime-dependent — use a string literal", - ); + const runtime = argNodes.length > 1 ? runtimeFormatString(lowerer, first) : null; + if (runtime === "other") return lowerRuntimeFormat(lowerer, argNodes, loc); + if (runtime !== null) { + // A string that substitutes formats at run time; any other string + // joins with the remaining arguments, as below. + const held = heldFormatArgs(lowerer, argNodes, loc); + if (!held.joinable) { + return { kind: "seqExpr", stmts: held.stmts, result: held.format(), type: STRING, loc }; + } + const joined: IrExpr[] = []; + held.args.forEach((arg, i) => { + if (i > 0) joined.push(strLit(" ", loc)); + joined.push(formatRestValue(lowerer, arg.node, arg.ref(), loc)); + }); + return { + kind: "seqExpr", + stmts: held.stmts, + result: { + kind: "ternary", + cond: held.specifies, + then: held.format(), + else_: concatAll(joined, loc), + type: STRING, + loc, + }, + type: STRING, + loc, + }; } // No substitutions possible: every argument joins with spaces. const parts: IrExpr[] = []; @@ -3030,6 +3517,240 @@ export function lowerFormatCall( return concatAll(parts, loc); } +/** One space-joined argument of util.format, as Node's formatter renders + * it after the format string: strings verbatim, everything else through + * inspect at the default depth. */ +function formatRestValue( + lowerer: Lowerer, + node: ts.Expression, + value: IrExpr, + loc: SrcLoc, +): IrExpr { + switch (value.type.kind) { + case "string": + return value; + case "f64": + return { kind: "libCall", fn: "insp.f64", args: [value], type: STRING, loc }; + case "bool": + return { kind: "toString", operand: value, type: STRING, loc }; + case "dyn": + return { kind: "libCall", fn: "insp.dynS", args: [value, numLit(2, loc)], type: STRING, loc }; + default: + return lowerConsoleInspectArg(lowerer, node, value, "util.format", loc); + } +} + +const UNCONVERTIBLE_HINT = + "the runtime formatter reads its arguments as checked-dynamic values — use a string literal format"; + +/** A string-valued expression that throws the SC2020 refusal where it runs + * (a JavaScript construct fence deferred to the branch that needs it). */ +function runtimeFenceValue( + lowerer: Lowerer, + surface: string, + node: ts.Node, + hint: string, + loc: SrcLoc, +): IrExpr { + const diag = noLoweringDiag(surface, locOf(node), hint); + lowerer.runtimeFences.push(diag); + const sf = ts.getSourceFileOfNode(node); + const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1; + const slot = lowerer.declareHiddenLocal("%never", STRING); + slot.mutable = true; + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: slot.id, init: null, loc }, + { + kind: "runtimeFence", + code: diag.code, + message: `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`, + loc, + }, + ], + result: varRef(slot.id, STRING, loc), + diverges: true, + type: STRING, + loc, + }; +} + +/** How a non-literal format string is known: "string" and "dyn" values + * test for a substitution at run time, "other" types (unions with a string + * arm) format at run time, and null types never hold a string. */ +export function runtimeFormatString( + lowerer: Lowerer, + first: ts.Expression, +): "string" | "dyn" | "other" | null { + const t = lowerer.mapTypeOf(lowerer.typeOf(first)); + if (t === null) return "other"; + if (t.kind === "string") return "string"; + if (t.kind === "dyn") return "dyn"; + if (t.kind === "union") { + const arms = lowerer.unions.get(t.unionId)?.arms ?? []; + return arms.some((arm) => arm.kind === "string" || arm.kind === "dyn") ? "other" : null; + } + return t.kind === "jsval" || t.kind === "caught" ? "other" : null; +} + +/** The runtime formatter over a call's arguments: one checked-dynamic + * pack in source order (spreads contribute their elements), after the + * options when `options` is given (null: an omitted options argument, + * which Node's validation rejects). A literal prefix joins a literal + * format string. */ +export function lowerRuntimeFormat( + lowerer: Lowerer, + argNodes: readonly ts.Expression[], + loc: SrcLoc, + options?: ts.Expression | null, + literalPrefix = "", +): IrExpr { + const stmts: IrStmt[] = []; + let optionsRef: IrExpr | null = null; + if (options !== undefined) { + const held = lowerer.declareHiddenLocal("%formatOptions", DYN); + stmts.push({ + kind: "varDecl", + localId: held.id, + init: options === null ? dynUndefinedExpr(loc) : lowerer.lowerExprExpecting(options, DYN), + loc, + }); + optionsRef = varRef(held.id, DYN, loc); + } + let nodes = argNodes; + const leading: IrExpr[] = []; + if (literalPrefix !== "") { + const first = argNodes[0] as ts.StringLiteralLike; + leading.push(strLit(literalPrefix + first.text, loc)); + nodes = argNodes.slice(1); + } + const pack = dynArgumentPack(lowerer, nodes, loc, { + leading, + blame: argNodes[0] ?? options ?? undefined, + }); + stmts.push(...pack.stmts); + const result: IrExpr = + optionsRef === null + ? { kind: "libCall", fn: "insp.format", args: [pack.pack()], type: STRING, loc } + : { + kind: "libCall", + fn: "insp.formatWithOptions", + args: [optionsRef, pack.pack()], + type: STRING, + loc, + }; + return { kind: "seqExpr", stmts, result, type: STRING, loc }; +} + +/** The arguments of a call whose first argument is a string or dyn value + * known only at run time, each evaluated once into a hidden local: + * `specifies` tests that string for a '%' before its last character, and + * `format()` is the runtime formatter over the held values. The caller + * renders the other branch (the space-joined arguments) from `args` when + * every argument is `joinable` there. */ +export function heldFormatArgs( + lowerer: Lowerer, + argNodes: readonly ts.Expression[], + loc: SrcLoc, +): { + stmts: IrStmt[]; + args: { node: ts.Expression; ref: () => IrExpr }[]; + specifies: IrExpr; + format: () => IrExpr; + joinable: boolean; +} { + const stmts: IrStmt[] = []; + let unconvertible: { node: ts.Expression; type: IrType } | null = null; + const unconvertibleSurface = (t: IrType): string => + `util.format with a runtime format string over '${lowerer.fmt(t)}' values`; + const args = argNodes.map((node) => { + const value = lowerer.lowerExpr(node); + if (value.type.kind === "void") { + lowerer.unsupported( + "SC1090", + node, + "util.format of a void call result (call it as its own statement, then pass undefined)", + ); + } + if (value.type.kind !== "dyn" && !lowerer.dynConvertible(value.type)) { + // JavaScript keeps the space-joined branch, which printed these + // values before, and refuses only a string that substitutes. + if (!isJsSourceFile(ts.getSourceFileOfNode(node))) { + lowerer.noLowering(unconvertibleSurface(value.type), node, UNCONVERTIBLE_HINT); + } + unconvertible ??= { node, type: value.type }; + } + // Unit values have no local: their effects run here and a literal + // stands in. + const k = value.type.kind; + if (k === "undefinedT" || k === "nullT") { + if (value.kind !== "unitLit") stmts.push({ kind: "exprStmt", expr: value, loc }); + const unit = k === "nullT" ? ("null" as const) : ("undefined" as const); + return { node, ref: (): IrExpr => ({ kind: "unitLit", unit, type: value.type, loc }) }; + } + const held = lowerer.declareHiddenLocal("%formatArg", value.type); + stmts.push({ kind: "varDecl", localId: held.id, init: value, loc }); + return { node, ref: () => varRef(held.id, value.type, loc) }; + }); + const first = args[0]!.ref(); + const specifies: IrExpr = + first.type.kind === "string" + ? { kind: "libCall", fn: "insp.formatSpecifies", args: [first], type: BOOL, loc } + : { + kind: "libCall", + fn: "insp.formatSpecifiesDyn", + args: [lowerer.coerceToExpected(first, DYN)], + type: BOOL, + loc, + }; + const format = (): IrExpr => { + if (unconvertible !== null) { + const fenced = unconvertible; + return runtimeFenceValue( + lowerer, + unconvertibleSurface(fenced.type), + fenced.node, + UNCONVERTIBLE_HINT, + loc, + ); + } + // Strings, numbers, booleans, null and undefined format without the + // checked-dynamic walk other values need, which keeps it out of the + // executable. + const primitive = (t: IrType): boolean => + t.kind === "string" || + t.kind === "f64" || + t.kind === "bool" || + t.kind === "undefinedT" || + t.kind === "nullT" || + (t.kind === "union" && (lowerer.unions.get(t.unionId)?.arms.every(primitive) ?? false)); + return { + kind: "libCall", + fn: args.every((arg) => primitive(arg.ref().type)) ? "insp.formatPrimitives" : "insp.format", + args: [ + { + kind: "dynArrLit", + elems: args.map((arg) => lowerer.coerceToExpected(arg.ref(), DYN)), + type: DYN, + loc, + }, + ], + type: STRING, + loc, + }; + }; + // The space-joined branch renders each argument statically; arguments + // the static rendering refuses take the runtime formatter in both cases. + const joinable = args.every(({ ref }) => { + const t = ref().type; + if (t.kind === "func") return false; + if (t.kind === "bytes") return true; + return inspectSupport(lowerer, t, new Set(), { recursive: false }) === null; + }); + return { stmts, args, specifies, format, joinable }; +} + /** Arrays anywhere in the tree — the %o (showHidden) gate. */ function typeTreeHasArray(lowerer: Lowerer, t: IrType, visiting: Set): boolean { switch (t.kind) { diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 5238333ad..8013b842f 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -93,6 +93,7 @@ import { createRequireProgramModuleDecl, } from "./builtins/module-bindings.js"; import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js"; +import { customInspectRegistrations } from "./lower-inspect.js"; import { registerBuiltinClassAlias } from "./builtins/class-values.js"; import { stripTypeCasts } from "./builtins/arguments.js"; import { isWebCryptoMemberType } from "./builtins/webcrypto.js"; @@ -1947,18 +1948,21 @@ export function collectGlobals( // symbol registers HERE, before any use site can lower, and the // statement lowering skips the declaration by the same check. A // bad promisify target reports here (recorded once; the statement - // lowering poisons too, like every collectGlobals failure). + // lowering poisons too, like every collectGlobals failure). Targets + // that promisify to a function value (typed callback-last functions + // and untyped functions) keep an ordinary global. if ( ts.isIdentifier(decl.name) && decl.initializer && isPromisifyCall(lowerer, decl.initializer) ) { + let registered = true; try { - lowerer.registerPromisifiedBuiltinDecl(decl.name, decl.initializer); + registered = lowerer.registerPromisifiedBuiltinDecl(decl.name, decl.initializer); } catch (e) { if (!(e instanceof PoisonError)) throw e; } - continue; + if (registered) continue; } // `const inspect = require("util").inspect` at file scope: a // named import in const clothing (builtinImportOf resolves uses) @@ -3523,6 +3527,9 @@ export function buildMain(lowerer: Lowerer): IrFunction { loc, }); } + // Classes with custom inspect methods register their compiled calls for + // the runtime's inspect walk before any module runs, in every thread. + body.unshift(...customInspectRegistrations(lowerer, loc)); // Node's startup refusal (a resolution the graph carries that Node // rejects — preflight's Node-order resolution walk — or a module-LINK // SyntaxError from either named-import checker): the graph is refused diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 93c6246d9..c2c60abdc 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -13677,6 +13677,10 @@ export class Lowerer { * are only dispatch sentinels for call-site-specific lowering. */ lowerBuiltinCallableValue(bi: { module: string; member: string }, loc: SrcLoc): IrExpr | null { if (bi.module === "util" && bi.member === "types") return utilTypesModuleValue(this, loc); + // util.inspect: the function custom inspect methods receive, one + // identity per thread. + if (bi.module === "util" && bi.member === "inspect") + return { kind: "libCall", fn: "insp.inspectFunction", args: [], type: DYN, loc }; if (bi.module === "util/types" && builtinModuleFnOf(this, bi.module, bi.member)) return lowerUtilTypeValue(this, bi.member, loc); if (bi.module === "util" && bi.member === "getSystemErrorMap") diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 8b826304b..80da35ad3 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1686,7 +1686,8 @@ export const BUILTIN_MODULE_FNS: Record< // lowerVarDecl / collectGlobals BEFORE any call lowering runs — and // inspect/format/formatWithOptions plus parseArgs through the util spoke // (lower-inspect.ts: per-type synthesized traversal helpers, - // compile-time format strings, checked-dynamic config/result for + // compile-time format strings and the runtime formatter for the others, + // checked-dynamic config/result for // parseArgs), which both dispatch paths try before the generic table. // parseArgs is tabled as well so the generated surface manifest records // the dedicated static member; the spoke owns its optional arity and diff --git a/packages/compiler/src/frontend/lowering/symbol-fields.ts b/packages/compiler/src/frontend/lowering/symbol-fields.ts index 56718615e..ea495d364 100644 --- a/packages/compiler/src/frontend/lowering/symbol-fields.ts +++ b/packages/compiler/src/frontend/lowering/symbol-fields.ts @@ -76,7 +76,36 @@ export function fenceSymbolFieldCopy( } } +/** The registry name util.inspect.custom denotes. */ +export const INSPECT_CUSTOM_DESCRIPTION = "nodejs.util.inspect.custom"; +/** The method slot of a class's `[util.inspect.custom]` member. */ +export const INSPECT_CUSTOM_MEMBER = `${FIELD_PREFIX}Symbol(${INSPECT_CUSTOM_DESCRIPTION})`; + +/** `inspect.custom` over node:util's inspect: a named import, `util.inspect` + * or the CommonJS `util_1.inspect`. */ +export function isInspectCustomAccess(lowerer: Lowerer, expr: ts.Expression): boolean { + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (!ts.isPropertyAccessExpression(expr) || expr.questionDotToken) return false; + if (!ts.isIdentifier(expr.name) || expr.name.text !== "custom") return false; + let target = expr.expression; + while (ts.isParenthesizedExpression(target)) target = target.expression; + const bi = ts.isIdentifier(target) + ? lowerer.builtinImportOf(target) + : ts.isPropertyAccessExpression(target) && !target.questionDotToken + ? lowerer.builtinMemberOf(target) + : null; + return bi?.module === "util" && bi.member === "inspect"; +} + export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSymbolKey | null { + // util.inspect.custom is the registered Symbol.for(INSPECT_CUSTOM_DESCRIPTION), + // one identity with the program's own Symbol.for calls of that name. + if (isInspectCustomAccess(lowerer, key)) { + let e = key; + while (ts.isParenthesizedExpression(e)) e = e.expression; + const sym = lowerer.checker.getSymbolAtLocation((e as ts.PropertyAccessExpression).name); + return sym ? symbolKeyOf(lowerer, sym, true, INSPECT_CUSTOM_DESCRIPTION) : null; + } if ( !ts.isIdentifier(key) && (!ts.isPropertyAccessExpression(key) || key.questionDotToken || !ts.isIdentifier(key.name)) diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index aef61c743..3db647f64 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -2955,6 +2955,30 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // spawn consumes a presence-sensitive option bag, including optional // stdio and env values. Keep it checked-dynamic for native normalization. if (isStdlibInterface("SpawnOptions")) return DYN; + // util.inspect as a value (a custom inspect method's third parameter is + // `typeof inspect`): the runtime's inspect function, called through the + // checked-dynamic boundary. + if ( + psym !== undefined && + ts.symbolName(psym) === "inspect" && + psym.declarations.some( + (d) => + ts.isFunctionDeclaration(d) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + isDeclaredInAmbientModule(d, "util"), + ) + ) + return DYN; + // util.inspect's options bag, which inspect and formatWithOptions read + // at run time (its sorted option can be a comparator function), and the + // options custom inspect functions receive. + if ( + (isStdlibInterface("InspectOptions") || isStdlibInterface("InspectOptionsStylized")) && + psym!.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientModule(d, "util"), + ) + ) + return DYN; // dns.LookupAllOptions (`all: true`, the overload whose callback takes // an address list): a lookup option bag dns.lookup reads at run time. // It rides checked-dynamic storage, so the lookup function type that diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index eb3f8c6ab..d676b05dd 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -180,6 +180,17 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "num.toStringRadix", "num.toStringRadixF64", "insp.jsonDyn", + // The runtime formatter calls toString, valueOf and toJSON, and refuses + // renderings it has no lowering for. + "insp.format", + "insp.formatWithOptions", + "insp.inspect", + // The options object reads the caller's options (getters included), and + // the checked-dynamic walk calls custom inspect functions. + "insp.customOptions", + "insp.customResult", + "insp.dyn", + "insp.dynS", // diagnostics_channel: publish runs subscribers synchronously (a throw // propagates — the documented divergence from triggerUncaughtException); // the subscribe/unsubscribe forms throw Node's ERR_INVALID_ARG_TYPE for diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 8edb223b3..6e07e1467 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4370,8 +4370,43 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "insp.errorCause": { argTypes: [null, F64, F64], result: STRING }, "insp.dyn": { argTypes: [DYN, F64, F64], result: STRING }, "insp.dynS": { argTypes: [DYN, F64], result: STRING }, + /** util.format over a dyn array of its arguments, and + * util.formatWithOptions(options, pack): the runtime formatter for format + * strings known at run time and spread arguments. */ + "insp.format": { argTypes: [DYN], result: STRING }, + /** insp.format over arguments that are all strings, numbers, booleans, + * null or undefined, without the checked-dynamic walk. */ + "insp.formatPrimitives": { argTypes: [DYN], result: STRING }, + "insp.formatWithOptions": { argTypes: [DYN, DYN], result: STRING }, + /** util.inspect(value, options, depth, colors) with the call's argument + * count: the runtime reads the options and renders the checked-dynamic + * view. */ + "insp.inspect": { argTypes: [DYN, DYN, DYN, DYN, F64], result: STRING }, + /** A [util.inspect.custom] method's call: whether ctx.customInspect is + * set, its depth argument (the remaining depth at recursion r of depth + * d), its options object and util.inspect, then its result rendered: a + * string re-indented at the current level, or a checked-dynamic value + * (customIsSelf tests for the instance itself). customInspect sets the + * flag for the renders that follow and answers the previous value, and + * depthKind does the same for how an unbounded depth was given (0 + * Infinity, 1 null, 2 undefined). */ + "insp.customEnabled": { argTypes: [], result: BOOL }, + /** Registers a class's compiled custom inspect function under its + * instances' typed-ref type key, for the checked-dynamic walk. */ + "insp.registerCustom": { argTypes: [STRING, DYN], result: VOID }, + "insp.customInspect": { argTypes: [BOOL], result: BOOL }, + "insp.depthKind": { argTypes: [F64], result: F64 }, + "insp.customDepth": { argTypes: [F64, F64], result: DYN }, + "insp.customOptions": { argTypes: [F64], result: DYN }, + "insp.inspectFunction": { argTypes: [], result: DYN }, + "insp.customText": { argTypes: [STRING], result: STRING }, + "insp.customIsSelf": { argTypes: [DYN, DYN], result: BOOL }, + "insp.customResult": { argTypes: [DYN, F64, F64], result: STRING }, + /** Whether a runtime format string has a '%' before its last character. */ + "insp.formatSpecifies": { argTypes: [STRING], result: BOOL }, + "insp.formatSpecifiesDyn": { argTypes: [DYN], result: BOOL }, "insp.begin": { argTypes: [F64], result: VOID }, - /** util.inspect's breakLength, compact (0 for false) and sorted for the + /** util.inspect's breakLength, compact (-1 for false) and sorted for the * renders that follow; the call site restores 80, 3 and false afterwards. */ "insp.layout": { argTypes: [F64, F64, BOOL], result: VOID }, "insp.entry": { argTypes: [STRING, BOOL], result: VOID }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7520-util-format-runtime.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7520-util-format-runtime.ts.json new file mode 100644 index 000000000..825fd9882 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7520-util-format-runtime.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7520-util-format-runtime.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7521-util-format-runtime.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7521-util-format-runtime.cjs.json new file mode 100644 index 000000000..0714d5c08 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7521-util-format-runtime.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7521-util-format-runtime.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7522-promisify-untyped.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7522-promisify-untyped.cjs.json new file mode 100644 index 000000000..bcd6b54c6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7522-promisify-untyped.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7522-promisify-untyped.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7523-promisify-untyped.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7523-promisify-untyped.ts.json new file mode 100644 index 000000000..20e96495a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7523-promisify-untyped.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7523-promisify-untyped.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7524-inspect-custom.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7524-inspect-custom.ts.json new file mode 100644 index 000000000..4f5a3d007 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7524-inspect-custom.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7524-inspect-custom.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7525-inspect-custom.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7525-inspect-custom.cjs.json new file mode 100644 index 000000000..dddad2358 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7525-inspect-custom.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7525-inspect-custom.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7526-inspect-options-runtime.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7526-inspect-options-runtime.ts.json new file mode 100644 index 000000000..70b202903 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7526-inspect-options-runtime.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7526-inspect-options-runtime.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inspect-custom-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inspect-custom-fences.ts.json new file mode 100644 index 000000000..dff7b7d6b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inspect-custom-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/inspect-custom-fences.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_console_native.c b/packages/runtime/src/scr_console_native.c index 8c7467127..492d57d82 100644 --- a/packages/runtime/src/scr_console_native.c +++ b/packages/runtime/src/scr_console_native.c @@ -523,7 +523,11 @@ ScrStr *scr_console_dir_dyn(ScrDyn *value, double depth) { scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return scr_str_new("", 0); } - return scr_insp_dyn(value, 0, depth); + /* console.dir is inspect with customInspect: false. */ + bool custom = scr_insp_custom_inspect(false); + ScrStr *out = scr_insp_dyn(value, 0, depth); + scr_insp_custom_inspect(custom); + return out; } /* ── console.table ─────────────────────────────────────────────────── diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 74b710dc9..fdb114535 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -863,6 +863,10 @@ ScrDyn *scr_dyn_open_iterator(const ScrDyn *value, int *kind, bool generators); * for-of protocol reads it. +1, or NULL with the error pending. */ ScrDyn *scr_dyn_iterator_result_get(const ScrDyn *result, const char *key, size_t len); void scr_dyn_install_iterator_symbol(ScrDyn *key); +/* Symbol.for('nodejs.util.inspect.custom'): the symbol unit installs it + * when the registry creates it; NULL (borrowed) until then. */ +void scr_dyn_install_inspect_custom_symbol(ScrDyn *key); +ScrDyn *scr_dyn_inspect_custom_symbol(void); void scr_dyn_install_async_iterator_symbol(ScrDyn *key); void scr_dyn_install_async_dispose_symbol(ScrDyn *key); /* True when key is Symbol.asyncDispose (registered by the symbol unit when diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index a4d124ac3..2c7951103 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -152,6 +152,154 @@ static size_t insp_width(const char *data, size_t len) { return width; } +/* ── colors (stylizeWithColor) ───────────────────────────────────────── */ + +/* ctx.colors for the render in progress: the checked-dynamic walk wraps + * each styled leaf in inspect.colors' SGR codes for its inspect.styles + * style, and the layout measures entries without the codes. Static + * renderings never run with it set. */ +static SCR_TL bool g_colors; + +typedef enum { + INSP_SPECIAL, + INSP_NUMBER, + INSP_BIGINT, + INSP_BOOLEAN, + INSP_UNDEFINED, + INSP_NULL, + INSP_STRING, + INSP_SYMBOL, + INSP_DATE, +} InspStyle; + +/* inspect.styles mapped through inspect.colors: [open, close]. */ +static const unsigned char insp_style_codes[][2] = { + {36, 39}, /* special: cyan */ + {33, 39}, /* number: yellow */ + {33, 39}, /* bigint: yellow */ + {33, 39}, /* boolean: yellow */ + {90, 39}, /* undefined: grey */ + {1, 22}, /* null: bold */ + {32, 39}, /* string: green */ + {32, 39}, /* symbol: green */ + {35, 39}, /* date: magenta */ +}; + +static void insp_style_open(InspBuf *b, InspStyle style) { + char code[8]; + int n = snprintf(code, sizeof code, "\x1b[%um", insp_style_codes[style][0]); + ib_bytes(b, code, (size_t)n); +} + +static void insp_style_close(InspBuf *b, InspStyle style) { + char code[8]; + int n = snprintf(code, sizeof code, "\x1b[%um", insp_style_codes[style][1]); + ib_bytes(b, code, (size_t)n); +} + +/* ctx.stylize(text, style): +1; moves `text` in. */ +static ScrStr *insp_styled(ScrStr *text, InspStyle style) { + if (!g_colors) return text; + InspBuf b = {0}; + insp_style_open(&b, style); + ib_str(&b, text); + insp_style_close(&b, style); + scr_str_release(text); + return ib_take(&b); +} + +static ScrStr *insp_styled_lit(const char *text, size_t len, InspStyle style) { + return insp_styled(scr_str_new(text, len), style); +} + +/* The SC2020 refusal for colored renderings of values whose styles this + * runtime does not produce; answers an empty string. */ +static ScrStr *insp_colors_fence(const char *what) { + InspBuf b = {0}; + ib_cstr(&b, "colored inspect output of "); + ib_cstr(&b, what); + ib_cstr(&b, " has no lowering"); + ScrStr *message = ib_take(&b); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); + return scr_str_new("", 0); +} + +static ScrStr *insp_colors_fence_values(const char *cls) { + InspBuf b = {0}; + ib_cstr(&b, cls); + ib_cstr(&b, " values"); + ScrStr *what = ib_take(&b); + ScrStr *out = insp_colors_fence(what->data); + scr_str_release(what); + return out; +} + +/* The SGR sequence Node's removeColors strips (\x1b[<1 or 2 digits>m) at + * data[i]: its length, or 0. */ +static size_t insp_sgr_at(const char *data, size_t len, size_t i) { + if (data[i] != '\x1b' || i + 3 >= len || data[i + 1] != '[') return 0; + size_t j = i + 2, digits = 0; + while (j < len && digits < 2 && data[j] >= '0' && data[j] <= '9') j++, digits++; + return digits > 0 && j < len && data[j] == 'm' ? j + 1 - i : 0; +} + +/* A control sequence stripVTControlCharacters removes at data[i] (CSI + * sequences after ESC [ or C1 CSI, and OSC sequences ended by BEL): its + * length, or 0. */ +static size_t insp_vt_at(const char *data, size_t len, size_t i) { + const unsigned char *p = (const unsigned char *)data; + size_t j; + if (p[i] == 0x1b && i + 1 < len && p[i + 1] == '[') j = i + 2; + else if (p[i] == 0xc2 && i + 1 < len && p[i + 1] == 0x9b) j = i + 2; + else if (p[i] == 0x1b && i + 1 < len && p[i + 1] == ']') { + for (j = i + 2; j < len; j++) + if (p[j] == 0x07) return j + 1 - i; + return 0; + } else return 0; + while (j < len && ((p[j] >= '0' && p[j] <= '9') || p[j] == ';' || p[j] == '?')) j++; + if (j >= len) return 0; + unsigned char c = p[j]; + bool final = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'P') || (c >= 'R' && c <= 'T') || + c == 'Z' || c == 'c' || (c >= 'f' && c <= 'n') || c == 'q' || c == 'r' || + c == 't' || c == 'y' || c == '=' || c == '>' || c == '<' || c == '~'; + return final ? j + 1 - i : 0; +} + +/* An entry's display width for the grid (getStringWidth(entry, + * ctx.colors)) and its UTF-16 length for the break-length test (Node's + * removeColors under ctx.colors). */ +static size_t insp_entry_width(const ScrStr *s) { + if (!g_colors) return insp_width(s->data, s->len); + size_t width = 0; + for (size_t i = 0; i < s->len;) { + size_t skip = insp_vt_at(s->data, s->len, i); + if (skip) { + i += skip; + continue; + } + uint32_t cp = insp_cp_at(s->data, s->len, &i); + if (insp_full_width(cp)) width += 2; + else if (!insp_zero_width(cp)) width += 1; + } + return width; +} + +static size_t insp_entry_len(const ScrStr *s) { + if (!g_colors) return insp_utf16_len(s->data, s->len); + size_t units = 0; + for (size_t i = 0; i < s->len;) { + size_t skip = insp_sgr_at(s->data, s->len, i); + if (skip) { + i += skip; + continue; + } + uint32_t cp = insp_cp_at(s->data, s->len, &i); + units += cp >= 0x10000 ? 2 : 1; + } + return units; +} + /* ── number rendering ────────────────────────────────────────────────── */ /* formatNumber: JS ToString EXCEPT -0 renders as '-0'. */ @@ -245,12 +393,12 @@ static SCR_TL size_t g_frames_cap; static SCR_TL size_t g_indent; /* ctx.indentationLvl */ static SCR_TL bool g_no_break; /* breakLength: Infinity (console.table cells) */ static SCR_TL double g_break_length = 80; /* ctx.breakLength */ -static SCR_TL double g_compact = 3; /* ctx.compact: a number, 0 for false */ +static SCR_TL double g_compact = 3; /* ctx.compact: a number, -1 for false */ static SCR_TL bool g_sorted; /* ctx.sorted === true */ static SCR_TL double g_cur_depth; /* ctx.currentDepth (last-entered composite) */ /* util.inspect's breakLength, compact and sorted for the renders that - * follow (compact false is 0); the call site restores 80, 3 and false + * follow (compact false is -1); the call site restores 80, 3 and false * after its render. */ void scr_insp_layout(double break_length, double compact, bool sorted) { g_break_length = break_length; @@ -369,7 +517,7 @@ void scr_insp_seen_push(const void *v) { ScrStr *scr_insp_circular(double id) { char buf[32]; int n = snprintf(buf, sizeof buf, "[Circular *%.0f]", id); - return scr_str_new(buf, (size_t)n); + return insp_styled_lit(buf, (size_t)n, INSP_SPECIAL); } /* Pops the value's seen entry after its frame closed. scr_insp_end already @@ -403,7 +551,7 @@ static void insp_group(InspFrame *f, bool trailing_more) { size_t *data_len = malloc((output_len ? output_len : 1) * sizeof(size_t)); if (!data_len) insp_oom(); for (size_t i = 0; i < output_len; i++) { - size_t w = insp_width(f->items[i]->data, f->items[i]->len); + size_t w = insp_entry_width(f->items[i]); data_len[i] = w; total_length += (double)w + 2; if ((double)w > max_length) max_length = (double)w; @@ -494,7 +642,7 @@ static bool insp_below_break_length(InspFrame *f, size_t start, const ScrStr *ba size_t total = f->len + start; if ((double)(total + f->len) > g_break_length) return false; for (size_t i = 0; i < f->len; i++) { - total += insp_utf16_len(f->items[i]->data, f->items[i]->len); + total += insp_entry_len(f->items[i]); if ((double)total > g_break_length) return false; } return base->len == 0 || !insp_contains_byte(base->data, base->len, '\n'); @@ -520,12 +668,16 @@ ScrStr *scr_insp_end(ScrStr *base, ScrStr *b0, ScrStr *b1, double recurse, bool int ref_id = f->seen ? insp_circ_id(f->seen) : 0; if (ref_id) { char buf[32]; - int n = snprintf(buf, sizeof buf, base->len ? " " : "", ref_id); - size_t len = (size_t)n + base->len; - ref_base = scr_str_alloc_raw(len, len); - memcpy(ref_base->data, buf, (size_t)n); - memcpy(ref_base->data + n, base->data, base->len); - ref_base->data[len] = '\0'; + int n = snprintf(buf, sizeof buf, "", ref_id); + InspBuf rb = {0}; + if (g_colors) insp_style_open(&rb, INSP_SPECIAL); + ib_bytes(&rb, buf, (size_t)n); + if (g_colors) insp_style_close(&rb, INSP_SPECIAL); + if (base->len) { + ib_char(&rb, ' '); + ib_str(&rb, base); + } + ref_base = ib_take(&rb); base = ref_base; } size_t entries = f->len; @@ -622,6 +774,19 @@ void scr_insp_nested_begin(void) { g_ncirc = 0; } +/* nested_end's restore alone: the caller's context returns. */ +void scr_insp_nested_restore(void) { + InspSaved *sv = &g_saved[--g_nsaved]; + free(g_seen); + g_indent = sv->indent; + g_cur_depth = sv->cur_depth; + g_seen = sv->seen; + g_nseen = sv->nseen; + g_seen_cap = sv->seen_cap; + memcpy(g_circ, sv->circ, sizeof g_circ); + g_ncirc = sv->ncirc; +} + ScrStr *scr_insp_nested_end(ScrStr *prefix, ScrStr *inner) { InspSaved *sv = &g_saved[--g_nsaved]; free(g_seen); @@ -717,13 +882,17 @@ ScrStr *scr_insp_str(ScrStr *s) { ib_cstr(&out, " +\n"); ib_spaces(&out, g_indent + 2); } + if (g_colors) insp_style_open(&out, INSP_STRING); insp_quote_into(&out, v + chunk_start, end - chunk_start); + if (g_colors) insp_style_close(&out, INSP_STRING); chunk_start = end; first = false; } } } else { + if (g_colors) insp_style_open(&out, INSP_STRING); insp_quote_into(&out, v, vlen); + if (g_colors) insp_style_close(&out, INSP_STRING); } if (trailer[0]) ib_cstr(&out, trailer); free(val.data); @@ -1007,7 +1176,9 @@ static void insp_key_into(InspBuf *b, const char *key, size_t len) { if (bare) { ib_bytes(b, key, len); } else { + if (g_colors) insp_style_open(b, INSP_STRING); insp_quote_into(b, key, len); + if (g_colors) insp_style_close(b, INSP_STRING); } } @@ -1330,7 +1501,7 @@ static ScrStr *scr_insp_dyn_collection(ScrDyn *d, double recurse, double depth, ib_char(&out, '['); ib_cstr(&out, label); ib_char(&out, ']'); - return ib_take(&out); + return insp_styled(ib_take(&out), INSP_SPECIAL); } ScrStr *base = ib_take(&head); scr_insp_begin(recurse + 1); @@ -1395,9 +1566,9 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char : scr_str_new("{}", 2); } if (recurse > depth) { - if (cls) return scr_insp_class_tag(cls, NULL); - return d->null_proto ? scr_str_new("[Object: null prototype]", 24) - : scr_str_new("[Object]", 8); + if (cls) return insp_styled(scr_insp_class_tag(cls, NULL), INSP_SPECIAL); + return d->null_proto ? insp_styled_lit("[Object: null prototype]", 24, INSP_SPECIAL) + : insp_styled_lit("[Object]", 8, INSP_SPECIAL); } scr_insp_begin(recurse + 1); for (size_t i = 0; i < d->v.obj.len; i++) { @@ -1412,6 +1583,7 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char ScrStr *entry = ib_take(&eb); scr_insp_entry(entry, false); scr_str_release(entry); + if (scr_exc_pending()) break; } for (size_t i = 0; i < nsymbols; i++) { ScrDynEntry *ent = scr_insp_symbol_entry(d, i); @@ -1455,12 +1627,12 @@ ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth) { if (recurse > depth) { char tag[32]; int n = snprintf(tag, sizeof tag, "[%s]", scr_bytes_elem_name(b->elem)); - return scr_str_new(tag, (size_t)n); + return insp_styled_lit(tag, (size_t)n, INSP_SPECIAL); } scr_insp_begin(recurse + 1); size_t shown = b->len < 100 ? b->len : 100; for (size_t i = 0; i < shown; i++) { - ScrStr *s = scr_insp_f64(scr_bytes_get(b, (double)i)); + ScrStr *s = insp_styled(scr_insp_f64(scr_bytes_get(b, (double)i)), INSP_NUMBER); scr_insp_entry(s, true); scr_str_release(s); } @@ -1480,27 +1652,374 @@ ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth) { return out; } +/* ── custom inspect functions (util.inspect.custom) ────────────────────── + * formatValue's hook: an object whose [util.inspect.custom] member is a + * function (other than util.inspect itself, on an object that is not its + * constructor's prototype) renders as that function's result. It runs + * before the circular and depth checks, with the remaining depth + * (ctx.depth - recurseTimes), the options object getUserOptions builds and + * util.inspect. A string result is re-indented at the current level; the + * object itself renders normally; any other value renders in its place. + * Class instances held as checked-dynamic values have compiled methods + * only, so one whose class defines the method throws SC2020. */ +static SCR_TL bool g_custom_inspect = true; /* ctx.customInspect */ +/* ctx.depth as the options gave it, for the custom functions' depth + * argument and options when the render's depth is unbounded: 0 the number + * g_depth_number (Infinity or NaN), 1 null, 2 undefined. */ +static SCR_TL int g_depth_kind = 1; +static SCR_TL double g_depth_number; +/* The options object that held keys inspect does not read: getUserOptions + * spreads it into the options the custom functions receive (borrowed for + * the render). */ +static SCR_TL const ScrDyn *g_user_options; + +double scr_insp_depth_kind(double kind) { + double previous = g_depth_kind; + g_depth_kind = (int)kind; + g_depth_number = INFINITY; + return previous; +} + +bool scr_insp_custom_inspect(bool enabled) { + bool previous = g_custom_inspect; + g_custom_inspect = enabled; + return previous; +} + +/* The style inspect.styles maps a styleType to, as [open, close] codes: + * the styles above plus regexp (red) and module (underline). false when + * the type has no style. */ +static bool insp_style_of(const ScrStr *type, unsigned *open, unsigned *close) { + static const struct { + const char *name; + unsigned open, close; + } styles[] = { + {"special", 36, 39}, {"number", 33, 39}, {"bigint", 33, 39}, {"boolean", 33, 39}, + {"undefined", 90, 39}, {"null", 1, 22}, {"string", 32, 39}, {"symbol", 32, 39}, + {"date", 35, 39}, {"regexp", 31, 39}, {"module", 4, 24}, + }; + for (size_t i = 0; i < sizeof styles / sizeof *styles; i++) { + if (type->len == strlen(styles[i].name) && memcmp(type->data, styles[i].name, type->len) == 0) { + *open = styles[i].open; + *close = styles[i].close; + return true; + } + } + return false; +} + +/* options.stylize: stylizeWithColor under colors (the text in its style's + * codes, or unchanged), stylizeNoColor otherwise (the argument itself). + * caps[0] holds whether colors apply. */ +static ScrDyn *insp_stylize_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrDyn *colors = scr_box_get_ref(closure->caps[0]); + bool colored = colors && scr_dyn_truthy(colors); + scr_dyn_release(colors); + ScrDyn *text = argc > 0 ? args[0] : (ScrDyn *)scr_dyn_undefined(); + if (!colored) return scr_dyn_retain(text); + ScrStr *str = scr_dyn_string_coerce_js(text); + if (!str) return NULL; + unsigned open, close; + ScrStr *type = argc > 1 && args[1]->kind == SCR_DYN_STR ? args[1]->v.str : NULL; + if (!type || !insp_style_of(type, &open, &close)) { + ScrDyn *out = scr_dyn_new_str(str); + scr_str_release(str); + return out; + } + InspBuf b = {0}; + char code[8]; + int n = snprintf(code, sizeof code, "\x1b[%um", open); + ib_bytes(&b, code, (size_t)n); + ib_str(&b, str); + n = snprintf(code, sizeof code, "\x1b[%um", close); + ib_bytes(&b, code, (size_t)n); + scr_str_release(str); + ScrStr *styled = ib_take(&b); + ScrDyn *out = scr_dyn_new_str(styled); + scr_str_release(styled); + return out; +} + +static ScrDyn *insp_inspect_call(ScrClosure *closure, ScrDyn *const *args, size_t argc); + +/* Whether `value` is the stylize function getUserOptions built for these + * colors. */ +static bool insp_own_stylize(const ScrDyn *value, bool colors) { + if (value->kind != SCR_DYN_FUNC || value->v.fn.thunk != insp_stylize_call) return false; + ScrDyn *flag = scr_box_get_ref(value->v.fn.clo->caps[0]); + bool colored = flag && scr_dyn_truthy(flag); + scr_dyn_release(flag); + return colored == colors; +} + +/* util.inspect as a function value (the custom functions' third + * argument): a fresh inspect context over the call's arguments, the + * caller's restored afterwards. One function per thread, so the argument + * compares equal to util.inspect. +1. */ +static SCR_TL ScrDyn *g_inspect_function; + +static void insp_inspect_function_cleanup(void) { + scr_dyn_release(g_inspect_function); + g_inspect_function = NULL; +} + +static ScrDyn *insp_inspect_value(void) { + if (!g_inspect_function) { + ScrClosure *closure = scr_closure_new(NULL, 0); + g_inspect_function = + scr_dyn_new_func(closure, insp_inspect_call, 2, "native:util.inspect", "inspect"); + scr_atexit(insp_inspect_function_cleanup); + } + return scr_dyn_retain(g_inspect_function); +} + +static void insp_opt_set(ScrDyn *options, const char *key, ScrDyn *value /* moves in */) { + scr_dyn_obj_set(options, key, strlen(key), value); +} + +/* getUserOptions(ctx) for the render in progress at `depth`. +1. */ +static ScrDyn *insp_user_options(double depth) { + ScrDyn *options = scr_dyn_new_obj(); + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(closure->caps[0], scr_dyn_new_bool(g_colors)); + insp_opt_set(options, "stylize", + scr_dyn_new_func(closure, insp_stylize_call, 2, "native:util.stylize", + g_colors ? "stylizeWithColor" : "stylizeNoColor")); + insp_opt_set(options, "showHidden", scr_dyn_new_bool(false)); + insp_opt_set(options, "depth", + isfinite(depth) ? scr_dyn_new_num(depth) + : g_depth_kind == 0 ? scr_dyn_new_num(g_depth_number) + : g_depth_kind == 1 ? scr_dyn_new_null() + : scr_dyn_retain(scr_dyn_undefined())); + insp_opt_set(options, "colors", scr_dyn_new_bool(g_colors)); + insp_opt_set(options, "customInspect", scr_dyn_new_bool(true)); + insp_opt_set(options, "showProxy", scr_dyn_new_bool(false)); + insp_opt_set(options, "maxArrayLength", scr_dyn_new_num(100)); + insp_opt_set(options, "maxStringLength", scr_dyn_new_num(10000)); + insp_opt_set(options, "breakLength", scr_dyn_new_num(g_break_length)); + insp_opt_set(options, "compact", g_compact < 0 ? scr_dyn_new_bool(false) : scr_dyn_new_num(g_compact)); + insp_opt_set(options, "sorted", scr_dyn_new_bool(g_sorted)); + insp_opt_set(options, "getters", scr_dyn_new_bool(false)); + insp_opt_set(options, "numericSeparator", scr_dyn_new_bool(false)); + if (g_user_options && g_user_options->kind == SCR_DYN_OBJ) { + for (size_t i = 0; i < g_user_options->v.obj.len; i++) { + const ScrDynEntry *ent = &g_user_options->v.obj.entries[i]; + if (!ent->enumerable) continue; + ScrDyn *value = scr_dyn_obj_read((ScrDyn *)g_user_options, ent->key, ent->key_len); + if (!value) { + scr_dyn_release(options); + return NULL; + } + scr_dyn_obj_set(options, ent->key, ent->key_len, value); + } + } + return options; +} + +/* The custom inspect functions of the program's classes, by the type key + * of their instances' typed refs: compiled functions (self, depth, + * options, inspect) that call the instance's method and answer its + * result as a checked-dynamic value. The program registers them before + * it runs. */ +typedef struct { + char *type_key; + ScrDyn *fn; +} InspCustomClass; + +static SCR_TL InspCustomClass *g_custom_classes; +static SCR_TL size_t g_ncustom_classes; + +static void insp_custom_classes_cleanup(void) { + for (size_t i = 0; i < g_ncustom_classes; i++) { + free(g_custom_classes[i].type_key); + scr_dyn_release(g_custom_classes[i].fn); + } + free(g_custom_classes); + g_custom_classes = NULL; + g_ncustom_classes = 0; +} + +void scr_insp_register_custom(ScrStr *type_key, ScrDyn *fn) { + if (g_ncustom_classes == 0) scr_atexit(insp_custom_classes_cleanup); + InspCustomClass *grown = + realloc(g_custom_classes, (g_ncustom_classes + 1) * sizeof *g_custom_classes); + if (!grown) insp_oom(); + g_custom_classes = grown; + char *key = malloc(type_key->len + 1); + if (!key) insp_oom(); + memcpy(key, type_key->data, type_key->len); + key[type_key->len] = '\0'; + g_custom_classes[g_ncustom_classes].type_key = key; + g_custom_classes[g_ncustom_classes].fn = scr_dyn_retain(fn); + g_ncustom_classes++; +} + +static ScrDyn *insp_custom_class_fn(const ScrDyn *d) { + for (size_t i = 0; i < g_ncustom_classes; i++) + if (strcmp(g_custom_classes[i].type_key, d->v.typed_ref.type_key) == 0) + return g_custom_classes[i].fn; + return NULL; +} + +/* The [util.inspect.custom] result for an object kind, or NULL when the + * value renders normally (*thrown reports a pending exception). +1. */ +static ScrStr *insp_custom(ScrDyn *d, double recurse, double depth, bool *thrown) { + *thrown = false; + if (!g_custom_inspect) return NULL; + ScrDyn *fn; + bool compiled = d->kind == SCR_DYN_TYPED_REF; + if (compiled) { + /* A class instance: its class's compiled function, when it has a + * custom inspect method. A class whose method has no compiled call + * (the compiler refuses such classes where it renders them) refuses + * here too. */ + fn = insp_custom_class_fn(d); + if (!fn) { + ScrStr *name = scr_str_new("%symbol:Symbol(nodejs.util.inspect.custom)", 42); + bool declared = scr_dyn_typed_ref_class_has(d, name); + scr_str_release(name); + if (!declared) return NULL; + static const char message[] = + "util.inspect of this class instance's [util.inspect.custom] method has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + *thrown = true; + return NULL; + } + fn = scr_dyn_retain(fn); + } else { + ScrDyn *key = scr_dyn_inspect_custom_symbol(); + if (!key) return NULL; + if (d->kind != SCR_DYN_OBJ && d->kind != SCR_DYN_ARR && d->kind != SCR_DYN_FUNC) return NULL; + fn = scr_dyn_reflect_get(d, key, d); + if (!fn) { + *thrown = true; + return NULL; + } + } + if (fn->kind != SCR_DYN_FUNC || fn->v.fn.thunk == insp_inspect_call) { + scr_dyn_release(fn); + return NULL; + } + /* A constructor's prototype object renders normally. */ + ScrDyn *ctor = !compiled && d->kind == SCR_DYN_OBJ ? scr_dyn_obj_read(d, "constructor", 11) : NULL; + if (ctor && ctor->kind == SCR_DYN_FUNC) { + ScrDyn *prototype = scr_dyn_fn_get(ctor, "prototype", 9); + bool own_prototype = prototype == d; + scr_dyn_release(prototype); + if (own_prototype) { + scr_dyn_release(ctor); + scr_dyn_release(fn); + return NULL; + } + } + scr_dyn_release(ctor); + ScrDyn *args[3]; + /* ctx.depth - recurseTimes, or null for a null depth. */ + args[0] = scr_insp_custom_depth(recurse, depth); + args[1] = insp_user_options(depth); + if (!args[1]) { + scr_dyn_release(args[0]); + scr_dyn_release(fn); + *thrown = true; + return NULL; + } + args[2] = insp_inspect_value(); + ScrDyn *ret; + if (compiled) { + ScrDyn *call[4] = {d, args[0], args[1], args[2]}; + ret = scr_dyn_call(fn, call, 4, "[nodejs.util.inspect.custom]"); + } else { + scr_dyn_this_push_dyn(d); + ret = scr_dyn_call(fn, args, 3, "[nodejs.util.inspect.custom]"); + scr_dyn_this_pop(); + } + for (size_t i = 0; i < 3; i++) scr_dyn_release(args[i]); + scr_dyn_release(fn); + if (!ret) { + *thrown = true; + return NULL; + } + if (ret == d || scr_insp_custom_is_self(ret, d)) { + scr_dyn_release(ret); + return NULL; + } + ScrStr *out; + if (ret->kind == SCR_DYN_STR) out = scr_insp_custom_text(ret->v.str); + else out = scr_insp_dyn(ret, recurse, depth); + scr_dyn_release(ret); + if (scr_exc_pending()) { + scr_str_release(out); + *thrown = true; + return NULL; + } + return out; +} + +bool scr_insp_custom_enabled(void) { return g_custom_inspect; } + +/* ctx.depth - recurseTimes, or null for a null depth (NaN for undefined). */ +ScrDyn *scr_insp_custom_depth(double recurse, double depth) { + if (isfinite(depth)) return scr_dyn_new_num(depth - recurse); + if (g_depth_kind == 0) return scr_dyn_new_num(g_depth_number - recurse); + return g_depth_kind == 1 ? scr_dyn_new_null() : scr_dyn_new_num(NAN); +} + +ScrDyn *scr_insp_custom_options(double depth) { return insp_user_options(depth); } + +ScrDyn *scr_insp_inspect_function(void) { return insp_inspect_value(); } + +bool scr_insp_custom_is_self(ScrDyn *result, ScrDyn *self) { + return result->kind == SCR_DYN_TYPED_REF && self->kind == SCR_DYN_TYPED_REF && + result->v.typed_ref.ptr == self->v.typed_ref.ptr; +} + +ScrStr *scr_insp_custom_result(ScrDyn *result, double recurse, double depth) { + if (result->kind == SCR_DYN_STR) return scr_insp_custom_text(result->v.str); + return scr_insp_dyn(result, recurse, depth); +} + +/* A custom function's string result: each newline followed by the + * current indentation. +1. */ +ScrStr *scr_insp_custom_text(ScrStr *text) { + if (!memchr(text->data, '\n', text->len)) return scr_str_retain(text); + InspBuf out = {0}; + for (size_t i = 0; i < text->len; i++) { + ib_char(&out, text->data[i]); + if (text->data[i] == '\n') ib_spaces(&out, g_indent); + } + return ib_take(&out); +} + ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { if (d->kind == SCR_DYN_PROXY) return scr_insp_dyn(d->v.proxy.target, recurse, depth); + if ((d->kind == SCR_DYN_TYPED_REF ? g_ncustom_classes > 0 : scr_dyn_inspect_custom_symbol() != NULL) && + (d->kind == SCR_DYN_OBJ || d->kind == SCR_DYN_ARR || d->kind == SCR_DYN_FUNC || + d->kind == SCR_DYN_TYPED_REF)) { + bool thrown; + ScrStr *custom = insp_custom(d, recurse, depth, &thrown); + if (custom) return custom; + if (thrown) return scr_str_new("", 0); + } switch (d->kind) { case SCR_DYN_NULL: - return scr_str_new("null", 4); + return insp_styled_lit("null", 4, INSP_NULL); case SCR_DYN_UNDEF: - return scr_str_new("undefined", 9); + return insp_styled_lit("undefined", 9, INSP_UNDEFINED); case SCR_DYN_BOOL: - return d->v.b ? scr_str_new("true", 4) : scr_str_new("false", 5); + return d->v.b ? insp_styled_lit("true", 4, INSP_BOOLEAN) : insp_styled_lit("false", 5, INSP_BOOLEAN); case SCR_DYN_BIGINT: - return scr_bigint_inspect(d->v.bigint); + return insp_styled(scr_bigint_inspect(d->v.bigint), INSP_BIGINT); case SCR_DYN_SYMBOL: - return d->v.symbol.render(d->v.symbol.value); + return insp_styled(d->v.symbol.render(d->v.symbol.value), INSP_SYMBOL); case SCR_DYN_NUM: - return scr_insp_f64(d->v.num); + return insp_styled(scr_insp_f64(d->v.num), INSP_NUMBER); case SCR_DYN_STR: 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); - if (recurse > depth) return scr_str_new("[Array]", 7); + if (recurse > depth) return insp_styled_lit("[Array]", 7, INSP_SPECIAL); scr_insp_begin(recurse + 1); /* Node's maxArrayLength (100) counts output entries; a run of holes * is one `` entry (formatSpecialArray). */ @@ -1512,7 +2031,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { while (next < len && !presence[next]) next++; char hole[48]; int hn = snprintf(hole, sizeof hole, "<%zu empty item%s>", next - i, next - i == 1 ? "" : "s"); - ScrStr *s = scr_str_new(hole, (size_t)hn); + ScrStr *s = insp_styled_lit(hole, (size_t)hn, INSP_UNDEFINED); scr_insp_entry(s, false); scr_str_release(s); i = next; @@ -1522,6 +2041,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { ScrStr *s = scr_insp_dyn(item, recurse + 1, depth); scr_insp_entry(s, item->kind == SCR_DYN_NUM); scr_str_release(s); + if (scr_exc_pending()) break; } bool more = i < len; if (more) { @@ -1542,7 +2062,10 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { /* An Error carried as a checked-dynamic value (a caught error, an * Error stored in an `unknown` slot): the error's own rendering, * never the encoding's internal marker. */ - if (scr_dyn_obj_get(d, "%error", 6) != NULL) return scr_insp_dyn_error(d, recurse, depth); + if (scr_dyn_obj_get(d, "%error", 6) != NULL) { + if (g_colors) return insp_colors_fence("Error values"); + return scr_insp_dyn_error(d, recurse, depth); + } /* Object.create(null)'s dictionary: Node prefixes the rendering * with "[Object: null prototype]" (formatValue's constructor-less * base), the empty form included, and the beyond-depth answer IS @@ -1562,9 +2085,12 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } else { ib_cstr(&out, "[Function (anonymous)]"); } - return ib_take(&out); + return insp_styled(ib_take(&out), INSP_SPECIAL); } case SCR_DYN_HANDLE: { + if (g_colors && d->v.handle.tag != SCR_DYNH_SET && d->v.handle.tag != SCR_DYNH_MAP && + d->v.handle.tag != SCR_DYNH_DATE) + return insp_colors_fence_values(scr_dyn_handle_cls(d)); if (d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER) { return scr_insp_array_buffer(d->v.handle.ptr, scr_dyn_handle_cls(d), recurse, depth); @@ -1574,7 +2100,8 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { if (d->v.handle.tag == SCR_DYNH_WEAK_MAP || d->v.handle.tag == SCR_DYNH_WEAK_SET) return scr_insp_weak_collection(d->v.handle.tag == SCR_DYNH_WEAK_MAP ? "WeakMap" : "WeakSet", recurse, depth); - if (d->v.handle.tag == SCR_DYNH_DATE) return scr_insp_date(scr_dyn_native_date_value(d)); + if (d->v.handle.tag == SCR_DYNH_DATE) + return insp_styled(scr_insp_date(scr_dyn_native_date_value(d)), INSP_DATE); if (d->v.handle.tag == SCR_DYNH_ITERATOR) { ScrStr *iterator = scr_insp_native_iterator(d, recurse, depth); if (iterator) return iterator; @@ -1623,9 +2150,14 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } case SCR_DYN_TYPED_REF: { + if (g_colors && scr_insp_is_generator(d)) return insp_colors_fence("generator objects"); if (scr_insp_is_generator(d)) return scr_insp_generator(d); const char *cls = NULL; ScrDyn *view = scr_event_ref_hooks ? scr_event_ref_hooks->inspect_view(d, &cls) : NULL; + if (view && g_colors) { + scr_dyn_release(view); + return insp_colors_fence_values(cls); + } if (view) { /* The handle rule above: the class name past the depth limit. */ if (recurse > depth) { @@ -1655,6 +2187,11 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { 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 && g_colors) { + scr_str_release(date_class); + scr_dyn_release(materialized); + return insp_colors_fence("Date subclass instances"); + } 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 @@ -1715,6 +2252,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } case SCR_DYN_BYTES: { ScrBytes *b = d->v.bytes; + if (b->is_data_view && g_colors) return insp_colors_fence("DataView values"); if (b->is_data_view) return scr_insp_data_view(b, recurse, depth); if (d->buffer) { /* Buffer-flavored (a stream chunk): Node's form. */ @@ -1734,6 +2272,535 @@ ScrStr *scr_insp_dyn_s(ScrDyn *d, double depth) { return scr_insp_dyn(d, 0, depth); } +/* ── util.format over checked-dynamic values ───────────────────────────── + * formatWithOptionsInternal over argument values whose types are known + * only at run time: util.format and util.formatWithOptions with a runtime + * format string or spread arguments, console output with a runtime format + * string, and debuglog's lines. The %-substitutions (%s %d %i %f %j %o %O + * %c %%) follow Node's argument cursor, then the remaining arguments join + * with spaces, strings verbatim and everything else inspected under the + * options. With colors, primitives take inspect's default styles; colored + * object renderings have no lowering. These entries call program code + * (toString, valueOf and toJSON), so unlike the rest of this file they + * can throw: NULL with the exception pending. */ +static void insp_fmt_fence(const ScrInspFormatOptions *o, const char *what) { + InspBuf b = {0}; + ib_cstr(&b, o->surface); + ib_char(&b, ' '); + ib_cstr(&b, what); + ScrStr *message = ib_take(&b); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); +} + +static bool insp_fmt_object(const ScrDyn *value) { + switch (value->kind) { + case SCR_DYN_OBJ: + case SCR_DYN_ARR: + case SCR_DYN_FUNC: + case SCR_DYN_BYTES: + case SCR_DYN_HANDLE: + case SCR_DYN_PROMISE: + case SCR_DYN_TYPED_REF: + case SCR_DYN_PROXY: + return true; + default: + return false; + } +} + +void scr_insp_format_defaults(ScrInspFormatOptions *o, const char *surface) { + o->surface = surface; + o->depth = 2; + o->depth_kind = 0; + o->depth_number = 2; + o->colors = false; + o->custom_inspect = true; + o->break_length = 80; + o->compact = 3; + o->sorted = false; + o->user_options = NULL; +} + +/* inspect(value, options) at the given depth, compact and colors, with + * the options' breakLength and sorted; the previous layout returns + * afterwards. +1, or NULL with the exception pending. */ +static ScrStr *insp_fmt_inspect(ScrDyn *value, const ScrInspFormatOptions *o, double depth, + double compact, bool colors) { + double break_length = g_break_length, saved_compact = g_compact; + bool sorted = g_sorted, saved_colors = g_colors, saved_custom = g_custom_inspect; + int saved_depth_kind = g_depth_kind; + double saved_depth_number = g_depth_number; + const ScrDyn *saved_user = g_user_options; + scr_insp_layout(o->break_length, compact, o->sorted); + g_colors = colors; + g_custom_inspect = o->custom_inspect; + g_depth_kind = o->depth_kind; + g_depth_number = o->depth_number; + g_user_options = o->user_options; + ScrStr *text = scr_insp_dyn(value, 0, depth); + g_colors = saved_colors; + g_custom_inspect = saved_custom; + g_depth_kind = saved_depth_kind; + g_depth_number = saved_depth_number; + g_user_options = saved_user; + scr_insp_layout(break_length, saved_compact, sorted); + if (scr_exc_pending()) { + scr_str_release(text); + return NULL; + } + return text; +} + +/* hasBuiltInToString's negative answer: a toString the program defined on + * the object or its prototype chain (1). Symbol-keyed members (where + * Symbol.toPrimitive would also count) are reported as unknown (-1), and a + * class instance whose class defines toString as -2: its methods are + * compiled for the class's own calls, which this runtime cannot make. */ +static int insp_fmt_custom_to_string(const ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) { + ScrStr *to_string = scr_str_new("toString", 8); + bool has = scr_dyn_typed_ref_class_has(value, to_string); + scr_str_release(to_string); + return has ? -2 : 0; + } + for (const ScrDyn *current = value; current && current->kind == SCR_DYN_OBJ; current = current->prototype) { + if (current->symbol_properties) return -1; + const ScrDyn *member = scr_dyn_obj_get(current, "toString", 8); + if (member) return member->kind == SCR_DYN_FUNC; + } + return 0; +} + +static ScrStr *insp_fmt_s(ScrDyn *value, const ScrInspFormatOptions *o) { + if (value->kind == SCR_DYN_STR) return scr_str_retain(value->v.str); + if (value->kind == SCR_DYN_NUM) return scr_insp_f64(value->v.num); + if (value->kind == SCR_DYN_BIGINT) return scr_bigint_inspect(value->v.bigint); + if (value->kind == SCR_DYN_FUNC) { + insp_fmt_fence(o, "%s of a function prints its source text, which a compiled binary does not keep"); + return NULL; + } + if ((value->kind == SCR_DYN_OBJ && scr_dyn_obj_get(value, "%error", 6) == NULL) || + value->kind == SCR_DYN_TYPED_REF) { + int custom = insp_fmt_custom_to_string(value); + if (custom == -2) { + insp_fmt_fence(o, "%s of a class instance whose class defines toString has no lowering"); + return NULL; + } + if (custom < 0) { + insp_fmt_fence(o, "%s of objects with symbol-keyed members has no lowering"); + return NULL; + } + if (custom) return scr_dyn_string_coerce_js(value); + } + if (!insp_fmt_object(value)) return scr_insp_dyn(value, 0, 0); + /* inspect(arg, { ...inspectOptions, depth: 0, colors: false, compact: 3 }) */ + return insp_fmt_inspect(value, o, 0, 3, false); +} + +static ScrStr *insp_fmt_number(ScrDyn *value, int spec) { + if (value->kind == SCR_DYN_BIGINT && spec != 'f') return scr_bigint_inspect(value->v.bigint); + if (value->kind == SCR_DYN_SYMBOL) return scr_str_new("NaN", 3); + double number; + if (spec == 'd') { + if (!scr_dyn_number_coerce_js(value, &number)) return NULL; + } else { + ScrStr *text = scr_dyn_string_coerce_js(value); + if (!text) return NULL; + number = spec == 'i' ? scr_parse_int(text, 0) : scr_parse_float(text); + scr_str_release(text); + } + return scr_insp_f64(number); +} + +/* One formatter's conversions: %s, %j, %d/%i/%f, %O/%o and the remaining + * arguments' inspect (depth -1: the options' depth). The loop references + * only the table it runs, so a program links only the conversions it uses. */ +typedef struct { + ScrStr *(*s)(ScrDyn *value, const ScrInspFormatOptions *o); + ScrStr *(*j)(const ScrDyn *value); + ScrStr *(*number)(ScrDyn *value, int spec); + ScrStr *(*inspect)(ScrDyn *value, const ScrInspFormatOptions *o, double depth); +} InspFmtOps; + +static ScrStr *insp_format_loop(ScrDyn *const *args, size_t argc, const ScrInspFormatOptions *o, + const InspFmtOps *ops) { + if (argc == 0) return scr_str_new("", 0); + InspBuf out = {0}; + size_t a = 0; + const char *join = ""; + if (args[0]->kind == SCR_DYN_STR) { + const ScrStr *first = args[0]->v.str; + if (argc == 1) return scr_str_retain((ScrStr *)first); + const char *text = first->data; + size_t last = 0; + for (size_t i = 0; first->len > 0 && i < first->len - 1; i++) { + if (text[i] != '%') continue; + int next = (unsigned char)text[++i]; + if (a + 1 != argc) { + ScrStr *piece = NULL; + switch (next) { + case 's': piece = ops->s(args[++a], o); break; + case 'j': piece = ops->j(args[++a]); break; + case 'd': + case 'i': + case 'f': piece = ops->number(args[++a], next); break; + case 'O': piece = ops->inspect(args[++a], o, o->depth); break; + case 'o': + if (insp_fmt_object(args[a + 1])) { + insp_fmt_fence(o, "%o of objects renders hidden properties, which have no lowering; use %O"); + break; + } + piece = ops->inspect(args[++a], o, 4); + break; + case 'c': + a += 1; + piece = scr_str_new("", 0); + break; + case '%': + ib_bytes(&out, text + last, i - last); + last = i + 1; + continue; + default: + continue; + } + if (!piece) { + free(out.data); + return NULL; + } + if (last != i - 1) ib_bytes(&out, text + last, i - 1 - last); + ib_str(&out, piece); + scr_str_release(piece); + last = i + 1; + } else if (next == '%') { + ib_bytes(&out, text + last, i - last); + last = i + 1; + } + } + if (last != 0) { + a++; + join = " "; + if (last < first->len) ib_bytes(&out, text + last, first->len - last); + } + } + for (; a < argc; a++) { + ib_cstr(&out, join); + if (args[a]->kind == SCR_DYN_STR) { + ib_str(&out, args[a]->v.str); + } else { + ScrStr *piece = ops->inspect(args[a], o, o->depth); + if (!piece) { + free(out.data); + return NULL; + } + ib_str(&out, piece); + scr_str_release(piece); + } + join = " "; + } + return ib_take(&out); +} + + +static ScrStr *insp_fmt_inspect_full(ScrDyn *value, const ScrInspFormatOptions *o, double depth) { + return insp_fmt_inspect(value, o, depth, o->compact, o->colors); +} + +static const InspFmtOps insp_fmt_full_ops = {insp_fmt_s, scr_util_format_j, insp_fmt_number, + insp_fmt_inspect_full}; + +ScrStr *scr_insp_format(ScrDyn *const *args, size_t argc, const ScrInspFormatOptions *o) { + return insp_format_loop(args, argc, o, &insp_fmt_full_ops); +} + +/* The formatter over arguments that are all strings, numbers, booleans, + * null or undefined (the compiler proves it from their types): the same + * rules for those kinds, without the checked-dynamic walk the other + * values need. */ +static ScrStr *insp_prim_to_string(const ScrDyn *v) { + switch (v->kind) { + case SCR_DYN_STR: return scr_str_retain(v->v.str); + case SCR_DYN_NUM: { + char buf[40]; + size_t n = scr_f64_to_str(v->v.num, buf); + return scr_str_new(buf, n); + } + case SCR_DYN_BOOL: return v->v.b ? scr_str_new("true", 4) : scr_str_new("false", 5); + case SCR_DYN_NULL: return scr_str_new("null", 4); + default: return scr_str_new("undefined", 9); + } +} + +static ScrStr *insp_prim_inspect(ScrDyn *v, const ScrInspFormatOptions *o, double depth) { + (void)o; + (void)depth; + if (v->kind == SCR_DYN_STR) return scr_insp_str(v->v.str); + if (v->kind == SCR_DYN_NUM) return scr_insp_f64(v->v.num); + return insp_prim_to_string(v); +} + +static ScrStr *insp_prim_s(ScrDyn *v, const ScrInspFormatOptions *o) { + (void)o; + if (v->kind == SCR_DYN_NUM) return scr_insp_f64(v->v.num); + return insp_prim_to_string(v); +} + +static ScrStr *insp_prim_j(const ScrDyn *v) { + if (v->kind == SCR_DYN_STR) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_put_json_str(&b, v->v.str); + return scr_jb_finish(&b); + } + if (v->kind == SCR_DYN_NUM && !isfinite(v->v.num)) return scr_str_new("null", 4); + return insp_prim_to_string(v); +} + +static ScrStr *insp_prim_number(ScrDyn *v, int spec) { + double number; + if (spec == 'd') { + number = v->kind == SCR_DYN_NUM ? v->v.num + : v->kind == SCR_DYN_STR ? scr_string_to_number(v->v.str) + : v->kind == SCR_DYN_BOOL ? (v->v.b ? 1 : 0) + : v->kind == SCR_DYN_NULL ? 0 + : NAN; + } else { + ScrStr *text = insp_prim_to_string(v); + number = spec == 'i' ? scr_parse_int(text, 0) : scr_parse_float(text); + scr_str_release(text); + } + return scr_insp_f64(number); +} + +static const InspFmtOps insp_fmt_primitive_ops = {insp_prim_s, insp_prim_j, insp_prim_number, + insp_prim_inspect}; + +ScrStr *scr_insp_format_primitives(ScrDyn *pack) { + ScrInspFormatOptions o; + scr_insp_format_defaults(&o, "util.format"); + if (pack->kind != SCR_DYN_ARR) return scr_str_new("", 0); + return insp_format_loop(pack->v.arr.items, pack->v.arr.len, &o, &insp_fmt_primitive_ops); +} + +/* The arguments of a dyn array pack (+1 text, or NULL pending). */ +static ScrStr *insp_fmt_pack(ScrDyn *pack, const ScrInspFormatOptions *o) { + if (pack->kind != SCR_DYN_ARR) return scr_insp_format(NULL, 0, o); + return scr_insp_format(pack->v.arr.items, pack->v.arr.len, o); +} + +ScrStr *scr_insp_format_pack(ScrDyn *pack) { + ScrInspFormatOptions o; + scr_insp_format_defaults(&o, "util.format"); + return insp_fmt_pack(pack, &o); +} + +/* Whether a format string can substitute: a '%' before its last character. + * A string without one formats as its arguments joined with spaces. */ +bool scr_insp_format_specifies(ScrStr *first) { + return first->len > 1 && memchr(first->data, '%', first->len - 1) != NULL; +} + +bool scr_insp_format_specifies_dyn(ScrDyn *first) { + return first->kind == SCR_DYN_STR && scr_insp_format_specifies(first->v.str); +} + +/* One own enumerable option of an inspect options object: +1 value, a + * borrowed undefined when absent, or NULL with the exception pending. */ +static ScrDyn *insp_opt(const ScrDyn *options, const char *key, bool *present) { + size_t len = strlen(key); + *present = options->kind == SCR_DYN_OBJ && scr_dyn_obj_get(options, key, len) != NULL && + scr_dyn_obj_enumerable(options, key, len); + if (!*present) return scr_dyn_retain(scr_dyn_undefined()); + return scr_dyn_obj_read(options, key, len); +} + +/* ctx.depth from an option or inspect's third argument: a number, null + * or undefined (no limit for null, undefined and NaN). */ +static bool insp_depth_option(const ScrDyn *value, ScrInspFormatOptions *o) { + if (value->kind == SCR_DYN_NUM) { + o->depth = isnan(value->v.num) ? INFINITY : value->v.num; + o->depth_kind = 0; + o->depth_number = value->v.num; + } else if (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF) { + o->depth = INFINITY; + o->depth_kind = value->kind == SCR_DYN_NULL ? 1 : 2; + } else { + return false; + } + return true; +} + +/* inspect's option keys (inspectDefaultOptions and stylize) over an + * options object, as `inspect(value, options)` copies them. Options at + * their defaults, depth, colors, compact (a number or false), + * breakLength (a number) and sorted (a boolean) lower; other values of + * the known options throw the SC2020 refusal. */ +bool scr_insp_format_read_options(const ScrDyn *options, ScrInspFormatOptions *o) { + ScrDyn *view = NULL; + if (options->kind == SCR_DYN_TYPED_REF) { + view = scr_dyn_typed_ref_materialize(options); + if (scr_exc_pending()) { + scr_dyn_release(view); + return false; + } + options = view; + } + bool ok = false, present; + ScrDyn *value = NULL; +#define INSP_OPT(key) \ + do { \ + scr_dyn_release(value); \ + value = insp_opt(options, key, &present); \ + if (!value) goto done; \ + } while (0) + INSP_OPT("depth"); + if (present) { + if (!insp_depth_option(value, o)) { + insp_fmt_fence(o, "with a depth option that is not a number or null has no lowering"); + goto done; + } + } + INSP_OPT("colors"); + if (present) o->colors = scr_dyn_truthy(value); + INSP_OPT("customInspect"); + if (present) o->custom_inspect = scr_dyn_truthy(value); + INSP_OPT("compact"); + if (present) { + if (value->kind == SCR_DYN_NUM) o->compact = value->v.num < 0 ? 0 : value->v.num; + else if (value->kind == SCR_DYN_BOOL && !value->v.b) o->compact = -1; + else { + insp_fmt_fence(o, "with this compact option has no lowering (compact must be false or a number)"); + goto done; + } + } + INSP_OPT("breakLength"); + if (present) { + if (value->kind != SCR_DYN_NUM) { + insp_fmt_fence(o, "with a breakLength option that is not a number has no lowering"); + goto done; + } + o->break_length = value->v.num; + } + INSP_OPT("sorted"); + if (present) { + if (value->kind != SCR_DYN_BOOL) { + insp_fmt_fence(o, "with a sorted option that is not a boolean has no lowering"); + goto done; + } + o->sorted = value->v.b; + } + /* The options whose non-default values change renderings this runtime + * does not produce. */ + static const struct { + const char *key; + int kind; /* 0: falsy, 1: true, 2: the number 100, 3: the number 10000 */ + } fixed[] = { + {"showHidden", 0}, {"showProxy", 0}, {"maxArrayLength", 2}, {"maxStringLength", 3}, + {"getters", 0}, {"numericSeparator", 0}, {"stylize", 0}, + }; + for (size_t i = 0; i < sizeof fixed / sizeof *fixed; i++) { + INSP_OPT(fixed[i].key); + if (!present) continue; + bool at_default = fixed[i].kind == 0 ? !scr_dyn_truthy(value) + : fixed[i].kind == 1 ? value->kind == SCR_DYN_BOOL && value->v.b + : value->kind == SCR_DYN_NUM && value->v.num == (fixed[i].kind == 2 ? 100 : 10000); + /* The stylize function a custom inspect function received (options + * passed back into util.inspect) is the default for its colors. */ + if (!at_default && strcmp(fixed[i].key, "stylize") == 0 && insp_own_stylize(value, o->colors)) + at_default = true; + /* stylize is replaced by the color stylizer when colors is set. */ + if (!at_default && !(o->colors && strcmp(fixed[i].key, "stylize") == 0)) { + InspBuf b = {0}; + ib_cstr(&b, "with the "); + ib_cstr(&b, fixed[i].key); + ib_cstr(&b, " option has no lowering"); + ScrStr *what = ib_take(&b); + insp_fmt_fence(o, what->data); + scr_str_release(what); + goto done; + } + } +#undef INSP_OPT + /* Keys inspect does not read: the custom functions receive them. */ + if (!view && options->kind == SCR_DYN_OBJ) { + static const char *const known[] = { + "showHidden", "depth", "colors", "customInspect", "showProxy", + "maxArrayLength", "maxStringLength", "breakLength", "compact", "sorted", + "getters", "numericSeparator", "stylize", + }; + for (size_t i = 0; i < options->v.obj.len && !o->user_options; i++) { + const ScrDynEntry *ent = &options->v.obj.entries[i]; + if (!ent->enumerable) continue; + bool read = false; + for (size_t k = 0; k < sizeof known / sizeof *known && !read; k++) + read = ent->key_len == strlen(known[k]) && memcmp(ent->key, known[k], ent->key_len) == 0; + if (!read) o->user_options = options; + } + } + ok = true; +done: + scr_dyn_release(value); + scr_dyn_release(view); + return ok; +} + +/* util.inspect(value[, options]) and inspect(value, showHidden, depth, + * colors) over checked-dynamic arguments; `argc` is the call's argument + * count. A third argument other than undefined sets the depth and a + * fourth the colors; a boolean second argument is showHidden, and an + * object supplies the options, which apply after the legacy arguments. + * +1, or NULL with the exception pending. */ +ScrStr *scr_insp_inspect(ScrDyn *value, ScrDyn *options, ScrDyn *depth, ScrDyn *colors, + double argc) { + ScrInspFormatOptions o; + scr_insp_format_defaults(&o, "util.inspect"); + if (argc >= 3 && depth->kind != SCR_DYN_UNDEF) { + if (!insp_depth_option(depth, &o)) { + insp_fmt_fence(&o, "with a depth argument that is not a number or null has no lowering"); + return NULL; + } + } + if (argc >= 4 && colors->kind != SCR_DYN_UNDEF) o.colors = scr_dyn_truthy(colors); + if (options->kind == SCR_DYN_BOOL) { + if (options->v.b) { + insp_fmt_fence(&o, "with showHidden has no lowering"); + return NULL; + } + } else if (options->kind == SCR_DYN_OBJ || options->kind == SCR_DYN_TYPED_REF) { + if (!scr_insp_format_read_options(options, &o)) return NULL; + } + return insp_fmt_inspect(value, &o, o.depth, o.compact, o.colors); +} + +static ScrDyn *insp_inspect_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *undefined = scr_dyn_undefined(); + scr_insp_nested_begin(); + ScrStr *text = scr_insp_inspect(argc > 0 ? args[0] : undefined, argc > 1 ? args[1] : undefined, + argc > 2 ? args[2] : undefined, argc > 3 ? args[3] : undefined, + (double)argc); + scr_insp_nested_restore(); + if (!text) return NULL; + ScrDyn *out = scr_dyn_new_str(text); + scr_str_release(text); + return out; +} + +/* util.formatWithOptions(inspectOptions, ...pack): validateObject (arrays + * pass, null and functions do not), then the options as inspect reads + * them. */ +ScrStr *scr_insp_format_with_options(ScrDyn *options, ScrDyn *pack) { + if (options->kind != SCR_DYN_OBJ && options->kind != SCR_DYN_ARR && + options->kind != SCR_DYN_TYPED_REF) { + scr_dyn_arg_type_fail("inspectOptions", "of type object", options); + return NULL; + } + ScrInspFormatOptions o; + scr_insp_format_defaults(&o, "util.formatWithOptions"); + if (!scr_insp_format_read_options(options, &o)) return NULL; + return insp_fmt_pack(pack, &o); +} + /* ── console.table cells ─────────────────────────────────────────────── */ size_t scr_insp_display_width(const char *data, size_t len) { return insp_width(data, len); } diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h index 5ac3aa9dd..a95ac1fde 100644 --- a/packages/runtime/src/scr_inspect.h +++ b/packages/runtime/src/scr_inspect.h @@ -47,8 +47,50 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth); ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth); /* format's %s / rest-args twin: dyn strings pass verbatim. */ ScrStr *scr_insp_dyn_s(ScrDyn *d, double depth); +/* util.format over checked-dynamic arguments (formatWithOptionsInternal): + * the options an inspect of each argument takes, and the API name the + * SC2020 refusals name. These entries call program code, so they answer + * NULL with the exception pending when it throws. */ +typedef struct ScrInspFormatOptions { + const char *surface; + double depth; /* Infinity for null, undefined and NaN */ + /* The depth as the options gave it, which custom inspect functions + * receive when the depth is unbounded: 0 a number (depth_number, + * Infinity or NaN), 1 null, 2 undefined. */ + int depth_kind; + double depth_number; + bool colors; + bool custom_inspect; + double break_length; + double compact; /* -1 for false */ + bool sorted; + /* The options object, when it holds keys inspect does not read: custom + * inspect functions receive them (borrowed; NULL otherwise). */ + const ScrDyn *user_options; +} ScrInspFormatOptions; +void scr_insp_format_defaults(ScrInspFormatOptions *o, const char *surface); +/* Reads inspect's options from an options object into o; false with the + * SC2020 refusal pending for values that have no lowering. */ +bool scr_insp_format_read_options(const ScrDyn *options, ScrInspFormatOptions *o); +ScrStr *scr_insp_format(ScrDyn *const *args, size_t argc, const ScrInspFormatOptions *o); +/* util.format(...pack) and util.formatWithOptions(options, ...pack) over a + * dyn array of the arguments (+1). */ +ScrStr *scr_insp_format_pack(ScrDyn *pack); +/* util.format(...pack) when every argument is a string, number, boolean, + * null or undefined, without the checked-dynamic walk (+1). */ +ScrStr *scr_insp_format_primitives(ScrDyn *pack); +ScrStr *scr_insp_format_with_options(ScrDyn *options, ScrDyn *pack); +/* util.inspect(value, options, depth, colors) over checked-dynamic + * arguments with Node's legacy-argument rules; argc is the call's + * argument count (+1, or NULL with the exception pending). */ +ScrStr *scr_insp_inspect(ScrDyn *value, ScrDyn *options, ScrDyn *depth, ScrDyn *colors, + double argc); +/* Whether a format string has a '%' before its last character, the only + * strings whose substitutions can change the output. */ +bool scr_insp_format_specifies(ScrStr *first); +bool scr_insp_format_specifies_dyn(ScrDyn *first); void scr_insp_begin(double recurse); -/* util.inspect's breakLength, compact (0 for false) and sorted for the +/* util.inspect's breakLength, compact (-1 for false) and sorted for the * renders that follow; the call site restores the defaults (80, 3, false) * afterwards. */ void scr_insp_layout(double break_length, double compact, bool sorted); @@ -75,6 +117,33 @@ ScrStr *scr_insp_end(ScrStr *base, ScrStr *b0, ScrStr *b1, double recurse, * caller's level (+1; borrows both). */ void scr_insp_nested_begin(void); ScrStr *scr_insp_nested_end(ScrStr *prefix, ScrStr *inner); +/* nested_end's restore without its text: the caller's context returns. */ +void scr_insp_nested_restore(void); +/* ctx.customInspect for the renders that follow; answers the previous + * value for the caller to restore. */ +bool scr_insp_custom_inspect(bool enabled); +/* How the unbounded depth of the renders that follow was given, for custom + * inspect functions: 0 Infinity, 1 null, 2 undefined. Answers the + * previous kind for the caller to restore. */ +double scr_insp_depth_kind(double kind); +/* A custom inspect function's string result re-indented at the current + * level (+1; borrows text). */ +ScrStr *scr_insp_custom_text(ScrStr *text); +/* The compiled [util.inspect.custom] calls: ctx.customInspect, the depth + * argument at recursion `recurse` of `depth` and the options object (+1 + * dyn values; options NULL with an exception pending), util.inspect as a + * function value (+1), whether a dyn result is the instance itself, and a + * dyn result's rendering at the instance's level (+1). */ +bool scr_insp_custom_enabled(void); +/* Registers a class's compiled custom inspect function under the type key + * of its instances' typed refs: fn(self, depth, options, inspect) answers + * the method's result (borrows both). */ +void scr_insp_register_custom(ScrStr *type_key, ScrDyn *fn); +ScrDyn *scr_insp_custom_depth(double recurse, double depth); +ScrDyn *scr_insp_custom_options(double depth); +ScrDyn *scr_insp_inspect_function(void); +bool scr_insp_custom_is_self(ScrDyn *result, ScrDyn *self); +ScrStr *scr_insp_custom_result(ScrDyn *result, double recurse, double depth); /* The buf.read* / buf.write* numeric families on u8 bytes, Node-exact: * boundsError's ladder (non-integer offset → 'It must be an integer', a diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index dfa81ace6..3a4e6190d 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -172,6 +172,24 @@ bool scr_dyn_is_async_dispose_symbol(const ScrDyn *key) { key->v.symbol.value == scr_async_dispose_symbol->v.symbol.value; } +/* Symbol.for('nodejs.util.inspect.custom'), installed by the symbol unit + * when the program creates it: the key util.inspect reads custom inspect + * functions under. */ +static SCR_TL ScrDyn *scr_inspect_custom_symbol; + +static void scr_inspect_custom_symbol_cleanup(void) { + scr_dyn_release(scr_inspect_custom_symbol); + scr_inspect_custom_symbol = NULL; +} + +void scr_dyn_install_inspect_custom_symbol(ScrDyn *key) { + if (scr_inspect_custom_symbol) return; + scr_inspect_custom_symbol = scr_dyn_retain(key); + scr_atexit(scr_inspect_custom_symbol_cleanup); +} + +ScrDyn *scr_dyn_inspect_custom_symbol(void) { return scr_inspect_custom_symbol; } + static void scr_to_primitive_symbol_cleanup(void) { scr_dyn_release(scr_to_primitive_symbol); scr_to_primitive_symbol = NULL; diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 304f2544f..e49edb370 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -125,6 +125,11 @@ ScrSym *scr_sym_for(ScrStr *key) { s->reg_key = scr_str_retain(key); s->reg_next = g_sym_registry; g_sym_registry = scr_sym_retain(s); + if (key->len == 26 && !memcmp(key->data, "nodejs.util.inspect.custom", 26)) { + ScrDyn *custom = scr_dyn_new_symbol(s); + scr_dyn_install_inspect_custom_symbol(custom); + scr_dyn_release(custom); + } return s; } diff --git a/packages/runtime/src/scr_util_callable.c b/packages/runtime/src/scr_util_callable.c index c905ea8ae..e3efd0be7 100644 --- a/packages/runtime/src/scr_util_callable.c +++ b/packages/runtime/src/scr_util_callable.c @@ -9,8 +9,8 @@ * and reported on the next tick (scr_warning_emit_deferred), which also * applies process.noDeprecation and process.throwDeprecation. debuglog * matches sections against NODE_DEBUG as the process started and writes - * Node's "SECTION PID: message" lines to stderr. All arguments are - * borrowed. */ + * Node's "SECTION PID: message" lines, formatted by scr_insp_format, to + * stderr. All arguments are borrowed. */ #include "scr_runtime.h" #include @@ -513,163 +513,6 @@ ScrDyn *scr_util_callbackify(ScrDyn *original) { return callbackified; } -/* ── util.format over dyn arguments ──────────────────────────────────── - * formatWithOptionsInternal for debuglog's lines: the %-substitutions - * (%s %d %i %f %j %o %O %c %%) with Node's argument cursor, then the - * remaining arguments space-joined, strings verbatim and everything else - * inspected. With terminal colors, primitives take inspect's default - * styles; colored object renderings have no lowering. */ -static bool uc_primitive(const ScrDyn *value) { - return !uc_object(value); -} - -static ScrStr *uc_inspect(ScrDyn *value, double depth, bool colors) { - if (!colors) return scr_insp_dyn(value, 0, depth); - if (!uc_primitive(value)) { - uc_fence("util.debuglog output of objects on a color terminal has no lowering"); - return NULL; - } - const char *open = "\x1b[33m", *close = "\x1b[39m"; - if (value->kind == SCR_DYN_UNDEF) open = "\x1b[90m"; - else if (value->kind == SCR_DYN_NULL) open = "\x1b[1m", close = "\x1b[22m"; - else if (value->kind == SCR_DYN_STR || value->kind == SCR_DYN_SYMBOL) open = "\x1b[32m"; - ScrStr *text = scr_insp_dyn(value, 0, depth); - ScrJsonBuf b; - scr_jb_init(&b); - scr_jb_puts(&b, open); - scr_jb_put_str(&b, text); - scr_jb_puts(&b, close); - scr_str_release(text); - return scr_jb_finish(&b); -} - -/* hasBuiltInToString's negative answer for checked objects: an own or - * inherited toString the program defined. Symbol-keyed members (where - * Symbol.toPrimitive would also count) are reported as unknown. */ -static int uc_custom_to_string(const ScrDyn *value) { - for (const ScrDyn *current = value; current && current->kind == SCR_DYN_OBJ; current = current->prototype) { - if (current->symbol_properties) return -1; - const ScrDyn *member = scr_dyn_obj_get(current, "toString", 8); - if (member) return member->kind == SCR_DYN_FUNC; - } - return 0; -} - -static ScrStr *uc_format_s(ScrDyn *value) { - if (value->kind == SCR_DYN_STR) return scr_str_retain(value->v.str); - if (value->kind == SCR_DYN_NUM) return scr_insp_f64(value->v.num); - if (value->kind == SCR_DYN_FUNC) { - uc_fence("util.debuglog %s of a function prints its source text, which a compiled binary does not keep"); - return NULL; - } - if (value->kind == SCR_DYN_OBJ && scr_dyn_obj_get(value, "%error", 6) == NULL) { - int custom = uc_custom_to_string(value); - if (custom < 0) { - uc_fence("util.debuglog %s of objects with symbol-keyed members has no lowering"); - return NULL; - } - if (custom) return scr_dyn_string_coerce_js(value); - } - return scr_insp_dyn_s(value, 0); -} - -static ScrStr *uc_format_number(ScrDyn *value, int spec) { - if (value->kind == SCR_DYN_BIGINT && spec != 'f') return scr_bigint_inspect(value->v.bigint); - if (value->kind == SCR_DYN_SYMBOL) return scr_str_new("NaN", 3); - double number; - if (spec == 'd') { - if (value->kind == SCR_DYN_BIGINT) return scr_bigint_inspect(value->v.bigint); - if (!scr_dyn_number_coerce_js(value, &number)) return NULL; - } else { - ScrStr *text = scr_dyn_string_coerce_js(value); - if (!text) return NULL; - number = spec == 'i' ? scr_parse_int(text, 0) : scr_parse_float(text); - scr_str_release(text); - } - return scr_insp_f64(number); -} - -static ScrStr *uc_format(ScrDyn *const *args, size_t argc, bool colors) { - if (argc == 0) return scr_str_new("", 0); - ScrJsonBuf out; - scr_jb_init(&out); - size_t a = 0; - const char *join = ""; - if (args[0]->kind == SCR_DYN_STR) { - const ScrStr *first = args[0]->v.str; - if (argc == 1) { - scr_str_release(scr_jb_finish(&out)); - return scr_str_retain((ScrStr *)first); - } - const char *text = first->data; - size_t last = 0; - for (size_t i = 0; first->len > 0 && i < first->len - 1; i++) { - if (text[i] != '%') continue; - int next = (unsigned char)text[++i]; - if (a + 1 != argc) { - ScrStr *piece = NULL; - switch (next) { - case 's': piece = uc_format_s(args[++a]); break; - case 'j': piece = scr_util_format_j(args[++a]); break; - case 'd': - case 'i': - case 'f': piece = uc_format_number(args[++a], next); break; - case 'O': piece = uc_inspect(args[++a], 2, colors); break; - case 'o': - if (!uc_primitive(args[a + 1])) { - uc_fence("util.debuglog %o of objects renders hidden properties, which have no lowering; use %O"); - break; - } - piece = uc_inspect(args[++a], 4, colors); - break; - case 'c': - a += 1; - piece = scr_str_new("", 0); - break; - case '%': - scr_jb_write(&out, text + last, i - last); - last = i + 1; - continue; - default: - continue; - } - if (!piece) { - scr_str_release(scr_jb_finish(&out)); - return NULL; - } - if (last != i - 1) scr_jb_write(&out, text + last, i - 1 - last); - scr_jb_put_str(&out, piece); - scr_str_release(piece); - last = i + 1; - } else if (next == '%') { - scr_jb_write(&out, text + last, i - last); - last = i + 1; - } - } - if (last != 0) { - a++; - join = " "; - if (last < first->len) scr_jb_write(&out, text + last, first->len - last); - } - } - for (; a < argc; a++) { - scr_jb_puts(&out, join); - if (args[a]->kind == SCR_DYN_STR) { - scr_jb_put_str(&out, args[a]->v.str); - } else { - ScrStr *piece = uc_inspect(args[a], 2, colors); - if (!piece) { - scr_str_release(scr_jb_finish(&out)); - return NULL; - } - scr_jb_put_str(&out, piece); - scr_str_release(piece); - } - join = " "; - } - return scr_jb_finish(&out); -} - /* ── util.debuglog ───────────────────────────────────────────────────── * The logger defers its NODE_DEBUG test to its first call or `enabled` * read. The first call also selects the section's shared implementation @@ -735,7 +578,10 @@ static bool uc_debug_test(const ScrStr *section) { static ScrDyn *uc_debug_write(ScrClosure *closure, ScrDyn *const *args, size_t argc) { bool colors = scr_getenv("FORCE_COLOR") ? scr_util_terminal_colors() : scr_process_is_tty(2) && scr_util_terminal_colors(); - ScrStr *message = uc_format(args, argc, colors); + ScrInspFormatOptions options; + scr_insp_format_defaults(&options, "util.debuglog"); + options.colors = colors; + ScrStr *message = scr_insp_format(args, argc, &options); if (!message) return NULL; ScrDyn *set = uc_cap(closure, 0); char pid[64]; diff --git a/tests/corpus/7520-util-format-runtime.ts b/tests/corpus/7520-util-format-runtime.ts new file mode 100644 index 000000000..d4a7f9ea6 --- /dev/null +++ b/tests/corpus/7520-util-format-runtime.ts @@ -0,0 +1,110 @@ +// util.format, util.formatWithOptions and console output with format +// strings known only at run time, spread arguments and options objects: +// every substitution, the argument cursor, %% and %c, missing and extra +// arguments, the space-joined form of strings without a '%', and Node's +// inspectOptions validation. +import * as util from "node:util"; + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +interface Row { + id: number; + tags: string[]; + nested: { deep: { deeper: { deepest: number } } }; +} + +const formats: string[] = [ + "%s and %s", + "%d|%i|%f", + "%j", + "%O / %o", + "100%% of %s", + "%c%s%c", + "no specifiers", + "trailing %", + "%x %s", + "%s", +]; +const row: Row = { id: 7, tags: ["a", "b"], nested: { deep: { deeper: { deepest: 1 } } } }; + +for (const format of formats) { + console.log(util.format(format, "first", 42, row)); + console.log(util.format(format, 3.5, "12.75")); + console.log(util.format(format)); +} + +function pick(i: number): string { + return ["%s=%d", "%j", "%O", "plain"][i % 4]!; +} +for (let i = 0; i < 4; i++) { + console.log(util.format(pick(i), new Point(1, 2), -0, [1, 2, 3])); + console.log(pick(i), new Map([["k", new Point(3, 4)]]), new Set([1n, 2n])); + console.error(pick(i), "to stderr", null, undefined); +} + +const label: string = process.argv.length > 99 ? "" : "count: %d of %s"; +console.log(label, 5, "five", "extra"); +console.log(util.format(label, 10n, Symbol("sym"))); +console.log(util.format(label, new Point(0, 0), [new Point(5, 6)])); +console.log(util.format(pick(0), { toString: "not a function" }, true)); + +// Arguments that are all strings, numbers, booleans, null or undefined. +const prim: string = process.argv.length > 99 ? "" : "%s|%d|%i|%f|%j|%O|%o|%c|%%"; +console.log(prim, -0, "12.5", -0, true, 'q"uote', "s", 3, "css", "rest", null, undefined); +console.log(prim, NaN, null, undefined, "0x10", Infinity, NaN, false); +console.log(util.format(prim, "a\nb", " 7 ", "-3.9e2", "", -Infinity, 'it\'s', 1e21)); +console.log(util.format(label, 2.5)); + +// A union first argument formats only when it holds a string. +const maybe: string | number = process.argv.length > 99 ? 1 : "%s/%s"; +const notString: string | number = process.argv.length > 99 ? "%s" : 9; +console.log(util.format(maybe, "u", "v")); +console.log(util.format(notString, "u", "v")); +console.log(maybe, "w", "x"); + +// An unknown first argument. +const loose: unknown = process.argv.length > 99 ? 0 : "%s-%i"; +console.log(util.format(loose, "a", "7.9")); +console.log(loose, "b", 3.2); + +// Spread arguments. +const values: unknown[] = ["%s + %s = %d", 1, 2, 3, "tail"]; +console.log(util.format(...values)); +console.log(util.format("%s|%s", ...values)); +console.log(...values); +console.log("prefix", ...new Set(["x", "y"]), "suffix"); +console.error(...[1, "two", [3]]); +console.log(util.format(...([] as string[]))); + +// formatWithOptions with an options object. +const deep = { a: { b: { c: { d: { e: 1 } } } } }; +const depth = process.argv.length > 99 ? 1 : 0; +console.log(util.formatWithOptions({ depth }, "%O", deep)); +console.log(util.formatWithOptions({ depth: null }, deep)); +console.log(util.formatWithOptions({ depth: undefined }, "x", deep)); +console.log(util.formatWithOptions({ depth: 0 }, "%s|%O", deep, deep)); +console.log(util.formatWithOptions({ compact: false }, "%s", "v", { a: 1, b: [1, 2] })); +console.log(util.formatWithOptions({ breakLength: 20 }, { alpha: 1, beta: 2, gamma: 3 })); +console.log(util.formatWithOptions({ sorted: true }, "%O", { b: 1, a: 2, c: { z: 1, y: 2 } })); +console.log(util.formatWithOptions({ colors: true }, "%s %d %O", "s", 2, 3), "|"); +console.log(util.formatWithOptions({ colors: true }, 1, "plain", null, undefined, true, 4n)); +const options: util.InspectOptions = { depth: 1, compact: 1 }; +console.log(util.formatWithOptions(options, "%O", deep)); +console.log(util.formatWithOptions(options, ...values)); +console.log(util.formatWithOptions([] as unknown as util.InspectOptions, "%s", 1)); +const invalid: unknown[] = [null, 1, "x", undefined, () => {}]; +for (const bad of invalid) { + try { + util.formatWithOptions(bad as util.InspectOptions, "a"); + } catch (error) { + const e = error as NodeJS.ErrnoException; + console.log(e.name, e.code, e.message); + } +} diff --git a/tests/corpus/7521-util-format-runtime.cjs b/tests/corpus/7521-util-format-runtime.cjs new file mode 100644 index 000000000..559d09243 --- /dev/null +++ b/tests/corpus/7521-util-format-runtime.cjs @@ -0,0 +1,64 @@ +// util.format, util.formatWithOptions and console output over untyped +// values: format strings held in variables and parameters, spread +// arguments, %s over objects with and without their own toString, %d and +// %i coercions, %j over toJSON, and options objects built at run time. +"use strict"; +const util = require("util"); + +function log(message, ...rest) { + console.log(message, ...rest); +} + +function formatAll(format, values) { + return util.format(format, ...values); +} + +const messages = ["%s: %d items", "%s", "%i%%", "%j", "plain text", 42]; +for (const message of messages) { + log(message, "cart", "3.7", { total: 9 }); + console.log(formatAll(message, ["a", 1n])); +} + +const withToString = { + name: "custom", + toString() { + return `<${this.name}>`; + }, +}; +const inherited = Object.create(withToString); +inherited.name = "inherited"; +const fmt = "%s / %s / %s"; +console.log(util.format(fmt, withToString, inherited, [1, [2, [3]]])); +console.log(fmt, new Date(0), new Error("boom").message, null, Symbol("s")); + +const numeric = "%d %i %f"; +console.log(util.format(numeric, "0x1f", "12.9px", "1e3")); +console.log(util.format(numeric, { valueOf: () => 5 }, true, [])); +console.log(util.format(numeric, 7n, 8n, 9)); + +const json = "%j|%j|%j"; +const circular = { name: "loop" }; +circular.self = circular; +console.log(util.format(json, { toJSON: () => ({ replaced: 1 }) }, circular, undefined)); + +const options = {}; +options.depth = 0; +options.breakLength = 40; +console.log(util.formatWithOptions(options, "%O", { a: { b: 1 } }, ["x", { y: { z: 2 } }])); +options.depth = null; +options.unknownOption = true; +console.log(util.formatWithOptions(options, { a: { b: { c: { d: 1 } } } })); +console.log(util.formatWithOptions({ compact: 1 }, ...[{ k: [1, 2] }, "end"])); +for (const bad of [null, 0, "options", () => {}]) { + try { + util.formatWithOptions(bad, "x"); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} + +const args = ["%s=%s", "key"]; +args.push("value", "rest"); +console.log(...args); +console.warn(...args); +console.log(util.format(...args.slice(1))); diff --git a/tests/corpus/7522-promisify-untyped.cjs b/tests/corpus/7522-promisify-untyped.cjs new file mode 100644 index 000000000..2e1476eac --- /dev/null +++ b/tests/corpus/7522-promisify-untyped.cjs @@ -0,0 +1,64 @@ +// util.promisify over JavaScript functions whose types are not known: +// arrows, function expressions, methods and functions chosen at run time. +// The callback's error rejects and its first value fulfills, a +// synchronous throw rejects, the call's receiver reaches the function, +// the wrapper keeps the original's name and length, and arguments that +// are not functions throw Node's ERR_INVALID_ARG_TYPE. +"use strict"; +const util = require("util"); + +function pick(kind) { + if (kind === "double") return (value, callback) => setImmediate(() => callback(null, value * 2)); + if (kind === "fail") return function failing(callback) { callback(new RangeError("failed")); }; + if (kind === "throw") return () => { throw new TypeError("thrown"); }; + return null; +} + +const store = { + prefix: "item", + read(key, callback) { + setTimeout(() => callback(null, `${this.prefix}:${key}`, "ignored"), 1); + }, +}; + +async function main() { + const double = util.promisify(pick("double")); + console.log("double:", await double(21), double.name, double.length); + try { + await util.promisify(pick("fail"))(); + } catch (error) { + console.log("rejected:", error.name, error.message); + } + try { + await util.promisify(pick("throw"))(); + } catch (error) { + console.log("sync:", error.name, error.message); + } + const read = util.promisify(store.read); + console.log("unbound:", await read.call(store, "a")); + store.readAsync = read; + console.log("method:", await store.readAsync("b")); + const bound = util.promisify(store.read.bind({ prefix: "bound" })); + console.log("bound:", await bound("c")); + const named = util.promisify(function lookup(a, b, callback) { + callback(null, [a, b]); + }); + console.log("named:", named.name, named.length, await named(1, 2)); + const values = []; + for (const n of [1, 2, 3]) values.push(await double(n)); + console.log("each:", values); + for (const value of [pick("none"), 42, "fn", {}, undefined]) { + try { + util.promisify(value); + } catch (error) { + console.log(error.name, error.code, error.message); + } + } + try { + util.promisify(); + } catch (error) { + console.log(error.code, error.message); + } +} + +main(); diff --git a/tests/corpus/7523-promisify-untyped.ts b/tests/corpus/7523-promisify-untyped.ts new file mode 100644 index 000000000..65d1d01c1 --- /dev/null +++ b/tests/corpus/7523-promisify-untyped.ts @@ -0,0 +1,38 @@ +// util.promisify over untyped functions in TypeScript: values typed `any` +// and functions whose parameters are `any` promisify at run time, and +// their promises settle with the callback's value (not the type of a +// builtin target's promisified form). +import { promisify } from "node:util"; + +const loose: any = (value: number, callback: (error: Error | null, result?: number) => void) => + callback(null, value + 1); +const looseAsync = promisify(loose); + +function legacy(a: any, b: any, callback: any): void { + setImmediate(() => (b === 0 ? callback(new RangeError("divide by zero")) : callback(null, a / b))); +} +const divide = promisify(legacy); + +const handlers: Record = { + echo: (text: string, callback: (error: null, value: string) => void) => callback(null, text), + later: (callback: (error: null, value: string[]) => void) => + setTimeout(() => callback(null, ["a", "b"]), 1), +}; + +async function main(): Promise { + console.log("loose:", await looseAsync(1)); + looseAsync(2).then((value: unknown) => console.log("then:", value)); + console.log("divide:", await divide(10, 4)); + try { + await divide(1, 0); + } catch (error) { + console.log("rejected:", (error as Error).message); + } + for (const name of ["echo", "later"]) { + const fn = promisify(handlers[name]); + console.log(`${name}:`, name === "echo" ? await fn("hi") : await fn()); + } + console.log("name:", divide.name, "length:", divide.length); +} + +main(); diff --git a/tests/corpus/7524-inspect-custom.ts b/tests/corpus/7524-inspect-custom.ts new file mode 100644 index 000000000..642ce75a9 --- /dev/null +++ b/tests/corpus/7524-inspect-custom.ts @@ -0,0 +1,91 @@ +// [util.inspect.custom] methods on classes: the method's string replaces +// the instance in util.inspect, console output and util.format, with +// Node's depth argument (negative past the depth limit, null for depth: +// null), options object and util.inspect; a multi-line result indents at +// its position, a returned value renders in the instance's place, and +// console.dir, customInspect: false, String() and JSON skip the method. +import * as util from "node:util"; +import { inspect } from "node:util"; + +class Money { + cents: number; + constructor(cents: number) { + this.cents = cents; + } + [inspect.custom](): string { + return `Money(${(this.cents / 100).toFixed(2)})`; + } +} + +class Probe { + label: string; + constructor(label: string) { + this.label = label; + } + [util.inspect.custom](depth: number | null, options: util.InspectOptionsStylized): string { + const keys = Object.keys(options).join(","); + return `Probe<${this.label} depth=${depth} optionsDepth=${options.depth} compact=${options.compact}> ${keys}`; + } +} + +class Lines { + [inspect.custom](): string { + return "first line\nsecond line"; + } +} + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + [inspect.custom](): { x: number; y: number; sum: number } { + return { x: this.x, y: this.y, sum: this.x + this.y }; + } +} + +class Wrapper { + inner: { values: number[]; nested: { deep: { deeper: number } } }; + constructor() { + this.inner = { values: [1, 2, 3], nested: { deep: { deeper: 4 } } }; + } + [inspect.custom](depth: number, options: util.InspectOptionsStylized, show: typeof inspect): string { + if (depth < 0) return options.stylize("[Wrapper]", "special"); + return `Wrapper ${show(this.inner, { ...options, depth: options.depth == null ? null : depth - 1 })}`; + } +} + +class Base { + name = "base"; + [inspect.custom](): string { + return `Custom(${this.name})`; + } +} +class Derived extends Base { + extra = 1; +} + +console.log(util.inspect(new Money(1234))); +console.log(new Money(5), [new Money(1), new Money(2)]); +console.log(util.inspect({ wallet: { cash: new Money(99) } })); +console.log(util.inspect(new Probe("top"))); +console.log(util.inspect({ a: { b: new Probe("nested") } })); +console.log(util.inspect([new Probe("past depth")], { depth: 0 })); +console.log(util.inspect(new Probe("unbounded"), { depth: null })); +console.log(util.inspect(new Probe("infinite"), { depth: Infinity })); +console.log(util.inspect([new Probe("undefined")], { depth: undefined })); +console.log(util.inspect(new Probe("compact"), { compact: false, breakLength: 40 })); +console.log(util.inspect({ key: new Lines(), list: [new Lines()] })); +console.log(util.inspect(new Map([["k", new Lines()]]))); +console.log(util.inspect(new Point(1, 2)), util.inspect({ p: new Point(3, 4) })); +console.log(util.inspect(new Wrapper()), util.inspect({ w: new Wrapper() }, { depth: 0 })); +console.log(util.inspect(new Derived()), util.inspect([new Base(), new Derived()])); +console.log(util.format("%s | %O", new Money(10), new Money(20))); +console.log("%s and %d", new Money(1), 2); +console.log(util.inspect(new Money(7), { customInspect: false })); +console.dir(new Money(8)); +console.log(String(new Money(9)), JSON.stringify(new Money(10))); +const values: unknown[] = [new Money(11), { money: new Money(12) }]; +console.log(values, util.inspect(values[1]), [new Lines(), values[0]]); diff --git a/tests/corpus/7525-inspect-custom.cjs b/tests/corpus/7525-inspect-custom.cjs new file mode 100644 index 000000000..9c7302fac --- /dev/null +++ b/tests/corpus/7525-inspect-custom.cjs @@ -0,0 +1,75 @@ +// [util.inspect.custom] in JavaScript: methods on classes and object +// literals (also keyed by Symbol.for('nodejs.util.inspect.custom')), the +// CommonJS call form (0, util_1.inspect)(value), results that are the +// object itself or another value, a non-function member under the key, +// and the options and util.inspect a method receives. +"use strict"; +const util_1 = require("util"); + +class Temperature { + constructor(degrees) { + this.degrees = degrees; + } + [util_1.inspect.custom](depth, options, inspect) { + return `Temperature<${this.degrees}> ${typeof options.stylize} ${inspect === util_1.inspect}`; + } +} + +const literal = { + id: 1, + [util_1.inspect.custom](depth) { + return `literal at depth ${depth}`; + }, +}; +const registered = { + [Symbol.for("nodejs.util.inspect.custom")]() { + return { replaced: [1, 2], nested: { x: 1 } }; + }, +}; +const self = { + v: 1, + [util_1.inspect.custom]() { + return this; + }, +}; +const notFunction = { x: 1, [util_1.inspect.custom]: "not a function" }; +const styled = { + [util_1.inspect.custom](depth, options) { + return options.stylize("styled", "special") + options.stylize(5, "number"); + }, +}; +const nesting = { + data: { a: { b: { c: 1 } } }, + [util_1.inspect.custom](depth, options, inspect) { + return `nesting ${inspect(this.data, { ...options, depth: 0 })}`; + }, +}; + +console.log((0, util_1.inspect)(new Temperature(21))); +console.log((0, util_1.inspect)({ t: new Temperature(22), list: [literal] })); +console.log(util_1.inspect([[literal]], { depth: 0 })); +console.log(util_1.inspect(registered), util_1.inspect({ r: registered })); +console.log(util_1.inspect(self), util_1.inspect(notFunction)); +console.log(util_1.inspect(styled), JSON.stringify(util_1.inspect(styled, { colors: true }))); +console.log(util_1.inspect(nesting), util_1.inspect({ n: nesting })); +console.log(literal, [registered]); +console.log((0, util_1.format)("%s / %O", literal, literal)); +console.log(util_1.inspect(literal, { customInspect: false })); +console.dir(literal); +console.log(util_1.inspect.custom === Symbol.for("nodejs.util.inspect.custom"), util_1.inspect.custom); + +const throwing = { + [util_1.inspect.custom]() { + throw new RangeError("from custom"); + }, +}; +try { + console.log([throwing]); +} catch (error) { + console.log("caught", error.name, error.message); +} +try { + util_1.inspect({ t: throwing }); +} catch (error) { + console.log("caught", error.message); +} diff --git a/tests/corpus/7526-inspect-options-runtime.ts b/tests/corpus/7526-inspect-options-runtime.ts new file mode 100644 index 000000000..b9f0c61d7 --- /dev/null +++ b/tests/corpus/7526-inspect-options-runtime.ts @@ -0,0 +1,62 @@ +// util.inspect with colors, option values and options objects known only +// at run time, and the legacy inspect(value, showHidden, depth, colors) +// form: Node's ANSI styles per value kind, color-aware line breaking and +// grouping, runtime depth and layout options, and colored output from +// util.formatWithOptions. +import * as util from "node:util"; + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +const sparse: number[] = [1, 2, 3]; +sparse.length = 5; +const values: unknown[] = [ + 1, + -0, + "text", + "it's", + null, + undefined, + true, + 10n, + Symbol("sym"), + [1, "two", [3, [4, [5]]]], + { a: 1, "b-c": "d", nested: { deep: { deeper: { deepest: 1 } } } }, + new Map([["k", { v: 1 }]]), + new Set(["s", 2]), + new Date(0), + new Uint8Array([1, 2, 3]), + Buffer.from("hi"), + function named() {}, + new Point(1, 2), + sparse, + Object.create(null), + Array.from({ length: 30 }, (_, i) => i * 37), + "a long string with a newline\nand more text than fits on one line of eighty characters", +]; +for (const value of values) { + console.log(JSON.stringify(util.inspect(value, { colors: true }))); +} +const depth = process.argv.length > 99 ? 5 : 1; +const breakLength = process.argv.length > 99 ? 80 : 30; +console.log(util.inspect(values[10], { depth })); +console.log(util.inspect(values[10], { depth: depth - 1, breakLength })); +console.log(util.inspect(values[20], { breakLength, compact: depth })); +console.log(util.inspect({ b: 2, a: 1 }, { sorted: depth > 0 })); +console.log(util.inspect(values[10], { depth: undefined })); +const options: util.InspectOptions = { depth: 0, colors: true }; +console.log(JSON.stringify(util.inspect(values[10], options))); +options.colors = false; +console.log(util.inspect(values[10], options)); +console.log(util.inspect(values[10], false, 0)); +console.log(util.inspect(values[10], false, null, false)); +console.log(JSON.stringify(util.inspect(values[11], false, 1, true))); +console.log(util.inspect(values[10], undefined, depth)); +console.log(JSON.stringify(util.formatWithOptions({ colors: true }, "%s %O", "x", values[12]))); +console.log(JSON.stringify(util.formatWithOptions({ colors: true }, values[10], values[9]))); diff --git a/tests/diagnostics/inspect-custom-fences.ts b/tests/diagnostics/inspect-custom-fences.ts new file mode 100644 index 000000000..f4c7c593d --- /dev/null +++ b/tests/diagnostics/inspect-custom-fences.ts @@ -0,0 +1,44 @@ +// [util.inspect.custom] methods keep their boundaries: a class whose +// subclass declares the method without declaring it itself, an Error +// subclass with the method, a method with more than three parameters, a +// method returning a class instance, and %o over instances with one. +import { format, inspect } from "node:util"; + +class Plain { + a = 1; +} +class Fancy extends Plain { + [inspect.custom](): string { + return "fancy"; + } +} +const items: Plain[] = [new Fancy()]; +console.log(inspect(items)); + +class Failure extends Error { + [inspect.custom](): string { + return "failure"; + } +} +console.log(new Failure("x").message); + +class Wide { + [inspect.custom](a: number, b: unknown, c: unknown, d: unknown): string { + return `${a}${String(b)}${String(c)}${String(d)}`; + } +} +console.log(inspect(new Wide())); + +class Echo { + [inspect.custom](): Echo { + return new Echo(); + } +} +console.log(inspect(new Echo())); + +class Shown { + [inspect.custom](): string { + return "shown"; + } +} +console.log(format("%o", new Shown())); diff --git a/tests/harness/__snapshots__/inspect-custom-fences.ts.txt b/tests/harness/__snapshots__/inspect-custom-fences.ts.txt new file mode 100644 index 000000000..ba7cefefc --- /dev/null +++ b/tests/harness/__snapshots__/inspect-custom-fences.ts.txt @@ -0,0 +1,49 @@ +inspect-custom-fences.ts:16:21 - error SC2020: 'util.inspect of 'Plain[]' values' is part of the standard library types but has no scriptc lowering yet + + 15 | const items: Plain[] = [new Fancy()]; + 16 | console.log(inspect(items)); + | ^~~~~ + 17 | + + hint: classes whose subclasses declare [util.inspect.custom] have no inspect lowering yet (declare the method on the class itself) + +inspect-custom-fences.ts:19:3 - error SC1090: [util.inspect.custom] methods on Error subclasses are not supported yet + + 18 | class Failure extends Error { + 19 | [inspect.custom](): string { + | ^~~~~~~~~~~~~~~~ + 20 | return "failure"; + +inspect-custom-fences.ts:23:13 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet + + 22 | } + 23 | console.log(new Failure("x").message); + | ^~~~~~~~~~~~~~~~ + 24 | + +inspect-custom-fences.ts:30:21 - error SC2020: 'util.inspect of 'Wide' values' is part of the standard library types but has no scriptc lowering yet + + 29 | } + 30 | console.log(inspect(new Wide())); + | ^~~~~~~~~~ + 31 | + + hint: [util.inspect.custom] methods with more than three parameters have no inspect lowering + +inspect-custom-fences.ts:37:21 - error SC2020: 'util.inspect of 'Echo' values' is part of the standard library types but has no scriptc lowering yet + + 36 | } + 37 | console.log(inspect(new Echo())); + | ^~~~~~~~~~ + 38 | + + hint: [util.inspect.custom] methods that return a class instance have no inspect lowering + +inspect-custom-fences.ts:44:26 - error SC2020: 'util.format %o over values with [util.inspect.custom] methods' is part of the standard library types but has no scriptc lowering yet + + 43 | } + 44 | console.log(format("%o", new Shown())); + | ^~~~~~~~~~~ + 45 | + + hint: %o passes showHidden to the methods — use %O \ No newline at end of file diff --git a/tests/harness/corpus-costs/7520-util-format-runtime.ts.json b/tests/harness/corpus-costs/7520-util-format-runtime.ts.json new file mode 100644 index 000000000..9cb750202 --- /dev/null +++ b/tests/harness/corpus-costs/7520-util-format-runtime.ts.json @@ -0,0 +1 @@ +{ "plain": 6414, "san": 9821 } diff --git a/tests/harness/corpus-costs/7521-util-format-runtime.cjs.json b/tests/harness/corpus-costs/7521-util-format-runtime.cjs.json new file mode 100644 index 000000000..05cc5696d --- /dev/null +++ b/tests/harness/corpus-costs/7521-util-format-runtime.cjs.json @@ -0,0 +1 @@ +{ "plain": 4635, "san": 6305 } diff --git a/tests/harness/corpus-costs/7522-promisify-untyped.cjs.json b/tests/harness/corpus-costs/7522-promisify-untyped.cjs.json new file mode 100644 index 000000000..c5358642b --- /dev/null +++ b/tests/harness/corpus-costs/7522-promisify-untyped.cjs.json @@ -0,0 +1 @@ +{ "plain": 3984, "san": 6122 } diff --git a/tests/harness/corpus-costs/7523-promisify-untyped.ts.json b/tests/harness/corpus-costs/7523-promisify-untyped.ts.json new file mode 100644 index 000000000..be46b70d4 --- /dev/null +++ b/tests/harness/corpus-costs/7523-promisify-untyped.ts.json @@ -0,0 +1 @@ +{ "plain": 3595, "san": 36166 } diff --git a/tests/harness/corpus-costs/7524-inspect-custom.ts.json b/tests/harness/corpus-costs/7524-inspect-custom.ts.json new file mode 100644 index 000000000..b9c089238 --- /dev/null +++ b/tests/harness/corpus-costs/7524-inspect-custom.ts.json @@ -0,0 +1 @@ +{ "plain": 5331, "san": 10004 } diff --git a/tests/harness/corpus-costs/7525-inspect-custom.cjs.json b/tests/harness/corpus-costs/7525-inspect-custom.cjs.json new file mode 100644 index 000000000..010f8b482 --- /dev/null +++ b/tests/harness/corpus-costs/7525-inspect-custom.cjs.json @@ -0,0 +1 @@ +{ "plain": 4178, "san": 6619 } diff --git a/tests/harness/corpus-costs/7526-inspect-options-runtime.ts.json b/tests/harness/corpus-costs/7526-inspect-options-runtime.ts.json new file mode 100644 index 000000000..0599901b9 --- /dev/null +++ b/tests/harness/corpus-costs/7526-inspect-options-runtime.ts.json @@ -0,0 +1 @@ +{ "plain": 4855, "san": 7570 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index c8b0b51b8..cfa8680ee 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1985,3 +1985,50 @@ console.log("unreachable"); ); }); }); + +// The runtime formatter (format strings known only at run time, spread +// arguments, options objects) throws a coded SC2020 error for renderings +// it has no lowering for. +describe("runtime format refusals", () => { + test("%s of a class instance with toString, %o of objects, colored errors and options", async () => { + const r = await compileAndRun( + "format-runtime-refusals", + `import * as util from "node:util"; +class Named { + name = "n"; + toString(): string { + return this.name; + } +} +const s: string = process.argv.length > 99 ? "" : "%s"; +const o: string = process.argv.length > 99 ? "" : "%o"; +const attempts: Array<() => string> = [ + () => util.format(s, new Named()), + () => util.format(o, { a: 1 }), + () => util.formatWithOptions({ colors: true }, s, "x", new Error("e")), + () => util.formatWithOptions({ maxArrayLength: 1 }, [1, 2]), + () => util.formatWithOptions({ compact: true }, [1, 2]), +]; +for (const attempt of attempts) { + try { + console.log(attempt()); + } catch (error) { + const e = error as Error & { code?: string }; + console.log(e.name, e.code, e.message); + } +} +console.log(util.format(s, { a: 1 }), util.formatWithOptions({ maxArrayLength: 100 }, [1, 2])); +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + "Error SC2020 util.format %s of a class instance whose class defines toString has no lowering\n" + + "Error SC2020 util.format %o of objects renders hidden properties, which have no lowering; use %O\n" + + "Error SC2020 colored inspect output of Error values has no lowering\n" + + "Error SC2020 util.formatWithOptions with the maxArrayLength option has no lowering\n" + + "Error SC2020 util.formatWithOptions with this compact option has no lowering (compact must be false or a number)\n" + + "{ a: 1 } [ 1, 2 ]\n", + ); + expect(r.stderr).toBe(""); + }); +}); From 6bbabd5fc9c680f5a0c4ae352f525672efd997cc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:15:02 -0500 Subject: [PATCH 008/273] Convert dynamic values into nested unions in one pass Converting a dynamic value into a union (`JSON.parse(text) as T`, a cast from `any`) tries the arms in order and builds the first arm whose literal discriminator and whole structure match. The builder of that arm converts its members, and a member typed as another union matched its arm's structure again. Every value was matched once per union around it, so converting a tree of depth d cost about d times as much as a flat one. The arm a union matched now builds as a trusted conversion (scr_dyn_trust_enter). Inside it, a nested union picks an arm without matching the arm's structure, which the enclosing match already checked, when the value's arm is known: a plain object's arm by its literal, when the arms' literals are disjoint and no arm without a literal takes plain objects, or the only arm that takes plain objects (a record) or arrays (an array), as in an optional or nullable member. Other unions, and values that are not plain objects or arrays, match as before. Getters and proxy traps run outside the trusted conversion. Failed conversions throw the same TypeError with the same path. --- packages/compiler/src/backend/llvm/dyn.ts | 103 ++++++++++- .../7160-json-parse-deep-unions.ts.json | 6 + .../7161-dynamic-values-deep-unions.ts.json | 6 + packages/runtime/src/scr_dyn.h | 12 ++ packages/runtime/src/scr_json.c | 22 ++- tests/corpus/7160-json-parse-deep-unions.ts | 164 ++++++++++++++++++ .../corpus/7161-dynamic-values-deep-unions.ts | 73 ++++++++ .../7160-json-parse-deep-unions.ts.json | 1 + .../7161-dynamic-values-deep-unions.ts.json | 1 + tests/harness/dyncheck.test.ts | 98 +++++++++++ 10 files changed, 478 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7160-json-parse-deep-unions.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7161-dynamic-values-deep-unions.ts.json create mode 100644 tests/corpus/7160-json-parse-deep-unions.ts create mode 100644 tests/corpus/7161-dynamic-values-deep-unions.ts create mode 100644 tests/harness/corpus-costs/7160-json-parse-deep-unions.ts.json create mode 100644 tests/harness/corpus-costs/7161-dynamic-values-deep-unions.ts.json diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index bb00e4563..40deb1d30 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -135,6 +135,18 @@ export const DYN_KIND = { SYMBOL: 15, } as const; +/** Union arm kinds whose matchers reject every plain dyn object + * (trustedArmByLiteral). */ +const NON_OBJECT_ARM_KINDS: ReadonlySet = new Set([ + "f64", + "string", + "bool", + "nullT", + "undefinedT", + "bigint", + "symbol", +]); + /** What the dyn helpers need beyond the walker host: interned immortal * unit instances (undefined-armed dynCheck targets build them). */ export interface DynHost extends WalkerHost { @@ -365,8 +377,56 @@ export class LlDyn { B.line(`call void @scr_dyn_build_add(ptr %d, ptr @${name}, ptr ${value})`); } + /** Whether a builder of `def` may pick a plain object's arm by its literal + * discriminator alone inside a trusted build (scr_dyn_trust_enter): the + * arms' literals are disjoint, and every arm without a literal has a kind + * no plain object matches. The value then matched exactly one arm with a + * literal, the arm whose literal it carries. */ + private trustedArmByLiteral(def: IrUnionDef): boolean { + const discriminant = def.discriminant; + if (!discriminant) return false; + const seen = new Set(); + for (const { values } of discriminant.cases) { + for (const value of values) { + const key = `${typeof value}:${String(value)}`; + if (seen.has(key)) return false; + seen.add(key); + } + } + return def.arms.every( + (arm, tag) => + discriminant.cases.some((candidate) => candidate.tag === tag) || + NON_OBJECT_ARM_KINDS.has(arm.kind), + ); + } + + /** Per arm of `def`, the dyn kind of the values a builder may convert into + * that arm without matching its structure inside a trusted build, or null. + * A value there already matched one of the union's arms, so the arm is + * known when it is the only arm that takes values of its kind: the arm + * whose literal a plain object carries (trustedArmByLiteral), or the one + * arm a plain object or an array can match at all. */ + private trustedArmKinds(def: IrUnionDef): (number | null)[] { + const byLiteral = this.trustedArmByLiteral(def); + const objectArms = def.arms.flatMap((arm, tag) => + NON_OBJECT_ARM_KINDS.has(arm.kind) ? [] : [tag], + ); + return def.arms.map((arm, tag) => { + if (def.discriminant?.cases.some((candidate) => candidate.tag === tag)) + return byLiteral ? DYN_KIND.OBJ : null; + if (objectArms.length !== 1 || objectArms[0] !== tag) return null; + if (arm.kind === "array") return DYN_KIND.ARR; + if (arm.kind === "record" && this.host.recordsById.get(arm.shapeId)?.tuple !== true) + return DYN_KIND.OBJ; + return null; + }); + } + /** Branch to a variant only after its literal discriminator and complete - * structure match. Use the same path for predicates and builders. */ + * structure match. Use the same path for predicates and builders. With a + * `trustedKind` (builders, trustedArmKinds), a value of that dyn kind + * inside a trusted build skips the structure match once its literal + * matched. */ private unionArmMatch( B: BlockBuilder, def: IrUnionDef, @@ -374,6 +434,7 @@ export class LlDyn { yes: string, no: string, preserveRefs = false, + trustedKind: number | null = null, ): void { const guard = def.discriminant?.cases.find((candidate) => candidate.tag === tag); if (guard && def.discriminant) { @@ -402,6 +463,19 @@ export class LlDyn { }); B.startBlock(structure); } + if (trustedKind !== null) { + this.host.declare(`declare zeroext i1 @scr_dyn_trusted_build()`); + const kind = this.kindOf(B, "%d"); + const sameKind = B.tmp(); + const trusted = B.tmp(); + const skip = B.tmp(); + B.line(`${sameKind} = icmp eq i32 ${kind}, ${trustedKind}`); + B.line(`${trusted} = call zeroext i1 @scr_dyn_trusted_build()`); + B.line(`${skip} = and i1 ${sameKind}, ${trusted}`); + const full = B.newLabel("du.full"); + B.condBr(skip, yes, full); + B.startBlock(full); + } const matches = B.tmp(); B.line( `${matches} = call zeroext i1 @${this.dynMatchHelper(def.arms[tag]!, preserveRefs)}(ptr %d)`, @@ -2357,11 +2431,22 @@ export class LlDyn { `llvm emitter bug: dynCheck of unknown union ${t.unionId}`, ); // Arms in CANONICAL order, first FULL match wins. The matched - // arm's builder can no longer fail. + // arm's builder can no longer fail, and it runs as a trusted build: + // a nested union inside it picks the arm trustedArmKinds names + // without matching the structure that this match already checked. + const trustedKinds = preserveRefs ? def.arms.map(() => null) : this.trustedArmKinds(def); + const trustedCall = (call: () => void): void => { + host.declare(`declare zeroext i1 @scr_dyn_trust_enter()`); + host.declare(`declare void @scr_dyn_trust_exit(i1 zeroext)`); + const previous = B.tmp(); + B.line(`${previous} = call zeroext i1 @scr_dyn_trust_enter()`); + call(); + B.line(`call void @scr_dyn_trust_exit(i1 zeroext ${previous})`); + }; def.arms.forEach((arm, i) => { const lHit = B.newLabel("dcu.h"); const lNext = B.newLabel("dcu.n"); - this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs); + this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs, trustedKinds[i]!); B.startBlock(lHit); if (arm.kind === "undefinedT" || arm.kind === "nullT") { // A matched unit arm builds nothing: THE interned immortal @@ -2388,14 +2473,22 @@ export class LlDyn { } else if (host.nullableUnions.has(t.unionId)) { // A nullable union is its (+1) reference payload. const x = B.tmp(); - B.line(`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`); + trustedCall(() => + B.line( + `${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`, + ), + ); B.terminate(`ret ptr ${x}`); } else { const rc = vAdapters(host, arm); host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`); const x = B.tmp(); const u = B.tmp(); - B.line(`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`); + trustedCall(() => + B.line( + `${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`, + ), + ); B.line( `${u} = call ptr @scr_union_new_ref(i32 ${i}, ptr ${x}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arm)})`, ); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7160-json-parse-deep-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7160-json-parse-deep-unions.ts.json new file mode 100644 index 000000000..fb4ef3c1f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7160-json-parse-deep-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7160-json-parse-deep-unions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7161-dynamic-values-deep-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7161-dynamic-values-deep-unions.ts.json new file mode 100644 index 000000000..8ad1579da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7161-dynamic-values-deep-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7161-dynamic-values-deep-unions.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 28c2accce..e9b4d33bc 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1442,6 +1442,18 @@ void scr_dyn_build_note(const ScrDyn *d, const void *builder, void *value); void scr_dyn_build_rekey(const ScrDyn *d, const void *builder, void *value); ScrDyn *scr_dyn_visit_view(const ScrDyn *capsule); +/* A union's builder converts the arm whose literal discriminator and + * complete structure matched; every value inside that arm then matched its + * own type too. Between scr_dyn_trust_enter and scr_dyn_trust_exit (which + * restores the value enter returned), scr_dyn_trusted_build answers true, + * and the builder of a nested union whose arms' literals are disjoint picks + * a plain object's arm by its literal alone instead of matching the arm's + * structure again (dyn.ts unionArmMatch). Code a getter or proxy trap runs + * sees false. */ +bool scr_dyn_trust_enter(void); +void scr_dyn_trust_exit(bool previous); +bool scr_dyn_trusted_build(void); + /* ── native handles in the checked-dynamic tree (SCR_DYN_HANDLE) ──────────────────────── * Per-tag dispatch ops, registered by the handle's owning unit * (scr_http_dyn_install / scr_net_dyn_install — emitted main() calls diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 324233028..9fb5efd6d 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -9245,6 +9245,9 @@ static SCR_TL ScrDynVisitLog scr_dyn_viewed_log; /* The member value a matcher or builder read last and holds one * reference to until it calls scr_dyn_release_held. */ static SCR_TL const ScrDyn *scr_dyn_visit_held; +/* Set while a union's builder builds the arm it matched in full + * (scr_dyn_trust_enter). */ +static SCR_TL bool scr_dyn_trusted; /* Tables up to this many slots stay allocated for the next session. */ #define SCR_DYN_VISIT_KEEP 4096 @@ -9356,13 +9359,15 @@ typedef struct { ScrDynVisitLog built_log, viewed_log; bool track; const ScrDyn *held; + bool trusted; } ScrDynVisitState; static void scr_dyn_visit_suspend(ScrDynVisitState *saved) { - *saved = (ScrDynVisitState){scr_dyn_visit_depth, scr_dyn_matching, scr_dyn_built, - scr_dyn_viewed, scr_dyn_built_log, scr_dyn_viewed_log, - scr_dyn_build_track, scr_dyn_visit_held}; + *saved = (ScrDynVisitState){scr_dyn_visit_depth, scr_dyn_matching, scr_dyn_built, + scr_dyn_viewed, scr_dyn_built_log, scr_dyn_viewed_log, + scr_dyn_build_track, scr_dyn_visit_held, scr_dyn_trusted}; scr_dyn_visit_depth = 0; + scr_dyn_trusted = false; scr_dyn_matching = scr_dyn_built = scr_dyn_viewed = (ScrDynVisitSet){NULL, 0, 0}; scr_dyn_built_log = scr_dyn_viewed_log = (ScrDynVisitLog){NULL, 0, 0}; scr_dyn_visit_held = NULL; @@ -9382,6 +9387,7 @@ static void scr_dyn_visit_resume(const ScrDynVisitState *saved) { scr_dyn_viewed_log = saved->viewed_log; scr_dyn_build_track = saved->track; scr_dyn_visit_held = saved->held; + scr_dyn_trusted = saved->trusted; } /* Calls a getter for a member read, outside any visit session. */ @@ -9404,6 +9410,16 @@ static ScrDyn *scr_dyn_visit_proxy_get(const ScrDyn *d, ScrStr *name) { return result; } +bool scr_dyn_trust_enter(void) { + bool previous = scr_dyn_trusted; + scr_dyn_trusted = true; + return previous; +} + +void scr_dyn_trust_exit(bool previous) { scr_dyn_trusted = previous; } + +bool scr_dyn_trusted_build(void) { return scr_dyn_trusted; } + ScrDyn *scr_dyn_obj_read_held(const ScrDyn *d, const char *key, size_t key_len) { ScrDyn *value = scr_dyn_obj_read(d, key, key_len); scr_dyn_visit_held = value; diff --git a/tests/corpus/7160-json-parse-deep-unions.ts b/tests/corpus/7160-json-parse-deep-unions.ts new file mode 100644 index 000000000..bcbf5ea9d --- /dev/null +++ b/tests/corpus/7160-json-parse-deep-unions.ts @@ -0,0 +1,164 @@ +// JSON.parse into recursive discriminated unions: deep and wide trees, +// unions with primitive arms beside the records, unions whose records share +// a literal, and unions with a record arm that has no literal. + +type Expr = + | { kind: "num"; value: number } + | { kind: "str"; text: string } + | { kind: "add"; left: Expr; right: Expr } + | { kind: "neg"; operand: Expr } + | { kind: "call"; callee: string; args: Expr[] } + | { kind: "cond"; test: Expr; then: Expr; otherwise: Expr | null } + | { kind: "label"; inner: Expr; name?: string }; + +type Stmt = + | { kind: "expr"; expr: Expr } + | { kind: "ret"; value: Expr | undefined } + | { kind: "block"; body: Stmt[] }; + +interface Fn { + name: string; + body: Stmt[]; +} + +function evaluate(e: Expr): number { + switch (e.kind) { + case "num": + return e.value; + case "str": + return e.text.length; + case "add": + return evaluate(e.left) + evaluate(e.right); + case "neg": + return -evaluate(e.operand); + case "call": + return e.args.reduce((sum, arg) => sum + evaluate(arg), e.callee.length); + case "cond": + return evaluate(e.test) !== 0 + ? evaluate(e.then) + : e.otherwise === null + ? 0 + : evaluate(e.otherwise); + case "label": + return evaluate(e.inner) + (e.name?.length ?? 0); + } +} + +function depth(e: Expr): number { + switch (e.kind) { + case "num": + case "str": + return 1; + case "add": + return 1 + Math.max(depth(e.left), depth(e.right)); + case "neg": + return 1 + depth(e.operand); + case "call": + return 1 + e.args.reduce((max, arg) => Math.max(max, depth(arg)), 0); + case "cond": + return 1 + Math.max(depth(e.test), depth(e.then), e.otherwise ? depth(e.otherwise) : 0); + case "label": + return 1 + depth(e.inner); + } +} + +function chain(n: number): string { + let text = '{"kind":"num","value":1}'; + for (let i = 0; i < n; i++) { + text = + i % 3 === 0 + ? `{"kind":"neg","operand":${text}}` + : i % 3 === 1 + ? `{"kind":"add","left":${text},"right":{"kind":"str","text":"ab"}}` + : `{"kind":"label","inner":${text}${i % 2 === 0 ? ',"name":"x"' : ""}}`; + } + return text; +} + +function wide(n: number, level: number): string { + if (level === 0) return `{"kind":"num","value":${n}}`; + const args: string[] = []; + for (let i = 0; i < 3; i++) args.push(wide(n + i, level - 1)); + return `{"kind":"call","callee":"f${level}","args":[${args.join(",")}]}`; +} + +for (const n of [0, 1, 5, 60, 400]) { + const e = JSON.parse(chain(n)) as Expr; + console.log("chain", n, e.kind, depth(e), evaluate(e)); +} + +const tree = JSON.parse(wide(1, 6)) as Expr; +console.log("wide", depth(tree), evaluate(tree)); + +const cond = JSON.parse( + '{"kind":"cond","test":{"kind":"num","value":0},"then":{"kind":"num","value":5},"otherwise":null}', +) as Expr; +console.log("cond", evaluate(cond)); + +const fn = JSON.parse( + JSON.stringify({ + name: "main", + body: [ + { kind: "expr", expr: JSON.parse(chain(30)) }, + { kind: "block", body: [{ kind: "ret", value: JSON.parse(wide(2, 3)) }, { kind: "ret" }] }, + ], + }), +) as Fn; +let statements = 0; +const visit = (stmts: Stmt[]): void => { + for (const s of stmts) { + statements++; + if (s.kind === "block") visit(s.body); + else if (s.kind === "expr") console.log("stmt expr", evaluate(s.expr)); + else console.log("stmt ret", s.value === undefined ? "none" : evaluate(s.value)); + } +}; +visit(fn.body); +console.log("statements", statements); + +// Primitive arms beside the records. +type Value = number | string | null | { kind: "list"; items: Value[] } | { kind: "pair"; first: Value; second: Value }; +function show(v: Value): string { + if (v === null || typeof v === "number" || typeof v === "string") return JSON.stringify(v); + return v.kind === "list" ? `[${v.items.map(show).join(" ")}]` : `(${show(v.first)} . ${show(v.second)})`; +} +const values = JSON.parse( + '[1,"a",null,{"kind":"list","items":[2,{"kind":"pair","first":"b","second":{"kind":"list","items":[null,3]}}]}]', +) as Value[]; +console.log("values", values.map(show).join(" | ")); + +// Two records that share a literal: the arm follows the other fields. +type Shared = { kind: "a"; count: number } | { kind: "a"; name: string } | { kind: "b"; next: Shared[] }; +function describe(s: Shared): string { + if (s.kind === "b") return `b(${s.next.map(describe).join(",")})`; + return "count" in s ? `count=${s.count}` : `name=${s.name}`; +} +const shared = JSON.parse( + '{"kind":"b","next":[{"kind":"a","count":1},{"kind":"a","name":"n"},{"kind":"b","next":[{"kind":"a","name":"m"}]}]}', +) as Shared; +console.log("shared", describe(shared)); + +// A record arm without a literal beside records that have one. +type Loose = { kind: "leaf"; value: number } | { kind: "node"; children: Loose[] } | { weight: number; child: Loose }; +function total(l: Loose): number { + if ("weight" in l) return l.weight * total(l.child); + return l.kind === "leaf" ? l.value : l.children.reduce((sum, c) => sum + total(c), 0); +} +const loose = JSON.parse( + '{"kind":"node","children":[{"kind":"leaf","value":2},{"weight":3,"child":{"kind":"node","children":[{"kind":"leaf","value":4},{"weight":2,"child":{"kind":"leaf","value":5}}]}}]}', +) as Loose; +console.log("loose", total(loose)); + +// Optional and nullable members holding records and arrays of records. +type Box = { value: number; child?: Box; rest: Box[] | null }; +function weigh(b: Box): number { + let total = b.value; + if (b.child !== undefined) total += 2 * weigh(b.child); + for (const r of b.rest ?? []) total += weigh(r); + return total; +} +let boxText = '{"value":1,"rest":null}'; +for (let i = 2; i < 120; i++) + boxText = i % 2 === 0 ? `{"value":${i},"child":${boxText},"rest":null}` : `{"value":${i},"rest":[${boxText},{"value":1,"rest":[]}]}`; +const boxes = JSON.parse(boxText) as Box; +console.log("boxes", weigh(boxes) % 1000003); diff --git a/tests/corpus/7161-dynamic-values-deep-unions.ts b/tests/corpus/7161-dynamic-values-deep-unions.ts new file mode 100644 index 000000000..3e85b2689 --- /dev/null +++ b/tests/corpus/7161-dynamic-values-deep-unions.ts @@ -0,0 +1,73 @@ +// Dynamic values cast into recursive discriminated unions: trees built as +// untyped objects, trees that mix typed records into untyped ones, and casts +// of the same values twice. + +type Node = + | { kind: "leaf"; value: number } + | { kind: "pair"; left: Node; right: Node } + | { kind: "wrap"; inner: Node; tag: string } + | { kind: "many"; items: Node[] }; + +function sum(n: Node): number { + switch (n.kind) { + case "leaf": + return n.value; + case "pair": + return sum(n.left) + sum(n.right); + case "wrap": + return sum(n.inner) + n.tag.length; + case "many": + return n.items.reduce((total, item) => total + sum(item), 0); + } +} + +function height(n: Node): number { + switch (n.kind) { + case "leaf": + return 1; + case "pair": + return 1 + Math.max(height(n.left), height(n.right)); + case "wrap": + return 1 + height(n.inner); + case "many": + return 1 + n.items.reduce((max, item) => Math.max(max, height(item)), 0); + } +} + +function untyped(levels: number): any { + let node: any = { kind: "leaf", value: 1 }; + for (let i = 0; i < levels; i++) { + node = + i % 3 === 0 + ? { kind: "wrap", inner: node, tag: "t" } + : i % 3 === 1 + ? { kind: "pair", left: node, right: { kind: "leaf", value: i } } + : { kind: "many", items: [node, { kind: "leaf", value: 2 }] }; + } + return node; +} + +for (const levels of [0, 2, 10, 90, 300]) { + const value: unknown = untyped(levels); + const node = value as Node; + console.log("untyped", levels, node.kind, height(node), sum(node)); +} + +// Typed records inside an untyped tree. +const typedLeaf: Node = { kind: "leaf", value: 7 }; +const typedPair: Node = { kind: "pair", left: typedLeaf, right: { kind: "leaf", value: 3 } }; +const mixed: any = { kind: "many", items: [typedPair, untyped(20), { kind: "wrap", inner: typedLeaf, tag: "xy" }] }; +const fromMixed = mixed as Node; +console.log("mixed", height(fromMixed), sum(fromMixed)); + +// The same untyped value cast twice gives equal trees. +const again: any = untyped(40); +const first = again as Node; +const second = again as Node; +console.log("twice", sum(first) === sum(second), height(first), height(second)); + +// A tree that changes between casts. +const changing: any = { kind: "pair", left: { kind: "leaf", value: 1 }, right: untyped(5) }; +console.log("before", sum(changing as Node)); +changing.left = { kind: "wrap", inner: { kind: "leaf", value: 10 }, tag: "abc" }; +console.log("after", sum(changing as Node)); diff --git a/tests/harness/corpus-costs/7160-json-parse-deep-unions.ts.json b/tests/harness/corpus-costs/7160-json-parse-deep-unions.ts.json new file mode 100644 index 000000000..5d8e97d52 --- /dev/null +++ b/tests/harness/corpus-costs/7160-json-parse-deep-unions.ts.json @@ -0,0 +1 @@ +{ "plain": 11054, "san": 84858 } diff --git a/tests/harness/corpus-costs/7161-dynamic-values-deep-unions.ts.json b/tests/harness/corpus-costs/7161-dynamic-values-deep-unions.ts.json new file mode 100644 index 000000000..bf9a71561 --- /dev/null +++ b/tests/harness/corpus-costs/7161-dynamic-values-deep-unions.ts.json @@ -0,0 +1 @@ +{ "plain": 5843, "san": 57446 } diff --git a/tests/harness/dyncheck.test.ts b/tests/harness/dyncheck.test.ts index 6a2d4dc61..fca914792 100644 --- a/tests/harness/dyncheck.test.ts +++ b/tests/harness/dyncheck.test.ts @@ -427,6 +427,104 @@ console.log("unreachable", t.label); ); }); + test("nested unions: a failure inside a matched arm names the union that holds it", async () => { + // A union's builder converts the arm that matched; the unions nested in + // it pick their arms by literal or by the kind of value only one arm + // takes. A value that fails anywhere below fails its outermost union, + // whose arms the message names. + const r = await compileAndRun( + "deep-discriminated", + `type Expr = + | { kind: "num"; value: number } + | { kind: "str"; text: string } + | { kind: "add"; left: Expr; right: Expr } + | { kind: "call"; callee: string; args: Expr[] } + | { kind: "neg"; operand: Expr } + | { kind: "opt"; inner: Expr | undefined; label?: string }; +type Stmt = + | { kind: "expr"; expr: Expr } + | { kind: "ret"; value: Expr | null } + | { kind: "block"; body: Stmt[] }; +type Fn = { name: string; body: Stmt[] }; + +function attempt(label: string, text: string, as: "expr" | "stmt" | "fn" | "list"): void { + try { + if (as === "expr") { + const e = JSON.parse(text) as Expr; + console.log(label, "ok", e.kind); + } else if (as === "stmt") { + const s = JSON.parse(text) as Stmt; + console.log(label, "ok", s.kind); + } else if (as === "fn") { + const f = JSON.parse(text) as Fn; + console.log(label, "ok", f.name, f.body.length); + } else { + const l = JSON.parse(text) as Expr[]; + console.log(label, "ok", l.length); + } + } catch (e) { + console.log(label, "error", (e as Error).message); + } +} + +const deep = (leaf: string, depth: number): string => { + let s = leaf; + for (let i = 0; i < depth; i++) s = \`{"kind":"neg","operand":\${s}}\`; + return s; +}; +attempt("valid", deep('{"kind":"num","value":1}', 20), "expr"); +attempt("deep wrong field", deep('{"kind":"num","value":"x"}', 5), "expr"); +attempt("deep unknown kind", deep('{"kind":"nope","value":1}', 5), "expr"); +attempt("deep string node", deep('"text"', 5), "expr"); +attempt("deep missing field", deep('{"kind":"add","left":{"kind":"num","value":1}}', 4), "expr"); +attempt("deep array arg", deep('{"kind":"call","callee":"f","args":[{"kind":"num","value":1},{"kind":"str","text":2}]}', 3), "expr"); +attempt("optional inner", deep('{"kind":"opt","inner":{"kind":"num","value":true}}', 3), "expr"); +attempt("optional label", deep('{"kind":"opt","inner":{"kind":"num","value":1},"label":3}', 3), "expr"); +attempt("stmt deep", '{"kind":"block","body":[{"kind":"expr","expr":' + deep('{"kind":"num","value":null}', 3) + '}]}', "stmt"); +attempt("ret null", '{"kind":"ret","value":null}', "stmt"); +attempt("fn deep", '{"name":"f","body":[{"kind":"ret","value":' + deep('{"kind":"str","text":5}', 4) + '}]}', "fn"); +attempt("list element", '[{"kind":"num","value":1},' + deep('{"kind":"num"}', 2) + "]", "list"); +attempt("kind not string", deep('{"kind":3,"value":1}', 2), "expr"); +attempt("valid list", '[' + deep('{"kind":"num","value":1}', 3) + ',{"kind":"str","text":"t"}]', "list"); + +type Box = { value: number; child?: Box; rest: Box[] | null }; +function box(label: string, text: string): void { + try { + const b = JSON.parse(text) as Box; + console.log(label, "ok", b.value); + } catch (e) { + console.log(label, "error", (e as Error).message); + } +} +box("box valid", '{"value":1,"rest":null,"child":{"value":2,"rest":[{"value":3,"rest":null}]}}'); +box("box deep child", '{"value":1,"rest":null,"child":{"value":2,"rest":null,"child":{"value":"x","rest":null}}}'); +box("box deep rest", '{"value":1,"rest":[{"value":2,"rest":[{"value":3,"rest":[1]}]}]}'); +box("box child array", '{"value":1,"rest":null,"child":[]}'); +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout.trimEnd().split("\n")).toEqual([ + "valid ok neg", + "deep wrong field error expected object | object | object | object | object | object at $, got object", + "deep unknown kind error expected object | object | object | object | object | object at $, got object", + "deep string node error expected object | object | object | object | object | object at $, got object", + "deep missing field error expected object | object | object | object | object | object at $, got object", + "deep array arg error expected object | object | object | object | object | object at $, got object", + "optional inner error expected object | object | object | object | object | object at $, got object", + "optional label error expected object | object | object | object | object | object at $, got object", + "stmt deep error expected object | object | object at $, got object", + "ret null ok ret", + "fn deep error expected object | object | object at $.body[0], got object", + "list element error expected object | object | object | object | object | object at $[1], got object", + "kind not string error expected object | object | object | object | object | object at $, got object", + "valid list ok 2", + "box valid ok 1", + "box deep child error expected number at $.child.child.value, got string", + "box deep rest error expected object at $.rest[0].rest[0].rest[0], got number", + "box child array error expected object | undefined at $.child, got array", + ]); + }); + test("array where a record is expected names the mismatch at the root", async () => { const r = await compileAndRun( "root-kind", From b306a4e11e37194c07f8c2718ba348662d0f13ce Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:18:02 -0500 Subject: [PATCH 009/273] Leave fresh unknown[] storage out of the view-hole fact Merged with current main, the hole fact for stores through dynamic views fired on the emitter's per-feature state (`states[slot] = create(emitter)` in emitter-state.ts writes past the end of `emitter.moduleStates`, an `unknown[]`). The viewed element types then widened reads of the IR function arrays, and the object spreads of `IrFunction | undefined` in unshare.ts, callback-specialization.ts and constant-globals.ts were refused when the LLVM emitter bootstraps. A receiver that only holds the array its declaration creates (a const binding or a readonly property whose initializer is an array literal, `new Array` or `Array.of`/`Array.from`, possibly through further such bindings) is never a view of a typed array, so stores through it add no hole fact. --- .../compiler/src/frontend/lowering/lowerer.ts | 46 ++++++++++++++++++- .../test/runtime-optional-precision.test.ts | 14 ++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ccb91291c..bd0981601 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -6432,10 +6432,54 @@ export class Lowerer { element !== undefined && (element.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 ); }; + // A binding that only ever holds the array its declaration creates (a + // const variable, or a readonly property with an initializer) is never + // a view of a native array, even when its elements are dynamic. + const freshDynamicStorage = (node: ts.Expression, depth: number): boolean => { + const e = peel(node); + if (ts.isArrayLiteralExpression(e)) return true; + if (ts.isNewExpression(e) || ts.isCallExpression(e)) { + const callee = peel(e.expression); + if (ts.isIdentifier(callee)) return this.isStdlibGlobal(callee, "Array"); + return ( + ts.isPropertyAccessExpression(callee) && + (callee.name.text === "of" || callee.name.text === "from") && + this.isStdlibGlobal(callee.expression, "Array") + ); + } + if (depth > 4 || (!ts.isIdentifier(e) && !ts.isPropertyAccessExpression(e))) return false; + // A typed array along the way is the array a view shows. + const type = typeOrNull(e); + if (type === null || !dynamicType(type)) return false; + const symbol = panicSafe( + () => this.checker.getSymbolAtLocation(ts.isIdentifier(e) ? e : e.name), + undefined, + ); + const declaration = symbol?.valueDeclaration; + if (!declaration) return false; + if (ts.isVariableDeclaration(declaration)) { + const list = declaration.parent; + return ( + ts.isVariableDeclarationList(list) && + (list.flags & ts.NodeFlags.Const) !== 0 && + declaration.initializer !== undefined && + freshDynamicStorage(declaration.initializer, depth + 1) + ); + } + return ( + ts.isPropertyDeclaration(declaration) && + declaration.initializer !== undefined && + (declaration.modifiers?.nodes.some( + (modifier) => modifier.kind === ts.SyntaxKind.ReadonlyKeyword, + ) ?? + false) && + freshDynamicStorage(declaration.initializer, depth + 1) + ); + }; const elementStates = new ArrayElementStates({ isDynamicArray: (node) => { const type = typeOrNull(node); - return type === null || dynamicType(type); + return (type === null || dynamicType(type)) && !freshDynamicStorage(node, 0); }, nativeViewKeys: collectNativeViews, elementAdmitsUndefined: (key) => { diff --git a/packages/compiler/test/runtime-optional-precision.test.ts b/packages/compiler/test/runtime-optional-precision.test.ts index 64f0bd86e..50f1fe138 100644 --- a/packages/compiler/test/runtime-optional-precision.test.ts +++ b/packages/compiler/test/runtime-optional-precision.test.ts @@ -143,6 +143,20 @@ bag["3"] = view; console.log(items.findIndex((item) => weigh(item) > 1)); `); expect(signature(record, "weigh")).toBe("weigh(Item): f64"); + // An unknown[] that only holds the array its declaration creates is never + // a view, so stores past its end leave the viewed arrays alone. + const fresh = await lower(`${ITEM} +class Slots { + readonly states: unknown[] = []; +} +const slots = new Slots(); +const items: Item[] = [new Item(1)]; +const view: unknown = items; +const states = slots.states; +states[3] = view; +console.log(items.findIndex((item) => weigh(item) > 1)); +`); + expect(signature(fresh, "weigh")).toBe("weigh(Item): f64"); }); test("holes that only their owning slot can observe stay out of callbacks", async () => { From 4780e7ef22243c28fd0ac36a25e19fdd9e87e08a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:01:18 -0500 Subject: [PATCH 010/273] Compare optional string index reads as code units `s[i]` with an index the frontend cannot prove in bounds reads the character or undefined: temps for the string and the index, a range and integer test, and a string-or-undefined union. A lexer that tests `src[pos] === " "` or `c < "a"` on `const c = src[pos]` compared that union with a union-wrapped literal, or boxed it for a dynamic comparison, on every character. Tests of such a read against a literal of one UTF-16 code unit now compare `i === Math.floor(i) ? s.charCodeAt(i) : NaN` instead. Undefined, like NaN, equals no literal and fails every ordered comparison, and charCodeAt reads NaN out of range. A read of plain operands is evaluated in place; others keep the read's temps. An immutable local initialized from such a read, and used only in those tests, in tests for undefined and in a switch over one-unit cases, holds the code unit. Tests for undefined become NaN tests. Copies of the local, such as a switch's discriminant temp, follow it. The code-unit reads are charCodeAt reads, so a loop version can cover them. --- .../compiler/src/ir/char-unit-locals.test.ts | 70 +++ packages/compiler/src/ir/char-unit-locals.ts | 404 ++++++++++++++++-- .../7304-string-optional-char-reads.ts.json | 6 + .../corpus/7304-string-optional-char-reads.ts | 121 ++++++ .../7304-string-optional-char-reads.ts.json | 1 + 5 files changed, 567 insertions(+), 35 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7304-string-optional-char-reads.ts.json create mode 100644 tests/corpus/7304-string-optional-char-reads.ts create mode 100644 tests/harness/corpus-costs/7304-string-optional-char-reads.ts.json diff --git a/packages/compiler/src/ir/char-unit-locals.test.ts b/packages/compiler/src/ir/char-unit-locals.test.ts index ad16ecebd..378a7f0df 100644 --- a/packages/compiler/src/ir/char-unit-locals.test.ts +++ b/packages/compiler/src/ir/char-unit-locals.test.ts @@ -96,3 +96,73 @@ console.log(escapes("a b", 1), rebound(" x", 0), wide("ab", 0), against("a", "b expect(fn(after, name)).toBe(fn(before, name)); } }); + +/** The union and dynamic comparisons left in the function. */ +function unionTests(f: IrFunction): string[] { + const out: string[] = []; + everyStmtList(f.body, { + stmt: () => true, + expr: (e) => { + if (e.kind === "unionEq" || e.kind === "unionIsTag") out.push(e.kind); + if (e.kind === "libCall" && e.fn === "js.compare") out.push(e.fn); + if (e.kind === "strIntrinsic") out.push(e.method); + return true; + }, + }); + return out; +} + +test("optional character reads compare their code unit", async () => { + const mod = holdCharLocalsAsUnits( + await lower(` +function scan(src: string, pos: number): number { + let n = 0; + if (src[pos] === " ") n += 1; + if (src[pos] !== "x") n += 2; + if (src[pos] < "a" || "z" <= src[pos]) n += 4; + if (src[pos + 1] === "=") n += 8; + const c = src[pos]; + if (c === undefined) n += 16; + if (c !== undefined && c > "m") n += 32; + switch (c) { + case "q": + n += 64; + break; + case "r": + n += 128; + } + return n; +} +console.log(scan("a b", 1), scan("q", 0), scan("ab", 5), scan("r=", 0.5)); +`), + ); + const scan = fn(mod, "scan"); + // Only charCodeAt reads remain: no union tests, dynamic compares, charAt + // or length reads. + expect(new Set(unionTests(scan))).toEqual(new Set(["charCodeAt"])); + expect(scan.locals.find((l) => l.name === "c")!.type).toEqual({ kind: "f64" }); +}); + +test("optional character reads with other uses stay reads", async () => { + const before = await lower(` +function kept(src: string, pos: number): string { + const c = src[pos]; + if (c === " ") return "space"; + return c ?? "end"; +} +function longer(src: string, pos: number): boolean { + return src[pos] === "ab"; +} +function twice(src: string, pos: number): boolean { + return src[pos * 2] === undefined; +} +console.log(kept("a b", 1), kept("ab", 0), kept("", 3), longer("ab", 0), twice("ab", 1)); +`); + const after = holdCharLocalsAsUnits(before); + for (const name of ["kept", "longer", "twice"]) { + expect(fn(before, name)).toBeDefined(); + expect(unionTests(fn(after, name))).toEqual(unionTests(fn(before, name))); + } + // The local's own test still reads it as a union. + expect(unionTests(fn(after, "kept"))).toContain("unionEq"); +}); diff --git a/packages/compiler/src/ir/char-unit-locals.ts b/packages/compiler/src/ir/char-unit-locals.ts index 4008f4e1e..9917a148b 100644 --- a/packages/compiler/src/ir/char-unit-locals.ts +++ b/packages/compiler/src/ir/char-unit-locals.ts @@ -11,8 +11,11 @@ import { mapExprChildren, mapStmtChildren } from "./traverse.js"; type CharAt = IrExpr & { kind: "strIntrinsic" }; type UnitUse = IrExpr & { kind: "strEq" | "strCmp" }; +type CompareOp = "<" | "<=" | ">" | ">="; -/** Character locals held as their code unit. +const FLIPPED = { "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const; + +/** Character reads and locals held as their code unit. * * `const c = s[i]` (or `s.charAt(i)`) followed only by tests such as * `c === " "`, `c >= "a" && c <= "z"` or `switch (c) { case "x": ... }` @@ -28,7 +31,16 @@ type UnitUse = IrExpr & { kind: "strEq" | "strCmp" }; * * Only immutable unboxed string locals qualify, with one declaration whose * initializer is a one-argument charAt, and with no other mention of the - * local. */ + * local. + * + * `s[i]` with an index not proven in bounds reads the character or + * undefined (an optional read, see `optionalCharRead`). Its tests against + * one-unit literals compare `i === Math.floor(i) ? s.charCodeAt(i) : NaN` + * instead: undefined, like NaN, equals no literal and fails every ordered + * comparison, and charCodeAt answers NaN out of range. That holds both for + * the read in place and for an immutable local initialized from it and + * used only in such tests and in tests for undefined, which become NaN + * tests. */ export function holdCharLocalsAsUnits(mod: IrModule): IrModule { let changed = false; const functions = mod.functions.map((fn) => { @@ -39,11 +51,275 @@ export function holdCharLocalsAsUnits(mod: IrModule): IrModule { return changed ? { ...mod, functions } : mod; } +/** The read of `s[i]` for an index not proven in bounds: temps for the + * string and the index, then the string's charAt when the index is an + * integer in [0, length) and undefined otherwise. */ +interface OptionalCharRead { + /** The temps' declarations, and whether both operands are plain (local + * reads and literals), which the unit read may evaluate in place. */ + temps: [IrStmt & { kind: "varDecl" }, IrStmt & { kind: "varDecl" }]; + plain: boolean; + receiver: IrExpr; + index: IrExpr; + unionId: string; + stringTag: number; + undefinedTag: number; +} + +function optionalCharRead(e: IrExpr): OptionalCharRead | null { + if (e.kind !== "seqExpr" || e.stmts.length !== 2 || e.diverges) return null; + const [str, idx] = e.stmts; + if (str?.kind !== "varDecl" || idx?.kind !== "varDecl" || !str.init || !idx.init) return null; + const isStr = (x: IrExpr): boolean => x.kind === "varRef" && x.localId === str.localId; + const isIdx = (x: IrExpr): boolean => x.kind === "varRef" && x.localId === idx.localId; + const r = e.result; + if (r.kind !== "ternary") return null; + const c = r.cond; + if (c.kind !== "logical" || c.op !== "&&" || c.left.kind !== "logical" || c.left.op !== "&&") + return null; + const [low, high, whole] = [c.left.left, c.left.right, c.right]; + if ( + !( + low.kind === "bin" && + low.op === ">=" && + isIdx(low.left) && + low.right.kind === "numLit" && + low.right.value === 0 + ) || + !( + high.kind === "bin" && + high.op === "<" && + isIdx(high.left) && + high.right.kind === "strIntrinsic" && + high.right.method === "length" && + isStr(high.right.receiver) + ) || + !( + whole.kind === "bin" && + whole.op === "===" && + isIdx(whole.left) && + whole.right.kind === "libCall" && + whole.right.fn === "math.floor" && + whole.right.args.length === 1 && + isIdx(whole.right.args[0]!) + ) + ) + return null; + const [present, absent] = [r.then, r.else_]; + if (present.kind !== "unionWrap" || absent.kind !== "unionWrap") return null; + if (present.unionId !== absent.unionId) return null; + const read = present.value; + if ( + read.kind !== "strIntrinsic" || + read.method !== "charAt" || + !isStr(read.receiver) || + read.args.length !== 1 || + !isIdx(read.args[0]!) + ) + return null; + if (absent.value.kind !== "unitLit" || absent.value.unit !== "undefined") return null; + return { + temps: [str, idx], + plain: plainOperand(str.init) && plainOperand(idx.init), + receiver: str.init, + index: idx.init, + unionId: present.unionId, + stringTag: present.tag, + undefinedTag: absent.tag, + }; +} + +function plainOperand(e: IrExpr): boolean { + return e.kind === "varRef" || e.kind === "numLit" || e.kind === "strLit"; +} + +/** `i === Math.floor(i) ? s.charCodeAt(i) : NaN`: the code unit of an + * optional read, or NaN where it reads undefined. Plain operands are read + * in place; others keep the read's temps, so each is evaluated once. */ +function unitRead(read: OptionalCharRead, loc: IrExpr["loc"]): IrExpr { + if (!read.plain) { + const [str, idx] = read.temps; + const temp = (decl: IrStmt & { kind: "varDecl" }, type: IrExpr["type"]): IrExpr => ({ + kind: "varRef", + localId: decl.localId, + type, + loc, + }); + return { + kind: "seqExpr", + stmts: [{ ...str }, { ...idx }], + result: unitOf(temp(str, read.receiver.type), () => temp(idx, F64), loc), + type: F64, + loc, + }; + } + return unitOf({ ...read.receiver }, () => ({ ...read.index }), loc); +} + +function unitOf(receiver: IrExpr, index: () => IrExpr, loc: IrExpr["loc"]): IrExpr { + return { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: index(), + right: { kind: "libCall", fn: "math.floor", args: [index()], type: F64, loc }, + type: BOOL, + loc, + }, + then: { + kind: "strIntrinsic", + method: "charCodeAt", + receiver, + args: [index()], + type: F64, + loc, + }, + else_: { kind: "numLit", value: NaN, type: F64, loc }, + type: F64, + loc, + }; +} + +/** A test of an operand that reads one character or undefined, against a + * literal of one code unit or for undefined, and the same test on the + * operand's code unit. `tagsOf` gives the operand's union tags. */ +interface UnitTest { + operand: IrExpr; + /** The test reads the unit twice. */ + twice?: boolean; + test: (unit: () => IrExpr) => IrExpr; +} + +function optionalUnitTest( + e: IrExpr, + tagsOf: (operand: IrExpr) => OptionalCharRead | null, +): UnitTest | null { + const loc = e.loc; + const compare = (op: "===" | "!==" | CompareOp, u: IrExpr, right: IrExpr): IrExpr => ({ + kind: "bin", + op, + left: u, + right, + type: BOOL, + loc, + }); + const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); + if (e.kind === "unionEq" && !e.sameValue) { + for (const [operand, other] of [ + [e.left, e.right], + [e.right, e.left], + ] as const) { + const tags = tagsOf(operand); + if ( + tags && + tags.unionId === e.unionId && + other.kind === "unionWrap" && + other.tag === tags.stringTag && + other.value.kind === "strLit" && + other.value.value.length === 1 + ) { + const code = other.value.value.charCodeAt(0); + return { + operand, + test: (unit) => compare(e.negated ? "!==" : "===", unit(), num(code)), + }; + } + // `case undefined:` of a switch: undefined is a NaN unit. + if ( + tags && + tags.unionId === e.unionId && + other.kind === "unionWrap" && + other.tag === tags.undefinedTag && + other.value.kind === "unitLit" && + other.value.unit === "undefined" + ) + return { + operand, + twice: true, + test: (unit) => compare(e.negated ? "===" : "!==", unit(), unit()), + }; + } + return null; + } + if (e.kind === "libCall" && e.fn === "js.compare" && e.args.length === 3) { + const [a, b, opArg] = e.args as [IrExpr, IrExpr, IrExpr]; + if (opArg.kind !== "strLit" || !(opArg.value in FLIPPED)) return null; + const op = opArg.value as CompareOp; + const literalOf = (x: IrExpr): number | null => + x.kind === "libCall" && + x.fn === "js.fromStr" && + x.args.length === 1 && + x.args[0]!.kind === "strLit" && + x.args[0]!.value.length === 1 + ? x.args[0]!.value.charCodeAt(0) + : null; + const operandOf = (x: IrExpr): IrExpr | null => + x.kind === "libCall" && + x.fn === "js.fromDyn" && + x.args.length === 1 && + x.args[0]!.kind === "dynFrom" && + tagsOf(x.args[0]!.value) + ? x.args[0]!.value + : null; + // The comparison with the operand on the left. + const left = operandOf(a); + const right = operandOf(b); + const code = left ? literalOf(b) : right ? literalOf(a) : null; + const operand = left ?? right; + if (operand === null || code === null) return null; + const ordered = left ? op : FLIPPED[op]; + return { operand, test: (unit) => compare(ordered, unit(), num(code)) }; + } + if (e.kind === "unionIsTag") { + const tags = tagsOf(e.value); + if (!tags || tags.unionId !== e.unionId) return null; + if (e.tag !== tags.undefinedTag && e.tag !== tags.stringTag) return null; + // Undefined is a NaN unit; a character is any other. + const present = (e.tag === tags.stringTag) !== e.negated; + return { + operand: e.value, + twice: true, + test: (unit) => compare(present ? "===" : "!==", unit(), unit()), + }; + } + return null; +} + function rewriteFunction(fn: IrFunction): IrFunction { + const locals = new Map(fn.locals.map((l) => [l.id, l])); const candidates = new Set( - fn.locals.filter((l) => !l.mutable && !l.boxed && l.type.kind === "string").map((l) => l.id), + fn.locals + .filter( + (l) => !l.mutable && !l.boxed && (l.type.kind === "string" || l.type.kind === "union"), + ) + .map((l) => l.id), ); - if (candidates.size === 0) return fn; + // The optional reads candidate union locals are declared from, directly + // or as copies of another such local (a switch's discriminant temp). + const reads = new Map(); + const copyOf = new Map(); + const collect = (value: unknown): void => { + if (Array.isArray(value)) for (const item of value) collect(item); + else if (value !== null && typeof value === "object") { + const node = value as Record; + if (node.kind === "varDecl" && candidates.has(node.localId as string) && node.init) { + const id = node.localId as string; + const init = node.init as IrExpr; + const read = optionalCharRead(init); + if (read) reads.set(id, read); + else if (init.kind === "varRef" && reads.has(init.localId)) { + reads.set(id, reads.get(init.localId)!); + copyOf.set(id, init.localId); + } + } + for (const key in node) if (key !== "loc") collect(node[key]); + } + }; + collect(fn.body); + const localTags = (operand: IrExpr): OptionalCharRead | null => + operand.kind === "varRef" ? (reads.get(operand.localId) ?? null) : null; + // Every mention of a candidate, in any field, against the mentions the // rewrite understands. const mentions = new Map(); @@ -59,7 +335,7 @@ function rewriteFunction(fn: IrFunction): IrFunction { for (const item of value) count(item); } else if (value !== null && typeof value === "object") { const node = value as Record; - const id = understoodMention(node); + const id = understoodMention(node, locals, reads, localTags); if (id !== null && candidates.has(id)) { note(understood, id); if (node.kind === "varDecl") note(declarations, id); @@ -68,15 +344,26 @@ function rewriteFunction(fn: IrFunction): IrFunction { } }; count(fn.body); + // A copy is held only with its source, and the source's mention in the + // copy's initializer is understood only while the copy is held. const held = new Set(); for (const id of candidates) - if ( - declarations.get(id) === 1 && - (mentions.get(id) ?? 0) > 0 && - mentions.get(id) === understood.get(id) - ) - held.add(id); - if (held.size === 0) return fn; + if (declarations.get(id) === 1 && (mentions.get(id) ?? 0) > 0) held.add(id); + for (let settled = false; !settled;) { + settled = true; + for (const id of held) { + let copies = 0; + for (const [copy, source] of copyOf) if (source === id && held.has(copy)) copies++; + const source = copyOf.get(id); + if ( + mentions.get(id) !== (understood.get(id) ?? 0) + copies || + (source !== undefined && !held.has(source)) + ) { + held.delete(id); + settled = false; + } + } + } const unitRef = (id: string, loc: IrExpr["loc"]): IrExpr => ({ kind: "varRef", @@ -84,10 +371,19 @@ function rewriteFunction(fn: IrFunction): IrFunction { type: F64, loc, }); + // Operands of optional unit tests: held union locals and reads in place. + const operandTags = (operand: IrExpr): OptionalCharRead | null => + operand.kind === "varRef" + ? held.has(operand.localId) + ? localTags(operand) + : null + : optionalCharRead(operand); + let changed = false; const rewriteExpr = (e: IrExpr): IrExpr => { if (e.kind === "strEq" || e.kind === "strCmp") { const use = unitUse(e); if (use && held.has(use.id)) { + changed = true; const code: IrExpr = { kind: "numLit", value: use.code, type: F64, loc: e.loc }; const c = unitRef(use.id, e.loc); if (e.kind === "strEq") @@ -100,9 +396,7 @@ function rewriteFunction(fn: IrFunction): IrFunction { loc: e.loc, }; // The comparison with the local on the left. - const op = use.left - ? e.op - : ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const)[e.op]; + const op = use.left ? e.op : FLIPPED[e.op]; if (op === ">" || op === ">=") return { kind: "bin", op, left: c, right: code, type: BOOL, loc: e.loc }; const negation: IrExpr = { @@ -116,21 +410,43 @@ function rewriteFunction(fn: IrFunction): IrFunction { return { kind: "unary", op: "!", operand: negation, type: BOOL, loc: e.loc }; } } + const optional = optionalUnitTest(e, operandTags); + if (optional) { + const operand = optional.operand; + if (operand.kind === "varRef") { + changed = true; + return optional.test(() => unitRef(operand.localId, e.loc)); + } + // A read that keeps its temps declares them once: one unit read. + const read = optionalCharRead(operand)!; + if (read.plain || !optional.twice) { + changed = true; + return optional.test(() => unitRead(read, operand.loc)); + } + } return mapExprChildren(e, rewriteExpr, rewriteStmt); }; const rewriteStmt = (s: IrStmt): IrStmt => { - if (s.kind === "varDecl" && held.has(s.localId) && s.init?.kind === "strIntrinsic") - return { - ...s, - init: { - ...s.init, - method: "charCodeAt", - receiver: rewriteExpr(s.init.receiver), - args: s.init.args.map(rewriteExpr), - type: F64, - }, - }; - if (s.kind === "switch" && s.disc.kind === "varRef" && held.has(s.disc.localId)) + if (s.kind === "varDecl" && held.has(s.localId) && s.init) { + changed = true; + const source = copyOf.get(s.localId); + if (source !== undefined) return { ...s, init: unitRef(source, s.init.loc) }; + const read = reads.get(s.localId); + if (read) return { ...s, init: unitRead(read, s.init.loc) }; + if (s.init.kind === "strIntrinsic") + return { + ...s, + init: { + ...s.init, + method: "charCodeAt", + receiver: rewriteExpr(s.init.receiver), + args: s.init.args.map(rewriteExpr), + type: F64, + }, + }; + } + if (s.kind === "switch" && s.disc.kind === "varRef" && held.has(s.disc.localId)) { + changed = true; return { ...s, disc: unitRef(s.disc.localId, s.disc.loc), @@ -143,11 +459,14 @@ function rewriteFunction(fn: IrFunction): IrFunction { body: c.body.map(rewriteStmt), })), }; + } return mapStmtChildren(s, rewriteExpr, rewriteStmt); }; + const body = fn.body.map(rewriteStmt); + if (!changed) return fn; return { ...fn, - body: fn.body.map(rewriteStmt), + body, locals: fn.locals.map((l) => (held.has(l.id) ? unitLocal(l) : l)), }; } @@ -157,22 +476,37 @@ function unitLocal(local: IrLocal): IrLocal { return { ...local, type: F64 }; } -/** The local a node mentions in a form the rewrite handles: the - * declaration from a one-argument charAt, a one-unit test, or a switch - * over one-unit cases. */ -function understoodMention(node: Record): string | null { +/** The local a node mentions in a form the rewrite handles: a string + * local's declaration from a one-argument charAt, one-unit test, or switch + * over one-unit cases; a union local's declaration from an optional read, + * or its test against a one-unit literal or for undefined. */ +function understoodMention( + node: Record, + locals: ReadonlyMap, + reads: ReadonlyMap, + localTags: (operand: IrExpr) => OptionalCharRead | null, +): string | null { const n = node as unknown as IrExpr | IrStmt; - if (n.kind === "varDecl") return n.init && isCharAt(n.init) ? n.localId : null; + if (n.kind === "varDecl") { + if (!n.init) return null; + const local = locals.get(n.localId); + if (local?.type.kind === "string") return isCharAt(n.init) ? n.localId : null; + const read = reads.get(n.localId); + return read && local?.type.kind === "union" && local.type.unionId === read.unionId + ? n.localId + : null; + } if (n.kind === "strEq" || n.kind === "strCmp") return unitUse(n)?.id ?? null; if (n.kind === "switch") { - if (n.disc.kind !== "varRef") return null; + if (n.disc.kind !== "varRef" || n.disc.type.kind !== "string") return null; if (!n.cases.some((c) => c.test !== null)) return null; const units = n.cases.every( (c) => c.test === null || (c.test.kind === "strLit" && c.test.value.length === 1), ); return units ? n.disc.localId : null; } - return null; + const optional = optionalUnitTest(n as IrExpr, localTags); + return optional?.operand.kind === "varRef" ? optional.operand.localId : null; } function isCharAt(e: IrExpr): e is CharAt { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7304-string-optional-char-reads.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7304-string-optional-char-reads.ts.json new file mode 100644 index 000000000..36ea6f8ec --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7304-string-optional-char-reads.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7304-string-optional-char-reads.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7304-string-optional-char-reads.ts b/tests/corpus/7304-string-optional-char-reads.ts new file mode 100644 index 000000000..13222219d --- /dev/null +++ b/tests/corpus/7304-string-optional-char-reads.ts @@ -0,0 +1,121 @@ +// s[i] with an index not proven in bounds, tested against one-unit +// literals: equality, inequality and ordered comparisons on either side, +// peeks at i - 1 and i + 1, tests for undefined, switches, character +// locals with and without other uses, fractional, negative, -0, NaN and +// infinite indices, reads past the end, a two-unit literal, Object.is, a +// class-based lexer with its position in a field, and ASCII, Latin-1, BMP, +// surrogate pairs, lone surrogates and empty strings. + +class Lexer { + pos = 0; + src: string; + constructor(src: string) { + this.src = src; + } + skipSpaces(): void { + const src = this.src; + let pos = this.pos; + while (pos < src.length && (src[pos] === " " || src[pos] === "\t")) pos++; + this.pos = pos; + } + word(): string { + const src = this.src; + const start = this.pos; + let pos = start; + while (pos < src.length) { + const c = src[pos]; + if ((c < "a" || c > "z") && (c < "0" || c > "9")) break; + pos++; + } + this.pos = pos; + return src.slice(start, pos); + } + operator(): string { + const src = this.src; + const c = src[this.pos]; + switch (c) { + case "=": + this.pos += src[this.pos + 1] === "=" ? 2 : 1; + return src[this.pos - 1] === "=" && src[this.pos - 2] === "=" ? "==" : "="; + case "+": + case "-": + this.pos++; + return c; + case undefined: + return ""; + default: + this.pos++; + return "?"; + } + } + tokens(): string[] { + const out: string[] = []; + while (this.pos < this.src.length) { + this.skipSpaces(); + const w = this.word(); + if (w !== "") out.push(w); + else if (this.pos < this.src.length) out.push(this.operator()); + } + return out; + } +} + +function tests(src: string, pos: number): string { + const bits: number[] = []; + bits.push(src[pos] === " " ? 1 : 0, src[pos] !== "x" ? 1 : 0); + bits.push(src[pos] < "a" ? 1 : 0, src[pos] <= "a" ? 1 : 0); + bits.push(src[pos] > "a" ? 1 : 0, src[pos] >= "a" ? 1 : 0); + bits.push("m" < src[pos] ? 1 : 0, "m" >= src[pos] ? 1 : 0); + bits.push(src[pos + 1] === "b" ? 1 : 0, src[pos - 1] > "\0" ? 1 : 0); + bits.push(src[pos] === undefined ? 1 : 0, src[pos] !== undefined ? 1 : 0); + bits.push(src[pos * 2] === undefined ? 1 : 0, src[pos] === "ab" ? 1 : 0); + bits.push(Object.is(src[pos], "a") ? 1 : 0); + return bits.join(""); +} + +function local(src: string, pos: number): string { + const c = src[pos]; + let n = 0; + if (c === undefined) n += 1; + if (c !== undefined && c >= "a") n += 2; + if ("z" > c) n += 4; + switch (c) { + case "é": + n += 8; + break; + case "b": + n += 16; + break; + default: + n += 32; + } + return String(n); +} + +function kept(src: string, pos: number): string { + const c = src[pos]; + if (c === " ") return "space"; + return c ?? "end"; +} + +const samples = ["abc", "", " ", "a b\tc", "café", "日本 語", "😀x", "\uD800z", "ab=b"]; +const indices = [0, 1, 2, 3, -1, -0, 0.5, 1.5, NaN, Infinity, 10]; +for (const s of samples) { + const row: string[] = []; + for (const i of indices) row.push(`${tests(s, i)}:${local(s, i)}:${kept(s, i)}`); + console.log(JSON.stringify(s), row.join(" ")); +} + +const programs = [ + "let x = a + b - c", + " alpha==beta gamma = delta1 ", + "x=y==z+", + "", + "abc é def", + "\tq\t=\t9", +]; +for (const p of programs) console.log(JSON.stringify(p), JSON.stringify(new Lexer(p).tokens())); + +let text = ""; +for (let k = 0; k < 30; k++) text += (k % 4 === 0 ? "==" : "w" + k) + " "; +console.log(new Lexer(text).tokens().length, new Lexer(text).tokens().slice(0, 6).join(",")); diff --git a/tests/harness/corpus-costs/7304-string-optional-char-reads.ts.json b/tests/harness/corpus-costs/7304-string-optional-char-reads.ts.json new file mode 100644 index 000000000..207668232 --- /dev/null +++ b/tests/harness/corpus-costs/7304-string-optional-char-reads.ts.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 4200 } From e48b2c343508a831bbef57e569b97705586fa2e9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:24:51 -0500 Subject: [PATCH 011/273] Keep held format arguments static and compiling The runtime format branch packed its arguments with a checked-dynamic array literal, a dynamic operation: a console.log with a string first argument put its function in the dynamic tier, and the property-bag analysis read the format calls as possible dynamic stores. The held arguments now go to the formatter as a native list of checked values (insp.formatList and insp.formatPrimitives), which is a tier boundary, and the format and inspect entries join the helpers that never store into a class instance. A first argument whose type may hold a string only through a union, or whose checker type does not map, packed every argument as a checked value and failed to compile when one did not convert (7380's `console.log(top ? [...top] : "none", byName)`). It now takes the held-argument form: the lowered value is tested for a substitution and otherwise joins as before. In TypeScript an argument without a checked-dynamic form is refused at compile time only when the joined form cannot print it either; otherwise the branch that substitutes throws SC2020, as in JavaScript. --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/lower-console.ts | 3 +- .../src/frontend/lowering/lower-inspect.ts | 55 +++++++++++++------ packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 3 +- packages/compiler/src/ir/int32-slots.ts | 12 ++++ packages/runtime/src/scr_inspect.c | 23 +++++++- packages/runtime/src/scr_inspect.h | 8 ++- 8 files changed, 81 insertions(+), 25 deletions(-) diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index b02cf44f3..365393326 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1036,6 +1036,7 @@ export const LIB_FN_SYMS: Record = { "insp.dynS": "scr_insp_dyn_s", "insp.format": "scr_insp_format_pack", "insp.formatPrimitives": "scr_insp_format_primitives", + "insp.formatList": "scr_insp_format_list", "insp.formatWithOptions": "scr_insp_format_with_options", "insp.inspect": "scr_insp_inspect", "insp.customEnabled": "scr_insp_custom_enabled", diff --git a/packages/compiler/src/frontend/lowering/lower-console.ts b/packages/compiler/src/frontend/lowering/lower-console.ts index db4d39e70..21540cf47 100644 --- a/packages/compiler/src/frontend/lowering/lower-console.ts +++ b/packages/compiler/src/frontend/lowering/lower-console.ts @@ -21,6 +21,7 @@ import { lowerInspectValue, lowerRuntimeFormat, runtimeFormatString, + formatStringConverts, } from "./lower-inspect.js"; import { streamSidesOf } from "./lower-stream.js"; @@ -372,7 +373,7 @@ export function lowerConsoleOutput( return consoleWrite(stdout, [lowerRuntimeFormat(lowerer, nodes, loc)], loc); const runtime = nodes.length > 1 && !isStringLiteralLike(first) ? runtimeFormatString(lowerer, first!) : null; - if (runtime === "other") + if (runtime === "other" && !formatStringConverts(lowerer, first!)) return consoleWrite(stdout, [lowerRuntimeFormat(lowerer, nodes, loc)], loc); if (runtime !== null) { const held = heldFormatArgs(lowerer, nodes, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index dd950a2a2..9afe12ed4 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -35,6 +35,7 @@ import { canConvertToDyn, canDynCheckTo, funcOf, + arrayOf, CLASS_FIELD_ATTRIBUTES_PREFIX, classDynViewSupported, DYN_CLASS_PROPERTIES, @@ -3205,7 +3206,8 @@ export function lowerFormatCall( if (!firstIsLiteral) { const runtime = argNodes.length > 1 ? runtimeFormatString(lowerer, first) : null; - if (runtime === "other") return lowerRuntimeFormat(lowerer, argNodes, loc); + if (runtime === "other" && !formatStringConverts(lowerer, first)) + return lowerRuntimeFormat(lowerer, argNodes, loc); if (runtime !== null) { // A string that substitutes formats at run time; any other string // joins with the remaining arguments, as below. @@ -3576,6 +3578,14 @@ function runtimeFenceValue( }; } +/** Whether the held-argument form can test a format string whose type + * only may hold a string: a union that converts to a checked-dynamic value, + * or a value whose checker type does not map (its lowered type decides). */ +export function formatStringConverts(lowerer: Lowerer, first: ts.Expression): boolean { + const t = lowerer.mapTypeOf(lowerer.typeOf(first)); + return t === null || t.kind === "dyn" || lowerer.dynConvertible(t); +} + /** How a non-literal format string is known: "string" and "dyn" values * test for a substitution at run time, "other" types (unions with a string * arm) format at run time, and null types never hold a string. */ @@ -3673,14 +3683,10 @@ export function heldFormatArgs( "util.format of a void call result (call it as its own statement, then pass undefined)", ); } - if (value.type.kind !== "dyn" && !lowerer.dynConvertible(value.type)) { - // JavaScript keeps the space-joined branch, which printed these - // values before, and refuses only a string that substitutes. - if (!isJsSourceFile(ts.getSourceFileOfNode(node))) { - lowerer.noLowering(unconvertibleSurface(value.type), node, UNCONVERTIBLE_HINT); - } + // The space-joined branch prints these values as before; only a string + // that substitutes refuses them, where it runs. + if (value.type.kind !== "dyn" && !lowerer.dynConvertible(value.type)) unconvertible ??= { node, type: value.type }; - } // Unit values have no local: their effects run here and a literal // stands in. const k = value.type.kind; @@ -3694,16 +3700,20 @@ export function heldFormatArgs( return { node, ref: () => varRef(held.id, value.type, loc) }; }); const first = args[0]!.ref(); + // A lowered value that is neither a string nor convertible holds no + // string, so it never substitutes. const specifies: IrExpr = first.type.kind === "string" ? { kind: "libCall", fn: "insp.formatSpecifies", args: [first], type: BOOL, loc } - : { - kind: "libCall", - fn: "insp.formatSpecifiesDyn", - args: [lowerer.coerceToExpected(first, DYN)], - type: BOOL, - loc, - }; + : first.type.kind === "dyn" || lowerer.dynConvertible(first.type) + ? { + kind: "libCall", + fn: "insp.formatSpecifiesDyn", + args: [lowerer.coerceToExpected(first, DYN)], + type: BOOL, + loc, + } + : { kind: "boolLit", value: false, type: BOOL, loc }; const format = (): IrExpr => { if (unconvertible !== null) { const fenced = unconvertible; @@ -3725,14 +3735,18 @@ export function heldFormatArgs( t.kind === "undefinedT" || t.kind === "nullT" || (t.kind === "union" && (lowerer.unions.get(t.unionId)?.arms.every(primitive) ?? false)); + // A native list of the checked values: converting into it is a tier + // boundary, so the statement stays static. return { kind: "libCall", - fn: args.every((arg) => primitive(arg.ref().type)) ? "insp.formatPrimitives" : "insp.format", + fn: args.every((arg) => primitive(arg.ref().type)) + ? "insp.formatPrimitives" + : "insp.formatList", args: [ { - kind: "dynArrLit", + kind: "arrayLit", elems: args.map((arg) => lowerer.coerceToExpected(arg.ref(), DYN)), - type: DYN, + type: arrayOf(DYN), loc, }, ], @@ -3748,6 +3762,11 @@ export function heldFormatArgs( if (t.kind === "bytes") return true; return inspectSupport(lowerer, t, new Set(), { recursive: false }) === null; }); + // Without a joined branch every call formats: TypeScript refuses the + // value at compile time. + const refused = unconvertible as { node: ts.Expression; type: IrType } | null; + if (!joinable && refused !== null && !isJsSourceFile(ts.getSourceFileOfNode(refused.node))) + lowerer.noLowering(unconvertibleSurface(refused.type), refused.node, UNCONVERTIBLE_HINT); return { stmts, args, specifies, format, joinable }; } diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index d676b05dd..357992a5a 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -183,6 +183,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // The runtime formatter calls toString, valueOf and toJSON, and refuses // renderings it has no lowering for. "insp.format", + "insp.formatList", "insp.formatWithOptions", "insp.inspect", // The options object reads the caller's options (getters included), and diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 6e07e1467..c127af93c 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4376,7 +4376,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "insp.format": { argTypes: [DYN], result: STRING }, /** insp.format over arguments that are all strings, numbers, booleans, * null or undefined, without the checked-dynamic walk. */ - "insp.formatPrimitives": { argTypes: [DYN], result: STRING }, + "insp.formatPrimitives": { argTypes: [arrayOf(DYN)], result: STRING }, + "insp.formatList": { argTypes: [arrayOf(DYN)], result: STRING }, "insp.formatWithOptions": { argTypes: [DYN, DYN], result: STRING }, /** util.inspect(value, options, depth, colors) with the call's argument * count: the runtime reads the options and renders the checked-dynamic diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index ac0747276..6a30eb92b 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -407,6 +407,18 @@ const DYN_READ_ONLY = new Set([ "fs.utimesSync", "insp.dyn", "insp.dynS", + // util.format and util.inspect over checked values: the inspect walk and + // the format conversions read them; insp.registerCustom keeps a class's + // custom inspect thunk in the runtime's registry. + "insp.customIsSelf", + "insp.customResult", + "insp.format", + "insp.formatList", + "insp.formatPrimitives", + "insp.formatSpecifiesDyn", + "insp.formatWithOptions", + "insp.inspect", + "insp.registerCustom", // The dynamic-tier operators run their dyn twins (scr_js_value.c); the // conversions move a value between dyn and jsval, and js.toF64 is // dynCheck's number check. diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 2c7951103..da49804c1 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -2575,11 +2575,21 @@ static ScrStr *insp_prim_number(ScrDyn *v, int spec) { static const InspFmtOps insp_fmt_primitive_ops = {insp_prim_s, insp_prim_j, insp_prim_number, insp_prim_inspect}; -ScrStr *scr_insp_format_primitives(ScrDyn *pack) { +/* A native list of checked values (the compiled format calls) as the + * runtime's argument pack. +1. */ +static ScrDyn *insp_list_pack(ScrArr *list) { + ScrDyn *pack = scr_dyn_new_arr(); + for (uint32_t i = 0; i < list->len; i++) scr_dyn_arr_push(pack, scr_arr_get_ref(list, i)); + return pack; +} + +ScrStr *scr_insp_format_primitives(ScrArr *list) { ScrInspFormatOptions o; scr_insp_format_defaults(&o, "util.format"); - if (pack->kind != SCR_DYN_ARR) return scr_str_new("", 0); - return insp_format_loop(pack->v.arr.items, pack->v.arr.len, &o, &insp_fmt_primitive_ops); + ScrDyn *pack = insp_list_pack(list); + ScrStr *out = insp_format_loop(pack->v.arr.items, pack->v.arr.len, &o, &insp_fmt_primitive_ops); + scr_dyn_release(pack); + return out; } /* The arguments of a dyn array pack (+1 text, or NULL pending). */ @@ -2594,6 +2604,13 @@ ScrStr *scr_insp_format_pack(ScrDyn *pack) { return insp_fmt_pack(pack, &o); } +ScrStr *scr_insp_format_list(ScrArr *list) { + ScrDyn *pack = insp_list_pack(list); + ScrStr *out = scr_insp_format_pack(pack); + scr_dyn_release(pack); + return out; +} + /* Whether a format string can substitute: a '%' before its last character. * A string without one formats as its arguments joined with spaces. */ bool scr_insp_format_specifies(ScrStr *first) { diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h index a95ac1fde..339fe781b 100644 --- a/packages/runtime/src/scr_inspect.h +++ b/packages/runtime/src/scr_inspect.h @@ -76,9 +76,13 @@ ScrStr *scr_insp_format(ScrDyn *const *args, size_t argc, const ScrInspFormatOpt /* util.format(...pack) and util.formatWithOptions(options, ...pack) over a * dyn array of the arguments (+1). */ ScrStr *scr_insp_format_pack(ScrDyn *pack); -/* util.format(...pack) when every argument is a string, number, boolean, +/* util.format over a native list of checked values, the compiled calls' + * held arguments (+1). */ +struct ScrArr; +ScrStr *scr_insp_format_list(struct ScrArr *list); +/* scr_insp_format_list when every argument is a string, number, boolean, * null or undefined, without the checked-dynamic walk (+1). */ -ScrStr *scr_insp_format_primitives(ScrDyn *pack); +ScrStr *scr_insp_format_primitives(struct ScrArr *list); ScrStr *scr_insp_format_with_options(ScrDyn *options, ScrDyn *pack); /* util.inspect(value, options, depth, colors) over checked-dynamic * arguments with Node's legacy-argument rules; argc is the call's From 56f70c47b1ae251451c8f1bb42da5fd859c5493e Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 20:06:30 -0700 Subject: [PATCH 012/273] Answer 'in' on native Node handles from their declared members `"closeAllConnections" in server` (the feature test servers use before calling it) was refused for an http.Server and the other native handles. A member Node's declarations give the class is on its prototype, so the test now answers true after evaluating the receiver. Undeclared names keep the refusal, since internal fields can still be present. Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/relational.ts | 48 +++++++++++++++++++ .../server/cases/http-in-members/driver.mjs | 7 +++ .../server/cases/http-in-members/main.ts | 18 +++++++ 3 files changed, 73 insertions(+) create mode 100644 tests/fixtures/server/cases/http-in-members/driver.mjs create mode 100644 tests/fixtures/server/cases/http-in-members/main.ts diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 0eb8ae653..edec5ba5f 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1762,6 +1762,25 @@ export function lowerInExpression( }; } } + // A native Node handle (an http.Server, a socket, a child process...): + // a member Node's declarations give the class is on its prototype, so + // `"closeAllConnections" in server` answers true. Undeclared names can + // still be real (internal `_`-prefixed fields), so they keep the fence. + if ( + NATIVE_HANDLE_KINDS.has(recv.type.kind) && + nodeDeclaredMember(lowerer, lowerer.typeOf(expr.right), key) + ) { + const answer: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc }; + return isSafeToDiscard(recv) + ? answer + : { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: recv, loc }], + result: answer, + type: BOOL, + loc, + }; + } if (recv.type.kind !== "record") { lowerer.unsupported( "SC1090", @@ -2059,3 +2078,32 @@ function dynInstanceOfRefusal(lowerer: Lowerer, expr: ts.BinaryExpression): stri const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1; return `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`; } + +/** IR kinds of native Node handles whose members come from Node's own + * declarations. */ +const NATIVE_HANDLE_KINDS: ReadonlySet = new Set([ + "netServer", + "netSocket", + "httpReq", + "httpRes", + "httpClientReq", + "child", + "dgramSocket", + "fsWatcher", + "http2Session", + "http2Stream", +]); + +/** Whether every constituent of `t` declares a required member `key` in + * a declaration file (Node's @types/node). */ +function nodeDeclaredMember(lowerer: Lowerer, t: ts.Type, key: string): boolean { + const parts = t.isUnionType() ? ts.constituentTypes(t) : [t]; + return parts.every((part) => { + const prop = lowerer.checker.getPropertyOfType(part, key); + if (prop === undefined || (prop.flags & ts.SymbolFlags.Optional) !== 0) return false; + return ( + prop.declarations.length > 0 && + prop.declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile) + ); + }); +} diff --git a/tests/fixtures/server/cases/http-in-members/driver.mjs b/tests/fixtures/server/cases/http-in-members/driver.mjs new file mode 100644 index 000000000..9c989bb70 --- /dev/null +++ b/tests/fixtures/server/cases/http-in-members/driver.mjs @@ -0,0 +1,7 @@ +import http from "node:http"; +const port = Number(process.argv[2]); +http.get({ host: "127.0.0.1", port, path: "/" }, (res) => { + let body = ""; + res.on("data", (c) => (body += c)); + res.on("end", () => console.log(res.statusCode, body)); +}); diff --git a/tests/fixtures/server/cases/http-in-members/main.ts b/tests/fixtures/server/cases/http-in-members/main.ts new file mode 100644 index 000000000..80babfc13 --- /dev/null +++ b/tests/fixtures/server/cases/http-in-members/main.ts @@ -0,0 +1,18 @@ +// `in` tests on native Node handles answer from Node's declared members. +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; + +const server = createServer((req, res) => { + console.log("request", "method" in req, "setHeader" in res, "writeHead" in res); + res.end("ok"); +}); +console.log("closeAllConnections" in server, "closeIdleConnections" in server, "listen" in server); +server.listen(0, "127.0.0.1", () => { + process.stderr.write(`PORT ${(server.address() as AddressInfo).port}\n`); +}); +server.on("request", () => { + setTimeout(() => { + if ("closeAllConnections" in server) server.closeAllConnections(); + server.close(() => console.log("closed")); + }, 10); +}); From e7bced94fd9082c9007d1af6f4cdefeb97f752f5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:20:04 -0500 Subject: [PATCH 013/273] Let LLVM merge the comparisons of a three-way string comparator --- .../compiler/src/backend/llvm/emit-module.ts | 6 +-- .../src/backend/llvm/string-lifetimes.test.ts | 4 +- tests/harness/string-compare-emission.test.ts | 47 +++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 tests/harness/string-compare-emission.test.ts diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 16a1b1601..3c0e91e20 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -1601,8 +1601,8 @@ export function helperDefs(emitter: LlEmitter): string[] { // order; other differing words decide by their first differing byte, // and strings shorter than a word by their first byte. Otherwise a // proven-ASCII string on either side orders by bytes (scr_str_cmp), and - // other pairs call scr_str_cmp_u16_bytes, which only reads the two - // strings, so LLVM can merge repeated comparisons of one pair. Callers + // other pairs call scr_str_cmp_u16_bytes. Both only read the two strings, + // so LLVM can merge repeated comparisons of one pair. Callers // only test the sign of the result. if (emitter.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) { const fn = "scr_str_cmp_u16"; @@ -1732,7 +1732,7 @@ export function helperDefs(emitter: LlEmitter): string[] { ` %anyascii = icmp slt i32 %cws, 0`, ` br i1 %anyascii, label %memorder, label %units`, `memorder:`, - ` %m = call i32 @scr_str_cmp(ptr %a, ptr %b)`, + ` %m = call i32 @scr_str_cmp(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`, ` ret i32 %m`, `units:`, ` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`, diff --git a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts index f85addcf8..07ad31153 100644 --- a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts +++ b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts @@ -478,7 +478,9 @@ test("string comparisons decide identity, lengths and leading bytes before the r expect(units).toContain("%below = icmp ult i8 %bx, %by"); expect(units).toContain("%tailable = and i1 %samelen, %short"); expect(units).toContain("%anyascii = icmp slt i32 %cws, 0"); - expect(units).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)"); + expect(units).toContain( + "call i32 @scr_str_cmp(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)", + ); expect(units).toContain( "call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)", ); diff --git a/tests/harness/string-compare-emission.test.ts b/tests/harness/string-compare-emission.test.ts new file mode 100644 index 000000000..2654f56ba --- /dev/null +++ b/tests/harness/string-compare-emission.test.ts @@ -0,0 +1,47 @@ +/* String comparison emission: the runtime calls behind a relational string + * operator only read the two strings, so LLVM merges the comparisons of one + * pair. A three-way comparator (`a < b ? -1 : a > b ? 1 : 0`) then compares + * its strings once, as a comparator that tests one operator does. */ +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); + +async function emittedAsm(name: string, source: string): Promise { + const key = createHash("sha256").update(source).digest("hex").slice(0, 16); + const outDir = join(cacheDir, `string-compare-${key}`); + mkdirSync(outDir, { recursive: true }); + const file = join(outDir, `${name}.ts`); + writeFileSync(file, source); + const result = await compile(file, { + outPath: join(outDir, name), + outDir, + backend: "llvm", + outputKind: "asm", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return readFileSync(result.artifact.path, "utf8"); +} + +const program = (body: string): string => `function order(a: string, b: string): number { + return ${body}; +} +const words = process.argv.slice(2); +words.sort(order); +console.log(words.join(",")); +`; + +const compareCalls = (asm: string): number => + asm.match(/\bcall[a-z]*\s+(?:sc_str_cmp_u16|scr_str_cmp_u16_bytes|scr_str_cmp)\b/g)?.length ?? 0; + +test("a three-way string comparator compares its pair once", async () => { + const once = compareCalls(await emittedAsm("once", program("a < b ? -1 : 1"))); + const both = compareCalls(await emittedAsm("both", program("a < b ? -1 : a > b ? 1 : 0"))); + expect(once).toBeGreaterThan(0); + expect(both).toBe(once); +}); From 03807c0c3574d395c53905aa4b8983ef8ec11d09 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 04:11:54 -0500 Subject: [PATCH 014/273] Run private members and super calls over foreign receivers like Node A JavaScript class's prototype method called with a receiver that is not an instance of its class (`C.prototype.m.call(other)`, or an object that inherits the method) now behaves as in Node when it uses private members or calls super methods, instead of throwing "expected C" for the receiver. - Private fields keep their brand check: reading or writing `this.#x` throws "Cannot read private member #x from an object whose class did not declare it" (or the write message), and `#x in this` answers false. - `this.#m()` brand-checks the receiver through a new `call` use of the private brand helpers and throws "Receiver must be an instance of class C" (V8's wording: the class's own identifier, "anonymous" without one, and "Cannot read properties of undefined (reading 'C')" for a nullish receiver). It also applies to private method calls on other checked-dynamic values. - `super.m(...)` looks the method up on the base class's complete prototype object and calls it with the receiver, as `super` reads [[HomeObject]].[[Prototype]]. Methods that use `super` in other positions, static methods with private names, and classes whose base chain includes a function-local, generic, mixin, Date or runtime-provided class keep the native receiver check. The receiver-independent method lowering and the prototype reflection now mark their frames as `this` boundaries, so lowering one class's members on top of another method's frames (a super call reflecting the base) never reaches the caller's `this`. --- .../frontend/lowering/class-method-values.ts | 50 ++++++- .../src/frontend/lowering/class-prototypes.ts | 3 + .../src/frontend/lowering/lower-calls.ts | 44 ++++++ .../src/frontend/lowering/lower-classes.ts | 14 +- .../src/frontend/lowering/private-brands.ts | 50 ++++++- ...1-class-methods-foreign-receivers.mjs.json | 6 + .../7621-class-methods-foreign-receivers.mjs | 130 ++++++++++++++++++ ...1-class-methods-foreign-receivers.mjs.json | 1 + 8 files changed, 292 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-class-methods-foreign-receivers.mjs.json create mode 100644 tests/corpus/7621-class-methods-foreign-receivers.mjs create mode 100644 tests/harness/corpus-costs/7621-class-methods-foreign-receivers.mjs.json diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts index 6e8256513..af2ef51d5 100644 --- a/packages/compiler/src/frontend/lowering/class-method-values.ts +++ b/packages/compiler/src/frontend/lowering/class-method-values.ts @@ -378,9 +378,17 @@ function reflectedMethodValue( !isJsSourceFile(ts.getSourceFileOfNode(declaration)) || info.localClass !== undefined || lowerer.errorHierarchyClassOf(info.def.name); + // `super.m(...)` in a prototype method reads the base class's prototype, + // which lowerSuperMethodCall answers over the call-site receiver when the + // base has a prototype object. Other uses of `super` keep the native + // receiver. + const superCalls = !staticMethod && info.base !== null && prototypeChainViewable(info.base); ts.walkPreorder(member, (node) => { - if (ts.isPrivateIdentifier(node) || node.kind === ts.SyntaxKind.SuperKeyword) + if (node.kind === ts.SyntaxKind.SuperKeyword && !(superCalls && isSuperMethodCallee(node))) needsNativeReceiver = true; + // Private members on a static method's receiver (the class value) keep + // the native receiver; prototype methods brand-check theirs. + if (staticMethod && ts.isPrivateIdentifier(node)) needsNativeReceiver = true; }); if (needsNativeReceiver) return null; // The method reads itself as a value inside its own body: it is the @@ -403,7 +411,11 @@ function reflectedMethodValue( loc: locOf(member), }; } + // The method may be lowered on top of an unrelated caller's frames (a + // `super` call in another class's method reflects this class): its `this` + // is its own receiver, never an enclosing function's. const context = newFnCtx(false, null, null, DYN); + context.thisBoundary = true; const previousClass = lowerer.currentClass; lowerer.currentClass = info; lowerer.fnStack.push(context); @@ -441,6 +453,42 @@ function reflectedMethodValue( } } +/** Whether `base` and every class above it have a complete prototype object + * (reflectClassPrototype): module-level program classes that neither extend + * Date nor a runtime-provided class. */ +function prototypeChainViewable(base: ClassInfo): boolean { + for (let c: ClassInfo | null = base; c; c = c.base) + if ( + c.decl === null || + c.localClass !== undefined || + c.generic !== undefined || + c.genericInstance !== undefined || + c.mixinInstance !== undefined || + c.callableBase !== undefined || + c.extendsDate !== undefined || + c.def.runtime || + c.builtinEmitter || + c.builtinStream || + c.builtinError || + c.builtinEventTarget + ) + return false; + return true; +} + +/** `super` as the receiver of a called member: `super.m(...)`. */ +function isSuperMethodCallee(node: ts.Node): boolean { + const access = node.parent; + return ( + access !== undefined && + ts.isPropertyAccessExpression(access) && + access.expression === node && + access.parent !== undefined && + ts.isCallExpression(access.parent) && + access.parent.expression === access + ); +} + /** Complete adapters after all native class bodies and instantiations exist. */ export function finalizeClassMethodValues(lowerer: Lowerer, functions: IrFunction[]): void { const byName = new Map(functions.map((fn) => [fn.name, fn])); diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index cb2ab2280..116150465 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -400,7 +400,10 @@ export function reflectClassPrototype( writable, ]), ); + // The members lower on top of whatever frames requested the view (a + // `super` call in a subclass's method); their `this` is their own receiver. const context = newFnCtx(false, null, null, DYN); + context.thisBoundary = true; lowerer.fnStack.push(context); try { for (const member of info.decl.members.nodes) { diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 82f5a27c1..d80f342c2 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,3 +1,4 @@ +import { checkedPrivateMethodReceiver, privateNameClass } from "./private-brands.js"; import { keyedHelperName } from "./stable-names.js"; import { perKeyOrder } from "./key-order-witness.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; @@ -547,6 +548,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { ) { return lowerer.lowerSuperMethodCall(expr, expr.expression); } + // `value.#m(...)` on a checked-dynamic value (`this` in a JavaScript + // class's receiver-independent method): the private method of the + // brand-checked receiver. + if ( + ts.isPropertyAccessExpression(expr.expression) && + ts.isPrivateIdentifier(expr.expression.name) && + !expr.expression.questionDotToken + ) { + const privateCall = lowerCheckedPrivateMethodCall(lowerer, expr, expr.expression); + if (privateCall) return privateCall; + } const consoleMember = lowerer.consoleCallMember(expr); const consoleMethod = consoleMember === null ? lowerer.consoleMethodMember(expr) : null; @@ -11286,3 +11298,35 @@ function lowerShimIntrinsicCall( ); return { kind: "libCall", fn: intrinsic.fn, args, type: intrinsic.ret, loc }; } + +/** `value.#m(args)` where `value` lowers to a checked-dynamic value and the + * declaration-level class whose body declares `#m` declares it as a method: + * the method called on the brand-checked receiver + * (checkedPrivateMethodReceiver). Null for any other private member call. */ +function lowerCheckedPrivateMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (!ts.isPrivateIdentifier(access.name)) return null; + const name = access.name.text; + const owner = privateNameClass(lowerer, access.name); + const found = owner ? lowerer.findMethodOn(owner, name) : null; + if (!owner || !found || found.declarer !== owner || found.sig.abstract === true) return null; + const probed = tryLowerExpression(lowerer, access.expression); + if (!probed || !checkedDynamicOperand(probed)) return null; + const value = checkedDynamicOperand(lowerer.lowerExpr(access.expression)); + if (!value) return null; + const loc = locOf(call); + const callee = `%${owner.def.name}.${name}`; + lowerer.noteEdge(callee); + const receiver = checkedPrivateMethodReceiver(lowerer, owner, name, value, loc); + const args = lowerer.completeArgs(call.arguments.nodes, found.sig.params, loc, call); + return { kind: "call", callee, args: [receiver, ...args], type: found.sig.ret, loc }; +} + +/** A checked-dynamic value, or the dynamic value under a checked extraction. */ +function checkedDynamicOperand(expr: IrExpr): IrExpr | null { + if (expr.type.kind === "dyn") return expr; + return expr.kind === "dynCheck" && expr.value.type.kind === "dyn" ? expr.value : null; +} diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 29afbb4fe..db8f35f95 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -181,7 +181,7 @@ import { construction, constructionPrefix } from "./js-class-fields.js"; import { lowerObjectFactoryNew } from "./object-factory-new.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerInstanceConstructorNew } from "./class-instance-constructor.js"; -import { classPrototypeData } from "./class-prototypes.js"; +import { classPrototypeData, reflectClassPrototype } from "./class-prototypes.js"; import { constructionEscape } from "./construction-escapes.js"; import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js"; import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js"; @@ -9384,8 +9384,16 @@ export function lowerSuperMethodCall( if (!thisLocal) lowerer.unsupported("SC1080", access); const loc = locOf(call); const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, type: thisLocal.type, loc }; - if (lowerer.prototypeMethodAccesses.has(access.name.text)) { - const prototype = classPrototypeData(lowerer, cls.base, loc); + // A method whose `this` is the call-site receiver (a JavaScript class's + // receiver-independent method value, reflectedClassMethodValue) looks the + // method up on the base class's complete prototype, as `super` reads + // [[HomeObject]].[[Prototype]], and calls it with that receiver. + const dynamicThis = thisRef.type.kind === "dyn"; + if (dynamicThis || lowerer.prototypeMethodAccesses.has(access.name.text)) { + const prototype = + dynamicThis && cls.base.decl + ? reflectClassPrototype(lowerer, cls.base, loc, classValueRef(lowerer, cls.base, access)) + : classPrototypeData(lowerer, cls.base, loc); if (prototype) { const callee: IrExpr = { kind: "dynKeyGet", diff --git a/packages/compiler/src/frontend/lowering/private-brands.ts b/packages/compiler/src/frontend/lowering/private-brands.ts index c7881bce1..92bee4fdd 100644 --- a/packages/compiler/src/frontend/lowering/private-brands.ts +++ b/packages/compiler/src/frontend/lowering/private-brands.ts @@ -17,8 +17,9 @@ import type { Lowerer } from "./lowerer.js"; import type { ClassInfo } from "./lower-classes.js"; /** How a brand helper uses its receiver: `in` answers `#x in v`, `read` and - * `write` return the receiver as the declaring class for a field access. */ -type BrandUse = "in" | "read" | "write"; + * `write` return the receiver as the declaring class for a field access, and + * `call` for a private method call. */ +type BrandUse = "in" | "read" | "write" | "call"; const PREFIX = "%private."; @@ -214,6 +215,35 @@ export function checkedPrivateValue( return { kind: "call", callee: helper, args: [receiver], type: resultType, loc }; } +/** `value.#m(...)` on a checked-dynamic `value` in a declaration-level class + * `info` that declares the private method `#m`: the receiver as an instance + * of `info` when the value is one, else Node's TypeError ("Receiver must be + * an instance of class C"). */ +export function checkedPrivateMethodReceiver( + lowerer: Lowerer, + info: ClassInfo, + name: string, + receiver: IrExpr, + loc: SrcLoc, +): IrExpr { + const className = info.def.name; + const resultType: IrType = { kind: "object", className }; + const helper = `${PREFIX}call:${className}:${name}`; + 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. */ @@ -358,6 +388,22 @@ export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[]) }, { kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc }, ); + } else if (use === "call") { + // V8 names the class by its own identifier (not a name inferred from + // a binding), "anonymous" without one, also for a nullish receiver. + const owner = lowerer.classes.get(className); + const identifier = owner?.decl?.name; + const shown = identifier !== undefined ? identifier.text : "anonymous"; + for (const unit of ["undefined", "null"] as const) { + body.push({ + kind: "if", + cond: { kind: "dynTest", test: unit, value, type: BOOL, loc }, + then: [typeError(strLit(`Cannot read properties of ${unit} (reading '${shown}')`, loc))], + else_: null, + loc, + }); + } + body.push(typeError(strLit(`Receiver must be an instance of class ${shown}`, loc))); } else { const verb = use === "read" ? "reading" : "setting"; const action = use === "read" ? "Cannot read properties" : "Cannot set properties"; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-class-methods-foreign-receivers.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-class-methods-foreign-receivers.mjs.json new file mode 100644 index 000000000..8f50b09a3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-class-methods-foreign-receivers.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7621-class-methods-foreign-receivers.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/7621-class-methods-foreign-receivers.mjs b/tests/corpus/7621-class-methods-foreign-receivers.mjs new file mode 100644 index 000000000..beb046ea4 --- /dev/null +++ b/tests/corpus/7621-class-methods-foreign-receivers.mjs @@ -0,0 +1,130 @@ +// Prototype methods called with a receiver that is not an instance of +// their class (`C.prototype.m.call(other)`): `#private` members throw +// Node's TypeErrors for such a receiver, and `super.m()` looks the method up +// on the base class's prototype and calls it with the receiver. +function report(label, run) { + try { + console.log(label, run()); + } catch (error) { + console.log(label, error.constructor.name, error.message); + } +} + +class Counter { + #count = 0; + #step = 1; + constructor(name) { + this.name = name; + } + read() { + return this.#count; + } + bump() { + this.#count += this.#step; + return this.#count; + } + reset(value) { + this.#count = value; + return this.name; + } + #label() { + return `${this.name}#${this.#count}`; + } + label() { + return this.#label(); + } + owns(value) { + return #count in value; + } + ownsSelf() { + return #count in this; + } +} + +const counter = new Counter("c"); +console.log(counter.bump(), counter.bump(), counter.read(), counter.label()); +console.log(counter.owns(counter), counter.owns({}), counter.ownsSelf(), counter.reset(7), counter.read()); + +const plain = { name: "plain" }; +report("read", () => Counter.prototype.read.call(plain)); +report("bump", () => Counter.prototype.bump.call(plain)); +report("reset", () => Counter.prototype.reset.call(plain, 3)); +report("label", () => Counter.prototype.label.call(plain)); +report("ownsSelf", () => Counter.prototype.ownsSelf.call(plain)); +report("owns primitive", () => Counter.prototype.owns.call(counter, 5)); +report("label undefined", () => Counter.prototype.label.call(undefined)); +report("label null", () => Counter.prototype.label.call(null)); +report("label number", () => Counter.prototype.label.call(4)); +report("read other instance", () => Counter.prototype.read.call(new Counter("d"))); +report("label other instance", () => Counter.prototype.label.call(new Counter("e"))); + +const Anonymous = class { + #m() { + return 1; + } + call() { + return this.#m(); + } +}; +const Named = class Inner { + #m() { + return 2; + } + call() { + return this.#m(); + } +}; +report("anonymous", () => Anonymous.prototype.call.call({})); +report("named", () => Named.prototype.call.call({})); +console.log(new Anonymous().call(), new Named().call()); + +class Shape { + constructor(name) { + this.name = name; + } + describe() { + return `shape ${this.name}`; + } + area() { + return 0; + } +} +class Square extends Shape { + describe() { + return `square: ${super.describe()} area ${this.area()}`; + } + area() { + return this.size * this.size; + } + both() { + return [super.describe(), super.area()].join("|"); + } +} +class Tiny extends Square { + #tag = "t"; + describe() { + return `tiny<${super.describe()}>`; + } + tag() { + return `${this.#tag}:${super.both()}`; + } +} + +const square = new Square("s"); +square.size = 3; +console.log(square.describe(), square.both()); +const tiny = new Tiny("t"); +tiny.size = 1; +console.log(tiny.describe(), tiny.tag()); +const foreign = { + name: "f", + size: 2, + area() { + return 40; + }, +}; +report("square describe", () => Square.prototype.describe.call(foreign)); +report("square both", () => Square.prototype.both.call(foreign)); +report("tiny describe", () => Tiny.prototype.describe.call(foreign)); +report("tiny tag", () => Tiny.prototype.tag.call(foreign)); +report("square describe on tiny", () => Square.prototype.describe.call(tiny)); diff --git a/tests/harness/corpus-costs/7621-class-methods-foreign-receivers.mjs.json b/tests/harness/corpus-costs/7621-class-methods-foreign-receivers.mjs.json new file mode 100644 index 000000000..bb2ade1c2 --- /dev/null +++ b/tests/harness/corpus-costs/7621-class-methods-foreign-receivers.mjs.json @@ -0,0 +1 @@ +{ "plain": 6415, "san": 10107 } From 956292c6274c1aa71b3dcb4ece09cddc920f82d1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 06:06:50 -0500 Subject: [PATCH 015/273] Store foreign elements only in number[] and string[] ToNumber and ToString of a foreign element are reached through a table that scr_arr_set_foreign installs, so a program that never stores a foreign element no longer links the dynamic tier through the array runtime's joins, sorts and numeric reads. Only number[] and string[] store a value written through a dynamic view that their element type cannot hold. Other element types keep the checked conversion's TypeError at the write. Counting every viewed element type as possibly undefined made callbacks over records whose arrays reach a package (zod3-infer) take optional elements and refuse their spreads. --- .../lowering/runtime-optional-elements.ts | 14 +++++++--- packages/runtime/src/scr_array.c | 28 ++++++++++++++++--- .../6068-foreign-elements-through-views.ts | 23 ++++----------- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts index 9a85bda1a..4db6ee175 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts @@ -62,6 +62,12 @@ const VIEW_HOLES = "%view"; /** The fact name of values stored through dynamic views. */ const VIEW_VALUES = "%view-values"; +/** Element keys whose arrays store a value written through a dynamic view + * that the element type cannot hold, as a foreign element: `number[]` and + * `string[]`. Arrays of other element types reject such a write with the + * checked conversion's TypeError. */ +const FOREIGN_ELEMENT_KEYS: ReadonlySet = new Set(["f64", "string"]); + /** What an array value can contain besides present values. */ interface Contents { undefined: boolean; @@ -373,7 +379,7 @@ export class ArrayElementStates { /** Values of any type stored through a dynamic view can be in arrays of * this key. */ private viewedValues(key: string): boolean { - return this.viewValues && this.isViewed(key); + return this.viewValues && FOREIGN_ELEMENT_KEYS.has(key) && this.isViewed(key); } private isViewed(key: string): boolean { @@ -382,11 +388,11 @@ export class ArrayElementStates { } /** Element keys whose arrays can hold elements outside their element - * type, stored through dynamic views; "*" for every array. Final once - * the fixed point is reached. */ + * type, stored through dynamic views (of FOREIGN_ELEMENT_KEYS). Final + * once the fixed point is reached. */ foreignKeys(): ReadonlySet { if (!this.viewValues) return new Set(); - return (this.viewKeys ??= this.host.nativeViewKeys()); + return new Set([...FOREIGN_ELEMENT_KEYS].filter((key) => this.isViewed(key))); } /** A store through `receiver`, which has no native element key, can diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index e83477142..c7399bcea 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -269,6 +269,25 @@ static uint64_t scr_elem_retain_slot(const ScrArr *a, uint64_t slot) { return scr_elem_retain_with(a->elem, scr_arr_ops(a), slot); } +/* ToNumber and ToString of a foreign element. Both can run user code and + * pull in the dynamic tier, so they are reached through this table, which + * scr_arr_set_foreign installs: every foreign element is stored by it, and + * a program that never stores one links neither conversion. */ +typedef struct ScrArrForeignConversions { + double (*to_number)(const ScrDyn *); + ScrStr *(*to_string)(const ScrDyn *); +} ScrArrForeignConversions; + +static const ScrArrForeignConversions scr_arr_dyn_conversions = { + scr_dyn_number_coerce, + scr_dyn_string_coerce_js, +}; +static const ScrArrForeignConversions *scr_arr_conversions; + +static const ScrArrForeignConversions *scr_arr_foreign_conversions(void) { + return __atomic_load_n(&scr_arr_conversions, __ATOMIC_RELAXED); +} + /* The array may hold SCR_ARR_FOREIGN elements (ScrArrExt.foreign). Such * an array always has states. */ static inline bool scr_arr_may_hold_foreign(const ScrArr *a) { @@ -2056,7 +2075,7 @@ static double *scr_arr_coerced_numbers(const ScrArr *a) { uint64_t slot; uint8_t state = scr_arr_f64_state_at(a, i, &slot); if (state == SCR_ARR_FOREIGN) { - out[i] = scr_dyn_number_coerce(scr_slot_to_ptr(slot)); + out[i] = scr_arr_foreign_conversions()->to_number(scr_slot_to_ptr(slot)); if (scr_exc_pending()) { free(out); return NULL; @@ -2252,7 +2271,7 @@ double scr_arr_get_number(const ScrArr *a, double i) { } else if (!scr_arr_prop_get_state(a, i, &slot, &state)) { return NAN; } - if (state == SCR_ARR_FOREIGN) return scr_dyn_number_coerce(scr_slot_to_ptr(slot)); + if (state == SCR_ARR_FOREIGN) return scr_arr_foreign_conversions()->to_number(scr_slot_to_ptr(slot)); return state == SCR_ARR_VALUE ? scr_slot_to_f64(slot) : NAN; } @@ -2368,6 +2387,7 @@ bool scr_arr_set_foreign(ScrArr *a, double i, ScrDyn *v) { * doubles first, and 1-byte and compact slots cannot hold one. */ if (a->elem == SCR_ELEM_I32) scr_arr_widen_i32(a); if (scr_arr_slot_width(a) != sizeof(uint64_t)) return false; + __atomic_store_n(&scr_arr_conversions, &scr_arr_dyn_conversions, __ATOMIC_RELAXED); scr_dyn_retain(v); scr_arr_need_states(a); if (idx >= a->len) a->len = idx + 1; @@ -3411,7 +3431,7 @@ static ScrArr *scr_arr_sort_foreign(ScrArr *a, bool copy) { if (state != SCR_ARR_VALUE && state != SCR_ARR_FOREIGN) continue; ScrStr *key; if (state == SCR_ARR_FOREIGN) { - key = scr_dyn_string_coerce_js(scr_slot_to_ptr(slot)); + key = scr_arr_foreign_conversions()->to_string(scr_slot_to_ptr(slot)); if (!key) failed = true; } else if (a->elem == SCR_ELEM_STR) { key = scr_str_retain(scr_slot_to_ptr(slot)); @@ -5372,7 +5392,7 @@ ScrStr *scr_arr_join(ScrArr *a, ScrStr *sep) { /* join converts every element but undefined and null with ToString. */ const ScrDyn *value = scr_slot_to_ptr(slot); if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) continue; - ScrStr *text = scr_dyn_string_coerce_js(value); + ScrStr *text = scr_arr_foreign_conversions()->to_string(value); if (!text) continue; scr_join_append(&out, text->data, text->len); scr_str_release(text); diff --git a/tests/corpus/6068-foreign-elements-through-views.ts b/tests/corpus/6068-foreign-elements-through-views.ts index 21cfe25a2..e6676eea4 100644 --- a/tests/corpus/6068-foreign-elements-through-views.ts +++ b/tests/corpus/6068-foreign-elements-through-views.ts @@ -1,8 +1,8 @@ -// A typed array seen as `unknown[]` holds the values written through the -// view that its element type cannot hold, as in Node. The view and the typed -// array are the same array: length, the view's reads and iteration, -// printing, JSON, join, searches, copies, Math over a spread, sorting and -// truncation see the written values. +// A number[] or string[] seen as `unknown[]` holds the values written +// through the view that its element type cannot hold, as in Node. The view +// and the typed array are the same array: length, the view's reads and +// iteration, printing, JSON, join, searches, copies, Math over a spread, +// sorting and truncation see the written values. const numbers: number[] = [1, 2]; const numbersView: unknown[] = numbers; @@ -49,19 +49,8 @@ truncated.length = 1; truncated.length = 3; console.log(truncated, truncatedView.length); -// string[] and class arrays hold values of other types the same way. +// string[] holds values of other types the same way. const words: string[] = ["a"]; const wordsView: unknown[] = words; wordsView.push(1, false); console.log(words, words.join("-"), words.length, typeof wordsView[1]); - -class Item { - id: string; - constructor(id: string) { - this.id = id; - } -} -const items: Item[] = [new Item("a")]; -const itemsView: unknown[] = items; -itemsView.push({ id: "plain" }); -console.log(items, items.length, items[0]!.id, itemsView[1] instanceof Item); From d22250f8305a5a21c78e85eca17db51a82da02a5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 08:05:50 -0500 Subject: [PATCH 016/273] Raise slow CI job time limits and prune closed pull request caches --- .github/workflows/ci.yml | 9 +++++--- .github/workflows/prune-caches.yml | 36 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/prune-caches.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f131747fa..fe601ed06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -524,7 +524,9 @@ jobs: if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }} name: test (tsgo and tsc-ts give the same IR) runs-on: ubuntu-24.04 - timeout-minutes: 30 + # The two corpus compiles run back to back and take close to 30 minutes on a + # slow hosted runner, so a 30-minute limit cancels otherwise healthy runs. + timeout-minutes: 45 steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -618,8 +620,9 @@ jobs: # Shard 1 additionally runs package/install and link-parity contracts # before its LLVM differential slice. The slice itself can take ~10 # minutes on a cold hosted runner, so 20 minutes cancels an otherwise - # healthy job before the differential completes. - timeout-minutes: 35 + # healthy job before the differential completes. Slow runners need more + # headroom than that, so the limit is 50 minutes. + timeout-minutes: 50 strategy: fail-fast: false matrix: diff --git a/.github/workflows/prune-caches.yml b/.github/workflows/prune-caches.yml new file mode 100644 index 000000000..008baaa43 --- /dev/null +++ b/.github/workflows/prune-caches.yml @@ -0,0 +1,36 @@ +name: Prune caches + +# Pull request caches are scoped to that pull request, so once it is closed no +# run can use them again. Deleting them keeps the repository under its cache +# size limit, so caches that main and open pull requests use are not evicted. +on: + pull_request: + types: [closed] + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + +permissions: + actions: write + pull-requests: read + +jobs: + prune: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Delete caches of closed pull requests + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + run: | + open=" $(gh api --paginate "repos/$REPO/pulls?state=open&per_page=100" --jq '.[].number' | tr '\n' ' ') " + gh api --paginate "repos/$REPO/actions/caches?per_page=100" \ + --jq '.actions_caches[] | select(.ref | startswith("refs/pull/")) | "\(.id) \(.ref)"' | + while read -r id ref; do + number=$(echo "$ref" | cut -d/ -f3) + case "$open" in + *" $number "*) ;; + *) gh api --method DELETE "repos/$REPO/actions/caches/$id" >/dev/null && echo "deleted cache $id ($ref)" ;; + esac + done From c7c8f5d45ec6b1004a269f33df5688f1f8aec3c6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 09:25:07 -0500 Subject: [PATCH 017/273] Run lone array callbacks as loops with their bodies inlined --- docs/content/docs/contracts.mdx | 2 +- docs/content/limitations/contract-types.mdx | 2 +- .../backend/llvm/appended-array-literals.ts | 35 +- .../src/backend/llvm/emit-statements.ts | 3 + packages/compiler/src/backend/llvm/emitter.ts | 9 +- .../src/backend/llvm/expr-primitives.ts | 57 ++- .../lowering/containers/array-callbacks.ts | 5 +- .../lowering/containers/array-pipelines.ts | 341 ++++++++++++++---- .../src/ir/callback-specialization.ts | 7 +- .../test/array-pipeline-fusion.test.ts | 46 ++- .../test/format-float-arguments.test.ts | 23 +- tests/diagnostics/contract-int32-escapes.ts | 3 +- .../contract-int32-arrays.ts.txt | 6 +- .../contract-int32-escapes.ts.txt | 2 +- 14 files changed, 450 insertions(+), 91 deletions(-) diff --git a/docs/content/docs/contracts.mdx b/docs/content/docs/contracts.mdx index 948fe7960..ad5953fa6 100644 --- a/docs/content/docs/contracts.mdx +++ b/docs/content/docs/contracts.mdx @@ -245,7 +245,7 @@ $ scriptc build scale.ts 1 error. ``` -A copy such as `slice()` or a spread is checked as if it were the same array as its source. The results of callbacks are not followed, so an array built by `map` or `Array.from` with a mapping function cannot be `Int32[]`; build it with a loop that stores `value | 0`. scriptc refuses an `Int32[]` slot whose array can come from code it cannot see, such as `JSON.parse`, dynamic values or the callers of a function used as a value, and an array that can reach one and escapes to such code, such as a call through a function value (SC7003). +A copy such as `slice()` or a spread is checked as if it were the same array as its source. The results of a callback are followed only when scriptc copies it into the loop that runs the array method: a callback written at a `map` or `filter` call whose captured variables are not reassigned, such as `xs.map((x) => (x * 2) | 0)`. Other callback results are not followed, so for example an array built by `Array.from` with a mapping function cannot be `Int32[]`; build it with a loop that stores `value | 0`. scriptc refuses an `Int32[]` slot whose array can come from code it cannot see, such as `JSON.parse`, dynamic values or the callers of a function used as a value, and an array that can reach one and escapes to such code, such as a call through a function value (SC7003). ## Immutable diff --git a/docs/content/limitations/contract-types.mdx b/docs/content/limitations/contract-types.mdx index 35d7a95c0..1fd24a81b 100644 --- a/docs/content/limitations/contract-types.mdx +++ b/docs/content/limitations/contract-types.mdx @@ -1,7 +1,7 @@ [Contract types](/docs/contracts) are checked only where they are written as the annotation of a parameter, variable, class instance field or function result, `Acyclic` only on classes, interfaces and object type aliases, and `ProgramLifetime` only in the `implements` clause of a class declaration; other aliases, interface members and nested type positions are reported. In programs that use worker threads, a surrounding `try`/`catch` does not discharge `NoThrow`. `Int32` refuses parameters of functions used as values and fields of classes whose instances are stored in `any` or `unknown` values. -`Int32[]` refuses arrays that can come from or escape to code scriptc cannot see, does not follow callback results (such as `map` results), and checks a copy made by `slice` or a spread as if it were its source array. +`Int32[]` refuses arrays that can come from or escape to code scriptc cannot see, follows callback results only for callbacks written at a `map` or `filter` call that scriptc runs inline, and checks a copy made by `slice` or a spread as if it were its source array. Element reads that find a value are int32s, but a scalar `Int32` slot still needs `a[i] | 0`, because a read can miss and produce `undefined`. `Immutable` refuses values stored in `any` or `unknown` or that another reference can still change. `ProgramLifetime` applies to root classes of executables only; instances the program drops are never reclaimed unless they belong to a released [lifetime region](/docs/experimental#lifetimeregion). diff --git a/packages/compiler/src/backend/llvm/appended-array-literals.ts b/packages/compiler/src/backend/llvm/appended-array-literals.ts index 3c09904e3..c65aded0c 100644 --- a/packages/compiler/src/backend/llvm/appended-array-literals.ts +++ b/packages/compiler/src/backend/llvm/appended-array-literals.ts @@ -9,7 +9,7 @@ * one directly or through a binding read from it. The choice never changes * behavior. */ -import type { IrExpr, IrModule } from "../../ir/ir.js"; +import type { IrExpr, IrModule, IrStmt } from "../../ir/ir.js"; import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js"; const APPENDING_METHODS: ReadonlySet = new Set([ @@ -120,3 +120,36 @@ export function findAppendedArrayLiterals( } return result; } + +/** `const xs = []` directly followed by a capacity hint for `xs` (an + * `arraySetLength` with `reserve`) whose length reads only literals and + * other locals: the literal can be constructed with that capacity, and the + * hint then has nothing left to do. */ +export function reservedArrayLength( + stmts: readonly IrStmt[], + i: number, +): { literal: IrExpr; hint: IrStmt & { kind: "arraySetLength" } } | null { + const decl = stmts[i]; + const next = stmts[i + 1]; + if (decl?.kind !== "varDecl" || decl.init === null || !isEmptyLiteral(decl.init)) return null; + if (next?.kind !== "arraySetLength" || next.reserve !== true) return null; + if (next.arr.kind !== "varRef" || next.arr.localId !== decl.localId) return null; + return plainLength(next.length, decl.localId) ? { literal: decl.init, hint: next } : null; +} + +function plainLength(e: IrExpr, array: string): boolean { + switch (e.kind) { + case "numLit": + return true; + case "varRef": + return e.localId !== array; + case "bin": + return plainLength(e.left, array) && plainLength(e.right, array); + case "ternary": + return ( + plainLength(e.cond, array) && plainLength(e.then, array) && plainLength(e.else_, array) + ); + default: + return false; + } +} diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index c2846a9d4..cbf8fb27d 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -458,6 +458,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.publishedTypes?.containerGuarded(s.arr.type) !== true ) break; + // The literal it follows was constructed with this capacity + // (reservedArrayLength). + if (emitter.consumedReserves.delete(s)) break; const arr = emitBorrowedInput(emitter, s.arr); const length = emitter.emitExpr(s.length); if (s.arr.type.kind !== "array") diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 050c3fb90..b3508b5d0 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -176,7 +176,7 @@ import { ReferenceEffects } from "./reference-effects.js"; import { ReferenceWrites, type Writes } from "./reference-writes.js"; import { LlvmDebugInfo } from "./debug-info.js"; import { StackCallbacks } from "./stack-callbacks.js"; -import { findAppendedArrayLiterals } from "./appended-array-literals.js"; +import { findAppendedArrayLiterals, reservedArrayLength } from "./appended-array-literals.js"; import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callbacks.js"; import { findLentStringParameters, @@ -1235,6 +1235,11 @@ export class LlEmitter { /** Locals whose array literal emitStmts left for the `await Promise.all` * that reads them (deferredPromiseAllLocal). */ private readonly deferredPromiseAllEntries = new Map>(); + /** Empty array literals that a capacity hint follows directly + * (reservedArrayLength), with that hint. Constructing the literal with + * the hinted capacity consumes the hint, which then emits nothing. */ + readonly reservedArrayLengths = new Map(); + readonly consumedReserves = new Set(); /** 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. */ @@ -5514,6 +5519,8 @@ export class LlEmitter { if (this.B.isTerminated()) return; if (cursor) cursor.index = i; this.stmtCursor = cursor; + const reserve = reservedArrayLength(stmts, i); + if (reserve !== null) this.reservedArrayLengths.set(reserve.literal, reserve.hint); const deferred = this.deferredPromiseAllLocal(stmts, i); if (deferred !== null) { this.deferredPromiseAllEntries.set((stmts[i] as { localId: string }).localId, deferred); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 1eec6a965..9791de8e5 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -870,6 +870,34 @@ export function emitStringExpr( } } +/** scr_arr_reserve_len reserves dense storage only up to this many slots + * (SCR_ARR_DENSE_LIMIT, scr_array.c). */ +const RESERVE_LIMIT = 2 ** 20; + +/** Elements the inline slots of a small array construction + * (scr_arr_new_small, scr_arr_new_ops_small) hold at least + * (SCR_ARR_SMALL_SLOTS, scr_array.c). */ +const SMALL_ARRAY_SLOTS = 3; + +/** The capacity scr_arr_reserve_len would reserve for `length` on an empty + * array: the length truncated when it is at least 1 and at most + * RESERVE_LIMIT, else 0. */ +function reservedCapacity(host: LlvmEmitterContext, length: IrExpr): string { + const B = host.B; + const n = host.emitExpr(length); + const atLeastOne = B.tmp(), + withinLimit = B.tmp(), + reserves = B.tmp(), + slots = B.tmp(), + capacity = B.tmp(); + B.line(`${atLeastOne} = fcmp oge double ${n.name}, ${f64Lit(1)}`); + B.line(`${withinLimit} = fcmp ole double ${n.name}, ${f64Lit(RESERVE_LIMIT)}`); + B.line(`${reserves} = and i1 ${atLeastOne}, ${withinLimit}`); + B.line(`${slots} = select i1 ${reserves}, double ${n.name}, double ${f64Lit(0)}`); + B.line(`${capacity} = fptoui double ${slots} to ${host.sizeType}`); + return capacity; +} + export function emitContainerExpr( host: LlvmEmitterContext, e: ExprOf< @@ -941,7 +969,34 @@ export function emitContainerExpr( ? `${arr} = call ptr @scr_arr_new_small(i32 ${SCR_ELEM_I32})` : `${arr} = call ptr @scr_arr_new(i32 ${SCR_ELEM_I32}, ${host.sizeType} ${capacity})`, ); - } else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`); + } else { + const hint = host.reservedArrayLengths.get(e); + if (hint !== undefined) { + host.reservedArrayLengths.delete(e); + host.consumedReserves.add(hint); + const capacity = reservedCapacity(host, hint.length); + // The small construction's inline slots hold at least + // SMALL_ARRAY_SLOTS elements; larger capacities are sized. + const fits = B.tmp(), + smallArr = B.tmp(), + sizedArr = B.tmp(); + const smallLabel = B.newLabel("arr.cap.small"), + sizedLabel = B.newLabel("arr.cap.sized"), + joined = B.newLabel("arr.cap.j"); + B.line(`${fits} = icmp ule ${host.sizeType} ${capacity}, ${SMALL_ARRAY_SLOTS}`); + B.condBr(fits, smallLabel, sizedLabel); + B.startBlock(smallLabel); + B.line(`${smallArr} = ${arrNewCall(host.shapeHost, elem, "0", true)}`); + B.br(joined); + B.startBlock(sizedLabel); + B.line(`${sizedArr} = ${arrNewCall(host.shapeHost, elem, capacity)}`); + B.br(joined); + B.startBlock(joined); + B.line( + `${arr} = phi ptr [ ${smallArr}, %${smallLabel} ], [ ${sizedArr}, %${sizedLabel} ]`, + ); + } else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`); + } const out = host.own({ name: arr, type: e.type }); const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem); let pending: LlValue[] = []; diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 86f214bc1..7349566d9 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -827,15 +827,16 @@ function buildArrayHofFn( ); returnType = outT; body = [ + readLen, { kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: outT, loc }, 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 + // has them. Storage for it is reserved first (with the result's + // construction, which directly precedes it) and the length is set // after the loop, so a result without holes is filled by appends and // gets no state bytes. ...(method === "map" diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index 2f839a20b..cdcd0d93c 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -1,4 +1,6 @@ import { keyedHelperName } from "../stable-names.js"; +import { writtenLocals } from "../../../ir/callback-specialization.js"; +import { AmbientReceiverReaders } from "../../../ir/receiver-readers.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { BOOL, @@ -16,9 +18,12 @@ import { typeKey, } from "../../../ir/ir.js"; import { + everyExpr, everyExprChild, + everyStmt, everyStmtChild, everyStmtList, + transformExpr, transformStmtList, } from "../../../ir/traverse.js"; import { @@ -537,6 +542,65 @@ function matchChain(lowerer: Lowerer, e: IrExpr): Chain | null { }; } +/** The outermost array-HOF call of `e` on its own, over its receiver. */ +function loneChain(lowerer: Lowerer, e: IrExpr): Chain | null { + const top = stageOf(lowerer, e); + if (!top) return null; + const { stage, receiver } = top; + if (receiver.type.kind !== "array") return null; + const stages: Stage[] = []; + let terminal: Terminal; + if (TERMINALS.has(stage.info.method)) { + const init = + stage.info.method === "reduce" && stage.info.hasInit && e.kind === "call" ? e.args[2]! : null; + terminal = { kind: "call", stage, init }; + } else if (PRODUCERS.has(stage.info.method)) { + terminal = { kind: "collect" }; + stages.push(stage); + } else return null; + return { + source: receiver, + sourceType: receiver.type, + slice: null, + stages, + terminal, + type: e.type, + loc: e.loc, + }; +} + +/** The callbacks of `chain` in order, or null when its stages cannot run + * in one loop: element types that do not line up, an array argument past + * the first stage, reduce without a seed, or findIndex over holes. */ +function chainCallbacks(chain: Chain): Stage[] | null { + const { stages, terminal } = chain; + const terminalStage = terminal.kind === "call" ? terminal.stage : null; + const callbacks = terminalStage ? [...stages, terminalStage] : stages; + // Element types must line up stage to stage. + let elem = chain.sourceType.elem; + for (const stage of callbacks) { + if (!typeEquals(stage.info.elem, elem)) return null; + if (stage.info.method === "filter" && !typeEquals(stage.info.outElem, stage.info.elem)) + return null; + if ( + (stage.info.method === "map" || stage.info.method === "flatMap") && + !typeEquals(stage.info.outElem, stage.info.fnRet) + ) + return null; + elem = stage.info.outElem; + } + // Index arguments are reproduced; an array argument only names the source. + for (let k = 0; k < callbacks.length; k++) { + const stage = callbacks[k]!; + const lead = stage.info.method === "reduce" ? 1 : 0; + if (stage.info.arity > lead + 3) return null; + if (stage.info.arity === lead + 3 && (k !== 0 || chain.slice)) return null; + } + if (terminalStage?.info.method === "reduce" && !terminalStage.info.hasInit) return null; + if (terminalStage?.info.method === "findIndex" && holeCarrying(chain)) return null; + return callbacks; +} + /* ── the fusion pass ─────────────────────────────────────────────────── */ /** The values a fused loop hands from one stage to the next: a local of a @@ -550,6 +614,10 @@ interface Plan { writes: Set; /** Some callback may throw or loop; at most one does. */ risky: boolean; + /** How the loop inlines callbacks, when not by their effect summaries. */ + inline?: InlineTarget; + /** Stack frame name of the loop: the inlined callback's, for a lone call. */ + frame?: string; } /** Fuse chains of array-HOF helper calls into one loop. @@ -596,12 +664,22 @@ class PipelineFusion { readonly added: IrFunction[] = []; private readonly effects: CallbackEffects; private readonly helpers = new Map(); + /** Bindings each function, or a closure it creates, writes after their + * declaration. */ + private readonly written: Map>; + /** Bindings of the function being fused that inlined callbacks captured. */ + private inlinedCaptures = new Set(); + /** The helper calls each callback with an undefined receiver; a copied + * body runs under the receiver around the array call instead. */ + private readonly receivers: AmbientReceiverReaders; constructor( private readonly lowerer: Lowerer, functions: readonly IrFunction[], ) { this.effects = new CallbackEffects(functions, lowerer.globalsList); + this.written = writtenLocals({ functions }); + this.receivers = new AmbientReceiverReaders(new Map(functions.map((fn) => [fn.name, fn]))); } run(fn: IrFunction): void { @@ -612,6 +690,7 @@ class PipelineFusion { }); if (!candidate) return; const removed = new Set(); + this.inlinedCaptures = new Set(); let body = this.lists(fn.body, fn, removed); body = transformStmtList(body, { stmt: (s) => s, @@ -619,6 +698,7 @@ class PipelineFusion { }); fn.body = body; if (removed.size > 0) fn.locals = fn.locals.filter((l) => !removed.has(l.id)); + if (this.inlinedCaptures.size > 0) unboxUncaptured(fn, this.inlinedCaptures); } /* ── statement lists: fold single-use chain bindings ── */ @@ -733,40 +813,51 @@ class PipelineFusion { /* ── expressions ── */ private fuse(e: IrExpr, fn: IrFunction): IrExpr | null { - const plan = this.plan(e, fn); + const plan = this.plan(e, fn) ?? this.lonePlan(e, fn); return plan ? this.emit(plan) : null; } + /** One array-HOF call on its own (or the last call of a chain that does + * not fuse), when every callback is a closure literal whose body can be + * copied into the loop. A single stage reorders nothing, so the + * callbacks' effects need no model; their captured bindings must keep + * one value while the loop runs (stableTarget), since the loop reads + * them once, when it starts. The loop keeps the helper's order: the + * length read once, each element read when it is visited, holes + * skipped (and kept by map). */ + private lonePlan(e: IrExpr, fn: IrFunction): Plan | null { + const chain = loneChain(this.lowerer, e); + if (!chain) return null; + const callbacks = chainCallbacks(chain); + if (!callbacks) return null; + const inline = this.stableTarget(fn); + let frame: string | undefined; + for (const stage of callbacks) { + for (const arg of [stage.slow, stage.fast]) { + if (!arg) continue; + const target = inline(arg); + if (!target) return null; + frame ??= target.sourceName; + } + } + return { + chain, + callbacks, + writes: new Set(), + risky: true, + inline, + ...(frame === undefined ? {} : { frame }), + }; + } + /** Decide whether `e` is a fusable chain; null when it is not. */ private plan(e: IrExpr, fn: IrFunction): Plan | null { const chain = matchChain(this.lowerer, e); if (!chain) return null; + const callbacks = chainCallbacks(chain); + if (!callbacks) return null; const { stages, terminal } = chain; const terminalStage = terminal.kind === "call" ? terminal.stage : null; - const callbacks = terminalStage ? [...stages, terminalStage] : stages; - // Element types must line up stage to stage. - let elem = chain.sourceType.elem; - for (const stage of callbacks) { - if (!typeEquals(stage.info.elem, elem)) return null; - if (stage.info.method === "filter" && !typeEquals(stage.info.outElem, stage.info.elem)) - return null; - if ( - (stage.info.method === "map" || stage.info.method === "flatMap") && - !typeEquals(stage.info.outElem, stage.info.fnRet) - ) - return null; - elem = stage.info.outElem; - } - // Index arguments are reproduced; an array argument only names the source. - for (let k = 0; k < callbacks.length; k++) { - const stage = callbacks[k]!; - const lead = stage.info.method === "reduce" ? 1 : 0; - if (stage.info.arity > lead + 3) return null; - if (stage.info.arity === lead + 3 && (k !== 0 || chain.slice)) return null; - } - if (terminalStage?.info.method === "reduce" && !terminalStage.info.hasInit) return null; - const holes = holeCarrying(chain); - if (terminalStage?.info.method === "findIndex" && holes) return null; // Effects of each callback, in chain order. const scope = this.effects.scope(fn, true); const summaries: Effects[] = []; @@ -831,30 +922,62 @@ class PipelineFusion { * are passed in by value, read when the fused call starts; no callback * in the chain assigns a binding another one reads). */ private readonly inlineTarget: InlineTarget = (arg) => { + const fn = this.closureTarget(arg); + if (!fn) return null; + const captures = new Set((fn.captures ?? []).map((c) => c.localId)); + const effects = this.effects.ofFunction(fn); + if (!effects || intersects(effects.writes, captures)) return null; + return inlinableBody(fn, captures) ? fn : null; + }; + + /** inlineTarget for a callback created in `fn` whose captured bindings + * nothing assigns after their declaration: `fn`'s own locals and + * parameters, written neither by `fn` nor by any closure that shares + * them, and not by the callback body. The callback must not observe the + * ambient receiver (AmbientReceiverReaders). */ + private stableTarget(fn: IrFunction): InlineTarget { + const locals = new Map(fn.locals.map((l) => [l.id, l])); + const inherited = new Set((fn.captures ?? []).map((c) => c.localId)); + const written = this.written.get(fn.name); + return (arg) => { + const target = this.closureTarget(arg); + if (!target || !written || arg.kind !== "closure") return null; + if (!this.receivers.ignores(target)) return null; + for (const id of arg.captures) { + const local = locals.get(id); + if (!local || local.tdz || inherited.has(id) || written.has(id)) return null; + } + const captures = new Set((target.captures ?? []).map((c) => c.localId)); + const assigns = !everyStmtList(target.body, { + stmt: (st) => !((st.kind === "assign" || st.kind === "forOf") && captures.has(st.localId)), + expr: (ex) => + !((ex.kind === "assignExpr" || ex.kind === "incDec") && captures.has(ex.localId)), + }); + return !assigns && inlinableBody(target, captures) ? target : null; + }; + } + + /** The function behind a closure-literal callback the loop can inline. */ + private closureTarget(arg: IrExpr): IrFunction | null { if (arg.kind !== "closure" || arg.adapts) return null; const fn = this.effects.fn(arg.fnName); if (!fn || fn.async || fn.generator || fn.classCaptures?.length) return null; - const captures = new Set((fn.captures ?? []).map((c) => c.localId)); - if (captures.size !== arg.captures.length) return null; - const effects = this.effects.ofFunction(fn); - if (!effects || intersects(effects.writes, captures)) return null; - if (fn.locals.some((l) => l.tdz || (l.boxed && !captures.has(l.id)))) return null; - let size = 0; - const small = everyStmtList(fn.body, { - expr: (e) => ++size <= MAX_INLINE_NODES && e.kind !== "closure" && e.kind !== "selfRef", - stmt: (s) => ++size <= MAX_INLINE_NODES && s.kind !== "tryCatch", - }); - return small ? fn : null; - }; + if ((fn.captures ?? []).length !== arg.captures.length) return null; + return fn; + } private emit(plan: Plan): IrExpr | null { - const built = new FusedLoop(this.lowerer, plan.chain, this.inlineTarget).build(); + const built = new FusedLoop(this.lowerer, plan.chain, plan.inline ?? this.inlineTarget).build(); if (!built) return null; + for (const id of built.inlinedCaptures) this.inlinedCaptures.add(id); let name = this.helpers.get(built.key); if (!name) { - name = keyedHelperName(this.lowerer, "%arr.fused", built.key); + name = keyedHelperName(this.lowerer, plan.inline ? "%arr.loop" : "%arr.fused", built.key); this.helpers.set(built.key, name); - this.added.push({ ...built.fn, name }); + const helper: IrFunction = { ...built.fn, name }; + // A lone call's loop stands in the inlined callback's stack frame. + if (plan.frame !== undefined) helper.sourceName = plan.frame; + this.added.push(helper); } return { kind: "call", @@ -866,6 +989,48 @@ class PipelineFusion { } } +/** Whether a statement inside `e` (a sequence expression's) returns. */ +function returns(e: IrExpr): boolean { + return !everyExpr(e, { expr: () => true, stmt: (s) => s.kind !== "return" }); +} + +/** Drops the box of each binding in `inlined` that no closure or class + * created in `fn` captures any more: the callbacks that captured it were + * copied into loops, which receive its value. A dead-zone binding keeps the + * box that represents its uninitialized state, and bindings `fn` itself + * captures keep theirs. */ +function unboxUncaptured(fn: IrFunction, inlined: ReadonlySet): void { + const captured = new Set(); + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "closure" || e.kind === "classRef") + for (const id of e.captures ?? []) captured.add(id); + return true; + }, + stmt: () => true, + }); + const inherited = new Set( + [...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId), + ); + fn.locals = fn.locals.map((local): IrLocal => { + if (local.boxed !== true || local.tdz === true || !inlined.has(local.id)) return local; + if (captured.has(local.id) || inherited.has(local.id)) return local; + const { boxed: _boxed, ...plain } = local; + return plain; + }); +} + +/** A callback body the loop can copy: small, no nested closures, recursion + * or try statements, and no boxed locals besides its captures. */ +function inlinableBody(fn: IrFunction, captures: ReadonlySet): boolean { + if (fn.locals.some((l) => l.tdz || (l.boxed && !captures.has(l.id)))) return false; + let size = 0; + return everyStmtList(fn.body, { + expr: (e) => ++size <= MAX_INLINE_NODES && e.kind !== "closure" && e.kind !== "selfRef", + stmt: (s) => ++size <= MAX_INLINE_NODES && s.kind !== "tryCatch", + }); +} + function intersects(a: ReadonlySet, b: ReadonlySet): boolean { for (const id of a) if (b.has(id)) return true; return false; @@ -1080,8 +1245,23 @@ class FusedLoop { const id = this.local(l.name, l.type, true); renamed.set(l.id, id); } + const rename = (id: string): string => renamed.get(id) ?? id; + const renameExpr = (e: IrExpr): IrExpr => { + switch (e.kind) { + case "varRef": + case "incDec": + case "assignExpr": + return { ...e, localId: rename(e.localId) }; + default: + return e; + } + }; + const bind: IrStmt[] = fn.params.flatMap((p, index): IrStmt[] => + aliased.has(p.localId) + ? [] + : [{ kind: "varDecl", localId: rename(p.localId), init: args[index]!, loc }], + ); const labels = new Map(); - const exit = this.fresh("%fused.exit"); const label = (name: string): string => { let id = labels.get(name); if (!id) { @@ -1090,19 +1270,48 @@ class FusedLoop { } return id; }; + /** Locals and labels of a copied statement; returns are the caller's. */ + const renameStmt = (st: IrStmt): IrStmt => { + switch (st.kind) { + case "varDecl": + case "assign": + return { ...st, localId: rename(st.localId) }; + case "break": + case "continue": + return st.label ? { ...st, label: label(st.label) } : st; + case "while": + case "doWhile": + case "for": + case "block": + return st.labels ? { ...st, labels: st.labels.map(label) } : st; + default: + return st; + } + }; + // A body whose only return is its last statement (`(x) => x * k`, + // `{ const y = x * 2; return y > 4; }`) needs no exit block: its other + // statements run after the bindings, and the call's value is the + // returned expression. + const last = fn.body[fn.body.length - 1]; + const prefix = fn.body.slice(0, -1); + if ( + last !== undefined && + last.kind === "return" && + last.value !== null && + typeEquals(last.value.type, fn.returnType) && + !returns(last.value) && + prefix.every((st) => everyStmt(st, { expr: () => true, stmt: (x) => x.kind !== "return" })) + ) { + const copy = { expr: renameExpr, stmt: renameStmt }; + return { + pre: [...bind, ...transformStmtList(prefix, copy)], + value: transformExpr(last.value, copy), + }; + } + const exit = this.fresh("%fused.exit"); const result = fn.returnType.kind === "void" ? null : this.local("ret", fn.returnType, true); - const rename = (id: string): string => renamed.get(id) ?? id; const body = transformStmtList(fn.body, { - expr: (e) => { - switch (e.kind) { - case "varRef": - case "incDec": - case "assignExpr": - return { ...e, localId: rename(e.localId) }; - default: - return e; - } - }, + expr: renameExpr, stmt: (s): IrStmt => { switch (s.kind) { case "return": { @@ -1122,27 +1331,14 @@ class FusedLoop { loc: s.loc, }; } - case "varDecl": - case "assign": - return { ...s, localId: rename(s.localId) }; case "break": case "continue": return s.label && s.label !== exit ? { ...s, label: label(s.label) } : s; - case "while": - case "doWhile": - case "for": - case "block": - return s.labels ? { ...s, labels: s.labels.map(label) } : s; default: - return s; + return renameStmt(s); } }, }); - const bind: IrStmt[] = fn.params.flatMap((p, index): IrStmt[] => - aliased.has(p.localId) - ? [] - : [{ kind: "varDecl", localId: rename(p.localId), init: args[index]!, loc }], - ); const pre: IrStmt[] = []; if (result !== null) pre.push({ kind: "varDecl", localId: result, init: null, loc }); pre.push({ kind: "block", labels: [exit], body: [...bind, ...body], loc }); @@ -1155,7 +1351,13 @@ class FusedLoop { }; } - build(): { fn: Omit; args: IrExpr[]; key: string } | null { + build(): { + fn: Omit; + args: IrExpr[]; + key: string; + /** Bindings the callbacks copied into the loop captured at this site. */ + inlinedCaptures: string[]; + } | null { const { chain, loc } = this; const arrT = chain.sourceType; const a = this.param("a", chain.source, "src"); @@ -1640,7 +1842,10 @@ class FusedLoop { }; foldKnownUnions(fn); scalarizeRecordTemporaries(this.lowerer, fn); - return { fn, args: this.args, key: this.keyParts.join("|") }; + const inlinedCaptures = this.callees.flatMap((c) => + c.inline && c.arg.kind === "closure" ? c.arg.captures : [], + ); + return { fn, args: this.args, key: this.keyParts.join("|"), inlinedCaptures }; } } diff --git a/packages/compiler/src/ir/callback-specialization.ts b/packages/compiler/src/ir/callback-specialization.ts index 6038ccaa5..d67b557c4 100644 --- a/packages/compiler/src/ir/callback-specialization.ts +++ b/packages/compiler/src/ir/callback-specialization.ts @@ -16,7 +16,10 @@ const COPIED_NODE_FRACTION = 0.25; * closure it creates writes through its environment (and transitively the * closures those create). A parameter or capture absent from its function's * set holds the value it entered with for the whole call. */ -export function writtenLocals(mod: IrModule, nodes?: IrNodeIndex): Map> { +export function writtenLocals( + mod: { readonly functions: readonly IrFunction[] }, + nodes?: IrNodeIndex, +): Map> { return rebindings(mod, nodes).written; } @@ -31,7 +34,7 @@ const REBINDING_KINDS: readonly IrNodeKind[] = [ ]; function rebindings( - mod: IrModule, + mod: { readonly functions: readonly IrFunction[] }, nodes?: IrNodeIndex, ): { written: Map>; diff --git a/packages/compiler/test/array-pipeline-fusion.test.ts b/packages/compiler/test/array-pipeline-fusion.test.ts index 55cd0025e..0ff456a2c 100644 --- a/packages/compiler/test/array-pipeline-fusion.test.ts +++ b/packages/compiler/test/array-pipeline-fusion.test.ts @@ -108,5 +108,49 @@ const kept = rows.filter((r) => r.value > 3); console.log(kept.map((r) => r.id).length, kept.length); ${postlude}`); expect(init(mod).locals.some((l) => l.name === "kept")).toBe(true); - expect(helperCalls(init(mod))).toEqual(["%arr.filter", "%arr.fused"]); + expect(helperCalls(init(mod))).toEqual(["%arr.fused", "%arr.loop"]); +}); + +test("a lone call with a stable closure-literal callback runs as a loop", async () => { + const mod = await lowerSource(`${prelude} +const scale = 3; +const scaled = rows.map((r) => r.value * scale); +let total = 0; +rows.forEach((r) => { total += r.value; }); +const big = rows.filter((r) => r.value > scale); +const sum = scaled.reduce((acc, v) => acc + v, 0); +console.log(scaled.length, total, big.length, sum, rows.some((r) => r.id === scale)); +${postlude}`); + // forEach assigns \`total\`, a binding of \`run\`: it keeps its helper. + expect(helperCalls(init(mod))).toEqual([ + "%arr.forEach", + "%arr.loop", + "%arr.loop", + "%arr.loop", + "%arr.loop", + ]); + const loops = mod.functions.filter((f) => f.name.startsWith("%arr.loop.")); + expect(loops).toHaveLength(4); + // The callback bodies are copied into the loops: no closure value is + // created or called, and the captured \`scale\` is a plain parameter. + for (const fn of loops) { + expect(count(fn, "callValue")).toBe(0); + expect(count(fn, "closure")).toBe(0); + } + expect(count(init(mod), "closure")).toBe(1); + // No closure captures \`scale\` any more, so it needs no box. + expect(init(mod).locals.find((l) => l.name === "scale")?.boxed).toBeUndefined(); +}); + +test("a lone call keeps its helper when a capture can change while it runs", async () => { + const mod = await lowerSource(`${prelude} +let limit = 2; +const bump = (): void => { limit++; }; +const a = rows.filter((r) => { bump(); return r.id < limit; }); +let step = 1; +step = 2; +const b = rows.map((r) => r.id * step); +console.log(a.length, b.length, limit); +${postlude}`); + expect(helperCalls(init(mod))).toEqual(["%arr.filter", "%arr.map"]); }); diff --git a/packages/compiler/test/format-float-arguments.test.ts b/packages/compiler/test/format-float-arguments.test.ts index 6ce9c4bc0..795e238ef 100644 --- a/packages/compiler/test/format-float-arguments.test.ts +++ b/packages/compiler/test/format-float-arguments.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; import { compile, deserializeModule, validateModule } from "../src/index.js"; +import { everyStmtList } from "../src/ir/traverse.js"; test("util.format %f lowers a boolean argument once", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-format-float-")); @@ -22,14 +23,20 @@ test("util.format %f lowers a boolean argument once", async () => { throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); const mod = deserializeModule(await readFile(outPath, "utf8")); expect(validateModule(mod)).toEqual([]); - // One lifted helper per argument: the evaluated-for-effect statement - // reuses the lowered value instead of lowering the argument again. - const helpers = mod.functions - .map((f) => f.name) - .filter((name) => name.startsWith("%arr.")) - .map((name) => name.replace(/\.[0-9a-f]+$/, "")) - .sort(); - expect(helpers).toEqual(["%arr.every", "%arr.some"]); + // One lifted helper call per argument: the evaluated-for-effect + // statement reuses the lowered value instead of lowering the argument + // again. Each call runs its callback as a loop (array-pipelines.ts). + const helpers: string[] = []; + for (const fn of mod.functions) + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "call" && e.callee.startsWith("%arr.")) + helpers.push(e.callee.replace(/\.[0-9a-f]+$/, "")); + return true; + }, + stmt: () => true, + }); + expect(helpers).toEqual(["%arr.loop", "%arr.loop"]); } finally { await rm(dir, { recursive: true, force: true }); } diff --git a/tests/diagnostics/contract-int32-escapes.ts b/tests/diagnostics/contract-int32-escapes.ts index fcbbcbfd9..57ed4ce39 100644 --- a/tests/diagnostics/contract-int32-escapes.ts +++ b/tests/diagnostics/contract-int32-escapes.ts @@ -14,4 +14,5 @@ class Box { const box = new Box(); const loose: any = box; loose.size = 0.5; -console.log([1.5, 2].map(double).join(","), box.size); +const pick: (x: number) => number = double; +console.log([1.5, 2].map(pick).join(","), box.size); diff --git a/tests/harness/__snapshots__/contract-int32-arrays.ts.txt b/tests/harness/__snapshots__/contract-int32-arrays.ts.txt index c326adbb5..56db22706 100644 --- a/tests/harness/__snapshots__/contract-int32-arrays.ts.txt +++ b/tests/harness/__snapshots__/contract-int32-arrays.ts.txt @@ -34,14 +34,14 @@ contract-int32-arrays.ts:16:13 - error SC7002: the element stored into 'counts' hint: make the int32 conversion explicit, for example 'Math.PI | 0', so Node stores the same value -contract-int32-arrays.ts:18:26 - error SC7002: the elements 'counts.map((x) => x * 2)' stores are not provably int32s (the array can reach 'doubled', declared Int32[]) +contract-int32-arrays.ts:18:44 - error SC7002: the element stored into 'counts.map((x) => x * 2)' is not provably an int32: 'x * 2' (the array can reach 'doubled', declared Int32[]) 17 | halveInto(counts, ids); 18 | const doubled: Int32[] = counts.map((x) => x * 2); - | ^~~~~~~~~~~~~~~~~~~~~~~~ + | ^~~~~ 19 | const parsed: Int32[] = JSON.parse("[1, 2]"); - hint: callback results are not followed; build the array with a loop that stores 'value | 0' + hint: make the int32 conversion explicit, for example '(x * 2) | 0', so Node stores the same value contract-int32-arrays.ts:19:15 - error SC7003: the Int32[] contract on 'parsed' cannot be checked: the array can come from dynamic (any or unknown) code (contract-int32-arrays.ts:19:25), whose element stores scriptc does not see diff --git a/tests/harness/__snapshots__/contract-int32-escapes.ts.txt b/tests/harness/__snapshots__/contract-int32-escapes.ts.txt index 920f4c4a4..1be04a0cf 100644 --- a/tests/harness/__snapshots__/contract-int32-escapes.ts.txt +++ b/tests/harness/__snapshots__/contract-int32-escapes.ts.txt @@ -21,6 +21,6 @@ contract-int32-escapes.ts:16:1 - error SC7002: the value stored into 'Box.size' 15 | const loose: any = box; 16 | loose.size = 0.5; | ^~~~~~~~~~~~~~~~ - 17 | console.log([1.5, 2].map(double).join(","), box.size); + 17 | const pick: (x: number) => number = double; hint: make the int32 conversion explicit, for example 'loose.size = 0.5 | 0', so Node computes the same value \ No newline at end of file From 116d163e2c96c30bc9c6a9e369ed4ac4476d7d64 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 09:30:37 -0500 Subject: [PATCH 018/273] Take the elements before converting foreign elements Math.max, Math.min and Math.hypot over a spread, and the default sort, convert foreign elements with user code. As in Node, the elements are taken first, each foreign value retained, so a conversion that changes the array changes neither the count nor the values the operation sees, and no slot read before the conversion is released under it. join and the numeric element read hold the value they convert, and join reads the length once and stops at a throw. toSorted of an array with foreign elements now builds a copy of the full length and reads its holes as undefined, as for other arrays. --- packages/runtime/src/scr_array.c | 156 ++++++++++++------ .../6068-foreign-elements-through-views.ts | 35 ++++ 2 files changed, 137 insertions(+), 54 deletions(-) diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index c7399bcea..1405589a3 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -2066,33 +2066,47 @@ ScrArr *scr_arr_fill_undefined(ScrArr *a, double start, double end) { * the empty array yields the zero-argument constants. Borrows the array. */ /* ToNumber of every element in order, as Math.max/min/hypot apply it to * their spread arguments before comparing; for arrays that may hold - * foreign elements, whose conversion can call user code. NULL with the - * exception pending when a conversion throws. +1 malloc'd buffer. */ -static double *scr_arr_coerced_numbers(const ScrArr *a) { - double *out = malloc((a->len ? a->len : 1) * sizeof(*out)); - if (!out) scr_arr_oom(); - for (size_t i = 0; i < a->len; i++) { + * foreign elements, whose conversion can call user code. As in the spread, + * the elements are taken first: the foreign values are retained before + * any conversion runs, so a conversion that changes the array changes + * neither the count (*count) nor the values. NULL with the exception + * pending when a conversion throws. +1 malloc'd buffer. */ +static double *scr_arr_coerced_numbers(const ScrArr *a, size_t *count) { + size_t n = a->len; + double *out = malloc((n ? n : 1) * sizeof(*out)); + ScrDyn **held = calloc(n ? n : 1, sizeof(*held)); + if (!out || !held) scr_arr_oom(); + for (size_t i = 0; i < n; i++) { uint64_t slot; uint8_t state = scr_arr_f64_state_at(a, i, &slot); - if (state == SCR_ARR_FOREIGN) { - out[i] = scr_arr_foreign_conversions()->to_number(scr_slot_to_ptr(slot)); - if (scr_exc_pending()) { - free(out); - return NULL; - } - } else { - out[i] = state == SCR_ARR_VALUE ? scr_slot_to_f64(slot) : NAN; - } + if (state == SCR_ARR_FOREIGN) held[i] = scr_dyn_retain(scr_slot_to_ptr(slot)); + out[i] = state == SCR_ARR_VALUE ? scr_slot_to_f64(slot) : NAN; } + bool failed = false; + for (size_t i = 0; i < n; i++) { + if (!held[i]) continue; + if (!failed) { + out[i] = scr_arr_foreign_conversions()->to_number(held[i]); + failed = scr_exc_pending(); + } + scr_dyn_release(held[i]); + } + free(held); + if (failed) { + free(out); + return NULL; + } + *count = n; return out; } static double scr_math_extreme_foreign(ScrArr *a, bool max) { - double *values = scr_arr_coerced_numbers(a); + size_t n; + double *values = scr_arr_coerced_numbers(a, &n); if (!values) return NAN; double best = max ? -INFINITY : INFINITY; bool nan = false; - for (size_t i = 0; i < a->len; i++) { + for (size_t i = 0; i < n; i++) { double v = values[i]; if (isnan(v)) nan = true; else if (max ? (v > best || (v == 0.0 && best == 0.0 && !signbit(v))) @@ -2135,8 +2149,9 @@ double scr_math_hypot_arr(ScrArr *a) { double scale = 0.0; bool has_nan = false; double *coerced = NULL; - if (scr_arr_may_hold_foreign(a) && !(coerced = scr_arr_coerced_numbers(a))) return NAN; - for (size_t i = 0; i < a->len; i++) { + size_t n = a->len; + if (scr_arr_may_hold_foreign(a) && !(coerced = scr_arr_coerced_numbers(a, &n))) return NAN; + for (size_t i = 0; i < n; i++) { uint64_t slot; double v; if (coerced) { @@ -2160,7 +2175,7 @@ double scr_math_hypot_arr(ScrArr *a) { } double sum = 0.0; double correction = 0.0; - for (size_t i = 0; i < a->len; i++) { + for (size_t i = 0; i < n; i++) { uint64_t slot = 0; if (!coerced) (void)scr_arr_f64_state_at(a, i, &slot); double v = (coerced ? coerced[i] : scr_slot_to_f64(slot)) / scale; @@ -2271,7 +2286,13 @@ double scr_arr_get_number(const ScrArr *a, double i) { } else if (!scr_arr_prop_get_state(a, i, &slot, &state)) { return NAN; } - if (state == SCR_ARR_FOREIGN) return scr_arr_foreign_conversions()->to_number(scr_slot_to_ptr(slot)); + if (state == SCR_ARR_FOREIGN) { + /* The conversion can call user code that removes the element. */ + ScrDyn *value = scr_dyn_retain(scr_slot_to_ptr(slot)); + double number = scr_arr_foreign_conversions()->to_number(value); + scr_dyn_release(value); + return number; + } return state == SCR_ARR_VALUE ? scr_slot_to_f64(slot) : NAN; } @@ -3404,20 +3425,21 @@ static void scr_arr_primitive_order(uint64_t *values, size_t count, } /* The default ordering of an array that may hold foreign elements: present - * elements sort by their string conversions (which may call user code, so - * they are computed first, and a throw leaves the array unchanged), then - * present undefined values, then holes. */ + * elements sort by their string conversions, then present undefined + * values, then holes (toSorted reads them as undefined). The conversions + * may call user code, so the elements are taken first, each retained, as + * the sort's element list is; a throw leaves the array unchanged. */ static ScrArr *scr_arr_sort_foreign(ScrArr *a, bool copy) { - size_t cap = (a->len < a->cap ? a->len : a->cap) + SCR_AX(a)->sparse_len; + size_t length = a->len; + size_t dense = a->len < a->cap ? a->len : a->cap; + size_t cap = dense + SCR_AX(a)->sparse_len; uint64_t *slots = cap ? malloc(cap * sizeof(*slots)) : NULL; uint8_t *states = cap ? malloc(cap) : NULL; ScrStr **keys = cap ? malloc(cap * sizeof(*keys)) : NULL; uint64_t *order_of = cap ? malloc(cap * sizeof(*order_of)) : NULL; if (cap && (!slots || !states || !keys || !order_of)) scr_arr_oom(); size_t count = 0, undefined_count = 0; - bool failed = false; - size_t dense = a->len < a->cap ? a->len : a->cap; - for (size_t at = 0; at < dense + SCR_AX(a)->sparse_len && !failed; at++) { + for (size_t at = 0; at < cap; at++) { uint64_t slot; uint8_t state; if (at < dense) { @@ -3429,10 +3451,17 @@ static ScrArr *scr_arr_sort_foreign(ScrArr *a, bool copy) { } if (state == SCR_ARR_UNDEFINED) undefined_count++; if (state != SCR_ARR_VALUE && state != SCR_ARR_FOREIGN) continue; + slots[count] = scr_arr_retain_state_slot(a, slot, state); + states[count] = state; + count++; + } + bool failed = false; + size_t keyed = 0; + for (; keyed < count; keyed++) { + uint64_t slot = slots[keyed]; ScrStr *key; - if (state == SCR_ARR_FOREIGN) { + if (states[keyed] == SCR_ARR_FOREIGN) { key = scr_arr_foreign_conversions()->to_string(scr_slot_to_ptr(slot)); - if (!key) failed = true; } else if (a->elem == SCR_ELEM_STR) { key = scr_str_retain(scr_slot_to_ptr(slot)); } else { @@ -3440,37 +3469,48 @@ static ScrArr *scr_arr_sort_foreign(ScrArr *a, bool copy) { size_t n = scr_f64_to_str(scr_slot_to_f64(slot), text); key = scr_str_new(text, n); } - if (failed) break; - slots[count] = slot; - states[count] = state; - keys[count] = key; - order_of[count] = count; - count++; + if (!key) { + failed = true; + break; + } + keys[keyed] = key; + order_of[keyed] = keyed; } - ScrArr *out = NULL; + ScrArr *out = a; if (!failed) { ScrPrimitiveOrder order = {a->elem, NULL, keys}; scr_arr_primitive_order(order_of, count, &order); - size_t length = a->len; if (copy) { - out = scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), count + undefined_count) - : scr_arr_new(a->elem, count + undefined_count); + /* The values, then present undefined for every other index. */ + out = scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), length) + : scr_arr_new(a->elem, length); + for (size_t i = 0; i < length; i++) { + if (i < count) scr_arr_store_state_owned(out, i, slots[order_of[i]], states[order_of[i]]); + else scr_arr_store_state_owned(out, i, 0, SCR_ARR_UNDEFINED); + out->len = (uint32_t)(i + 1); + } } else { - out = a; + /* The values, then the present undefined values; holes after them. + * The taken elements own their references; release the ones the + * array holds now, which a conversion may have changed. */ + size_t now = a->len < a->cap ? a->len : a->cap; + for (size_t i = 0; i < now; i++) + scr_arr_release_state_slot(a, scr_arr_dense_get(a, i), scr_arr_dense_state(a, i)); + for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) + scr_arr_release_state_slot(a, SCR_AX(a)->sparse[i].slot, SCR_AX(a)->sparse[i].state); ScrArrStorage old = scr_arr_take_storage(a); scr_arr_free_storage(&old); + a->len = (uint32_t)length; + for (size_t i = 0; i < count; i++) + scr_arr_store_state_owned(a, i, slots[order_of[i]], states[order_of[i]]); + for (size_t i = count; i < count + undefined_count; i++) + scr_arr_store_state_owned(a, i, 0, SCR_ARR_UNDEFINED); } - out->len = length; - for (size_t i = 0; i < count; i++) { - size_t k = order_of[i]; - uint64_t slot = copy ? scr_arr_retain_state_slot(a, slots[k], states[k]) : slots[k]; - scr_arr_store_state_owned(out, i, slot, states[k]); - } - for (size_t i = count; i < count + undefined_count; i++) - scr_arr_store_state_owned(out, i, 0, SCR_ARR_UNDEFINED); scr_arr_refill(out); + } else { + for (size_t i = 0; i < count; i++) scr_arr_release_state_slot(a, slots[i], states[i]); } - for (size_t i = 0; i < count; i++) scr_str_release(keys[i]); + for (size_t i = 0; i < keyed; i++) scr_str_release(keys[i]); free(slots); free(states); free(keys); @@ -5384,16 +5424,24 @@ ScrStr *scr_arr_join(ScrArr *a, ScrStr *sep) { * across its seam; the finished string then needs fusing. */ bool lone = scr_wtf8_any(); bool lead_low = lone && a->len > 1 && scr_wtf8_low_at(sep->data); - for (size_t i = 0; i < a->len; i++) { + /* The length is read once: a conversion that shortens the array leaves + * the indices past its new end empty. */ + size_t length = a->len; + for (size_t i = 0; i < length; i++) { if (i > 0) scr_join_append(&out, sep->data, sep->len); + if (i >= a->len) continue; uint64_t slot; uint8_t state = scr_arr_state_at(a, i, &slot); if (state == SCR_ARR_FOREIGN) { - /* join converts every element but undefined and null with ToString. */ - const ScrDyn *value = scr_slot_to_ptr(slot); + /* join converts every element but undefined and null with ToString, + * which can call user code that removes the element; a throw ends + * the join with the exception pending. */ + ScrDyn *value = scr_slot_to_ptr(slot); if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) continue; + scr_dyn_retain(value); ScrStr *text = scr_arr_foreign_conversions()->to_string(value); - if (!text) continue; + scr_dyn_release(value); + if (!text) break; scr_join_append(&out, text->data, text->len); scr_str_release(text); continue; diff --git a/tests/corpus/6068-foreign-elements-through-views.ts b/tests/corpus/6068-foreign-elements-through-views.ts index e6676eea4..76997c624 100644 --- a/tests/corpus/6068-foreign-elements-through-views.ts +++ b/tests/corpus/6068-foreign-elements-through-views.ts @@ -54,3 +54,38 @@ const words: string[] = ["a"]; const wordsView: unknown[] = words; wordsView.push(1, false); console.log(words, words.join("-"), words.length, typeof wordsView[1]); + +// A conversion that changes the array does not change the spread's values, +// and toSorted reads holes as undefined. +const grown: number[] = [3, 1]; +const grownView: unknown[] = grown; +grownView.push({ + valueOf: () => { + for (let i = 0; i < 64; i++) grown.push(i); + grown.length = 1; + return 2; + }, +}); +console.log(Math.max(...grown), Math.min(...grown), grown.length); +const holey: string[] = ["c", "a"]; +const holeyView: unknown[] = holey; +holeyView[4] = 7; +holeyView[6] = undefined; +console.log(holey.toSorted(), holey.length); +holey.sort(); +console.log(holey, holey.length); + +// A conversion that removes the element it converts. +const shrinking: number[] = [1]; +const shrinkingView: unknown[] = shrinking; +shrinkingView.push({ + toString: () => { + shrinking.length = 0; + return "gone"; + }, +}); +console.log(shrinking.join("+"), shrinking.length); +const cut: string[] = ["a", "b"]; +const cutView: unknown[] = cut; +cutView.splice(1, 0, { toString: () => ((cut.length = 0), "x") }); +console.log(JSON.stringify(cut.join("-")), cut.length); From 52792d8c52919b16f703246c305247d7a5ed835f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 09:54:58 -0500 Subject: [PATCH 019/273] Find weak collection entries through hashed indexes WeakMap and WeakSet kept each collection's entries in one list and found a key by walking it, and every collection's entries for a key in 1,024 shared buckets. Filling a collection and reading it back took quadratic time: 40,000 keys took 6.7 s against Node's 0.2 s, and 200,000 keys did not finish. Each thread now indexes its entries twice, in open-addressing tables with linear probing and backward-shift deletion: by collection, key and kind, for get, set, has and delete, and by key pointer, for a key's disposal, which ends the key's entries in every collection. A collection's entries and a key's entries are doubly linked, so an entry leaves both in constant time. The tables grow at half load and are freed when their last entry goes, so an exiting worker leaves none. 200,000 keys now take 0.24 s (Node 0.30 s). Closes #1702 Co-authored-by: ejc3 <1694850+ejc3@users.noreply.github.com> --- .../7742-weak-collections-many-keys.ts.json | 6 + packages/runtime/src/scr_json.c | 203 +++++++++++++++--- .../corpus/7742-weak-collections-many-keys.ts | 70 ++++++ .../7742-weak-collections-many-keys.ts.json | 1 + 4 files changed, 247 insertions(+), 33 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7742-weak-collections-many-keys.ts.json create mode 100644 tests/corpus/7742-weak-collections-many-keys.ts create mode 100644 tests/harness/corpus-costs/7742-weak-collections-many-keys.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7742-weak-collections-many-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7742-weak-collections-many-keys.ts.json new file mode 100644 index 000000000..e1a75c14e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7742-weak-collections-many-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7742-weak-collections-many-keys.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index da18a1c0b..4841da53d 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -15019,26 +15019,173 @@ typedef struct ScrWeakEntry { unsigned kind; ScrDyn *value; ScrWeakMap *owner; - struct ScrWeakEntry *next, *all_next; + /* The owner's entries, and every collection's entries for the same key + * pointer (whatever their kind): both doubly linked, so an entry leaves + * either list in constant time. */ + struct ScrWeakEntry *next, *prev, *key_next, *key_prev; } ScrWeakEntry; struct ScrWeakMap { size_t rc; ScrWeakEntry *entries; bool is_set; }; /* Weak observers borrow key identities. Values are traced strong edges; - * value-to-key cycles require explicit deletion or container disposal. */ -#define SCR_WEAK_BUCKETS 1024 -static SCR_TL ScrWeakEntry *scr_weak_entries[SCR_WEAK_BUCKETS]; -static size_t scr_weak_bucket(void *key) { - uintptr_t bits = (uintptr_t)key; - return ((bits >> 4) ^ (bits >> 14)) & (SCR_WEAK_BUCKETS - 1); + * value-to-key cycles require explicit deletion or container disposal. + * + * Two open-addressing indexes per thread find entries in expected constant + * time: scr_weak_index by (collection, key, kind), for get/set/has/delete, + * and scr_weak_keys by key pointer, the head of that key's entries, for a + * disposal, which ends the key's entries in every collection. Both use + * linear probing with backward-shift deletion, grow at half load, and are + * freed when their last entry goes. */ +static SCR_TL ScrWeakEntry **scr_weak_index; +static SCR_TL size_t scr_weak_index_mask, scr_weak_index_used; +typedef struct { void *key; ScrWeakEntry *head; } ScrWeakKeySlot; +static SCR_TL ScrWeakKeySlot *scr_weak_keys; +static SCR_TL size_t scr_weak_keys_mask, scr_weak_keys_used; + +static inline size_t scr_weak_mix(uintptr_t bits) { + bits ^= bits >> 33; + bits *= 0xff51afd7ed558ccdull; + bits ^= bits >> 29; + return (size_t)bits; +} +static inline size_t scr_weak_entry_hash(const ScrWeakMap *owner, const void *key, unsigned kind) { + return scr_weak_mix((uintptr_t)key ^ ((uintptr_t)owner * 0x9E3779B97F4A7C15ull) ^ ((uintptr_t)kind << 56)); +} +static inline size_t scr_weak_key_hash(const void *key) { return scr_weak_mix((uintptr_t)key); } + +static void scr_weak_index_put(ScrWeakEntry *entry) { + size_t j = scr_weak_entry_hash(entry->owner, entry->key, entry->kind) & scr_weak_index_mask; + while (scr_weak_index[j]) j = (j + 1) & scr_weak_index_mask; + scr_weak_index[j] = entry; +} +static void scr_weak_index_reserve(void) { + if (scr_weak_index && (scr_weak_index_used + 1) * 2 <= scr_weak_index_mask + 1) return; + ScrWeakEntry **old = scr_weak_index; + size_t n = old ? scr_weak_index_mask + 1 : 0, cap = n ? n * 2 : 64; + scr_weak_index = calloc(cap, sizeof *scr_weak_index); + if (!scr_weak_index) scr_json_oom(); + scr_weak_index_mask = cap - 1; + for (size_t i = 0; i < n; i++) + if (old[i]) scr_weak_index_put(old[i]); + free(old); +} +static ScrWeakEntry *scr_weak_find(ScrWeakMap *map, void *ptr, unsigned kind) { + if (!scr_weak_index) return NULL; + for (size_t j = scr_weak_entry_hash(map, ptr, kind) & scr_weak_index_mask;; j = (j + 1) & scr_weak_index_mask) { + ScrWeakEntry *entry = scr_weak_index[j]; + if (!entry) return NULL; + if (entry->owner == map && entry->key == ptr && entry->kind == kind) return entry; + } +} +static void scr_weak_index_delete(ScrWeakEntry *entry) { + size_t mask = scr_weak_index_mask; + size_t i = scr_weak_entry_hash(entry->owner, entry->key, entry->kind) & mask; + while (scr_weak_index[i] != entry) i = (i + 1) & mask; + if (--scr_weak_index_used == 0) { + /* Empty: give the table back, so a thread that exits leaves none. */ + free(scr_weak_index); + scr_weak_index = NULL; + return; + } + for (size_t k = i;;) { + scr_weak_index[i] = NULL; + for (;;) { + k = (k + 1) & mask; + ScrWeakEntry *e = scr_weak_index[k]; + if (!e) return; + size_t home = scr_weak_entry_hash(e->owner, e->key, e->kind) & mask; + /* e stays unless its home lies cyclically outside (i, k]. */ + if (i <= k ? (i < home && home <= k) : (i < home || home <= k)) continue; + scr_weak_index[i] = e; + i = k; + break; + } + } +} + +static ScrWeakKeySlot *scr_weak_key_slot(const void *key) { + if (!scr_weak_keys) return NULL; + for (size_t j = scr_weak_key_hash(key) & scr_weak_keys_mask;; j = (j + 1) & scr_weak_keys_mask) { + if (scr_weak_keys[j].key == key) return &scr_weak_keys[j]; + if (!scr_weak_keys[j].key) return NULL; + } +} +static ScrWeakKeySlot *scr_weak_key_slot_put(void *key) { + if (!scr_weak_keys || (scr_weak_keys_used + 1) * 2 > scr_weak_keys_mask + 1) { + ScrWeakKeySlot *old = scr_weak_keys; + size_t n = old ? scr_weak_keys_mask + 1 : 0, cap = n ? n * 2 : 64; + scr_weak_keys = calloc(cap, sizeof *scr_weak_keys); + if (!scr_weak_keys) scr_json_oom(); + scr_weak_keys_mask = cap - 1; + for (size_t i = 0; i < n; i++) { + if (!old[i].key) continue; + size_t j = scr_weak_key_hash(old[i].key) & scr_weak_keys_mask; + while (scr_weak_keys[j].key) j = (j + 1) & scr_weak_keys_mask; + scr_weak_keys[j] = old[i]; + } + free(old); + } + size_t j = scr_weak_key_hash(key) & scr_weak_keys_mask; + while (scr_weak_keys[j].key && scr_weak_keys[j].key != key) j = (j + 1) & scr_weak_keys_mask; + if (!scr_weak_keys[j].key) { + scr_weak_keys[j].key = key; + scr_weak_keys[j].head = NULL; + scr_weak_keys_used++; + } + return &scr_weak_keys[j]; +} +static void scr_weak_key_slot_delete(ScrWeakKeySlot *slot) { + size_t mask = scr_weak_keys_mask; + size_t i = (size_t)(slot - scr_weak_keys); + if (--scr_weak_keys_used == 0) { + free(scr_weak_keys); + scr_weak_keys = NULL; + return; + } + for (size_t k = i;;) { + scr_weak_keys[i].key = NULL; + scr_weak_keys[i].head = NULL; + for (;;) { + k = (k + 1) & mask; + if (!scr_weak_keys[k].key) return; + size_t home = scr_weak_key_hash(scr_weak_keys[k].key) & mask; + if (i <= k ? (i < home && home <= k) : (i < home || home <= k)) continue; + scr_weak_keys[i] = scr_weak_keys[k]; + i = k; + break; + } + } +} + +/* Indexes a new entry (its key, kind and owner set) under its key. */ +static void scr_weak_link(ScrWeakEntry *entry) { + scr_weak_index_reserve(); + scr_weak_index_put(entry); + scr_weak_index_used++; + ScrWeakKeySlot *slot = scr_weak_key_slot_put(entry->key); + entry->key_prev = NULL; + entry->key_next = slot->head; + if (slot->head) slot->head->key_prev = entry; + slot->head = entry; +} + +/* Takes an entry out of both indexes (it stays in its owner's list). */ +static void scr_weak_unlink(ScrWeakEntry *entry) { + scr_weak_index_delete(entry); + if (entry->key_next) entry->key_next->key_prev = entry->key_prev; + if (entry->key_prev) entry->key_prev->key_next = entry->key_next; + else { + ScrWeakKeySlot *slot = scr_weak_key_slot(entry->key); + if (entry->key_next) slot->head = entry->key_next; + else scr_weak_key_slot_delete(slot); + } } static void scr_weak_remove_impl(ScrWeakEntry *entry, bool collected) { - ScrWeakEntry **slot = &entry->owner->entries; - while (*slot != entry) slot = &(*slot)->next; - *slot = entry->next; - slot = &scr_weak_entries[scr_weak_bucket(entry->key)]; - while (*slot != entry) slot = &(*slot)->all_next; - *slot = entry->all_next; + ScrWeakMap *owner = entry->owner; + if (entry->next) entry->next->prev = entry->prev; + if (entry->prev) entry->prev->next = entry->next; + else owner->entries = entry->next; + scr_weak_unlink(entry); ScrDyn *value = entry->value; free(entry); /* Unlink before release: releasing a value can dispose another key. */ @@ -15062,10 +15209,9 @@ static void scr_weak_map_gcfree(void *ptr) { static void scr_weak_disposed(void *key) { for (;;) { - ScrWeakEntry *entry = scr_weak_entries[scr_weak_bucket(key)]; - while (entry && entry->key != key) entry = entry->all_next; - if (!entry) return; - scr_weak_remove(entry); + ScrWeakKeySlot *slot = scr_weak_key_slot(key); + if (!slot) return; + scr_weak_remove(slot->head); } } @@ -15122,12 +15268,6 @@ static int scr_weak_key(const ScrDyn *key, void **ptr, unsigned *kind) { } } -static ScrWeakEntry *scr_weak_find(ScrWeakMap *map, void *ptr, unsigned kind) { - for (ScrWeakEntry *entry = map->entries; entry; entry = entry->next) - if (entry->key == ptr && entry->kind == kind) return entry; - return NULL; -} - static bool scr_weak_set(ScrWeakMap *map, ScrDyn *key, ScrDyn *value) { void *ptr = NULL; unsigned kind = 0; @@ -15156,10 +15296,9 @@ static bool scr_weak_set(ScrWeakMap *map, ScrDyn *key, ScrDyn *value) { entry->value = scr_dyn_retain(value); entry->owner = map; entry->next = map->entries; + if (map->entries) map->entries->prev = entry; map->entries = entry; - size_t bucket = scr_weak_bucket(ptr); - entry->all_next = scr_weak_entries[bucket]; - scr_weak_entries[bucket] = entry; + scr_weak_link(entry); return true; } @@ -16371,16 +16510,14 @@ static bool scr_dyn_decl_rekey(ScrDyn *node, void *ptr) { bool moved = false; if (!scr_weak_observing) return false; for (;;) { - ScrWeakEntry **slot = &scr_weak_entries[scr_weak_bucket(node)]; - while (*slot && !((*slot)->key == (void *)node && (*slot)->kind == 0)) slot = &(*slot)->all_next; - ScrWeakEntry *entry = *slot; + ScrWeakKeySlot *slot = scr_weak_key_slot(node); + ScrWeakEntry *entry = slot ? slot->head : NULL; + while (entry && entry->kind != 0) entry = entry->key_next; if (!entry) return moved; - *slot = entry->all_next; + scr_weak_unlink(entry); entry->key = ptr; entry->kind = 32; - ScrWeakEntry **head = &scr_weak_entries[scr_weak_bucket(ptr)]; - entry->all_next = *head; - *head = entry; + scr_weak_link(entry); moved = true; } } diff --git a/tests/corpus/7742-weak-collections-many-keys.ts b/tests/corpus/7742-weak-collections-many-keys.ts new file mode 100644 index 000000000..2cb2b103e --- /dev/null +++ b/tests/corpus/7742-weak-collections-many-keys.ts @@ -0,0 +1,70 @@ +// Weak collections with many keys: entries found among tens of thousands +// in WeakMaps and WeakSets that share keys, deleted and replaced, and gone +// with their keys, so blocks a new key reuses are not found under it. +// Keys in reference cycles die by the cycle collector. +class Key { + readonly id: number; + peer: Key | null = null; + constructor(id: number) { + this.id = id; + } +} + +const n = 40000; +let keys: Key[] = []; +for (let i = 0; i < n; i++) keys.push(new Key(i)); +const values = new WeakMap(); +const labels = new WeakMap(); +const marked = new WeakSet(); +for (const k of keys) { + values.set(k, k.id * 2); + if (k.id % 3 === 0) labels.set(k, "k" + k.id); + if (k.id % 2 === 0) marked.add(k); +} +let sum = 0; +let labelled = 0; +let counted = 0; +for (const k of keys) { + sum += values.get(k) ?? 0; + if (labels.has(k)) labelled++; + if (marked.has(k)) counted++; +} +console.log("filled", sum, labelled, counted); + +// Deleting every fourth key from one collection leaves the others alone. +let deleted = 0; +for (const k of keys) if (k.id % 4 === 0 && values.delete(k)) deleted++; +let remaining = 0; +for (const k of keys) if (values.has(k)) remaining++; +console.log("deleted", deleted, remaining, values.delete(keys[0]!), marked.has(keys[0]!), labels.get(keys[3]!)); + +// Overwriting keeps one entry per key. +for (const k of keys) if (k.id % 5 === 0) values.set(k, -1); +let negative = 0; +for (const k of keys) if (values.get(k) === -1) negative++; +console.log("overwritten", negative); + +// Dropping the keys ends their entries; new keys are not found. +keys = []; +const fresh: Key[] = []; +for (let i = 0; i < n; i++) fresh.push(new Key(n + i)); +let stale = 0; +for (const k of fresh) if (values.has(k) || labels.has(k) || marked.has(k)) stale++; +console.log("fresh", fresh.length, stale); + +// Keys that reach each other die together through the cycle collector. +for (let round = 0; round < 3; round++) { + let ring: Key[] = []; + for (let i = 0; i < 1000; i++) ring.push(new Key(i)); + for (let i = 0; i < ring.length; i++) ring[i]!.peer = ring[(i + 1) % ring.length]!; + for (const k of ring) { + values.set(k, round); + marked.add(k); + } + ring = []; +} +const after: Key[] = []; +for (let i = 0; i < 3000; i++) after.push(new Key(i)); +let found = 0; +for (const k of after) if (values.has(k) || marked.has(k)) found++; +console.log("cycles", found); diff --git a/tests/harness/corpus-costs/7742-weak-collections-many-keys.ts.json b/tests/harness/corpus-costs/7742-weak-collections-many-keys.ts.json new file mode 100644 index 000000000..97e5fc450 --- /dev/null +++ b/tests/harness/corpus-costs/7742-weak-collections-many-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 6000 } From e1358fb5635b9fe65a24c1854170366773437dd9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 10:16:30 -0500 Subject: [PATCH 020/273] Read Math, JSON and Reflect as real objects in JavaScript In JavaScript sources, Math, JSON or Reflect read as a value lowered to an identity token, the string "[builtin Math]": typeof, String, `in`, descriptors and member calls through an alias all had string semantics. Each of these namespaces read as a value is now one object per program, built on first use, with Node 24's own properties in Node's order: function members are writable, configurable and non-enumerable with Node's `length`, the Math constants are read-only, and Symbol.toStringTag names the namespace. Members hold the same function values the spelled member read gives; a member without one throws its fence when called. Global reads, globalThis, destructuring and shorthand properties all produce the same object, and an object with a string Symbol.toStringTag coerces to "[object ]". Direct member calls keep their static lowerings, so replacing or deleting a function member would not reach them: such changes throw SC1090. New properties and attribute-only redefinitions behave as in Node. --- .../compiler/src/backend/llvm/expr-dynamic.ts | 4 + .../compiler/src/backend/llvm/lib-shared.ts | 2 + .../frontend/lowering/builtin-namespaces.ts | 285 ++++++++++++++++++ .../src/frontend/lowering/builtins/json.ts | 12 +- .../lowering/expressions/object-literals.ts | 7 +- .../lowering/expressions/property-access.ts | 12 + .../src/frontend/lowering/lower-stdlib.ts | 65 ++-- .../src/frontend/lowering/lower-stmts.ts | 4 +- .../compiler/src/frontend/lowering/lowerer.ts | 8 + .../src/frontend/lowering/surfaces.ts | 3 + .../compiler/src/ir/builtin-signatures.ts | 2 + packages/compiler/src/ir/ir.ts | 12 +- packages/compiler/src/ir/runtime-features.ts | 2 + .../test/namespace-member-fences.test.ts | 67 ++++ .../7186-namespace-object-values.js.json | 6 + .../7187-namespace-object-reflection.js.json | 6 + .../7188-namespace-object-extensions.js.json | 6 + packages/runtime/src/scr_dyn.h | 22 ++ packages/runtime/src/scr_dyn_invoke.c | 29 ++ packages/runtime/src/scr_js_object.c | 4 +- packages/runtime/src/scr_json.c | 90 +++++- packages/runtime/src/scr_symbol.c | 5 + tests/corpus/7186-namespace-object-values.js | 22 ++ .../7187-namespace-object-reflection.js | 23 ++ .../7188-namespace-object-extensions.js | 16 + .../7186-namespace-object-values.js.json | 1 + .../7187-namespace-object-reflection.js.json | 1 + .../7188-namespace-object-extensions.js.json | 1 + 28 files changed, 678 insertions(+), 39 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/builtin-namespaces.ts create mode 100644 packages/compiler/test/namespace-member-fences.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7186-namespace-object-values.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7187-namespace-object-reflection.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7188-namespace-object-extensions.js.json create mode 100644 tests/corpus/7186-namespace-object-values.js create mode 100644 tests/corpus/7187-namespace-object-reflection.js create mode 100644 tests/corpus/7188-namespace-object-extensions.js create mode 100644 tests/harness/corpus-costs/7186-namespace-object-values.js.json create mode 100644 tests/harness/corpus-costs/7187-namespace-object-reflection.js.json create mode 100644 tests/harness/corpus-costs/7188-namespace-object-extensions.js.json diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 07a538184..fac708b82 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -115,6 +115,10 @@ export function emitDynamicExpr( const box = host.dyn.dynFuncBoxHelper(v.type); const t = B.tmp(); B.line(`${t} = call ptr @${box}(ptr ${v.name}, ptr ${name})`); + if (e.fnLength !== undefined) { + host.declare(`declare void @scr_dyn_function_set_length(ptr, i32)`); + B.line(`call void @scr_dyn_function_set_length(ptr ${t}, i32 ${e.fnLength})`); + } return host.own({ name: t, type: e.type }); } const conv = host.dyn.toDynHelper(v.type); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index af32c6936..44db20d03 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1027,6 +1027,8 @@ export const LIB_FN_SYMS: Record = { "dyn.nativeDateHandleIs": "scr_dyn_native_date_is", "dyn.globalConstructor": "scr_dyn_global_constructor", "dyn.builtinFunction": "scr_dyn_builtin_function", + "dyn.namespaceNew": "scr_dyn_namespace_new", + "dyn.namespaceDefine": "scr_dyn_namespace_define", "date.nativeNew": "scr_dyn_native_date_new", "date.nativeNewFields": "scr_dyn_native_date_new_fields", "date.nativeNewValue": "scr_dyn_native_date_new_value", diff --git a/packages/compiler/src/frontend/lowering/builtin-namespaces.ts b/packages/compiler/src/frontend/lowering/builtin-namespaces.ts new file mode 100644 index 000000000..bfe3c5e39 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtin-namespaces.ts @@ -0,0 +1,285 @@ +/** The standard namespace objects (`Math`, `JSON`, `Reflect`) read as + * JavaScript values. Each is one object per program, built on first use, + * with Node's own properties in Node's order: the functions are writable, + * configurable and non-enumerable, the Math constants are read-only and + * non-configurable, and `Symbol.toStringTag` names the namespace. Function + * members hold the same function values the spelled `Math.abs` read gives; + * members without one hold a function that throws the named fence when + * called. Direct member calls (`Math.abs(x)`) keep their static lowerings. */ +import { + BOOL, + DYN, + F64, + STRING, + SYMBOL_T, + VOID, + type IrExpr, + type IrStmt, + type SrcLoc, + funcOf, +} from "../../ir/ir.js"; +import { helperLoc, varRef } from "../../ir/build.js"; +import { unsupportedDiag } from "../../diagnostics/diagnostic.js"; +import type { Lowerer } from "./lowerer.js"; +import { InternalCompilerError } from "../../errors.js"; +import { mathMemberValue } from "./lower-stdlib.js"; +import { jsonMemberValue } from "./builtins/json.js"; + +/** A function member with its `length`, or a constant (`length` -1). */ +interface NamespaceMember { + readonly key: string; + readonly length: number; +} + +/** Node 24's own string-keyed properties of the namespace `name`, in + * order, or null when `name` is not a namespace. */ +function namespaceMembers(name: string): readonly NamespaceMember[] | null { + switch (name) { + case "Math": + return [ + { key: "abs", length: 1 }, + { key: "acos", length: 1 }, + { key: "acosh", length: 1 }, + { key: "asin", length: 1 }, + { key: "asinh", length: 1 }, + { key: "atan", length: 1 }, + { key: "atanh", length: 1 }, + { key: "atan2", length: 2 }, + { key: "ceil", length: 1 }, + { key: "cbrt", length: 1 }, + { key: "expm1", length: 1 }, + { key: "clz32", length: 1 }, + { key: "cos", length: 1 }, + { key: "cosh", length: 1 }, + { key: "exp", length: 1 }, + { key: "floor", length: 1 }, + { key: "fround", length: 1 }, + { key: "hypot", length: 2 }, + { key: "imul", length: 2 }, + { key: "log", length: 1 }, + { key: "log1p", length: 1 }, + { key: "log2", length: 1 }, + { key: "log10", length: 1 }, + { key: "max", length: 2 }, + { key: "min", length: 2 }, + { key: "pow", length: 2 }, + { key: "random", length: 0 }, + { key: "round", length: 1 }, + { key: "sign", length: 1 }, + { key: "sin", length: 1 }, + { key: "sinh", length: 1 }, + { key: "sqrt", length: 1 }, + { key: "tan", length: 1 }, + { key: "tanh", length: 1 }, + { key: "trunc", length: 1 }, + { key: "E", length: -1 }, + { key: "LN10", length: -1 }, + { key: "LN2", length: -1 }, + { key: "LOG10E", length: -1 }, + { key: "LOG2E", length: -1 }, + { key: "PI", length: -1 }, + { key: "SQRT1_2", length: -1 }, + { key: "SQRT2", length: -1 }, + { key: "f16round", length: 1 }, + ]; + case "JSON": + return [ + { key: "parse", length: 2 }, + { key: "stringify", length: 3 }, + { key: "rawJSON", length: 1 }, + { key: "isRawJSON", length: 1 }, + ]; + case "Reflect": + return [ + { key: "defineProperty", length: 3 }, + { key: "deleteProperty", length: 2 }, + { key: "apply", length: 3 }, + { key: "construct", length: 2 }, + { key: "get", length: 2 }, + { key: "getOwnPropertyDescriptor", length: 2 }, + { key: "getPrototypeOf", length: 1 }, + { key: "has", length: 2 }, + { key: "isExtensible", length: 1 }, + { key: "ownKeys", length: 1 }, + { key: "preventExtensions", length: 1 }, + { key: "set", length: 3 }, + { key: "setPrototypeOf", length: 2 }, + ]; + default: + return null; + } +} + +/** Property attribute bits for dyn.namespaceDefine (the members are never + * enumerable). */ +const WRITABLE = 1; +const CONFIGURABLE = 4; + +/** The namespace object the stdlib global `canonical` reads as in + * JavaScript, or null when it is not a namespace. */ +export function builtinNamespaceValue( + lowerer: Lowerer, + canonical: string, + loc: SrcLoc, +): IrExpr | null { + const members = namespaceMembers(canonical); + if (members === null) return null; + const name = `%builtin.namespace.${canonical}`; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) + defineNamespaceBuilder(lowerer, canonical, members, name, helperLoc(loc)); + lowerer.noteEdge(name); + return { kind: "call", callee: name, args: [], type: DYN, loc }; +} + +/** The lifted function that builds the namespace object once and answers + * the same object afterwards. */ +function defineNamespaceBuilder( + lowerer: Lowerer, + canonical: string, + members: readonly NamespaceMember[], + name: string, + loc: SrcLoc, +): void { + const storage = `%g.${name}`; + const ready = `${storage}%ready`; + lowerer.globalsList.push( + { id: storage, name: `${canonical} namespace`, type: DYN, mutable: true }, + { id: ready, name: "%ready", type: BOOL, mutable: true }, + ); + const object = varRef("namespace.0", DYN, loc); + const body: IrStmt[] = [ + { + kind: "if", + cond: varRef(ready, BOOL, loc), + then: [{ kind: "return", value: varRef(storage, DYN, loc), loc }], + else_: null, + loc, + }, + { + kind: "varDecl", + localId: "namespace.0", + init: { + kind: "libCall", + fn: "dyn.namespaceNew", + args: [ + { kind: "strLit", value: canonical, type: STRING, loc }, + { + kind: "dynFrom", + value: { + kind: "libCall", + fn: "sym.wellKnown", + args: [{ kind: "strLit", value: "toStringTag", type: STRING, loc }], + type: SYMBOL_T, + loc, + }, + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }, + loc, + }, + ]; + for (const { key, length } of members) { + const value = + length < 0 + ? constantValue(lowerer, canonical, key, loc) + : functionValue(lowerer, canonical, key, loc); + const attributes = length < 0 ? 0 : WRITABLE | CONFIGURABLE; + body.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.namespaceDefine", + args: [ + object, + { kind: "strLit", value: key, type: STRING, loc }, + value, + { kind: "numLit", value: attributes, type: F64, loc }, + { kind: "numLit", value: length, type: F64, loc }, + ], + type: VOID, + loc, + }, + loc, + }); + } + body.push( + { kind: "assign", localId: storage, value: object, loc }, + { + kind: "assign", + localId: ready, + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }, + { kind: "return", value: varRef(storage, DYN, loc), loc }, + ); + lowerer.liftedFns.push({ + name, + params: [], + locals: [{ id: "namespace.0", name: "namespace", type: DYN, mutable: false }], + returnType: DYN, + body, + loc, + }); +} + +/** A Math constant, boxed. */ +function constantValue(lowerer: Lowerer, canonical: string, key: string, loc: SrcLoc): IrExpr { + const value = canonical === "Math" ? mathMemberValue(lowerer, key, loc) : null; + if (value === null) + throw new InternalCompilerError( + `builtin namespace '${canonical}.${key}' has no constant value`, + ); + return { kind: "dynFrom", value, type: DYN, loc }; +} + +/** A function member: its function value under its own name, or a function + * that throws the named fence when called. */ +function functionValue(lowerer: Lowerer, canonical: string, key: string, loc: SrcLoc): IrExpr { + const value = + canonical === "Math" + ? mathMemberValue(lowerer, key, loc) + : canonical === "JSON" + ? jsonMemberValue(lowerer, key, loc) + : null; + if (value !== null) + return value.type.kind === "dyn" + ? value + : { kind: "dynFrom", value, fnName: key, type: DYN, loc }; + return fencedMember(lowerer, `${canonical}.${key}`, key, loc); +} + +/** One function per member without a function value: calling it throws the + * SC1090 fence, while `typeof`, identity and the property's presence stay + * as in Node. */ +function fencedMember(lowerer: Lowerer, display: string, key: string, loc: SrcLoc): IrExpr { + const name = `%builtin.fence.${display}`; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const diag = unsupportedDiag( + "SC1090", + loc, + `'${display}' as a function value (call it directly)`, + ); + lowerer.runtimeFences.push(diag); + lowerer.liftedFns.push({ + name, + params: [], + locals: [], + returnType: DYN, + body: [ + { kind: "runtimeFence", code: diag.code, message: `${diag.message} [${diag.code}]`, loc }, + ], + loc, + }); + } + return { + kind: "dynFrom", + value: { kind: "closure", fnName: name, captures: [], type: funcOf([], DYN), loc }, + fnName: key, + type: DYN, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 64f829865..c1cdac334 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -606,8 +606,15 @@ export function lowerJsonProperty( ): IrExpr | null { const member = lowerer.stdlibGlobalMember(expr, "JSON"); if (member === null) return null; + const value = jsonMemberValue(lowerer, member, locOf(expr)); + if (value !== null) return value; + lowerer.unsupported("SC1090", expr, `JSON methods as values (call '${member}' directly)`); +} + +/** The function value of the JSON member `member` (stringify and parse), + * or null for members without one. */ +export function jsonMemberValue(lowerer: Lowerer, member: string, loc: SrcLoc): IrExpr | null { if (member === "stringify") { - const loc = locOf(expr); const name = "%builtin.JSON.stringify"; if (!lowerer.builtinCallableValueFns.has(name)) { lowerer.builtinCallableValueFns.set(name, name); @@ -654,7 +661,6 @@ export function lowerJsonProperty( if (member === "parse") { // JSON.parse as a value: ToString of the text, then the reviver form // (a reviver that is not a function is ignored, as in Node). - const loc = locOf(expr); const name = "%builtin.JSON.parse"; if (!lowerer.builtinCallableValueFns.has(name)) { lowerer.builtinCallableValueFns.set(name, name); @@ -703,5 +709,5 @@ export function lowerJsonProperty( loc, }; } - lowerer.unsupported("SC1090", expr, `JSON methods as values (call '${member}' directly)`); + return null; } diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 4cc0bee1a..1d6c887c1 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -38,6 +38,7 @@ import { planUnionRetag } from "../union-retag.js"; import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js"; import { builtinGlobalValue } from "./property-access.js"; import { holdsEvolvingArray } from "../lower-stmts.js"; +import { builtinNamespaceValue } from "../builtin-namespaces.js"; /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the * contextual type when tsc has one (annotated declarations, arguments, @@ -3262,7 +3263,11 @@ function shorthandBuiltinValue( 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); + const name = ts.symbolName(symbol); + return ( + (js ? builtinNamespaceValue(lowerer, name, locOf(prop.name)) : null) ?? + builtinGlobalValue(name, locOf(prop.name), js) + ); } export function lowerShorthandValue( diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 7b12297a8..5ec81f5ca 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1,5 +1,6 @@ /* 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 { builtinNamespaceValue } from "../builtin-namespaces.js"; import { staticallySelected } from "../static-typeof.js"; import * as ts from "../../ts7/adapter.js"; import { lowerHistogramProperty } from "../builtins/performance.js"; @@ -1105,11 +1106,18 @@ export function lowerPropertyAccessExpr( ) ) { const fnName = jsFuncNameOf(expr.expression); + // A builtin function value whose length is not its adapter's + // parameter count (`Math.max.length` is 2). + const fnLength = + probed.kind === "closure" + ? lowerer.builtinCallableValueLengths.get(probed.fnName) + : undefined; const boxed: IrExpr = { kind: "dynFrom", value: probed, type: DYN, ...(fnName !== null ? { fnName } : {}), + ...(fnLength !== undefined ? { fnLength } : {}), loc, }; const key: IrExpr = { @@ -4218,6 +4226,10 @@ export function builtinGlobalValueAt( loc: SrcLoc, js: boolean, ): IrExpr | null { + // A namespace object models its own properties, so member reads and + // reflection read it like any object. + const namespace = js ? builtinNamespaceValue(lowerer, canonical, loc) : null; + if (namespace !== null) return namespace; if (readsBuiltinGlobalMembers(expr)) return null; const value = builtinGlobalValue(canonical, loc, js); if (value === null) return null; diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index e671a0a08..097ccadbe 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -2553,35 +2553,8 @@ export function lowerMathProperty( const member = lowerer.stdlibGlobalMember(expr, "Math"); if (member === null) return null; const loc = locOf(expr); - const staticValue = own(STATIC_MATH_PROPS, member); - if (staticValue !== undefined) { - return { kind: "numLit", value: staticValue, type: F64, loc }; - } - const native = own(STATIC_MATH_FNS, member); - if (native && (member === "min" || member === "max" || member === "hypot")) - return lowerer.lowerNativeCallableValue( - { - fn: member === "min" ? "math.minArr" : member === "max" ? "math.maxArr" : "math.hypotArr", - params: [arrayOf(F64)], - result: F64, - valueParams: [{ mode: "rest", type: F64 }], - }, - `Math.${member}`, - loc, - ); - if (native && member !== "min" && member !== "max" && member !== "hypot") { - const params = Array.from({ length: native.arity }, () => F64); - return lowerer.lowerNativeCallableValue( - { - fn: native.fn, - params, - result: F64, - valueParams: params.map((type) => ({ mode: "required", type })), - }, - `Math.${member}`, - loc, - ); - } + const value = mathMemberValue(lowerer, member, loc); + if (value !== null) return value; const propType = own(STDLIB_SURFACE.math.props, member); if (propType !== undefined) { lowerer.refuseUnsupportedApi(`'Math.${member}'`, expr); @@ -2595,6 +2568,40 @@ export function lowerMathProperty( return null; // declared-but-untabled members fall through generically } +/** The value of the Math member `member`: a constant, or the interned + * native function value of a Math function. Null for other members. */ +export function mathMemberValue(lowerer: Lowerer, member: string, loc: SrcLoc): IrExpr | null { + const staticValue = own(STATIC_MATH_PROPS, member); + if (staticValue !== undefined) { + return { kind: "numLit", value: staticValue, type: F64, loc }; + } + const native = own(STATIC_MATH_FNS, member); + if (!native) return null; + if (member === "min" || member === "max" || member === "hypot") + return lowerer.lowerNativeCallableValue( + { + fn: member === "min" ? "math.minArr" : member === "max" ? "math.maxArr" : "math.hypotArr", + params: [arrayOf(F64)], + result: F64, + valueParams: [{ mode: "rest", type: F64 }], + valueLength: 2, + }, + `Math.${member}`, + loc, + ); + const params = Array.from({ length: native.arity }, () => F64); + return lowerer.lowerNativeCallableValue( + { + fn: native.fn, + params, + result: F64, + valueParams: params.map((type) => ({ mode: "required", type })), + }, + `Math.${member}`, + loc, + ); +} + export function npmPackageOf(lowerer: Lowerer, type: ts.Type): string | null { const pkg = lowerer.npmPackageOfSymbol(type.getAliasSymbol() ?? type.getSymbol()); if (pkg) return pkg; diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 66ecea602..b83333397 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -26,6 +26,7 @@ import { InternalCompilerError } from "../../errors.js"; * explicit resource management, and blocked-binding poisoning. */ import * as ts from "../ts7/adapter.js"; import { lowerOptionalIndex } from "./undefined-index.js"; +import { builtinNamespaceValue } from "./builtin-namespaces.js"; import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js"; import { requireInitFlagStmt } from "./require-initialization.js"; import { isJsonRequireDecl } from "./json-require.js"; @@ -4248,7 +4249,8 @@ function stdlibGlobalTokenDestructure( value = canonical === "crypto" ? { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc: locOf(el) } - : builtinGlobalValue(canonical, locOf(el), true); + : (builtinNamespaceValue(lowerer, canonical, 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 3c74cfbad..6c95f00a9 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2969,6 +2969,9 @@ export class Lowerer { readonly builtinCallableValueFns = new Map(); /** Each builtin function-value adapter's Node name, for boxed values. */ readonly builtinCallableValueNames = new Map(); + /** Builtin function values whose `length` differs from their adapter's + * parameter count (BuiltinModuleFn.valueLength), by adapter name. */ + readonly builtinCallableValueLengths = new Map(); /** Optional-chain lowering state. While a chain body lowers, the guarded * receiver NODE reads as a chainRecv (typed by the narrowed arm) instead * of re-lowering, its checker type reads non-nullish (typeOf), and the @@ -9719,6 +9722,8 @@ export class Lowerer { // name Node's function carries (`[Function: randomUUID]`). const builtinName = expr.kind === "closure" ? this.builtinCallableValueNames.get(expr.fnName) : undefined; + const builtinLength = + expr.kind === "closure" ? this.builtinCallableValueLengths.get(expr.fnName) : undefined; return { kind: "dynFrom", value: expr, @@ -9726,6 +9731,7 @@ export class Lowerer { loc: expr.loc, ...(liveRef ? { liveRef: true as const } : {}), ...(builtinName !== undefined ? { fnName: builtinName } : {}), + ...(builtinLength !== undefined ? { fnLength: builtinLength } : {}), }; } return expr; @@ -13901,6 +13907,8 @@ export class Lowerer { fnName = keyedHelperName(this, `%builtin.value.${fn.valueFn ?? fn.fn}`, key); this.builtinCallableValueFns.set(key, fnName); this.builtinCallableValueNames.set(fnName, display.slice(display.lastIndexOf(".") + 1)); + if (fn.valueLength !== undefined) + this.builtinCallableValueLengths.set(fnName, fn.valueLength); const params = funcType.params.map((type, index) => ({ localId: `arg.${index}`, name: `arg${index}`, diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 44564f2f1..a77ad2990 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -805,6 +805,9 @@ export interface BuiltinModuleFn { * direct-call row's `fn` (a direct call picks an arity-specific entry * point; the value accepts every arity through one). */ valueFn?: IrLibFn; + /** The function value's `length` when it is not the count of its + * required value parameters (`Math.max`'s 2 over its rest pack). */ + valueLength?: number; } export type BuiltinValueParam = diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 1be0f95cd..cd5b4eb20 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -289,6 +289,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * timer APIs (it pulls in the event loop). The name is a compiler-chosen * literal. */ "dyn.builtinFunction": { argTypes: [STRING], result: DYN }, + "dyn.namespaceNew": { argTypes: [STRING, DYN], result: DYN }, + "dyn.namespaceDefine": { argTypes: [DYN, STRING, DYN, F64, F64], result: VOID }, "date.nativeNew": { argTypes: [DYN], result: DYN }, /** `new Date(year, monthIndex, ...)` in JavaScript: the argument array's * first seven values pass through ToNumber in order (later ones are diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 878088d65..ec95926ac 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -3325,7 +3325,17 @@ export type IrExpr = * from a union) whose Web API contract exposes the reference again, such * as stream chunks and abort reasons; ordinary JSON-shaped values retain * the documented deep-copy boundary. */ - | { kind: "dynFrom"; value: IrExpr; fnName?: string; liveRef?: true; type: IrType; loc: SrcLoc } + | { + kind: "dynFrom"; + value: IrExpr; + fnName?: string; + /** A boxed function's `length` when it differs from its parameter + * count (builtin function values). */ + fnLength?: number; + liveRef?: true; + type: IrType; + loc: SrcLoc; + } /** CALLING a dyn value — `fn(a, b)` where fn is checked-dynamic (an * implicit-any JS binding, a dyn record member, a dynKeyGet result). * Arguments are ALREADY dyn-typed (typed values box through dynFrom at diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 4ad3dcc5c..1495d2e2b 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -309,6 +309,8 @@ const DYN_INVOKE_LIB_FNS: ReadonlySet = new Set([ "dyn.definePrototypeProps", "dyn.functionApply", "dyn.globalConstructor", + "dyn.namespaceDefine", + "dyn.namespaceNew", "dyn.objCreateWithProperties", "dyn.objectTag", "stream.dynNative", diff --git a/packages/compiler/test/namespace-member-fences.test.ts b/packages/compiler/test/namespace-member-fences.test.ts new file mode 100644 index 000000000..c040f35c7 --- /dev/null +++ b/packages/compiler/test/namespace-member-fences.test.ts @@ -0,0 +1,67 @@ +import { spawnSync } 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 { compile } from "../src/index.js"; + +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +// Static member calls (`Math.abs(x)`) do not read the namespace objects, so +// a change to a function member's value fences (SC1090) instead of leaving +// those calls with the original. New properties and attribute-only changes +// behave as in Node (corpus 7188). +test("replacing or deleting a namespace member fences", async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-namespace-fences-")); + try { + const entry = join(dir, "main.js"); + writeFileSync( + entry, + ` +const m = /** @type {any} */ (Math); +const j = /** @type {any} */ (JSON); +function attempt(label, run) { + try { console.log(label, run()); } catch (error) { console.log(label, error.code); } +} +attempt("alias", () => { m.abs = () => 0; return "replaced"; }); +attempt("direct", () => { Math.max = () => 0; return "replaced"; }); +attempt("define", () => Object.defineProperty(m, "min", { value: () => 0 }) === m); +attempt("same", () => Object.defineProperty(m, "min", { value: Math.min, enumerable: true }) === m); +attempt("reflect", () => Reflect.set(j, "parse", () => 0)); +attempt("assign", () => Object.assign(m, { floor: 1 }) === m); +attempt("delete", () => delete m.ceil); +attempt("added", () => { m.extra = 5; return m.extra; }); +console.log(Math.abs(-1), Math.max(1, 2), Math.min(1, 2), JSON.parse("[3]")[0], Math.floor(1.5), Math.ceil(1.5)); +console.log(Object.keys(Math).join(",")); +`, + ); + const result = await compile(entry, { + backend: "llvm", + sanitize, + outDir: dir, + outPath: join(dir, "program"), + }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + if (!result.ok) return; + const child = spawnSync(result.binaryPath, [], { encoding: "utf8" }); + expect(child.stderr).toBe(""); + expect(child.status).toBe(0); + expect(child.stdout).toBe( + [ + "alias SC1090", + "direct SC1090", + "define SC1090", + "same true", + "reflect SC1090", + "assign SC1090", + "delete SC1090", + "added 5", + "1 2 1 3 1 2", + "min,extra", + "", + ].join("\n"), + ); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7186-namespace-object-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7186-namespace-object-values.js.json new file mode 100644 index 000000000..a130b4af6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7186-namespace-object-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7186-namespace-object-values.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7187-namespace-object-reflection.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7187-namespace-object-reflection.js.json new file mode 100644 index 000000000..b2d9bc751 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7187-namespace-object-reflection.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7187-namespace-object-reflection.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7188-namespace-object-extensions.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7188-namespace-object-extensions.js.json new file mode 100644 index 000000000..96cbad8c2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7188-namespace-object-extensions.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7188-namespace-object-extensions.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 669f4aedb..a9f4dc2c6 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -319,6 +319,9 @@ typedef struct { * must not inspect the value: an earlier cycle member may already be freed. * Ordinary mutations need not maintain this collector-only decision. */ bool value_traced : 1; + /* A namespace object's function member (scr_dyn_namespace_define), which + * static member calls bypass: a change to its value fences instead. */ + bool fenced : 1; /* Ordinary object insertion sets all three attributes true. */ bool writable; bool enumerable; @@ -870,6 +873,19 @@ 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); +/* A standard namespace object (Math, JSON, Reflect read as a JavaScript + * value): an ordinary object whose Symbol.toStringTag is `tag` (+1). + * `tag_key` is the Symbol.toStringTag key (borrowed), which the compiler + * passes so this unit needs no symbol entry points. */ +ScrDyn *scr_dyn_namespace_new(const ScrStr *tag, ScrDyn *tag_key); +/* Defines the own data property `key` of a namespace object. `attributes` + * has bits 1 writable, 2 enumerable, 4 configurable; a function value + * takes `length` as its length when it is not negative. */ +void scr_dyn_namespace_define(ScrDyn *object, const ScrStr *key, ScrDyn *value, double attributes, + double length); +/* Marks the own property `key` of `object` as a fenced namespace member + * (ScrDynEntry.fenced). */ +void scr_dyn_fence_member(ScrDyn *object, const ScrStr *key); /* A constant-key read of an Object.prototype member name: takes the * ordinary read's +1 result and answers the inherited member in place of * an undefined one (scr_dyn_invoke.c). */ @@ -905,6 +921,9 @@ ScrDyn *scr_dyn_iterator_result_get(const ScrDyn *result, const char *key, size_ void scr_dyn_install_iterator_symbol(ScrDyn *key); void scr_dyn_install_has_instance_symbol(ScrDyn *key); void scr_dyn_install_async_iterator_symbol(ScrDyn *key); +/* Registered by the symbol unit when Symbol.toStringTag is created; the + * core's Object.prototype.toString reads it. */ +void scr_dyn_install_to_string_tag_symbol(ScrDyn *key); void scr_dyn_install_async_dispose_symbol(ScrDyn *key); /* True when key is Symbol.asyncDispose (registered by the symbol unit when * the program names it); handle symbol_get ops outside that unit use it. */ @@ -1380,6 +1399,9 @@ ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, con /* 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); +/* Sets a freshly boxed function's `length` (a builtin function value whose + * length is not its adapter's parameter count). */ +void scr_dyn_function_set_length(ScrDyn *fn, uint32_t length); 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_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 364017e05..a55f748e5 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -2236,6 +2236,35 @@ ScrDyn *scr_dyn_builtin_function(const ScrStr *name) { return scr_dyn_retain(value); } +/* An own data property of a namespace object. Attribute bits: 1 writable, + * 2 enumerable, 4 configurable. */ +static void dyn_namespace_put(ScrDyn *object, ScrDyn *key, ScrDyn *value, unsigned attributes) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "value", 5, scr_dyn_retain(value)); + scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool((attributes & 1) != 0)); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool((attributes & 2) != 0)); + scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool((attributes & 4) != 0)); + scr_dyn_release(scr_dyn_define_property(object, key, descriptor)); + scr_dyn_release(descriptor); +} + +ScrDyn *scr_dyn_namespace_new(const ScrStr *tag, ScrDyn *tag_key) { + ScrDyn *object = scr_dyn_new_obj(); + ScrDyn *value = scr_dyn_new_str((ScrStr *)tag); + dyn_namespace_put(object, tag_key, value, 4); + scr_dyn_release(value); + return object; +} + +void scr_dyn_namespace_define(ScrDyn *object, const ScrStr *key, ScrDyn *value, double attributes, + double length) { + if (length >= 0 && value->kind == SCR_DYN_FUNC) value->v.fn.arity = (uint32_t)length; + ScrDyn *name = scr_dyn_new_str((ScrStr *)key); + dyn_namespace_put(object, name, value, (unsigned)attributes); + scr_dyn_release(name); + if (length >= 0) scr_dyn_fence_member(object, key); +} + ScrDyn *scr_dyn_array_prototype(void) { static SCR_TL bool initialized; ScrDyn *prototype = scr_dyn_array_prototype_base(); diff --git a/packages/runtime/src/scr_js_object.c b/packages/runtime/src/scr_js_object.c index 63929ab57..29849fea1 100644 --- a/packages/runtime/src/scr_js_object.c +++ b/packages/runtime/src/scr_js_object.c @@ -268,7 +268,7 @@ static bool scr_js_error_synced_key(const ScrStr *key) { static bool scr_js_replace(ScrDyn *receiver, size_t slot, ScrDyn *value) { ScrDynEntry *found = &receiver->v.obj.entries[slot]; - if (found->accessor || !found->writable) return false; + if (found->accessor || !found->writable || found->fenced) return false; scr_dyn_region_store(receiver, value); ScrDyn *old = found->value; found->value = scr_dyn_retain(value); @@ -313,7 +313,7 @@ bool scr_js_store_probe(ScrDyn *receiver, const ScrStr *key, ScrDyn *value, ScrJ size_t slot = scr_js_shape_find(shape, key->data, key->len); if (slot != SIZE_MAX) { const ScrDynEntry *found = &receiver->v.obj.entries[slot]; - if (found->accessor || !found->writable) return false; + if (found->accessor || !found->writable || found->fenced) return false; scr_js_ic_add(ic, stale, (ScrJsIcEntry){shape, shape, NULL, NULL, slot}); return scr_js_replace(receiver, slot, value); } diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index da18a1c0b..a67740f40 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -52,6 +52,14 @@ static bool scr_dyn_error_prototype_fence(const ScrDyn *value) { SCR_TL const ScrHandleStreamHooks *scr_handle_stream_hooks = NULL; +/* Static member calls (`Math.abs(x)`) do not read the namespace objects, + * so replacing or deleting one of their function members would not reach + * those calls: the change throws instead. */ +static void scr_dyn_namespace_member_fence(void) { + static const char message[] = "Replacing or deleting a Math, JSON or Reflect member is not supported"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC1090"); +} + /* 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) { @@ -87,6 +95,7 @@ static void scr_json_oom(void) { static SCR_TL ScrDyn *scr_iterator_symbol; static SCR_TL ScrDyn *scr_async_iterator_symbol; static SCR_TL ScrDyn *scr_to_primitive_symbol; +static SCR_TL ScrDyn *scr_to_string_tag_symbol; static bool scr_dyn_generator(const ScrDyn *value) { return value && value->kind == SCR_DYN_TYPED_REF && (value->v.typed_ref.type_key[0] == 'g' || value->v.typed_ref.type_key[0] == 'a') && @@ -183,6 +192,17 @@ void scr_dyn_install_to_primitive_symbol(ScrDyn *key) { scr_atexit(scr_to_primitive_symbol_cleanup); } +static void scr_to_string_tag_symbol_cleanup(void) { + scr_dyn_release(scr_to_string_tag_symbol); + scr_to_string_tag_symbol = NULL; +} + +void scr_dyn_install_to_string_tag_symbol(ScrDyn *key) { + if (scr_to_string_tag_symbol) return; + scr_to_string_tag_symbol = scr_dyn_retain(key); + scr_atexit(scr_to_string_tag_symbol_cleanup); +} + void scr_dyn_install_async_iterator_symbol(ScrDyn *key) { if (scr_async_iterator_symbol) return; scr_async_iterator_symbol = scr_dyn_retain(key); @@ -1728,6 +1748,11 @@ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn ScrDynEntry *existing = obj->v.obj.index ? scr_dyn_find_entry(obj, key, key_len) : scr_dyn_find_linear_entry(obj, key, key_len); if (existing) { + if (existing->fenced) { + scr_dyn_release(value); + scr_dyn_namespace_member_fence(); + return; + } if (existing->accessor || !existing->writable) { scr_dyn_release(value); static const char message[] = "Cannot assign to read only property"; @@ -1769,6 +1794,7 @@ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn e->getter = NULL; e->setter = NULL; e->accessor = false; + e->fenced = false; e->writable = true; e->enumerable = true; e->configurable = true; @@ -4426,6 +4452,15 @@ ScrDyn *scr_dyn_new_chunk(const ScrBytes *b) { /* A boxed static function value (the compiler's static→dyn converters). * Ownership of the closure MOVES in; sig/name are static literals. */ +void scr_dyn_function_set_length(ScrDyn *fn, uint32_t length) { + if (fn->kind == SCR_DYN_FUNC) fn->v.fn.arity = length; +} + +void scr_dyn_fence_member(ScrDyn *object, const ScrStr *key) { + ScrDynEntry *entry = scr_dyn_entry(object, key); + if (entry) entry->fenced = true; +} + ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name) { if (clo->function_kind & 8) { ScrDyn *original = scr_box_get_ref(clo->caps[0]); @@ -5296,6 +5331,11 @@ void scr_dyn_obj_set_accessor(ScrDyn *obj, const char *key, size_t key_len, ScrD void scr_dyn_obj_set_field(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value) { ScrDynEntry *existing = obj->v.obj.index ? scr_dyn_find_entry(obj, key, key_len) : scr_dyn_find_linear_entry(obj, key, key_len); + if (existing && existing->fenced) { + scr_dyn_release(value); + scr_dyn_namespace_member_fence(); + return; + } if (existing && !existing->accessor) { scr_dyn_region_store(obj, value); ScrDyn *old = existing->value; @@ -7294,6 +7334,13 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); return NULL; } + if (prior && prior->fenced && + (accessor || (value && !scr_dyn_property_same_value(prior->value, value)))) { + scr_dyn_descriptor_fields_drop(fields); + scr_str_release(name); + scr_dyn_namespace_member_fence(); + return NULL; + } if (prior) { if (accessor != prior->accessor) { ScrDyn *old_value = prior->value, *old_getter = prior->getter, *old_setter = prior->setter; @@ -7497,7 +7544,37 @@ void scr_dyn_throw(ScrDyn *d) { scr_dyn_trace_v, scr_dyn_is_object(d)); } +/* Object.prototype.toString's tag from a string-valued Symbol.toStringTag + * data property of an object or its prototypes ("[object Math]"), or NULL + * when there is none. */ +static ScrStr *scr_dyn_own_string_tag(const ScrDyn *d) { + /* The symbol unit installs the key when the program creates it; without + * it no object can carry the property. */ + const ScrDyn *key = scr_to_string_tag_symbol; + if (!key) return NULL; + ScrStr *out = NULL; + for (const ScrDyn *p = d; p; p = p->prototype) { + if (!p->symbol_properties) continue; + ScrDynEntry *entry = scr_dyn_own_symbol_entry(p, key); + if (!entry) continue; + if (!entry->accessor && entry->value && entry->value->kind == SCR_DYN_STR) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "[object "); + scr_jb_write(&b, entry->value->v.str->data, entry->value->v.str->len); + scr_jb_putc(&b, ']'); + out = scr_jb_finish(&b); + } + break; + } + return out; +} + ScrStr *scr_dyn_object_tag(const ScrDyn *d) { + if (d->kind == SCR_DYN_OBJ) { + ScrStr *tag = scr_dyn_own_string_tag(d); + if (tag) return tag; + } if (d->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Symbol.toStringTag lookup"); return NULL; @@ -8739,7 +8816,9 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) { scr_error_release(error); return text; } - return scr_str_new("[object Object]", 15); + // Object.prototype.toString: "[object Object]", or the object's + // Symbol.toStringTag ("[object Math]"). + return scr_dyn_object_tag(d); } if (!m || m->kind != SCR_DYN_FUNC) { scr_dyn_release(m); continue; } /* OrdinaryToPrimitive performs a method call, not a bare function @@ -10477,6 +10556,7 @@ static void scr_dyn_obj_append_shared(ScrDyn *obj, char *key, size_t key_len, Sc e->getter = NULL; e->setter = NULL; e->accessor = false; + e->fenced = false; e->writable = true; e->enumerable = true; e->configurable = true; @@ -11271,6 +11351,10 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) { for (size_t i = 0; i < object->v.obj.len; i++) { ScrDynEntry *entry = &object->v.obj.entries[i]; if (entry->key_len != key->len || memcmp(entry->key, key->data, key->len) != 0) continue; + if (entry->fenced) { + scr_dyn_namespace_member_fence(); + return; + } ScrDynEntry removed = *entry; memmove(entry, entry + 1, (object->v.obj.len - i - 1) * sizeof *entry); object->v.obj.len--; @@ -12866,7 +12950,9 @@ bool scr_dyn_reflect_define(ScrDyn *receiver, ScrDyn *raw_key, ScrDyn *value) { ScrDynEntry *entry = key->kind == SCR_DYN_SYMBOL ? (symbols ? scr_dyn_symbol_entry(symbols, key, true) : NULL) : receiver->kind == SCR_DYN_OBJ ? scr_dyn_entry(receiver, key->v.str) : receiver->kind == SCR_DYN_ARR && receiver->v.arr.properties ? scr_dyn_entry(receiver->v.arr.properties, key->v.str) : NULL; - if (!(entry && (entry->accessor || !entry->writable)) && !(receiver->non_extensible && !scr_dyn_has_own_computed(receiver, key))) { + if (entry && entry->fenced) { + scr_dyn_namespace_member_fence(); + } else if (!(entry && (entry->accessor || !entry->writable)) && !(receiver->non_extensible && !scr_dyn_has_own_computed(receiver, key))) { if (entry) { ScrDyn *old = entry->value; entry->value = scr_dyn_retain(value); diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 647c31dc3..1f7f867dc 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -105,6 +105,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) { scr_dyn_install_to_primitive_symbol(key); scr_dyn_release(key); } + if (name->len == 11 && !memcmp(name->data, "toStringTag", 11)) { + ScrDyn *key = scr_dyn_new_symbol(symbol); + scr_dyn_install_to_string_tag_symbol(key); + scr_dyn_release(key); + } return symbol; } diff --git a/tests/corpus/7186-namespace-object-values.js b/tests/corpus/7186-namespace-object-values.js new file mode 100644 index 000000000..c61d96e11 --- /dev/null +++ b/tests/corpus/7186-namespace-object-values.js @@ -0,0 +1,22 @@ +// Math, JSON and Reflect read as values are objects with Node's own +// properties: typeof, String, `in`, aliasing and member calls through +// the value behave as in Node, and every spelling is the same object. +function describe(value) { + return [typeof value, String(value), Object.prototype.toString.call(value)].join(" "); +} +console.log(describe(Math), "|", describe(JSON), "|", describe(Reflect)); + +const m = Math; +console.log(m.abs(-2), m.max(1, 5, 3), m.PI.toFixed(5), "trunc" in m, m === Math); +console.log(m === globalThis.Math, JSON === globalThis.JSON); +const { Math: fromGlobal } = globalThis; +console.log(fromGlobal === Math, fromGlobal.sqrt(81)); + +const known = new Set([Math, JSON, Reflect]); +console.log(known.has(Math), known.has(globalThis.JSON), known.size); + +const parse = JSON.parse; +console.log(parse("[1,2]").length, JSON.stringify({ a: [1] })); + +console.log(typeof Math === "object" && typeof Reflect === "object"); +console.log(Math.max.length, Math.min.length, Math.hypot.length, Math.abs.length); diff --git a/tests/corpus/7187-namespace-object-reflection.js b/tests/corpus/7187-namespace-object-reflection.js new file mode 100644 index 000000000..d62699dea --- /dev/null +++ b/tests/corpus/7187-namespace-object-reflection.js @@ -0,0 +1,23 @@ +// Reflection over the namespace objects answers Node's property +// attributes: members are own, non-enumerable properties, the Math +// constants are read-only, and Symbol.toStringTag names the namespace. +function show(object, key) { + const d = Object.getOwnPropertyDescriptor(object, key); + if (d === undefined) return `${String(key)}: none`; + const value = typeof d.value === "function" ? `fn/${d.value.length}/${d.value.name}` : String(d.value); + return `${String(key)}: ${value} ${d.writable} ${d.enumerable} ${d.configurable}`; +} +function inspect(object) { + return [ + Object.hasOwn(object, "parse"), + Object.hasOwn(object, "abs"), + Object.keys(object).length, + Object.getOwnPropertyNames(object).length, + ].join(" "); +} +console.log(show(Math, "abs"), "|", show(Math, "PI"), "|", show(Math, "max")); +console.log(show(Math, Symbol.toStringTag), "|", show(JSON, "stringify"), "|", show(Reflect, "apply")); +console.log(inspect(Math), "|", inspect(JSON), "|", inspect(Reflect)); +console.log(Object.getOwnPropertyNames(JSON).join(",")); +console.log(Object.getOwnPropertyNames(Math).slice(0, 5).join(","), Object.getOwnPropertyNames(Math).at(-1)); +console.log(Object.prototype.hasOwnProperty.call(Math, "random"), Object.keys(Reflect).length); diff --git a/tests/corpus/7188-namespace-object-extensions.js b/tests/corpus/7188-namespace-object-extensions.js new file mode 100644 index 000000000..0a33f7155 --- /dev/null +++ b/tests/corpus/7188-namespace-object-extensions.js @@ -0,0 +1,16 @@ +// Namespace objects take new properties and attribute-only redefinitions +// as in Node, and their members keep the values static calls use. +const m = Math; +m.tau = 2 * Math.PI; +console.log(m.tau.toFixed(4), Math.tau === m.tau, "tau" in Math, Object.keys(Math).join(",")); +Object.defineProperty(Math, "hypot", { enumerable: true }); +Object.defineProperty(Math, "trunc", { value: Math.trunc, writable: false }); +const d = Object.getOwnPropertyDescriptor(Math, "trunc"); +console.log(Object.keys(Math).join(","), d.writable, d.enumerable, d.configurable, Math.trunc(-1.5)); +console.log(Math.hypot(3, 4), m.hypot === Math.hypot, Object.getOwnPropertyDescriptor(m, "hypot").enumerable); +const j = JSON; +j.label = "json"; +console.log(j.label, Object.keys(JSON).join(","), Reflect.ownKeys(j).length); +const r = Reflect; +r.extra = [1, 2]; +console.log(Reflect.extra.length, Object.getOwnPropertyNames(Reflect).at(-1), "extra" in r); diff --git a/tests/harness/corpus-costs/7186-namespace-object-values.js.json b/tests/harness/corpus-costs/7186-namespace-object-values.js.json new file mode 100644 index 000000000..b9c097972 --- /dev/null +++ b/tests/harness/corpus-costs/7186-namespace-object-values.js.json @@ -0,0 +1 @@ +{ "plain": 3233, "san": 3781 } diff --git a/tests/harness/corpus-costs/7187-namespace-object-reflection.js.json b/tests/harness/corpus-costs/7187-namespace-object-reflection.js.json new file mode 100644 index 000000000..82f5d5ebf --- /dev/null +++ b/tests/harness/corpus-costs/7187-namespace-object-reflection.js.json @@ -0,0 +1 @@ +{ "plain": 3183, "san": 3882 } diff --git a/tests/harness/corpus-costs/7188-namespace-object-extensions.js.json b/tests/harness/corpus-costs/7188-namespace-object-extensions.js.json new file mode 100644 index 000000000..8d90b4234 --- /dev/null +++ b/tests/harness/corpus-costs/7188-namespace-object-extensions.js.json @@ -0,0 +1 @@ +{ "plain": 3075, "san": 3749 } From bbaa7d7ba40427eb7462551f0dc065280af1c68e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 10:28:44 -0500 Subject: [PATCH 021/273] Pass callbacks through bindings in tests of the array-helper and closure paths --- .../test/callback-pending-emission.test.ts | 19 ++++++++++++------- .../test/runtime-optional-precision.test.ts | 4 +++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/compiler/test/callback-pending-emission.test.ts b/packages/compiler/test/callback-pending-emission.test.ts index 9dcbbf0a1..3b2ed3823 100644 --- a/packages/compiler/test/callback-pending-emission.test.ts +++ b/packages/compiler/test/callback-pending-emission.test.ts @@ -9,7 +9,9 @@ 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. +// entry keeps it; a callback that can throw keeps it on both paths. The +// callbacks are passed through bindings: a closure literal at the call +// would run as a loop with its body copied in (array-pipelines.ts). 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 { @@ -18,13 +20,16 @@ test("known callbacks outside the may-throw set test the pending flag only on th writeFileSync( entry, `const values = [1, 2, 3, 4]; -console.log(values.every((v) => v > 0)); -console.log(values.map((v) => v * 2).join(",")); +const positive = (v: number): boolean => v > 0; +const double = (v: number): number => v * 2; +const stopAtThree = (v: number): boolean => { + if (v > 2) throw new Error("stop at " + v); + return true; +}; +console.log(values.every(positive)); +console.log(values.map(double).join(",")); try { - console.log(values.every((v) => { - if (v > 2) throw new Error("stop at " + v); - return true; - })); + console.log(values.every(stopAtThree)); } catch (error) { console.log("caught", (error as Error).message); } diff --git a/packages/compiler/test/runtime-optional-precision.test.ts b/packages/compiler/test/runtime-optional-precision.test.ts index 7840b0c62..b47cb828d 100644 --- a/packages/compiler/test/runtime-optional-precision.test.ts +++ b/packages/compiler/test/runtime-optional-precision.test.ts @@ -158,7 +158,9 @@ test("an unchanged captured parameter is borrowed and boxed only where a closure const module = await lower(`${ITEM} function heavier(item: Item, limits: number[]): boolean { if (limits.length === 0) return false; - return limits.some((limit) => weigh(item) > limit); + // A closure value: a closure literal at the call would run as a loop. + const over = (limit: number): boolean => weigh(item) > limit; + return limits.some(over); } function replaced(item: Item, other: Item): number { const read = () => item.weight; From 81e2e35e6446973e35a9040e747241c4fe967e37 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 11:09:31 -0500 Subject: [PATCH 022/273] Hold array and record elements whose serialization can call user code JSON.stringify walked a dense native array through its borrowed storage and wrote record fields borrowed from the record. When an element's own serialization can run user code (it reaches an array that may hold foreign elements, whose getters and toJSON run, or a class instance or checked-dynamic value), that code could grow the array, reallocating the storage the walk still read, or replace the field it was writing. In programs that store foreign elements, such arrays now take the general walk, which reads each index afresh and holds its element, and such record fields are held across their write. Programs without foreign elements compile as before. --- packages/compiler/src/backend/llvm/walkers.ts | 65 +++++++++++++++---- .../6068-foreign-elements-through-views.ts | 25 +++++++ 2 files changed, 77 insertions(+), 13 deletions(-) diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts index 77643ca87..ab8c2430c 100644 --- a/packages/compiler/src/backend/llvm/walkers.ts +++ b/packages/compiler/src/backend/llvm/walkers.ts @@ -26,6 +26,7 @@ import { llFieldType, loadStructField, releaseSym, + retainSym, traceAdapter, type ShapeHost, } from "./shapes.js"; @@ -323,9 +324,7 @@ export class LlWalkers { if (i > 0) this.putc(B, "%b", "44"); // ',' if (edgeable(f.type)) this.jbEdgeIdx(B, String(i)); 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)}]`, - ); + this.writeField(B, v, f.type, `[${llvmCommentText(f.name)}]`); }); this.putc(B, "%b", "93"); // ']' if (cyclic) this.jbLeave(B); @@ -357,9 +356,7 @@ export class LlWalkers { 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)}`, - ); + this.writeField(B, v, f.type, `${llvmCommentText(f.name)}`); }); } else { const first = B.slot(); @@ -394,9 +391,7 @@ export class LlWalkers { 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)}`, - ); + this.writeField(B, v, f.type, `${llvmCommentText(f.name)}`); if (skip !== null) { B.br(skip); B.startBlock(skip); @@ -518,9 +513,7 @@ export class LlWalkers { B.startBlock(lKey); this.puts(B, "%b", jsonObjectKeyLabel(fieldName)); if (cyclic && traceAdapter(this.host, f.type) !== null) this.jbEdgeProp(B, fieldName); - B.line( - `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(fieldName)}`, - ); + this.writeField(B, v, f.type, `${llvmCommentText(fieldName)}`); B.terminate("ret i1 true"); }); B.startBlock(lSkip); @@ -676,6 +669,47 @@ export class LlWalkers { B.line(`call void @scr_arr_release(ptr ${ks})`); } + /** Serializing a value of `t` can call user code that changes the + * arrays or records holding it: it reaches an array that can hold + * foreign elements (their toJSON and getters), or a value whose own + * serialization may (a class instance, a checked-dynamic value), in a + * program that has foreign elements at all. */ + private serializationMayCallUserCode(t: IrType, seen = new Set()): boolean { + const host = this.host; + if (!host.mayHoldForeign({ kind: "f64" }) && !host.mayHoldForeign({ kind: "string" })) + return false; + const key = typeKey(t); + if (seen.has(key)) return false; + seen.add(key); + switch (t.kind) { + case "dyn": + case "object": + return true; + case "array": + return host.mayHoldForeign(t.elem) || this.serializationMayCallUserCode(t.elem, seen); + case "record": + return (host.recordsById.get(t.shapeId)?.fields ?? []).some((field) => + this.serializationMayCallUserCode(field.type, seen), + ); + case "union": + return (host.unionsById.get(t.unionId)?.arms ?? []).some((arm) => + this.serializationMayCallUserCode(arm, seen), + ); + default: + return false; + } + } + + /** Write a field value loaded borrowed from its record. When its + * serialization can call user code that replaces the field, the value + * is held across the write. */ + private writeField(B: BlockBuilder, v: string, t: IrType, comment: string): void { + const held = isRefCounted(t) && this.serializationMayCallUserCode(t); + if (held) B.line(`call ptr ${retainSym(this.host, t)}(ptr ${v})`); + B.line(`call void @${this.jsonWriteHelper(t)}(ptr %b, ${this.valTy(t)} ${v}) ; ${comment}`); + if (held) B.line(`call void ${releaseSym(this.host, t)}(ptr ${v})`); + } + private emitArrayWriter(B: BlockBuilder, elem: IrType, cyclic: boolean): void { const host = this.host; const w = this.jsonWriteHelper(elem); @@ -685,7 +719,12 @@ export class LlWalkers { if (cyclic) this.jbEnter(B, true); this.putc(B, "%b", "91"); // '[' const finish = B.newLabel("jwa.end"); - if (elem.kind !== "bool") this.emitDenseArrayElements(B, elem, w, cyclic, finish); + // The dense walk borrows the storage and each element. When an + // element's serialization can call user code, which can grow or + // shrink the array or replace the element, the general walk below + // reads each index afresh and holds its element instead. + if (elem.kind !== "bool" && !this.serializationMayCallUserCode(elem)) + this.emitDenseArrayElements(B, elem, w, cyclic, finish); const len = B.tmp(); B.line(`${len} = call double @scr_arr_len(ptr %v)`); B.countedLoop(len, (i) => { diff --git a/tests/corpus/6068-foreign-elements-through-views.ts b/tests/corpus/6068-foreign-elements-through-views.ts index 76997c624..6b1715e01 100644 --- a/tests/corpus/6068-foreign-elements-through-views.ts +++ b/tests/corpus/6068-foreign-elements-through-views.ts @@ -89,3 +89,28 @@ const cut: string[] = ["a", "b"]; const cutView: unknown[] = cut; cutView.splice(1, 0, { toString: () => ((cut.length = 0), "x") }); console.log(JSON.stringify(cut.join("-")), cut.length); + +// JSON of elements whose getters change the array or record holding them. +const fillers: number[][] = []; +const rows: number[][] = [[1], [2]]; +const rowView: unknown[] = rows[0]!; +rowView.push({ + get grown() { + for (let i = 0; i < 64; i++) rows.push([i]); + rows.length = 3; + // Reuse the memory the old storage held. + for (let i = 0; i < 64; i++) fillers.push([i, i]); + return true; + }, +}); +console.log(JSON.stringify(rows), rows.length); +const holder = { values: [5] as number[], name: "h" }; +const holderView: unknown[] = holder.values; +holderView.push({ + get replaced() { + holder.values = [9]; + for (let i = 0; i < 64; i++) fillers.push([i]); + return true; + }, +}); +console.log(JSON.stringify(holder), JSON.stringify(holder)); From 53115c7e50cc89220cd223daf18638e6f2a4ae21 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 11:15:27 -0500 Subject: [PATCH 023/273] Merge main and fix the Node island report --- .../compiler/src/diagnostics/diagnostic.ts | 15 ++++ .../compiler/src/node-islands/report.test.ts | 78 ++++++++++++++++ packages/compiler/src/node-islands/report.ts | 89 +++++++++++++------ packages/compiler/src/project-config.test.ts | 9 ++ packages/compiler/src/project-config.ts | 4 +- 5 files changed, 167 insertions(+), 28 deletions(-) diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index bc5cc4918..bb5efba31 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -249,6 +249,21 @@ export function nodeIslandsOffDiag(pkg: string, why: string, loc: SrcLoc): ScrDi }; } +/** SC8002 — a user module is marked to run in a Node island while + * Node islands are off. */ +export function nodeIslandsOffModuleDiag( + file: string, + source: "config" | "cli", + loc: SrcLoc, +): ScrDiagnostic { + return { + code: "SC8002", + message: `user module '${file}' is marked to run in Node (${source === "cli" ? "--node-island" : "scriptc.nodeIslands.modules"}) and Node islands are off`, + loc, + hint: "allow Node islands (--node-islands=auto or explicit), or remove the marking", + }; +} + /** SC8003 — a package marked "native" is outside the native subset. */ export function nativePackageDiag(pkg: string, why: string, loc: SrcLoc): ScrDiagnostic { return { diff --git a/packages/compiler/src/node-islands/report.test.ts b/packages/compiler/src/node-islands/report.test.ts index a11d9fb8b..d4221d31f 100644 --- a/packages/compiler/src/node-islands/report.test.ts +++ b/packages/compiler/src/node-islands/report.test.ts @@ -199,3 +199,81 @@ test("globMatch", () => { expect(globMatch("/a/?.js", "/a/x.js")).toBe(true); expect(globMatch("/a/x.js", "/a/x.js")).toBe(true); }); + +describe("reachability", () => { + const userEdge = (fromFile: string, to: string): PackageEdge => ({ + ...edge(null, to), + fromFile, + loc: { file: fromFile, start: 0, end: 1 }, + }); + const codeFromString = (pkg: string) => + mod(pkg, `${NM}/${pkg}/index.js`, [ + marker(`${NM}/${pkg}/index.js`, "R1.1", "code-from-string"), + ]); + const build = (scan: Omit, options: NodeIslandOptions = {}) => + buildNodeIslandReport({ + entry: "/app/main.ts", + scan: { ...scan, refusals: [] }, + npm: [], + diagnostics: [], + fences: [], + policy: nodeIslandPolicy(emptyProjectConfig(), options), + }); + + test("a marked user module owns the packages only it imports", () => { + const r = build( + { + modules: [ + mod(null, "/app/main.ts"), + mod(null, "/app/plugin.ts"), + mod("x", `${NM}/x/index.js`), + mod("y", `${NM}/y/index.js`), + ], + edges: [userEdge("/app/plugin.ts", "x"), userEdge("/app/plugin.ts", "y"), edge(null, "y")], + }, + { islands: ["/app/plugin.ts"] }, + ); + expect(r.packages.map((p) => [p.package, p.status, p.islands])).toEqual([ + ["x", "inside", ["/app/plugin.ts"]], + ["y", "shared", ["/app/plugin.ts"]], + ]); + }); + + test("an island imported by a native package lists that import site", () => { + const r = build({ + modules: [mod(null, "/app/main.ts"), mod("n", `${NM}/n/index.js`), codeFromString("i")], + edges: [edge(null, "n"), edge("n", "i")], + }); + expect(r.islands.map((i) => [i.package, i.importedFrom.map((l) => l.file)])).toEqual([ + ["i", [`${NM}/n/index.js`]], + ]); + }); + + test("the dependencies of a shared package are shared with the island", () => { + const r = build({ + modules: [ + mod(null, "/app/main.ts"), + codeFromString("a"), + mod("s", `${NM}/s/index.js`), + mod("d", `${NM}/d/index.js`), + ], + edges: [edge(null, "a"), edge(null, "s"), edge("a", "s"), edge("s", "d")], + }); + expect(r.packages.map((p) => [p.package, p.status, p.islands])).toEqual([ + ["d", "shared", ["a"]], + ["s", "shared", ["a"]], + ]); + }); + + test("with islands off, a marked user module is SC8002 and keeps the user JavaScript rules", () => { + const file = "/app/legacy.js"; + const r = build( + { modules: [mod(null, file, [marker(file, "R2.1", "constructor-function")])], edges: [] }, + { mode: "off", islands: [file], strictUserJs: true }, + ); + expect(r.userModules).toEqual([]); + expect(r.violations.map((v) => v.code)).toEqual(["SC8002"]); + expect(r.violations[0]!.message).toContain(`user module '${file}'`); + expect(r.userJs.map((d) => d.code)).toEqual(["SC8101"]); + }); +}); diff --git a/packages/compiler/src/node-islands/report.ts b/packages/compiler/src/node-islands/report.ts index a6898e855..c82d0f664 100644 --- a/packages/compiler/src/node-islands/report.ts +++ b/packages/compiler/src/node-islands/report.ts @@ -14,6 +14,7 @@ import { nativePackageDiag, nodeIslandsOffDiag, + nodeIslandsOffModuleDiag, userJsObjectModelDiag, type ScrDiagnostic, } from "../diagnostics/diagnostic.js"; @@ -67,7 +68,9 @@ interface PackageState { dir: string | null; reasons: NodeIslandReason[]; importers: Set; - nativeSites: SrcLoc[]; + /** Every import site, with its importer ("" native user code, + * "user:" a marked user module, else a package key). */ + sites: { from: string; loc: SrcLoc }[]; island: boolean; decision: IslandDecision; carriedBy: string | null; @@ -88,7 +91,7 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR dir, reasons: [], importers: new Set(), - nativeSites: [], + sites: [], island: false, decision: "auto", carriedBy: null, @@ -113,16 +116,22 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR const s = state(m.packageName, m.packageDir); for (const marker of m.markers) addReason(s, classifyMarker(marker)); } - // Import edges. + // Import edges. A marked user module is an island owner of its own + // (unless islands are off, where it is a violation and stays native). + const USER = "user:"; + const userIsland = (file: string): boolean => + policy.mode !== "off" && isMarkedUserModule(policy, file) !== null; + const imports = new Map>(); for (const e of input.scan.edges) { if (e.shim || ignored(e.to)) continue; const s = state(e.to, e.toDir); - if (e.fromDir === null) { - s.importers.add(""); - s.nativeSites.push(e.loc); - } else if (e.fromDir !== s.key) { - s.importers.add(e.fromDir); - } + const from = e.fromDir ?? (userIsland(e.fromFile) ? USER + e.fromFile : ""); + if (from === s.key) continue; + s.importers.add(from); + s.sites.push({ from, loc: e.loc }); + let out = imports.get(from); + if (out === undefined) imports.set(from, (out = new Set())); + out.add(s.key); } // Lowering refusals and fences inside npm modules. const addDiagnostics = (diags: readonly ScrDiagnostic[], source: NodeIslandReasonSource) => { @@ -161,7 +170,7 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR const forcedNode = policy.forcedNode.get(s.name); const forcedNative = policy.forcedNative.get(s.name); const outside = s.reasons.length > 0; - const loc = s.nativeSites[0] ?? fallbackLoc; + const loc = (s.sites.find((x) => x.from === "") ?? s.sites[0])?.loc ?? fallbackLoc; if (forcedNative !== undefined) { if (outside) { s.violation = true; @@ -197,6 +206,16 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR s.decision = "auto"; } + // The island that owns an importer: a marked user module, an island + // package, or the island a carried package runs inside; null for native. + const ownerOf = (from: string): string | null => { + if (from.startsWith(USER)) return from; + const importer = states.get(from); + return importer === undefined ? null : importer.island ? importer.key : importer.carriedBy; + }; + const ownerName = (owner: string): string => + owner.startsWith(USER) ? owner.slice(USER.length) : states.get(owner)!.name; + // Closure: packages imported only by islands (or by packages inside // them) run inside the island. for (let changed = true; changed;) { @@ -207,9 +226,7 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR let carrier: string | null = null; let all = true; for (const from of s.importers) { - const importer = states.get(from); - const owner = - importer === undefined ? null : importer.island ? importer.key : importer.carriedBy; + const owner = ownerOf(from); if (owner === null) { all = false; break; @@ -223,6 +240,31 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR } } + // Transitive reach: every non-island package an island loads, through + // carried and shared packages alike (an island package is a boundary). + const reach = new Map>(); + const owners = [...imports.keys()].filter( + (k) => k.startsWith(USER) || states.get(k)?.island === true, + ); + for (const owner of owners) { + const name = ownerName(owner); + const queue = [...(imports.get(owner) ?? new Set())]; + const seen = new Set(queue); + while (queue.length > 0) { + const key = queue.pop()!; + if (states.get(key)?.island === true) continue; + let names = reach.get(key); + if (names === undefined) reach.set(key, (names = new Set())); + names.add(name); + for (const next of imports.get(key) ?? new Set()) { + if (!seen.has(next)) { + seen.add(next); + queue.push(next); + } + } + } + } + // Entries. const islands: NodeIslandEntry[] = []; const packages: NativePackage[] = []; @@ -248,7 +290,7 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR reasons: s.reasons, modules: modules.size, carries, - importedFrom: s.nativeSites, + importedFrom: s.sites.filter((x) => ownerOf(x.from) === null).map((x) => x.loc), evaluation: "eager", }); continue; @@ -257,23 +299,13 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR // A package that resolves to no installed JavaScript never loads (an // optional dependency that is absent): nothing to compile or island. if (s.dir === null && s.reasons.length === 0) continue; - const reachingIslands: string[] = []; - for (const from of s.importers) { - const importer = states.get(from); - if (importer === undefined) continue; - const owner = importer.island ? importer.key : importer.carriedBy; - if (owner !== null) { - const name = states.get(owner)!.name; - if (!reachingIslands.includes(name)) reachingIslands.push(name); - } - } - reachingIslands.sort(); + const reachingIslands = [...(reach.get(s.key) ?? new Set())].sort(); packages.push({ package: s.name, version, path: s.dir, status: s.carriedBy !== null ? "inside" : reachingIslands.length > 0 ? "shared" : "native", - islands: s.carriedBy !== null ? [states.get(s.carriedBy)!.name] : reachingIslands, + islands: s.carriedBy !== null ? [ownerName(s.carriedBy)] : reachingIslands, }); } islands.sort( @@ -292,10 +324,13 @@ export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandR for (const m of input.scan.modules) { if (m.origin !== "user") continue; const marked = isMarkedUserModule(policy, m.file); - if (marked !== null) { + if (marked !== null && policy.mode !== "off") { userModules.push({ file: m.file, source: marked }); continue; } + if (marked !== null) { + violations.push(nodeIslandsOffModuleDiag(m.file, marked, { file: m.file, start: 0, end: 0 })); + } if (!policy.strictUserJs) continue; for (const marker of m.markers) { const code = ruleInfo(marker.rule).userJsCode; diff --git a/packages/compiler/src/project-config.test.ts b/packages/compiler/src/project-config.test.ts index b9750f24e..ce475a4ae 100644 --- a/packages/compiler/src/project-config.test.ts +++ b/packages/compiler/src/project-config.test.ts @@ -124,3 +124,12 @@ test("a non-object packages or nodeIslands value is SC8004", () => { // Other keys of the scriptc object belong to other readers. expect(config({ somethingElse: 1 }).diagnostics).toEqual([]); }); + +test("package setting keys named like Object.prototype members are unknown settings", () => { + const c = config({ packages: { debug: { constructor: "node", toString: "node" } } }); + expect(c.diagnostics.map((d) => [d.code, d.message])).toEqual([ + ["SC8004", "package.json 'scriptc.packages.debug.constructor' is not a known package setting"], + ["SC8004", "package.json 'scriptc.packages.debug.toString' is not a known package setting"], + ]); + expect(c.packages.has("debug")).toBe(false); +}); diff --git a/packages/compiler/src/project-config.ts b/packages/compiler/src/project-config.ts index cc13a868d..bc68e6476 100644 --- a/packages/compiler/src/project-config.ts +++ b/packages/compiler/src/project-config.ts @@ -201,7 +201,9 @@ export function parseProjectConfig(manifest: string, text: string): ProjectConfi } let valid = true; for (const key of Object.keys(value)) { - const allowed = PACKAGE_OBJECT_KEYS[key]; + const allowed = Object.hasOwn(PACKAGE_OBJECT_KEYS, key) + ? PACKAGE_OBJECT_KEYS[key] + : undefined; if (allowed === undefined) { problem( ["packages", name, key], From d10ddff54807cee1b8baef43c7fc4c4004c88a3c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 11:36:23 -0500 Subject: [PATCH 024/273] Compile createRequire probes for optional addons as deferred refusals Packages such as pi-tui look for their prebuilt Node-API addons at run time: a createRequire require call with a computed path, and the loader's resolve passed as a value, each inside a try block whose catch takes a fallback. Preflight refused these packages. Preflight now classifies each use of a createRequire loader. Inside the try block of a try statement in the same function, a call with a computed specifier or another argument shape, and resolve or resolve.paths read as a value, no longer refuse the package. Outside a try block they keep it refused, with a reason that names the computed specifier. The loader itself as a value, writes and optional calls stay refused. In JavaScript source, such a call evaluates its arguments and then throws a runtime fence, and the resolve value is a function that throws one when called, so `(options.resolve ?? require.resolve)(id)` still calls the option. Both are listed in coverage as deferred to runtime. TypeScript source keeps the compile-time refusals. Refs #1777 --- docs/content/limitations/native-addons.mdx | 6 ++ .../lowering/builtins/module-resolution.ts | 89 +++++++++++++++++-- .../lowering/expressions/property-access.ts | 22 +++++ packages/compiler/src/frontend/program.ts | 76 ++++++++++++---- tests/fixtures/npm-static/addon-probe-cli.ts | 5 ++ .../node_modules/addon-probe/index.d.ts | 2 + .../node_modules/addon-probe/index.js | 28 ++++++ .../node_modules/addon-probe/package.json | 7 ++ .../__snapshots__/create-require.ts.txt | 4 +- tests/harness/npm-static.test.ts | 59 ++++++++++++ 10 files changed, 275 insertions(+), 23 deletions(-) create mode 100644 tests/fixtures/npm-static/addon-probe-cli.ts create mode 100644 tests/fixtures/npm-static/node_modules/addon-probe/index.d.ts create mode 100644 tests/fixtures/npm-static/node_modules/addon-probe/index.js create mode 100644 tests/fixtures/npm-static/node_modules/addon-probe/package.json diff --git a/docs/content/limitations/native-addons.mdx b/docs/content/limitations/native-addons.mdx index 469d1cdd2..5fff3d7e8 100644 --- a/docs/content/limitations/native-addons.mdx +++ b/docs/content/limitations/native-addons.mdx @@ -2,5 +2,11 @@ Node-API (N-API) and V8 `.node` addons depend on Node.js itself, so compiled pro Direct `createRequire` calls to local addons are rejected at compile time. A package that requires an addon needs a JavaScript implementation that compiles, or a replacement through native FFI. +A package that looks for an optional addon at run time compiles with `--npm-static` when the lookup is inside a `try` block. +There, a `createRequire` require call with a computed specifier throws an error where it runs, and its `require.resolve` read as a value is a function that throws when called. +Coverage lists both as deferred to runtime. +The package's `catch` clause then runs as it does in Node.js when the addon is missing. +Outside a `try` block, the same uses keep the package refused. + Use [Native FFI](/docs/ffi#replacing-a-node-api-addon) to call a plain C ABI function linked from an object or static archive. The guide includes a replacement for a small Node-API helper. diff --git a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts index 6b2e9e589..42ecfdd50 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts @@ -2,7 +2,7 @@ import { nodeThrowExpr, boolLit, strLit, varRef } from "../../../ir/build.js"; import { dirname, isAbsolute, resolve } from "node:path"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, PoisonError } from "../lowerer.js"; -import { canonicalBuiltinModule, isNodeEsmFile, locOf } from "../../program.js"; +import { canonicalBuiltinModule, isJsSourceFile, isNodeEsmFile, locOf } from "../../program.js"; import { isRelativeSpecifier } from "../../workspace-registry.js"; import { probeNodeRequireRefusal } from "../../npm.js"; import { isNpmStaticPackage } from "../../npm-static.js"; @@ -17,6 +17,7 @@ import { import { invalidJsonModuleDiag, nativeAddonDiag, + noLoweringDiag, unsupportedPackageImportDiag, } from "../../../diagnostics/diagnostic.js"; import { NODE_BUILTIN_MODULES_V24 } from "../surfaces.js"; @@ -24,12 +25,14 @@ import { BOOL, DYN, type IrExpr, + type IrStmt, type IrType, NULL_T, STRING, type SrcLoc, UNDEFINED_T, arrayOf, + funcOf, } from "../../../ir/ir.js"; import { lowerFfiMemoryModule } from "../native-ffi.js"; import { @@ -38,6 +41,7 @@ import { createRequireCalleeFileOf, } from "./module-bindings.js"; import { stripTypeCasts } from "./arguments.js"; +import { keyedHelperName } from "../stable-names.js"; /** node:module's two compiler-only calls. isBuiltin compares one evaluated * string against the pinned Node 24 list (bare builtins also accept their @@ -134,6 +138,61 @@ export function lowerNodeModuleCall( }; } +/** The fence a createRequire use with no build-time lowering throws in + * JavaScript source, where preflight admits it inside a try block + * (createRequireUse7 in program.ts). The diagnostic joins the runtime-fence + * ledger, so coverage lists the use as deferred to runtime. */ +function createRequireFence( + lowerer: Lowerer, + surface: string, + node: ts.Node, + hint: string, +): IrStmt & { kind: "runtimeFence" } { + const loc = locOf(node); + const d = noLoweringDiag(surface, loc, hint); + lowerer.runtimeFences.push(d); + const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1; + return { + kind: "runtimeFence", + code: d.code, + message: `${d.message} [${d.code} at ${loc.file}:${line}]`, + loc, + }; +} + +/** A createRequire loader's `resolve` (or `resolve.paths`) read as a value + * in JavaScript source: a function that throws the fence when called, so + * `(options.resolve ?? require.resolve)(id)` still calls the option when + * one is given. */ +export function createRequireResolveFenceValue( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, + loc: SrcLoc, +): IrExpr { + const surface = "createRequire's require.resolve as a value"; + const hint = + 'call it with a string literal (require.resolve("")), which resolves at build time'; + if (!isJsSourceFile(ts.getSourceFileOfNode(access))) lowerer.noLowering(surface, access, hint); + const name = keyedHelperName(lowerer, "%createRequire.fence", `${loc.file}:${loc.start}`); + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + lowerer.liftedFns.push({ + name, + params: [], + locals: [], + returnType: DYN, + body: [createRequireFence(lowerer, surface, access, hint)], + loc, + }); + } + return { + kind: "dynFrom", + value: { kind: "closure", fnName: name, captures: [], type: funcOf([], DYN), loc }, + fnName: access.name.text, + type: DYN, + loc, + }; +} + export function lowerCreateRequireCall( lowerer: Lowerer, call: ts.CallExpression, @@ -142,11 +201,31 @@ export function lowerCreateRequireCall( const cr = createRequireSpecOf(lowerer, call); if (cr === null) return null; if (cr.spec === null) { - lowerer.noLowering( - "createRequire's require with this argument shape", - call, - 'the compiled module graph is fixed at build time — the one lowered form is require("")', + const surface = "createRequire's require with a computed specifier or this argument shape"; + const hint = + 'the compiled module graph is fixed at build time — the one lowered form is require(""); native addons are not loaded'; + if (!isJsSourceFile(ts.getSourceFileOfNode(call))) lowerer.noLowering(surface, call, hint); + // Node evaluates the arguments, then the require throws. + const stmts: IrStmt[] = call.arguments.nodes.map((arg) => ({ + kind: "exprStmt", + expr: lowerer.lowerExpr(arg), + loc: locOf(arg), + })); + const type = lowerer.irTypeOf(call); + const slot = lowerer.declareHiddenLocal("%never", type); + slot.mutable = true; + stmts.push( + { kind: "varDecl", localId: slot.id, init: null, loc }, + createRequireFence(lowerer, surface, call, hint), ); + return { + kind: "seqExpr", + stmts, + result: varRef(slot.id, type, loc), + diverges: true, + type, + loc, + }; } const spec = cr.spec; if (spec === "node:ffi") { diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 7b12297a8..539f27f80 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -144,6 +144,8 @@ import { nullishGenericBindingUnitOf } from "../binding-analysis.js"; import { blockedBindingUseDiag } from "../../../diagnostics/diagnostic.js"; import { npmStaticPackageOfPath } from "../../npm-static.js"; import { importMetaField, isImportMeta } from "../import-meta.js"; +import { createRequireCalleeFileOf } from "../builtins/module-bindings.js"; +import { createRequireResolveFenceValue } from "../builtins/module-resolution.js"; import { builtinPrototypeMethod, lowerArrayIsArrayValue, @@ -283,6 +285,21 @@ const DGRAM_METHOD_VALUES = new Set([ "unref", ]); +function isCreateRequireResolveValue(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean { + let resolve: ts.Expression = expr; + if ( + expr.name.text === "paths" && + ts.isPropertyAccessExpression(expr.expression) && + !expr.expression.questionDotToken + ) + resolve = expr.expression; + return ( + ts.isPropertyAccessExpression(resolve) && + resolve.name.text === "resolve" && + createRequireCalleeFileOf(lowerer, resolve.expression) !== null + ); +} + export function lowerPropertyAccessExpr( lowerer: Lowerer, expr: ts.PropertyAccessExpression, @@ -1266,6 +1283,11 @@ export function lowerPropertyAccessExpr( return declared && declared.kind !== "void" ? lowerer.coerceToExpected(read, declared) : read; } } + // A createRequire loader's resolve read as a value (passed on, or + // picked by `??`): resolution happens at build time, so only calls + // with a literal specifier lower. + if (isCreateRequireResolveValue(lowerer, expr)) + return createRequireResolveFenceValue(lowerer, expr, loc); // The lib fence's PROPERTY chokepoint: a stdlib-declared member that // no lowering above claimed ([1,2].entries, Math.SQRT2, Promise.all, // re.exec as a value, ...) reports SC2020 here. diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 6d8ffe4c0..54dd83714 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -2320,12 +2320,27 @@ export function isCreateRequireBinding7(program: ts.Program, callee: ts.Identifi ); } -/** Only literal calls and literal resolution queries can erase the loader - * value. In particular, assignments, exported aliases, shorthand properties, - * optional calls, and passing the loader to user code cannot pass this test. */ -function staticCreateRequireUse7(expr: ts.Expression): boolean { +/** How a use of a createRequire loader lowers: + * - "static": a literal call or a literal resolution query, which erases + * the loader value. + * - "deferred": a call with a computed specifier or another argument shape, + * or `resolve` / `resolve.paths` read as a value, inside the try block + * of a try statement in the same function. The call throws a runtime + * fence, and the resolve value is a function that throws one when called + * (module-resolution.ts), so the statement's catch clause handles them + * (packages that look for optional native addons). + * - "computed": the same shapes outside a try block. + * - "escaping": the loader itself as a value, writes, optional calls, and + * other members. */ +const COMPUTED_CREATE_REQUIRE_REASON = + "its createRequire result is used with a computed specifier, or its resolve as a value, outside a try block (static requires need literal specifiers)"; + +type CreateRequireUse7 = "static" | "deferred" | "computed" | "escaping"; + +function createRequireUse7(expr: ts.Expression): CreateRequireUse7 { let use: ts.Node = expr; while (ts.isParenthesizedExpression(use.parent)) use = use.parent; + let member = false; if ( ts.isPropertyAccessExpression(use.parent) && use.parent.expression === use && @@ -2333,6 +2348,7 @@ function staticCreateRequireUse7(expr: ts.Expression): boolean { !use.parent.questionDotToken ) { use = use.parent; + member = true; if ( ts.isPropertyAccessExpression(use.parent) && use.parent.expression === use && @@ -2342,15 +2358,42 @@ function staticCreateRequireUse7(expr: ts.Expression): boolean { use = use.parent; } const call = use.parent; + if (ts.isCallExpression(call) && call.expression === use) { + if (call.questionDotToken) return "escaping"; + if (call.arguments.nodes.length === 1 && ts.isStringLiteralLike(call.arguments.nodes[0]!)) + return "static"; + } else if (!member || isAssignmentTarget7(use)) { + return "escaping"; + } + return insideTryBlock7(use) ? "deferred" : "computed"; +} + +function isAssignmentTarget7(node: ts.Node): boolean { + const parent = node.parent; return ( - ts.isCallExpression(call) && - call.expression === use && - !call.questionDotToken && - call.arguments.nodes.length === 1 && - ts.isStringLiteralLike(call.arguments.nodes[0]!) + ts.isBinaryExpression(parent) && + parent.left === node && + parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment ); } +/** True when `node` is inside the try block of a try statement in the same + * function (a catch clause or finally block does not count). A class field + * initializer runs when the class is instantiated, so it is a boundary too. */ +function insideTryBlock7(node: ts.Node): boolean { + for (let child = node, parent = node.parent; parent; child = parent, parent = parent.parent) { + if (ts.isTryStatement(parent) && parent.tryBlock === child) return true; + if ( + ts.isFunctionLike(parent) || + ts.isClassStaticBlockDeclaration(parent) || + ts.isPropertyDeclaration(parent) + ) + return false; + } + return false; +} + function stableCreateRequireBindingReason7( program: ts.Program, decl: ts.VariableDeclaration, @@ -2383,9 +2426,10 @@ function stableCreateRequireBindingReason7( : checker.getSymbolAtLocation(ref); if (target && (target.flags & ts.SymbolFlags.Alias) !== 0) target = checker.getAliasedSymbol(target); - if (target === symbol && !staticCreateRequireUse7(ref)) { - return "its createRequire binding is reassigned, escapes, or uses a computed specifier"; - } + if (target !== symbol) continue; + const use = createRequireUse7(ref); + if (use === "computed") return COMPUTED_CREATE_REQUIRE_REASON; + if (use === "escaping") return "its createRequire binding is reassigned or escapes"; } const preceding = stmt.declarationList.declarations.nodes.slice( 0, @@ -2448,10 +2492,10 @@ function npmStaticModuleImportReason7( checker.prefetchSymbolNodesExact(identifierOccurrences7(sf, specifier.name.text)); let reason: string | null = null; const checkUse = (expr: ts.Expression): void => { - if (!staticCreateRequireUse7(expr)) { - reason = - "its createRequire result escapes or is used with a computed specifier (static requires need literal specifiers)"; - } + const use = createRequireUse7(expr); + if (use === "computed") reason = COMPUTED_CREATE_REQUIRE_REASON; + else if (use === "escaping") + reason = "its createRequire result escapes (static requires need literal specifiers)"; }; ts.walkPreorder(sf, (node) => { if ( diff --git a/tests/fixtures/npm-static/addon-probe-cli.ts b/tests/fixtures/npm-static/addon-probe-cli.ts new file mode 100644 index 000000000..4515fb5bf --- /dev/null +++ b/tests/fixtures/npm-static/addon-probe-cli.ts @@ -0,0 +1,5 @@ +import { probe, resolveHelper } from "addon-probe"; +import { join } from "node:path"; + +console.log(join("a", "b"), [probe("clipboard"), probe("keys"), resolveHelper("addon-probe-helper")].join(",")); +console.log(resolveHelper("other", (id) => `custom:${id}`)); diff --git a/tests/fixtures/npm-static/node_modules/addon-probe/index.d.ts b/tests/fixtures/npm-static/node_modules/addon-probe/index.d.ts new file mode 100644 index 000000000..cfacc4764 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/addon-probe/index.d.ts @@ -0,0 +1,2 @@ +export declare function probe(name: string): string; +export declare function resolveHelper(name: string, resolvePackage?: (id: string) => string): string; diff --git a/tests/fixtures/npm-static/node_modules/addon-probe/index.js b/tests/fixtures/npm-static/node_modules/addon-probe/index.js new file mode 100644 index 000000000..e65b1ad0e --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/addon-probe/index.js @@ -0,0 +1,28 @@ +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const load = createRequire(import.meta.url); +const here = dirname(fileURLToPath(import.meta.url)); + +// Looks for a prebuilt addon in each packaging location, the way +// packages with optional native helpers do. +export function probe(name) { + for (const dir of ["prebuilds", "native"]) { + try { + const addon = load(join(here, dir, `${name}.node`)); + if (typeof addon?.run === "function") return "loaded"; + } catch { + // Try the next location. + } + } + return "absent"; +} + +export function resolveHelper(name, resolvePackage) { + try { + return (resolvePackage ?? load.resolve)(name); + } catch { + return "unresolved"; + } +} diff --git a/tests/fixtures/npm-static/node_modules/addon-probe/package.json b/tests/fixtures/npm-static/node_modules/addon-probe/package.json new file mode 100644 index 000000000..a2f7406f5 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/addon-probe/package.json @@ -0,0 +1,7 @@ +{ + "name": "addon-probe", + "version": "1.0.0", + "type": "module", + "main": "index.js", + "types": "index.d.ts" +} diff --git a/tests/harness/__snapshots__/create-require.ts.txt b/tests/harness/__snapshots__/create-require.ts.txt index 219af92a4..a3c56a033 100644 --- a/tests/harness/__snapshots__/create-require.ts.txt +++ b/tests/harness/__snapshots__/create-require.ts.txt @@ -7,14 +7,14 @@ create-require.ts:13:13 - error SC2020: 'module.createRequire' is part of the st hint: the lowered shape is a const binding (or a proven stable top-level let/var) over createRequire(import.meta.url) (or __filename) whose require calls take STATIC string literals — builtins, project modules (including #imports), relative .json documents and natively compiled npm packages resolve at build time; dynamic specifiers cannot exist in a compiled binary's fixed module graph -create-require.ts:20:1 - error SC2020: 'createRequire's require with this argument shape' is part of the standard library types but has no scriptc lowering yet +create-require.ts:20:1 - error SC2020: 'createRequire's require with a computed specifier or this argument shape' is part of the standard library types but has no scriptc lowering yet 19 | const which = "left" + "-pad"; 20 | require(which); | ^~~~~~~~~~~~~~ 21 | - hint: the compiled module graph is fixed at build time — the one lowered form is require("") + hint: the compiled module graph is fixed at build time — the one lowered form is require(""); native addons are not loaded create-require.ts:23:1 - error SC2020: 'createRequire's require of './helper.js'' is part of the standard library types but has no scriptc lowering yet diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 388da2138..026da8f4e 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -423,6 +423,40 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { }, ); + test.each(["llvm"] as const)( + "a package probing for optional addons through createRequire compiles with deferred refusals (%s)", + async (backend) => { + const entry = join(pilotRoot, "addon-probe-cli.ts"); + const { coverage } = analyze(entry, { npmStatic: "auto" }); + expect(coverage.npmStatic).toEqual([{ package: "addon-probe", status: "static" }]); + expect(coverage.diagnostics).toHaveLength(0); + expect( + (coverage.runtimeFences ?? []).filter((f) => f.code === "SC2020").map((f) => f.message), + ).toEqual([ + expect.stringContaining("'createRequire's require with a computed specifier"), + expect.stringContaining("'createRequire's require.resolve as a value'"), + ]); + const outDir = join(cacheDir, "addon-probe", sanitize ? "san" : "plain", backend); + const result = await compile(entry, { + backend, + npmStatic: "auto", + sanitize, + outDir, + outPath: join(outDir, "program"), + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const [nodeRes, nativeRes] = await Promise.all([ + runBinary(process.execPath, [entry]), + runBinary(result.binaryPath, []), + ]); + expect(nativeRes.stdout.toString()).toBe("a/b absent,absent,unresolved\ncustom:other\n"); + expect(nativeRes.stdout).toEqual(nodeRes.stdout); + expect(comparableStderr(nativeRes.stderr)).toEqual(nodeRes.stderr); + expect(nativeRes.exitCode).toBe(nodeRes.exitCode); + }, + ); + test.each([ [ "computed specifier", @@ -430,6 +464,31 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { "computed specifier", ], ["escaped require", 'const alias = load; const path = alias("node:path");', "escapes"], + [ + "computed specifier in a catch clause", + 'try { load("node:path"); } catch { load(String(1)); } const path = load("node:path");', + "outside a try block", + ], + [ + "computed specifier in a function inside a try block", + 'try { [1].map((n) => load(String(n))); } catch {} const path = load("node:path");', + "outside a try block", + ], + [ + "computed specifier in a class field inside a try block", + 'try { new (class { addon = load(String(1)); })(); } catch {} const path = load("node:path");', + "outside a try block", + ], + [ + "resolve value outside a try block", + 'const find = load.resolve; const path = load("node:path");', + "outside a try block", + ], + [ + "escaped require in a try block", + 'try { const alias = load; alias("node:path"); } catch {} const path = load("node:path");', + "escapes", + ], [ "reassigned require", 'let load = createLoader(import.meta.url); load = () => ({}); const path = load("node:path");', From ccd45c748f91d060ec0631c53df93f34bac383dc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 12:21:50 -0500 Subject: [PATCH 025/273] Address the Node island report review findings --- packages/compiler/src/cli/command.ts | 15 +++- packages/compiler/src/diagnostics/envelope.ts | 5 +- packages/compiler/src/diagnostics/render.ts | 27 ++++++- packages/compiler/src/executable/prepare.ts | 6 +- packages/compiler/src/frontend/analysis.ts | 4 +- .../compiler/src/frontend/pipeline-native.ts | 4 +- packages/compiler/src/frontend/pipeline.ts | 78 ++++++++++++------- packages/compiler/src/index.ts | 9 ++- packages/compiler/src/native/compiler.ts | 3 +- .../node-islands-config/main.ts.json | 6 -- .../node-islands-config/main.ts | 4 + .../node-islands-config/package.json | 0 tests/diagnostics/node-islands-config/main.ts | 3 - .../node-islands-config/main.ts.txt | 35 --------- tests/harness/coverage.test.ts | 28 +++++++ 15 files changed, 141 insertions(+), 86 deletions(-) delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json create mode 100644 tests/coverage-fixtures/node-islands-config/main.ts rename tests/{diagnostics => coverage-fixtures}/node-islands-config/package.json (100%) delete mode 100644 tests/diagnostics/node-islands-config/main.ts delete mode 100644 tests/harness/__snapshots__/node-islands-config/main.ts.txt diff --git a/packages/compiler/src/cli/command.ts b/packages/compiler/src/cli/command.ts index 2d15cbbb3..a660d339a 100644 --- a/packages/compiler/src/cli/command.ts +++ b/packages/compiler/src/cli/command.ts @@ -19,7 +19,7 @@ import { } from "../node-islands/policy.js"; import { TIER_MODES, type TierMode } from "../frontend/tier-analysis.js"; import { buildEnvelope } from "../diagnostics/envelope.js"; -import { renderWarnings } from "../diagnostics/render.js"; +import { renderConfigWarnings, 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"; @@ -609,14 +609,21 @@ async function main(args: string[], host: CliHost): Promise { // warning names likely-hot functions that use untyped values. `run` // keeps stderr for the program's own output. const color = process.stderr.isTTY ?? false; - const divergences = warnings.filter((d) => !isPerformanceCode(d.code)); + const config = warnings.filter((d) => d.code === "SC8004"); + const divergences = warnings.filter((d) => !isPerformanceCode(d.code) && d.code !== "SC8004"); const performance = warnings.filter((d) => isPerformanceCode(d.code)); - if (divergences.length > 0) + if (config.length > 0) + process.stderr.write( + renderConfigWarnings(config, result.sourceTexts ?? new Map(), { color }) + "\n", + ); + if (divergences.length > 0) { + if (config.length > 0) process.stderr.write("\n"); process.stderr.write( renderWarnings(divergences, result.sourceTexts ?? new Map(), { color }) + "\n", ); + } if (performance.length > 0) { - if (divergences.length > 0) process.stderr.write("\n"); + if (divergences.length > 0 || config.length > 0) process.stderr.write("\n"); process.stderr.write(renderPerformanceWarnings(performance) + "\n"); } } diff --git a/packages/compiler/src/diagnostics/envelope.ts b/packages/compiler/src/diagnostics/envelope.ts index ff08c3968..ad2e31507 100644 --- a/packages/compiler/src/diagnostics/envelope.ts +++ b/packages/compiler/src/diagnostics/envelope.ts @@ -334,7 +334,10 @@ export function locateDiagnostic( return { code: diag.code, category: categoryOf(diag), - severity: isDivergenceCode(diag.code) || isPerformanceCode(diag.code) ? "warning" : "error", + severity: + isDivergenceCode(diag.code) || isPerformanceCode(diag.code) || diag.code === "SC8004" + ? "warning" + : "error", scope, file: diag.loc.file, line: start.line, diff --git a/packages/compiler/src/diagnostics/render.ts b/packages/compiler/src/diagnostics/render.ts index e5a75c6bc..16fe3f0bf 100644 --- a/packages/compiler/src/diagnostics/render.ts +++ b/packages/compiler/src/diagnostics/render.ts @@ -102,14 +102,12 @@ export function renderDiagnostic( frame.push( c(DIM, ` ${" ".repeat(gutterWidth)} | `) + " ".repeat(caretCol) + - c(/^SC6\d{3}$/.test(diag.code) ? YELLOW : RED, "^" + "~".repeat(spanOnLine - 1)), + c(isWarningCode(diag.code) ? YELLOW : RED, "^" + "~".repeat(spanOnLine - 1)), ); emitLine(lineNum + 1); } - // The divergence band (SC6xxx) is the one warning severity: the program - // compiles, and the site can behave differently from Node. - const warning = /^SC6\d{3}$/.test(diag.code); + const warning = isWarningCode(diag.code); out.push( `${c(BOLD, `${diag.loc.file}:${lineNum}:${colNum}`)} - ${warning ? c(YELLOW, "warning") : c(RED, "error")} ${c(BOLD, diag.code)}: ${diag.message}`, ); @@ -154,6 +152,27 @@ export function renderDiagnostics( /** Divergence warnings after a successful build: the rendered sites plus a * one-line count, on stderr. */ +/** The warning severities: the divergence band (SC6xxx), where the program + * compiles and the site can behave differently from Node, and unknown + * settings in the project's scriptc configuration (SC8004), which builds do + * not use. */ +export function isWarningCode(code: string): boolean { + return /^SC6\d{3}$/.test(code) || code === "SC8004"; +} + +/** A build's SC8004 warnings: the unknown scriptc settings it ignored. */ +export function renderConfigWarnings( + diags: readonly ScrDiagnostic[], + sourceTextByFile: Map, + opts: RenderOptions = {}, +): string { + const n = diags.length; + return ( + renderDiagnostics([...diags], sourceTextByFile, opts) + + `\n\n${n} warning${n === 1 ? "" : "s"}: the build does not use ${n === 1 ? "this setting" : "these settings"}; scriptc coverage reports Node islands once they are valid.` + ); +} + export function renderWarnings( diags: readonly ScrDiagnostic[], sourceTextByFile: Map, diff --git a/packages/compiler/src/executable/prepare.ts b/packages/compiler/src/executable/prepare.ts index 1771f98f5..027bd6ca6 100644 --- a/packages/compiler/src/executable/prepare.ts +++ b/packages/compiler/src/executable/prepare.ts @@ -122,6 +122,10 @@ export function prepareExecutableModule( mod: lowered.module!, sourceTexts, stats: lowered.stats, - warnings: [...(lowered.divergences ?? []), ...(lowered.performance ?? [])], + warnings: [ + ...(fe.warnings ?? []), + ...(lowered.divergences ?? []), + ...(lowered.performance ?? []), + ], }; } diff --git a/packages/compiler/src/frontend/analysis.ts b/packages/compiler/src/frontend/analysis.ts index 403cb70b7..848306b66 100644 --- a/packages/compiler/src/frontend/analysis.ts +++ b/packages/compiler/src/frontend/analysis.ts @@ -38,7 +38,9 @@ export function analyzeWithFrontend( } ffi = loaded.profile; } - const fe = createFrontend(entryPath, opts.npmStatic, opts.externalTypes); + const fe = createFrontend(entryPath, opts.npmStatic, opts.externalTypes, undefined, { + nodeIslands: true, + }); try { const emptyStats = { statementsTotal: 0, diff --git a/packages/compiler/src/frontend/pipeline-native.ts b/packages/compiler/src/frontend/pipeline-native.ts index 511fd62f2..f2df52608 100644 --- a/packages/compiler/src/frontend/pipeline-native.ts +++ b/packages/compiler/src/frontend/pipeline-native.ts @@ -1,4 +1,4 @@ -import { runFrontend, type Frontend } from "./pipeline.js"; +import { runFrontend, type Frontend, type FrontendOptions } from "./pipeline.js"; import { loadProgram } from "./program.js"; import { FrontendServices, type ComptimeEvaluator } from "./services.js"; import { createNativeTs7Api } from "./ts7/native-api.js"; @@ -13,6 +13,7 @@ export function runNativeFrontend( externalTypes?: Readonly>, evaluateComptime?: ComptimeEvaluator, libraryNpmStatic: readonly string[] = [], + options: FrontendOptions = {}, ): Frontend { const services = new FrontendServices( (options) => createNativeTs7Api({ ...options, executable }), @@ -31,6 +32,7 @@ export function runNativeFrontend( npmStatic, externalTypes, libraryNpmStatic, + options, ); return { ...frontend, diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 1021202cc..4e6a1c0f5 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -115,18 +115,30 @@ export interface Frontend { /** The Node island detector's input (report-only): the program's * modules with their pre-scan markers, package import edges, and how * each refused package was refused. Call before dispose. */ - nodeIslandScan?: (opts: { userJs: boolean }) => NodeIslandScan; + nodeIslandScan?: ((opts: { userJs: boolean }) => NodeIslandScan) | undefined; + /** Diagnostics that do not fail a build: unknown settings in the + * project's scriptc configuration (SC8004), which builds do not use. */ + warnings?: ScrDiagnostic[]; /** Releases the frontend's resources (the spawned tsgo server). Call * exactly once, after the last lower(). */ dispose: () => void; } /** Hosts own transport lifetimes; shared compiler stages own the frontend. */ +/** What a frontend computes beyond the build's own needs. */ +export interface FrontendOptions { + /** The Node island report (coverage): the project's scriptc settings and + * the detector's scan of the program and its npm packages. Builds do not + * read either. */ + nodeIslands?: boolean; +} + export type FrontendFactory = ( entryPath: string, npmStatic?: readonly string[] | "auto" | "lib", externalTypes?: Readonly>, libraryNpmStatic?: readonly string[], + options?: FrontendOptions, ) => Frontend; /** Adds the program modules a literal `import()` reaches to `scanned`, @@ -426,6 +438,7 @@ export function runFrontend( npmStatic?: readonly string[] | "auto" | "lib", externalTypes?: Readonly>, libraryNpmStatic: readonly string[] = [], + options: FrontendOptions = {}, ): Frontend { // A preflight or package probe may throw after its host has opened. Track // only live loads, releasing each immediately on normal fallback; this @@ -465,6 +478,7 @@ export function runFrontend( externalTypes, [...new Set(names)], transitive, + options, ); } catch (error) { for (const load of active) { @@ -486,6 +500,7 @@ function loadFrontend( externalTypes?: Readonly>, libraryNpmStatic: readonly string[] = [], transitive: Set = new Set(), + options: FrontendOptions = {}, ): Frontend { // Resolver package/workspace metadata is intentionally shared across the // several load attempts of ONE auto-detection fixpoint, but never across @@ -632,7 +647,7 @@ function loadFrontend( } const dropping = [...reasons.keys()].filter((p) => effective.has(p)); if (dropping.length === 0) break; - scanDroppedPackages(load, dropping, droppedScan); + if (options.nodeIslands === true) scanDroppedPackages(load, dropping, droppedScan); for (const p of dropping) { effective.delete(p); statuses.push({ package: p, status: "refused", detail: reasons.get(p)! }); @@ -684,13 +699,15 @@ function loadFrontend( probe.dispose(); if (probeDiags.some((d) => d.code === "SC0001")) dropWithNote(p); } - scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan); + if (options.nodeIslands === true) + scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan); load.dispose(); load = loadProgram(entryPath, { npmStatic: [...effective], externalTypes }); preflight = checkPreflight(load); if (preflight.some((d) => d.code === "SC0001") && effective.size > 0) { for (const p of [...effective]) dropWithNote(p); - scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan); + if (options.nodeIslands === true) + scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan); load.dispose(); load = loadProgram(entryPath, { npmStatic: [...effective], externalTypes }); preflight = checkPreflight(load); @@ -711,12 +728,16 @@ function loadFrontend( } const finalLoad = load; - // An unknown key or value in the project's scriptc configuration fails - // the build before anything else (SC8004). + // An unknown key or value in the project's scriptc configuration makes + // the Node island report unavailable (SC8004). The settings configure + // only that report: a build warns about them and goes on. const projectConfig = readProjectConfig(entryPath); const configDiagnostics = projectConfig.diagnostics; + const report = options.nodeIslands === true; return { - preflight: configDiagnostics.length > 0 ? [...configDiagnostics, ...preflight] : preflight, + preflight: + report && configDiagnostics.length > 0 ? [...configDiagnostics, ...preflight] : preflight, + ...(!report && configDiagnostics.length > 0 ? { warnings: configDiagnostics } : {}), entryText: () => finalLoad.entry.text, entryExports: () => entryFunctionExports(finalLoad.entry), // The contract scans the PROGRAM's source files, not the runtime @@ -779,26 +800,29 @@ function loadFrontend( }, npmStatic: statuses, npmImportSites: npmSites, - nodeIslandScan: ({ userJs }) => { - const files = new Set([finalLoad.entry, ...finalLoad.moduleOrder]); - addDynamicImportGraph(finalLoad, files); - // Package edges matter only when the program has npm packages: one - // that loads, one that was refused, or one judged and dropped. - const npm = - statuses.length > 0 || - droppedScan.modules.length > 0 || - [...files].some((sf) => sf.fileName.includes("/node_modules/")); - const scanned = scanFiles(finalLoad.program, [...files], { - userJs, - dropped: false, - edges: npm, - }); - return { - modules: [...droppedScan.modules, ...scanned.modules], - edges: [...droppedScan.edges, ...scanned.edges], - refusals, - }; - }, + nodeIslandScan: + options.nodeIslands !== true + ? undefined + : ({ userJs }) => { + const files = new Set([finalLoad.entry, ...finalLoad.moduleOrder]); + addDynamicImportGraph(finalLoad, files); + // Package edges matter only when the program has npm packages: one + // that loads, one that was refused, or one judged and dropped. + const npm = + statuses.length > 0 || + droppedScan.modules.length > 0 || + [...files].some((sf) => sf.fileName.includes("/node_modules/")); + const scanned = scanFiles(finalLoad.program, [...files], { + userJs, + dropped: false, + edges: npm, + }); + return { + modules: [...droppedScan.modules, ...scanned.modules], + edges: [...droppedScan.edges, ...scanned.edges], + refusals, + }; + }, dispose: finalLoad.dispose, }; } diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 41f5cd619..14bdebf3c 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -412,8 +412,13 @@ export async function analyzeCached( return result; } -const nodeFrontend: FrontendFactory = (entry, npmStatic, externalTypes, libraryNpmStatic) => - runFrontend(entry, loadProgram, npmStatic, externalTypes, libraryNpmStatic); +const nodeFrontend: FrontendFactory = ( + entry, + npmStatic, + externalTypes, + libraryNpmStatic, + options, +) => runFrontend(entry, loadProgram, npmStatic, externalTypes, libraryNpmStatic, options); /** The checker's part of the executable cache key (`checkerCacheIdentity`), as the CLI's startup route computes it. */ function checkerField(): { checker?: string } { diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts index a37d76f80..8f88356e9 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -108,7 +108,7 @@ export class NativeCompiler { private readonly cache: NativeCache | null = null, ) { const evaluator = toolchain.comptimeExecutable; - this.frontend = (entry, npmStatic, externalTypes, libraryNpmStatic) => + this.frontend = (entry, npmStatic, externalTypes, libraryNpmStatic, options) => runNativeFrontend( entry, toolchain.ts7Executable, @@ -119,6 +119,7 @@ export class NativeCompiler { : (source, timeout) => evaluateNativeComptime(source, timeout, toolchain.ts7Executable, evaluator), libraryNpmStatic, + options, ); } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json deleted file mode 100644 index 6b86e7f9a..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/node-islands-config/main.ts" - ], - "diags": [] -} diff --git a/tests/coverage-fixtures/node-islands-config/main.ts b/tests/coverage-fixtures/node-islands-config/main.ts new file mode 100644 index 000000000..5bacea88a --- /dev/null +++ b/tests/coverage-fixtures/node-islands-config/main.ts @@ -0,0 +1,4 @@ +// Unknown values in scriptc.packages or scriptc.nodeIslands are SC8004: a +// build warns about them and goes on (it does not use the settings), and +// coverage reports no Node islands until they are valid. +console.log("configured"); diff --git a/tests/diagnostics/node-islands-config/package.json b/tests/coverage-fixtures/node-islands-config/package.json similarity index 100% rename from tests/diagnostics/node-islands-config/package.json rename to tests/coverage-fixtures/node-islands-config/package.json diff --git a/tests/diagnostics/node-islands-config/main.ts b/tests/diagnostics/node-islands-config/main.ts deleted file mode 100644 index cc7b3735f..000000000 --- a/tests/diagnostics/node-islands-config/main.ts +++ /dev/null @@ -1,3 +0,0 @@ -// An unknown value in scriptc.packages or scriptc.nodeIslands is SC8004: -// the configuration the project asked for is never silently ignored. -console.log("configured"); diff --git a/tests/harness/__snapshots__/node-islands-config/main.ts.txt b/tests/harness/__snapshots__/node-islands-config/main.ts.txt deleted file mode 100644 index 03cc714aa..000000000 --- a/tests/harness/__snapshots__/node-islands-config/main.ts.txt +++ /dev/null @@ -1,35 +0,0 @@ -node-islands-config/package.json:6:7 - error SC8004: package.json 'scriptc.packages.encoding' has the unknown value "unloded" - - 5 | "packages": { - 6 | "encoding": "unloded", - | ^ - 7 | "debug": { "run": "island" } - - hint: use one of "unloaded", "node", "native", "auto", or an object with run, evaluate, share, marshal, schedule - -node-islands-config/package.json:7:18 - error SC8004: package.json 'scriptc.packages.debug.run' has the unknown value "island" - - 6 | "encoding": "unloded", - 7 | "debug": { "run": "island" } - | ^ - 8 | }, - - hint: use one of "native", "node", "auto" - -node-islands-config/package.json:10:7 - error SC8004: package.json 'scriptc.nodeIslands.auto' must be true or false, not "yes" - - 9 | "nodeIslands": { - 10 | "auto": "yes", - | ^ - 11 | "mode": "auto" - - hint: use a boolean - -node-islands-config/package.json:11:7 - error SC8004: package.json 'scriptc.nodeIslands.mode' is not a known Node island setting - - 10 | "auto": "yes", - 11 | "mode": "auto" - | ^ - 12 | } - - hint: the settings are auto, modules, node, nodeVersion, packaging, speculativeStart \ No newline at end of file diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 2c34a69e1..8dd7173ad 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -5,6 +5,7 @@ import { join } from "node:path"; import { expect, test } from "vitest"; import { analyze, + compile, coverageEnvelope, coveragePasses, renderCoverage, @@ -871,6 +872,33 @@ test("Node islands (report-only): one package per class, gap islands first", asy expect(coveragePasses(coverage, "node-islands")).toBe(false); }); +test("unknown scriptc settings warn in a build and make the island report unavailable", async () => { + const entry = fixture("node-islands-config/main.ts"); + const outDir = mkdtempSync(join(tmpdir(), "scriptc-island-config-")); + try { + const built = await compile(entry, { + outputKind: "ir", + outPath: join(outDir, "main.ir.json"), + outDir, + }); + expect(built.ok).toBe(true); + if (!built.ok) return; + expect((built.warnings ?? []).map((d) => [d.code, d.message])).toEqual([ + ["SC8004", `package.json 'scriptc.packages.encoding' has the unknown value "unloded"`], + ["SC8004", `package.json 'scriptc.packages.debug.run' has the unknown value "island"`], + ["SC8004", `package.json 'scriptc.nodeIslands.auto' must be true or false, not "yes"`], + ["SC8004", "package.json 'scriptc.nodeIslands.mode' is not a known Node island setting"], + ]); + } finally { + rmSync(outDir, { recursive: true, force: true }); + } + const { coverage } = analyze(entry); + expect(coverage.nodeIslands).toBeUndefined(); + expect(renderCoverage({ ...coverage, file: "main.ts" }, { root: repoRoot })).toContain( + "not analyzable: the project's scriptc configuration has 4 unknown settings", + ); +}); + test("Node island policy from the command line: native, forced, off", () => { const entry = fixture("node-islands/main.ts"); const forced = analyze(entry, { From 8afec299b7816138e187c8f18e6d2c1a1f46e771 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 17:52:58 -0500 Subject: [PATCH 026/273] Kill corpus 6611's children after they start, and run the second module after the first close The children 6611 spawned ran `process.stdout.write('ok')` and were killed right after spawn. When a child exited before the SIGTERM took effect, Node reported `close 0` for it and closed it after the second child, so the expected output depended on process start-up timing. - The program re-runs itself as the child (`main.cjs child`), which writes `ready` and then waits. Each parent kills its child on the first stdout data, so both closes report `null` and `SIGTERM`. - definer.cjs runs on the first child's `close` instead of its `exit`, so the first close always prints before the second module's output. --- .../definer.cjs | 2 +- .../main.cjs | 33 +++++++++++-------- .../spawner.cjs | 2 +- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/tests/corpus/6611-child-properties-across-modules/definer.cjs b/tests/corpus/6611-child-properties-across-modules/definer.cjs index 8b1c8f1a3..ea58bc046 100644 --- a/tests/corpus/6611-child-properties-across-modules/definer.cjs +++ b/tests/corpus/6611-child-properties-across-modules/definer.cjs @@ -21,5 +21,5 @@ module.exports = function run() { console.log(e instanceof TypeError, e.message.length, e.message.slice(0, 30), e.message.slice(-40)); } child.on("close", (code) => console.log("close", code, child.killedBy)); - child.kill("SIGTERM"); + child.stdout.once("data", () => child.kill("SIGTERM")); }; diff --git a/tests/corpus/6611-child-properties-across-modules/main.cjs b/tests/corpus/6611-child-properties-across-modules/main.cjs index a3f422c64..3dfb6756a 100644 --- a/tests/corpus/6611-child-properties-across-modules/main.cjs +++ b/tests/corpus/6611-child-properties-across-modules/main.cjs @@ -5,17 +5,24 @@ // and writes a getter-only property with a long name. const patched = require("./patcher.cjs"); const definer = require("./definer.cjs"); -const child = patched(); -console.log(child.label); -child.label = "x"; -console.log(child.stored, child.same); -try { - child.same = 1; -} catch (e) { - console.log(e instanceof TypeError, e.message); +if (process.argv.at(2) === "child") { + // The child reports that it started, then waits for the parent's SIGTERM. + // It exits after 30 seconds without one. + process.stdout.write("ready"); + setTimeout(() => {}, 30000); +} else { + const child = patched(); + console.log(child.label); + child.label = "x"; + console.log(child.stored, child.same); + try { + child.same = 1; + } catch (e) { + console.log(e instanceof TypeError, e.message); + } + child.on("close", (code) => { + console.log("close", code, child.killedBy); + }); + child.stdout.once("data", () => child.kill("SIGTERM")); + child.on("close", () => definer()); } -child.on("close", (code) => { - console.log("close", code, child.killedBy); -}); -child.kill("SIGTERM"); -child.on("exit", () => definer()); diff --git a/tests/corpus/6611-child-properties-across-modules/spawner.cjs b/tests/corpus/6611-child-properties-across-modules/spawner.cjs index d0c52d9b7..5701145a8 100644 --- a/tests/corpus/6611-child-properties-across-modules/spawner.cjs +++ b/tests/corpus/6611-child-properties-across-modules/spawner.cjs @@ -1,5 +1,5 @@ "use strict"; const { spawn } = require("child_process"); module.exports = function start() { - return spawn(process.execPath, ["-e", "process.stdout.write('ok')"]); + return spawn(process.execPath, [process.argv[1], "child"]); }; From 072543f5b98e5757b73a23b00127e8c7e2186417 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 18:01:19 -0500 Subject: [PATCH 027/273] Support X509Certificate fields and methods, TLS certificates and tls.Server contexts --- docs/content/docs/limitations.mdx | 8 + .../limitations/builtin-apis/crypto.mdx | 4 + docs/content/limitations/tls-certificates.mdx | 11 + docs/content/limitations/tls-sessions.mdx | 3 + .../src/generated/node-v24-compatibility.json | 126 +- .../generated/node-v24-internal.json | 232 ++- internal/compatibility/static-support.json | 8 + .../ambient/scriptc-node-fallback.d.ts | 108 +- .../src/backend/executable-features.ts | 1 + packages/compiler/src/backend/library-pack.ts | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 22 + packages/compiler/src/backend/llvm/lib-tls.ts | 25 + .../src/backend/native-link-info-core.ts | 3 + .../compiler/src/backend/native/contracts.ts | 4 + .../compiler/src/backend/native/executable.ts | 3 + .../compiler/src/backend/native/library.ts | 3 +- .../src/backend/runtime-pack-targets.test.ts | 1 + .../compiler/src/backend/runtime-pack.test.ts | 1 + packages/compiler/src/backend/warm-cache.ts | 1 + .../src/executable/executable-cache.test.ts | 1 + .../src/executable/executable-cache.ts | 2 + .../src/frontend/lowering/builtins/x509.ts | 487 +++++ .../frontend/lowering/expressions/binary.ts | 2 + .../lowering/expressions/object-literals.ts | 8 + .../lowering/expressions/property-access.ts | 6 + .../src/frontend/lowering/lower-calls.ts | 3 + .../src/frontend/lowering/lower-classes.ts | 286 ++- .../src/frontend/lowering/lower-server.ts | 125 +- .../src/frontend/lowering/lower-stmts.ts | 2 + .../compiler/src/frontend/lowering/lowerer.ts | 3 + packages/compiler/src/frontend/type-mapper.ts | 74 +- packages/compiler/src/index.ts | 3 + packages/compiler/src/ir/builtin-effects.ts | 15 + .../compiler/src/ir/builtin-signatures.ts | 46 + packages/compiler/src/ir/runtime-features.ts | 32 + packages/compiler/src/library/ir-policy.ts | 3 + .../src/library/library-cache.test.ts | 13 + .../compiler/src/library/library-cache.ts | 3 + packages/compiler/src/native/cli.ts | 1 + packages/compiler/src/native/prepare.ts | 1 + .../compiler/src/native/sanitizer.test.ts | 1 + .../6936-x509-certificate-fields.ts.json | 6 + .../corpus/7090-x509-check-names.ts.json | 6 + .../corpus/7091-x509-certificate-keys.ts.json | 6 + .../corpus/7092-x509-legacy-object.ts.json | 6 + .../corpus/7093-x509-methods-js.mjs.json | 6 + .../corpus/7095-tls-peer-certificates.ts.json | 6 + .../7096-tls-peer-certificates.mjs.json | 6 + .../7097-tls-client-certificates.ts.json | 6 + ...7098-tls-client-certificates-paths.ts.json | 6 + .../7099-tls-client-certificates.mjs.json | 6 + .../7440-tls-server-add-context.ts.json | 6 + .../corpus/7441-tls-client-error.ts.json | 6 + .../corpus/7442-tls-server-contexts.mjs.json | 6 + .../corpus/7443-tls-final-flight.ts.json | 6 + .../corpus/7444-tls-final-flight.mjs.json | 6 + .../corpus/7454-tls-peer-embedded-pem.ts.json | 6 + .../corpus/7455-tls-als-contexts.mjs.json | 6 + .../tests/diagnostics/x509-fences.ts.json | 6 + .../tls-client-certificates.ts.json | 6 + .../node-types/tls-peer-certificates.ts.json | 6 + .../node-types/tls-server-contexts.ts.json | 6 + .../fixtures/node-types/x509-methods.ts.json | 6 + .../runtime-pack-common/library-matrix.mjs | 1 + .../runtime-pack-matrix.mjs | 3 + .../runtime-wasm32-wasi/scripts/build.mjs | 1 + packages/runtime/src/scr_crypto.h | 31 + packages/runtime/src/scr_lib.c | 21 +- packages/runtime/src/scr_net.c | 67 +- packages/runtime/src/scr_net.h | 18 + packages/runtime/src/scr_pki.c | 217 ++ packages/runtime/src/scr_tls.c | 966 ++++++++- packages/runtime/src/scr_tls.h | 19 + packages/runtime/src/scr_tls_x509.c | 119 ++ packages/runtime/src/scr_x509.c | 1820 +++++++++++++++++ tests/corpus/6936-x509-certificate-fields.ts | 189 ++ tests/corpus/7090-x509-check-names.ts | 141 ++ tests/corpus/7091-x509-certificate-keys.ts | 332 +++ tests/corpus/7092-x509-legacy-object.ts | 158 ++ tests/corpus/7093-x509-methods-js.mjs | 158 ++ tests/corpus/7095-tls-peer-certificates.ts | 188 ++ tests/corpus/7096-tls-peer-certificates.mjs | 35 + tests/corpus/7097-tls-client-certificates.ts | 84 + .../7098-tls-client-certificates-paths.ts | 161 ++ tests/corpus/7099-tls-client-certificates.mjs | 34 + tests/corpus/7440-tls-server-add-context.ts | 119 ++ tests/corpus/7441-tls-client-error.ts | 93 + tests/corpus/7442-tls-server-contexts.mjs | 51 + tests/corpus/7443-tls-final-flight.ts | 114 ++ tests/corpus/7444-tls-final-flight.mjs | 28 + tests/corpus/7454-tls-peer-embedded-pem.ts | 46 + tests/corpus/7455-tls-als-contexts.mjs | 48 + tests/diagnostics/x509-fences.ts | 21 + .../node-types/tls-client-certificates.ts | 32 + .../node-types/tls-peer-certificates.ts | 37 + .../node-types/tls-server-contexts.ts | 38 + tests/fixtures/node-types/x509-methods.ts | 20 + tests/fixtures/server/certs/README.md | 12 + tests/fixtures/server/certs/client-key.pem | 5 + tests/fixtures/server/certs/client.cnf | 7 + tests/fixtures/server/certs/client.pem | 11 + tests/fixtures/server/certs/embedded-key.pem | 5 + tests/fixtures/server/certs/embedded.cnf | 9 + tests/fixtures/server/certs/embedded.pem | 26 + tests/fixtures/tls/empty-ca.ts | 36 + .../tls-sni-callback-fences.ts.txt | 2 +- .../harness/__snapshots__/x509-fences.ts.txt | 48 + .../6936-x509-certificate-fields.ts.json | 1 + .../7090-x509-check-names.ts.json | 1 + .../7091-x509-certificate-keys.ts.json | 1 + .../7092-x509-legacy-object.ts.json | 1 + .../7093-x509-methods-js.mjs.json | 1 + .../7095-tls-peer-certificates.ts.json | 1 + .../7096-tls-peer-certificates.mjs.json | 1 + .../7097-tls-client-certificates.ts.json | 1 + ...7098-tls-client-certificates-paths.ts.json | 1 + .../7099-tls-client-certificates.mjs.json | 1 + .../7440-tls-server-add-context.ts.json | 1 + .../7441-tls-client-error.ts.json | 1 + .../7442-tls-server-contexts.mjs.json | 1 + .../7443-tls-final-flight.ts.json | 1 + .../7444-tls-final-flight.mjs.json | 1 + .../7454-tls-peer-embedded-pem.ts.json | 1 + .../7455-tls-als-contexts.mjs.json | 1 + tests/harness/errors.test.ts | 157 +- tests/harness/library-asyncfree.test.ts | 9 + tests/harness/library-mode.test.ts | 15 + tests/harness/project-config.test.ts | 7 + .../self-hosting-native-toolchain.test.ts | 1 + .../harness/self-hosting-runtime-pack.test.ts | 1 + tests/harness/tls-socket-leaks.test.ts | 10 +- tests/harness/tls-trust-store.test.ts | 42 + tests/library-mode/x509/lib.ts | 41 + tests/library-mode/x509/probe.c | 23 + tests/library-mode/x509/profile.json | 14 + 135 files changed, 7408 insertions(+), 314 deletions(-) create mode 100644 docs/content/limitations/tls-certificates.mdx create mode 100644 docs/content/limitations/tls-sessions.mdx create mode 100644 packages/compiler/src/frontend/lowering/builtins/x509.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-x509-certificate-fields.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7090-x509-check-names.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7091-x509-certificate-keys.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7092-x509-legacy-object.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7093-x509-methods-js.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7095-tls-peer-certificates.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7096-tls-peer-certificates.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7097-tls-client-certificates.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7098-tls-client-certificates-paths.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7099-tls-client-certificates.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7440-tls-server-add-context.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-tls-client-error.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7442-tls-server-contexts.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7443-tls-final-flight.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7444-tls-final-flight.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7454-tls-peer-embedded-pem.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7455-tls-als-contexts.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/x509-fences.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-client-certificates.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-peer-certificates.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-server-contexts.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/x509-methods.ts.json create mode 100644 packages/runtime/src/scr_tls_x509.c create mode 100644 packages/runtime/src/scr_x509.c create mode 100644 tests/corpus/6936-x509-certificate-fields.ts create mode 100644 tests/corpus/7090-x509-check-names.ts create mode 100644 tests/corpus/7091-x509-certificate-keys.ts create mode 100644 tests/corpus/7092-x509-legacy-object.ts create mode 100644 tests/corpus/7093-x509-methods-js.mjs create mode 100644 tests/corpus/7095-tls-peer-certificates.ts create mode 100644 tests/corpus/7096-tls-peer-certificates.mjs create mode 100644 tests/corpus/7097-tls-client-certificates.ts create mode 100644 tests/corpus/7098-tls-client-certificates-paths.ts create mode 100644 tests/corpus/7099-tls-client-certificates.mjs create mode 100644 tests/corpus/7440-tls-server-add-context.ts create mode 100644 tests/corpus/7441-tls-client-error.ts create mode 100644 tests/corpus/7442-tls-server-contexts.mjs create mode 100644 tests/corpus/7443-tls-final-flight.ts create mode 100644 tests/corpus/7444-tls-final-flight.mjs create mode 100644 tests/corpus/7454-tls-peer-embedded-pem.ts create mode 100644 tests/corpus/7455-tls-als-contexts.mjs create mode 100644 tests/diagnostics/x509-fences.ts create mode 100644 tests/fixtures/node-types/tls-client-certificates.ts create mode 100644 tests/fixtures/node-types/tls-peer-certificates.ts create mode 100644 tests/fixtures/node-types/tls-server-contexts.ts create mode 100644 tests/fixtures/node-types/x509-methods.ts create mode 100644 tests/fixtures/server/certs/client-key.pem create mode 100644 tests/fixtures/server/certs/client.cnf create mode 100644 tests/fixtures/server/certs/client.pem create mode 100644 tests/fixtures/server/certs/embedded-key.pem create mode 100644 tests/fixtures/server/certs/embedded.cnf create mode 100644 tests/fixtures/server/certs/embedded.pem create mode 100644 tests/fixtures/tls/empty-ca.ts create mode 100644 tests/harness/__snapshots__/x509-fences.ts.txt create mode 100644 tests/harness/corpus-costs/6936-x509-certificate-fields.ts.json create mode 100644 tests/harness/corpus-costs/7090-x509-check-names.ts.json create mode 100644 tests/harness/corpus-costs/7091-x509-certificate-keys.ts.json create mode 100644 tests/harness/corpus-costs/7092-x509-legacy-object.ts.json create mode 100644 tests/harness/corpus-costs/7093-x509-methods-js.mjs.json create mode 100644 tests/harness/corpus-costs/7095-tls-peer-certificates.ts.json create mode 100644 tests/harness/corpus-costs/7096-tls-peer-certificates.mjs.json create mode 100644 tests/harness/corpus-costs/7097-tls-client-certificates.ts.json create mode 100644 tests/harness/corpus-costs/7098-tls-client-certificates-paths.ts.json create mode 100644 tests/harness/corpus-costs/7099-tls-client-certificates.mjs.json create mode 100644 tests/harness/corpus-costs/7440-tls-server-add-context.ts.json create mode 100644 tests/harness/corpus-costs/7441-tls-client-error.ts.json create mode 100644 tests/harness/corpus-costs/7442-tls-server-contexts.mjs.json create mode 100644 tests/harness/corpus-costs/7443-tls-final-flight.ts.json create mode 100644 tests/harness/corpus-costs/7444-tls-final-flight.mjs.json create mode 100644 tests/harness/corpus-costs/7454-tls-peer-embedded-pem.ts.json create mode 100644 tests/harness/corpus-costs/7455-tls-als-contexts.mjs.json create mode 100644 tests/harness/tls-trust-store.test.ts create mode 100644 tests/library-mode/x509/lib.ts create mode 100644 tests/library-mode/x509/probe.c create mode 100644 tests/library-mode/x509/profile.json diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index a86ed217e..cb6b93d0d 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -220,6 +220,14 @@ title: "Limitations" ../limitations/tls-ciphers.mdx +### TLS certificates + +../limitations/tls-certificates.mdx + +### TLS sessions + +../limitations/tls-sessions.mdx + ### Console and process streams ../limitations/console-and-process-streams.mdx diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 329aa6809..cba219788 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -41,6 +41,10 @@ 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. + X509Certificate supports `publicKey`, `verify()`, `checkIssued()`, `checkPrivateKey()`, `checkHost()`, `checkEmail()`, `checkIP()`, `toLegacyObject()`, `toString()`, and `toJSON()`, and `String()`, template literals, and `+` give its PEM text as `toString()` does. + `publicKey`, `verify()`, and `checkPrivateKey()` are refused in wasm32-wasi builds, and an Ed25519 certificate's `verify()` needs the `ed25519` literal like the other Ed25519 operations. + Own-key walks over a certificate (`Object.keys()`, `Object.entries()`, spread, `for...in`, and `hasOwnProperty`) are refused, since Node keeps its fields as prototype getters. + `JSON.stringify()` and `console.log()` of a certificate, a certificate stored in an `unknown` or `any` slot, and its methods on a union-typed value are also refused. 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`, `x448`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused. Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report. diff --git a/docs/content/limitations/tls-certificates.mdx b/docs/content/limitations/tls-certificates.mdx new file mode 100644 index 000000000..bbe4ceaf9 --- /dev/null +++ b/docs/content/limitations/tls-certificates.mdx @@ -0,0 +1,11 @@ +`tlsSocket.getPeerCertificate()`, `getCertificate()`, `getPeerX509Certificate()`, and `getX509Certificate()` follow Node, including the `detailed` issuer chain, on sockets whose type is known. +On a TLS socket held in an `unknown` or `any` value, or in an untyped JavaScript parameter, they throw an error that names the call as not supported. +Client certificates follow Node: `tls.createServer`, `https.createServer`, and `new tls.TLSSocket` take `requestCert`, `rejectUnauthorized`, and `ca`, and `tls.connect` and `https.request` take `cert` and `key`. +A server's `authorized` and `authorizationError` report the client certificate's verification as Node does, and a server, client, or `tls.createSecureContext` given a `ca` also sends the chain it completes, as OpenSSL's automatic chain does. +`requestCert` beside a TLSSocket's `secureContext` or an `SNICallback`, client certificates on http2 secure servers, and a `cert` without a `key` (or the reverse) are refused. +`server.addContext()` and `server.setSecureContext()` follow Node on tls and https servers, including wildcard names, which match one label without regard to case. +A name with regular-expression groups or alternation, or one outside ASCII, is refused, as are both methods on a server that is not a tls or https server. +The `'tlsClientError'` event reports Node's error for a failed handshake: OpenSSL's code and reason, or `socket hang up` for a client that leaves. +OpenSSL's messages leave out the thread identifier Node prints before `error:` and the source location after the reason. +A failure whose OpenSSL reason the runtime does not know, such as a malformed client hello, is reported as an error with code `SC2020` that says so. +The X509Certificate those accessors return has the same refusals as any other certificate (see Crypto), so in JavaScript narrow it, for example with `if (cert)`, before reading its fields. diff --git a/docs/content/limitations/tls-sessions.mdx b/docs/content/limitations/tls-sessions.mdx new file mode 100644 index 000000000..8d34573a8 --- /dev/null +++ b/docs/content/limitations/tls-sessions.mdx @@ -0,0 +1,3 @@ +TLS servers send no session tickets, so a connection to them cannot be resumed. +A client's `'session'` event fires once per connection, when its handshake finishes and before `secureConnect`. +Node fires it for each ticket the server sends, after `secureConnect`, and OpenSSL servers send two. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 1039bc3d8..e463b872d 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -13822,9 +13822,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" } }, { @@ -13845,9 +13845,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" } }, { @@ -13868,9 +13868,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" } }, { @@ -13891,9 +13891,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" } }, { @@ -13914,9 +13914,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" } }, { @@ -13937,9 +13937,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" } }, { @@ -13960,9 +13960,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" } }, { @@ -13983,9 +13983,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" } }, { @@ -14006,9 +14006,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" } }, { @@ -14029,9 +14029,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" } }, { @@ -14236,9 +14236,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" } }, { @@ -23790,9 +23790,9 @@ "anchor": "err_tls_required_server_name", "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" } }, { @@ -67484,9 +67484,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" } }, { @@ -67599,9 +67599,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" } }, { @@ -67783,9 +67783,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" } }, { @@ -67921,9 +67921,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" } }, { @@ -68013,9 +68013,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" } }, { @@ -68059,9 +68059,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" } }, { @@ -68174,9 +68174,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" } }, { @@ -68312,9 +68312,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" } }, { @@ -68335,9 +68335,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 b3d91e453..1ee5cacd6 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19394,6 +19394,8 @@ "tests": [ "tests/corpus/1358-crypto-random.ts", "tests/corpus/1534-crypto-hash-chain.ts", + "tests/corpus/1568-x509-fingerprint.ts", + "tests/corpus/1572-x509-validity-date-gettime.ts", "tests/corpus/2556-crypto-introspection.ts", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/5250-cipher-aes-gcm.ts", @@ -19436,6 +19438,7 @@ "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", + "tests/corpus/6936-x509-certificate-fields.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", @@ -19443,6 +19446,10 @@ "tests/corpus/7082-crypto-key-objects.ts", "tests/corpus/7083-crypto-sign-verify.ts", "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", "tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs", "tests/corpus/7150-cipher-legacy-modes.ts", @@ -19461,6 +19468,7 @@ "tests/fixtures/node-types/dh.ts", "tests/fixtures/node-types/ecdh.ts", "tests/fixtures/node-types/keys.ts", + "tests/fixtures/node-types/x509-methods.ts", "tests/harness/npm-static.test.ts" ] }, @@ -23784,8 +23792,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:X509Certificate", + "tests": [ + "tests/corpus/1568-x509-fingerprint.ts", + "tests/corpus/1572-x509-validity-date-gettime.ts", + "tests/corpus/6936-x509-certificate-fields.ts" + ] }, "anchorSource": "exact" }, @@ -23807,8 +23820,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.checkEmail", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23830,8 +23850,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.checkHost", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23853,8 +23880,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.checkIP", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23876,8 +23910,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.checkIssued", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23899,8 +23940,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.checkPrivateKey", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23922,8 +23970,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.toJSON", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23945,8 +24000,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.toLegacyObject", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23968,8 +24030,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.toString", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -23991,8 +24060,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:x509.verify", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "exact" }, @@ -24198,8 +24274,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-feature:crypto.publicKey", + "tests": [ + "tests/corpus/7090-x509-check-names.ts", + "tests/corpus/7091-x509-certificate-keys.ts", + "tests/corpus/7092-x509-legacy-object.ts", + "tests/corpus/7093-x509-methods-js.mjs", + "tests/fixtures/node-types/x509-methods.ts" + ] }, "anchorSource": "ancestor" }, @@ -32679,6 +32762,7 @@ "tests": [ "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6708-dh-errors.js", + "tests/corpus/7440-tls-server-add-context.ts", "tests/corpus/caught-system-error-fields.ts", "tests/corpus/error-stack-frames.ts", "tests/corpus/error-stack-limit.ts", @@ -39008,8 +39092,11 @@ "anchor": "err_tls_required_server_name", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-dedicated:ERR_TLS_REQUIRED_SERVER_NAME", + "tests": [ + "tests/corpus/7440-tls-server-add-context.ts" + ] }, "anchorSource": "exact" }, @@ -95873,6 +95960,17 @@ "tests/corpus/6722-tls-min-max-version.ts", "tests/corpus/7087-tls-socket-wrap.ts", "tests/corpus/7088-tls-socket-connect-tunnel.ts", + "tests/corpus/7095-tls-peer-certificates.ts", + "tests/corpus/7096-tls-peer-certificates.mjs", + "tests/corpus/7097-tls-client-certificates.ts", + "tests/corpus/7098-tls-client-certificates-paths.ts", + "tests/corpus/7099-tls-client-certificates.mjs", + "tests/corpus/7440-tls-server-add-context.ts", + "tests/corpus/7441-tls-client-error.ts", + "tests/corpus/7442-tls-server-contexts.mjs", + "tests/fixtures/node-types/tls-client-certificates.ts", + "tests/fixtures/node-types/tls-peer-certificates.ts", + "tests/fixtures/node-types/tls-server-contexts.ts", "tests/fixtures/node-types/tls-socket-wrap.ts" ] }, @@ -96241,8 +96339,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:server.addContext", + "tests": [ + "tests/corpus/7440-tls-server-add-context.ts", + "tests/corpus/7441-tls-client-error.ts", + "tests/corpus/7442-tls-server-contexts.mjs", + "tests/fixtures/node-types/tls-server-contexts.ts" + ] }, "anchorSource": "exact" }, @@ -96368,8 +96472,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:server.setSecureContext", + "tests": [ + "tests/corpus/7440-tls-server-add-context.ts", + "tests/corpus/7441-tls-client-error.ts", + "tests/corpus/7442-tls-server-contexts.mjs", + "tests/fixtures/node-types/tls-server-contexts.ts" + ] }, "anchorSource": "exact" }, @@ -96552,8 +96662,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:'tlsClientError'", + "tests": [ + "tests/corpus/7440-tls-server-add-context.ts", + "tests/corpus/7441-tls-client-error.ts", + "tests/corpus/7442-tls-server-contexts.mjs", + "tests/fixtures/node-types/tls-server-contexts.ts" + ] }, "anchorSource": "exact" }, @@ -96695,8 +96811,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:tlsSocket.getCertificate", + "tests": [ + "tests/corpus/7095-tls-peer-certificates.ts", + "tests/corpus/7096-tls-peer-certificates.mjs", + "tests/fixtures/node-types/tls-peer-certificates.ts" + ] }, "anchorSource": "exact" }, @@ -96787,8 +96908,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:tlsSocket.getPeerCertificate", + "tests": [ + "tests/corpus/7095-tls-peer-certificates.ts", + "tests/corpus/7096-tls-peer-certificates.mjs", + "tests/fixtures/node-types/tls-peer-certificates.ts" + ] }, "anchorSource": "exact" }, @@ -96856,8 +96982,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:tlsSocket.getPeerX509Certificate", + "tests": [ + "tests/corpus/7095-tls-peer-certificates.ts", + "tests/corpus/7096-tls-peer-certificates.mjs", + "tests/fixtures/node-types/tls-peer-certificates.ts" + ] }, "anchorSource": "exact" }, @@ -96974,8 +97105,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:tlsSocket.getX509Certificate", + "tests": [ + "tests/corpus/7095-tls-peer-certificates.ts", + "tests/corpus/7096-tls-peer-certificates.mjs", + "tests/fixtures/node-types/tls-peer-certificates.ts" + ] }, "anchorSource": "exact" }, @@ -97115,8 +97251,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-dedicated:tlsSocket.authorizationError", + "tests": [ + "tests/corpus/7097-tls-client-certificates.ts", + "tests/corpus/7098-tls-client-certificates-paths.ts", + "tests/corpus/7099-tls-client-certificates.mjs", + "tests/fixtures/node-types/tls-client-certificates.ts" + ] }, "anchorSource": "exact" }, @@ -97138,8 +97280,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-feature:tls.authorized", + "tests": [ + "tests/corpus/7097-tls-client-certificates.ts", + "tests/corpus/7098-tls-client-certificates-paths.ts", + "tests/corpus/7099-tls-client-certificates.mjs", + "tests/fixtures/node-types/tls-client-certificates.ts" + ] }, "anchorSource": "ancestor" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 92124f4f5..a098390f2 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -202,6 +202,8 @@ "stdlib.abort": ["tests/harness/fetch-conformance.test.ts"] }, "features": [ + { "chapter": "tls", "parent": "Class: `tls.TLSSocket`", "signatures": ["`authorized` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/7097-tls-client-certificates.ts", "tests/corpus/7098-tls-client-certificates-paths.ts", "tests/corpus/7099-tls-client-certificates.mjs", "tests/fixtures/node-types/tls-client-certificates.ts"] }, + { "chapter": "crypto", "parent": "Class: `X509Certificate`", "signatures": ["`publicKey` Type: {KeyObject}"], "status": "partial", "evidence": ["tests/corpus/7090-x509-check-names.ts", "tests/corpus/7091-x509-certificate-keys.ts", "tests/corpus/7092-x509-legacy-object.ts", "tests/corpus/7093-x509-methods-js.mjs", "tests/fixtures/node-types/x509-methods.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", "tests/corpus/7081-webcrypto-ed25519.mjs", "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"] }, { "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"] }, @@ -427,6 +429,11 @@ { "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/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": ["crypto.webcrypto", "subtle"], "status": "partial", "evidence": ["tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs"] }, + { "chapter": "tls", "symbols": ["server.addContext", "server.setSecureContext", "'tlsClientError'"], "status": "partial", "evidence": ["tests/corpus/7440-tls-server-add-context.ts", "tests/corpus/7441-tls-client-error.ts", "tests/corpus/7442-tls-server-contexts.mjs", "tests/fixtures/node-types/tls-server-contexts.ts"] }, + { "chapter": "errors", "symbols": ["ERR_TLS_REQUIRED_SERVER_NAME"], "status": "partial", "evidence": ["tests/corpus/7440-tls-server-add-context.ts"] }, + { "chapter": "tls", "symbols": ["tlsSocket.authorizationError"], "status": "partial", "evidence": ["tests/corpus/7097-tls-client-certificates.ts", "tests/corpus/7098-tls-client-certificates-paths.ts", "tests/corpus/7099-tls-client-certificates.mjs", "tests/fixtures/node-types/tls-client-certificates.ts"] }, + { "chapter": "tls", "symbols": ["tlsSocket.getPeerCertificate", "tlsSocket.getCertificate", "tlsSocket.getPeerX509Certificate", "tlsSocket.getX509Certificate"], "status": "partial", "evidence": ["tests/corpus/7095-tls-peer-certificates.ts", "tests/corpus/7096-tls-peer-certificates.mjs", "tests/fixtures/node-types/tls-peer-certificates.ts"] }, + { "chapter": "crypto", "symbols": ["x509.checkEmail", "x509.checkHost", "x509.checkIP", "x509.checkIssued", "x509.checkPrivateKey", "x509.toJSON", "x509.toLegacyObject", "x509.toString", "x509.verify"], "status": "partial", "evidence": ["tests/corpus/7090-x509-check-names.ts", "tests/corpus/7091-x509-certificate-keys.ts", "tests/corpus/7092-x509-legacy-object.ts", "tests/corpus/7093-x509-methods-js.mjs", "tests/fixtures/node-types/x509-methods.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"] }, @@ -437,6 +444,7 @@ { "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": ["X509Certificate"], "status": "partial", "evidence": ["tests/corpus/1568-x509-fingerprint.ts", "tests/corpus/1572-x509-validity-date-gettime.ts", "tests/corpus/6936-x509-certificate-fields.ts"] }, { "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] }, { "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] }, { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 0be5bdca2..9cd0abdd3 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3533,17 +3533,45 @@ declare module "crypto" { keylen: number, callback: (error: Error | null, derivedKey: ArrayBuffer) => void, ): void; - /* The lowered X509Certificate surface is the data-record slice: - * fingerprint (the SHA-1 of the DER, uppercase colon-separated) and - * the validFrom/validTo validity window (Node's ASN1_TIME_print - * strings — "Jul 1 00:00:00 2026 GMT"; the cert-expiry idiom composes - * them with new Date(...).getTime()). All compute at construction; - * unparseable input throws Node's ERR_OSSL_PEM_NO_START_LINE Error. */ + /* The lowered X509Certificate surface is the data-record slice: the + * fields below, all computed at construction (raw decodes the input, + * and the others read raw). validFrom/validTo are Node's + * ASN1_TIME_print strings ("Jul 1 00:00:00 2026 GMT"). Unparseable + * input throws Node's ERR_OSSL_PEM_NO_START_LINE Error. */ export class X509Certificate { constructor(buffer: Uint8Array | string); + readonly ca: boolean; readonly fingerprint: string; + readonly fingerprint256: string; + readonly fingerprint512: string; + readonly subject: string; + readonly subjectAltName: string | undefined; + readonly infoAccess: string | undefined; + readonly issuer: string; + readonly raw: Buffer; + readonly serialNumber: string; + readonly signatureAlgorithm: string | undefined; + readonly signatureAlgorithmOid: string; readonly validFrom: string; + readonly validFromDate: Date; readonly validTo: string; + readonly validToDate: Date; + /* the certificate's public key; every read answers the same KeyObject */ + readonly publicKey: KeyObject; + verify(publicKey: KeyObject): boolean; + checkPrivateKey(privateKey: KeyObject): boolean; + checkIssued(otherCert: X509Certificate): boolean; + checkHost(name: string, options?: X509CheckOptions): string | undefined; + checkEmail(email: string, options?: X509CheckOptions): string | undefined; + checkIP(ip: string): string | undefined; + toLegacyObject(): import("tls").PeerCertificate; + } + export interface X509CheckOptions { + subject?: "always" | "default" | "never"; + wildcards?: boolean; + partialWildcards?: boolean; + multiLabelWildcards?: boolean; + singleLabelSubdomains?: boolean; } /* The crypto introspection statics: getFips() answers 0 (a compiled * binary is never a FIPS build), the three name lists bake as fresh @@ -5439,6 +5467,11 @@ declare module "net" { /* The demux route: hand an accepted socket to another server. */ emit(event: "connection", socket: Socket): boolean; on(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; + /* tls and https servers: a handshake that failed */ + on( + event: "tlsClientError", + listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void, + ): void; on(event: "close" | "listening", listener: () => void): void; on(event: "drop", listener: (data?: DropArgument) => void): void; on(event: "timeout", listener: (socket: Socket) => void): void; @@ -5458,6 +5491,11 @@ declare module "net" { ): void; /* addListener IS on (Node aliases them) — the suite spells both. */ addListener(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; + /* tls and https servers: a handshake that failed */ + addListener( + event: "tlsClientError", + listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void, + ): void; addListener(event: "close" | "listening", listener: () => void): void; addListener(event: "drop", listener: (data?: DropArgument) => void): void; addListener(event: "timeout", listener: (socket: Socket) => void): void; @@ -5471,6 +5509,11 @@ declare module "net" { listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void, ): void; once(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; + /* tls and https servers: a handshake that failed */ + once( + event: "tlsClientError", + listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void, + ): void; once(event: "close" | "listening", listener: () => void): void; once(event: "drop", listener: (data?: DropArgument) => void): void; once(event: "timeout", listener: (socket: Socket) => void): void; @@ -5924,8 +5967,15 @@ declare module "node:http" { * are PEM strings or Buffers. Handshake failures tear the socket down * without emitting an event, Node's default for 'tlsClientError' with no listener. */ declare module "tls" { - import { Server, Socket } from "net"; - export { Server }; + import { Server as NetServer, Socket } from "net"; + /** tls.Server: a net server whose connections handshake first. */ + export interface Server extends NetServer { + /* serve `context` to clients whose SNI name matches `hostname` ('*' + * matches within one label); the newest match wins */ + addContext(hostname: string, context: TlsOptions | SecureContext): void; + /* new connections serve a context made from `options` */ + setSecureContext(options: TlsOptions): void; + } export interface TlsOptions { /* Optional in the DECLARATION only (real Node programs build these * literals with every TLS knob): the lowering still requires both — @@ -5959,7 +6009,47 @@ declare module "tls" { getCipher(): CipherNameAndProtocol; /* whether the handshake resumed a cached session */ isSessionReused(): boolean; + /* the peer's certificate: {} before the handshake or without one, + * null once the socket is destroyed; detailed adds issuerCertificate */ + getPeerCertificate(detailed: true): DetailedPeerCertificate; + getPeerCertificate(detailed?: false): PeerCertificate; + getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate; + /* the certificate this socket serves, the same way */ + getCertificate(): PeerCertificate | object | null; + /* a new X509Certificate on each call, or undefined without one */ + getPeerX509Certificate(): import("crypto").X509Certificate | undefined; + getX509Certificate(): import("crypto").X509Certificate | undefined; } + /* A certificate as getPeerCertificate() and X509Certificate's + * toLegacyObject() answer it: subject and issuer map attribute short + * names to values, infoAccess its " - " keys to values. */ + export interface PeerCertificate { + subject: { [attribute: string]: string | string[] }; + issuer: { [attribute: string]: string | string[] }; + subjectaltname?: string; + infoAccess?: { [key: string]: string[] }; + ca: boolean; + modulus?: string; + exponent?: string; + pubkey?: Buffer; + bits?: number; + valid_from: string; + valid_to: string; + fingerprint: string; + fingerprint256: string; + fingerprint512: string; + ext_key_usage?: string[]; + serialNumber: string; + raw: Buffer; + asn1Curve?: string; + nistCurve?: string; + } + /* getPeerCertificate(true): each certificate links to its issuer; a + * self-issued root is its own issuerCertificate */ + export interface DetailedPeerCertificate extends PeerCertificate { + issuerCertificate: DetailedPeerCertificate; + } + /** 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, @@ -6014,6 +6104,8 @@ declare module "tls" { /** An opaque parsed cert/key pair — what an SNI callback answers with * (the value is a runtime handle; the members are runtime-internal). */ export interface SecureContext {} + /* @types/node's name for createSecureContext's options */ + export type SecureContextOptions = TlsOptions; export function createSecureContext(options: TlsOptions): SecureContext; /** The CA-store introspection surface (scr_tls_ca.c): per-type cached * PEM string arrays. The host bundle stands in for Node's compiled-in diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index a9076af22..48be5b564 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -61,6 +61,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { dh: features.dh, pki: features.pki, ed25519: features.ed25519, + x509: features.x509, 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 79be316db..e7fa7a782 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -64,6 +64,7 @@ export async function compilePackedLibrary( dh: false, pki: false, ed25519: false, + x509: options.x509 ?? 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 9df33e63f..5383ca034 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1082,6 +1082,28 @@ export const LIB_FN_SYMS: Record = { "crypto.verifyOneshot": "scr_crypto_verify_oneshot", "crypto.createSign": "scr_crypto_create_sign", "crypto.createVerify": "scr_crypto_create_verify", + "crypto.x509Raw": "scr_crypto_x509_raw", + "crypto.x509RawDer": "scr_crypto_x509_raw_der", + "crypto.x509RawStr": "scr_crypto_x509_raw_str", + "crypto.x509Subject": "scr_crypto_x509_subject", + "crypto.x509Issuer": "scr_crypto_x509_issuer", + "crypto.x509SubjectAltName": "scr_crypto_x509_subject_alt_name", + "crypto.x509InfoAccess": "scr_crypto_x509_info_access", + "crypto.x509SerialNumber": "scr_crypto_x509_serial_number", + "crypto.x509Fingerprint256": "scr_crypto_x509_fingerprint256", + "crypto.x509Fingerprint512": "scr_crypto_x509_fingerprint512", + "crypto.x509Ca": "scr_crypto_x509_ca", + "crypto.x509SignatureAlgorithm": "scr_crypto_x509_signature_algorithm", + "crypto.x509SignatureAlgorithmOid": "scr_crypto_x509_signature_algorithm_oid", + "crypto.x509CheckHost": "scr_crypto_x509_check_host", + "crypto.x509CheckEmail": "scr_crypto_x509_check_email", + "crypto.x509CheckIP": "scr_crypto_x509_check_ip", + "crypto.x509CheckIssued": "scr_crypto_x509_check_issued", + "crypto.x509LegacyObject": "scr_crypto_x509_legacy_object", + "crypto.x509Pem": "scr_crypto_x509_pem", + "crypto.x509PublicKey": "scr_crypto_x509_public_key", + "crypto.x509Verify": "scr_crypto_x509_verify", + "crypto.x509CheckPrivateKey": "scr_crypto_x509_check_private_key", "fs.writeFileModeSync": "scr_fs_write_file_mode", "fs.chownSync": "scr_fs_chown", "fs.lchownSync": "scr_fs_lchown", diff --git a/packages/compiler/src/backend/llvm/lib-tls.ts b/packages/compiler/src/backend/llvm/lib-tls.ts index 1d1cf659c..307aeb3dd 100644 --- a/packages/compiler/src/backend/llvm/lib-tls.ts +++ b/packages/compiler/src/backend/llvm/lib-tls.ts @@ -110,6 +110,31 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu return finishRuntimeCall(host, e, "scr_tls_sock_get_cipher", a); case "tls.sockAlpnProtocol": return finishRuntimeCall(host, e, "scr_tls_sock_alpn_protocol", a); + case "tls.serverAddContext": + return finishRuntimeCall(host, e, "scr_tls_server_add_context", [...a, ptr()]); + case "tls.serverAddContextCtx": + return finishRuntimeCall(host, e, "scr_tls_server_add_context", [a[0]!, a[1]!, ptr(), a[2]!]); + case "tls.serverSetSecureContext": + return finishRuntimeCall(host, e, "scr_tls_server_set_secure_context", a); + case "tls.serverOnClientError": { + // (err, tlsSocket) listeners by arity: the adapter retains what the + // listener takes. + const cb = args[1]!; + const thunk = `scr_net_client_error_thunk${callbackType(cb).params.length}`; + return finishRuntimeCall(host, e, "scr_net_server_on_client_error", [ + a[0]!, + ...callbackAdapter(host, cb, thunk, ["ptr", "ptr", "ptr"]), + a[2]!, + ]); + } + case "tls.sockGetPeerCertificate": + return finishRuntimeCall(host, e, "scr_tls_sock_get_peer_certificate", a); + case "tls.sockGetCertificate": + return finishRuntimeCall(host, e, "scr_tls_sock_get_certificate", a); + case "tls.sockPeerCertDer": + return finishRuntimeCall(host, e, "scr_tls_sock_peer_cert_der", a); + case "tls.sockOwnCertDer": + return finishRuntimeCall(host, e, "scr_tls_sock_own_cert_der", a); case "tls.sockAuthError": { const result = callRuntime(host, "scr_tls_sock_auth_error", "ptr", a); return nullableReference(host, e.type, result, "string", "nullT"); diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index c073bb9a6..1235a3f3e 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -104,6 +104,9 @@ export interface NativeLinkFeatures { /** Ed25519 keys and signatures with pki: scr_ed25519.c (the vendored * libsodium Ed25519). */ ed25519: boolean; + /** X509Certificate fields beyond the validity window and fingerprint: + * scr_x509.c. */ + x509: 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 53e160984..ef84f4d10 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -269,6 +269,8 @@ export interface CcOptions { /** Ed25519 with pki: compiles scr_ed25519.c (the vendored libsodium * Ed25519). */ ed25519?: boolean; + /** X509Certificate fields: compiles scr_x509.c. */ + x509?: boolean; /** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */ webcrypto?: boolean; /** perf_hooks event-loop metrics: compiles scr_perf_loop.c. */ @@ -354,6 +356,8 @@ export interface LibArchiveOptions { searchParams?: boolean; emitter?: boolean; zlib?: boolean; + /** X509Certificate fields: scr_x509.c. */ + x509?: boolean; copying?: boolean; textDecoderLegacy?: boolean; } diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 1578a0c80..59957f664 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -309,6 +309,9 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ 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"), + unit((u) => u.opts.x509 === true, "scr_x509.c"), + // The TLS socket certificate accessors read scr_tls.c and build with scr_x509.c. + unit((u) => u.tls && u.opts.x509 === true, "scr_tls_x509.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 diff --git a/packages/compiler/src/backend/native/library.ts b/packages/compiler/src/backend/native/library.ts index f04e9926c..cca706896 100644 --- a/packages/compiler/src/backend/native/library.ts +++ b/packages/compiler/src/backend/native/library.ts @@ -73,7 +73,7 @@ import { CacheInputsChangedError } from "./session.js"; * async_free refusal already guarantees nothing references them — and * scr_library.c (sink, arena, reset registry, library funnel) joins. The gated * units a library may reach are the pure-data ones (regex + the vendored matcher, assert, - * inspect, symbol, searchParams, emitter+dyn_handle, zlib); every + * inspect, symbol, searchParams, emitter+dyn_handle, zlib, x509); every * loop-hooked or ambient unit was refused at SC4005 before emission. * External-symbol contract: undefined references only to the target's C/math * runtime and system APIs. Windows embedders additionally link advapi32, @@ -115,6 +115,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise ...(opts.dynInvoke && opts.dynBuffer ? ["scr_bytes_dyn.c"] : []), ...(opts.dynTier === true ? ["scr_js_value.c", "scr_js_object.c"] : []), ...(opts.zlib ? ["scr_zlib.c"] : []), + ...(opts.x509 ? ["scr_x509.c"] : []), ...(opts.copying ? ["scr_copying.c"] : []), ]; const cflags = [ diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 9846e8d89..9312ecfd0 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -129,6 +129,7 @@ const features = { dh: false, pki: false, ed25519: false, + x509: 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 e2b24f8ca..13f74d4e1 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -65,6 +65,7 @@ const BASE: NativeLinkFeatures = { dh: false, pki: false, ed25519: false, + x509: false, webcrypto: false, perfLoop: false, }; diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index 02acb5462..c42ceb79e 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -71,6 +71,7 @@ export async function warmNativeCaches( dh: false, pki: false, ed25519: false, + x509: 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 e066c0412..4aaa86e0d 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -69,6 +69,7 @@ const native: EarlyExecutableNativeFeatures = { dh: false, pki: false, ed25519: false, + x509: false, webcrypto: false, perfLoop: false, }; diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 96922ca13..10fcae7c5 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -102,6 +102,7 @@ export interface EarlyExecutableNativeFeatures { dh: boolean; pki: boolean; ed25519: boolean; + x509: boolean; perfLoop: boolean; tlsCa: boolean; webcrypto: boolean; @@ -247,6 +248,7 @@ const BOOLEAN_NATIVE_KEYS = [ "dh", "pki", "ed25519", + "x509", "webcrypto", "perfLoop", ] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[]; diff --git a/packages/compiler/src/frontend/lowering/builtins/x509.ts b/packages/compiler/src/frontend/lowering/builtins/x509.ts new file mode 100644 index 000000000..bc0092369 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/x509.ts @@ -0,0 +1,487 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { locOf } from "../../program.js"; +import { + BOOL, + BYTES_U8, + DYN, + F64, + STRING, + UNDEFINED_T, + type IrExpr, + type IrFunction, + type IrLibFn, + type IrType, + type SrcLoc, +} from "../../../ir/ir.js"; +import { everyStmtList } from "../../../ir/traverse.js"; +import { unsupportedDiag } from "../../../diagnostics/diagnostic.js"; +import { X509_PUBLIC_KEY_FIELD } from "../../type-mapper.js"; +import { keyedHelperName } from "../stable-names.js"; +import { x509RecordCall } from "../lower-classes.js"; + +/* node:crypto's X509Certificate members beyond its fields: the methods + * and the publicKey getter. A certificate is the Dirent-style record + * (type-mapper.ts), and each member reads its raw DER. checkHost, + * checkEmail, checkIP, checkIssued, toLegacyObject and the PEM of + * toString/toJSON run in scr_x509.c; + * publicKey, verify and checkPrivateKey read keys in scr_pki.c. The first + * publicKey read stores the KeyObject in the record's hidden slot, so + * every read answers the same object, as in Node. */ + +/** Whether `t` is the X509Certificate record (its hidden publicKey slot + * marks the shape; no program can name that field). */ +export function isX509Type(lowerer: Lowerer, t: IrType | null | undefined): boolean { + return ( + t?.kind === "record" && + lowerer.shapes.get(t.shapeId)?.fields.some((f) => f.name === X509_PUBLIC_KEY_FIELD) === true + ); +} + +/** Whether `node` is an X509Certificate record. */ +function isX509Record(lowerer: Lowerer, node: ts.Expression): boolean { + const type = lowerer.typeOf(node); + const sym = type.getSymbol(); + if (!sym || ts.symbolName(sym) !== "X509Certificate" || !lowerer.isStdlibSymbol(sym)) + return false; + return isX509Type(lowerer, lowerer.mapTypeOf(type)); +} + +/** A certificate in an `unknown` or `any` slot would cross as a plain + * object of its fields, which Node's certificate is not: its fields are + * prototype getters, and toString() and toJSON() give its PEM. Every such + * conversion in the lowered program, including a certificate inside an + * array or record that converts, is refused where it happens. */ +export function refuseX509DynValues(lowerer: Lowerer, functions: readonly IrFunction[]): void { + // Certificates exist only where `new X509Certificate` built one. + if ( + !lowerer.valueHelpers.has("x509.record:bytes") && + !lowerer.valueHelpers.has("x509.record:str") && + !lowerer.valueHelpers.has("x509.record:der") + ) + return; + const seen = new Map(); + const holds = (t: IrType): boolean => { + switch (t.kind) { + case "record": { + const known = seen.get(t.shapeId); + if (known !== undefined) return known; + seen.set(t.shapeId, false); + const shape = lowerer.shapes.get(t.shapeId); + const result = + isX509Type(lowerer, t) || + (shape !== undefined && + (shape.fields.some((f) => holds(f.type)) || + (shape.indexValue !== undefined && holds(shape.indexValue)))); + seen.set(t.shapeId, result); + return result; + } + case "array": + case "set": + return holds(t.elem); + case "map": + return holds(t.key) || holds(t.value); + case "promise": + return holds(t.inner); + case "union": + return lowerer.unions.get(t.unionId)?.arms.some(holds) === true; + default: + return false; + } + }; + const visitor = { + expr: (e: IrExpr): boolean => { + if (e.kind === "dynFrom" && holds(e.value.type)) { + lowerer.pushDiag( + unsupportedDiag( + "SC1090", + e.loc, + "X509Certificate values in 'unknown' or 'any' slots (Node keeps a certificate's fields as prototype getters, and its toString() and toJSON() give the PEM; keep the X509Certificate type)", + ), + ); + } + return true; + }, + stmt: (): boolean => true, + }; + for (const fn of functions) everyStmtList(fn.body, visitor); +} + +/** String(x509), `${x509}` and x509 + "": the certificate's toString(), + * its PEM. */ +export function x509ToString(record: IrExpr, loc: SrcLoc): IrExpr { + return { kind: "libCall", fn: "crypto.x509Pem", args: [rawOf(record, loc)], type: STRING, loc }; +} + +/** Node keeps a certificate's fields as prototype getters, so own-key + * walks (Object.keys/values/entries, for-in, spread, hasOwnProperty) see + * none of them. Those walks over a certificate fence instead of listing + * the fields. */ +export function fenceX509KeyWalk( + lowerer: Lowerer, + t: IrType | null | undefined, + node: ts.Node, + what: string, +): void { + if (!isX509Type(lowerer, t)) return; + lowerer.unsupported( + "SC1090", + node, + `${what} over X509Certificate values (Node keeps their fields as prototype getters, so own-key walks see none; read the fields)`, + ); +} + +function rawOf(record: IrExpr, loc: SrcLoc): IrExpr { + if (record.type.kind !== "record") return record; + return { + kind: "recordGet", + obj: record, + shapeId: record.type.shapeId, + field: "raw", + type: BYTES_U8, + loc, + }; +} + +const dynUndefined = (loc: SrcLoc): IrExpr => ({ + kind: "dynFrom", + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type: DYN, + loc, +}); + +/** The method's string answer as `string | undefined`: "" is undefined. + * A lifted helper takes the text once. */ +function optionalString(lowerer: Lowerer, text: IrExpr, type: IrType, loc: SrcLoc): IrExpr { + if (type.kind !== "union") return text; + const def = lowerer.unions.get(type.unionId); + const stringTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; + const undefinedTag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1; + if (stringTag < 0 || undefinedTag < 0) return text; + const key = `x509.optional:${type.unionId}`; + let helper = lowerer.valueHelpers.get(key); + if (!helper) { + helper = keyedHelperName(lowerer, "%x509.optional", key); + lowerer.valueHelpers.set(key, helper); + const s: IrExpr = { kind: "varRef", localId: "s.0", type: STRING, loc }; + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "s.0", name: "s", type: STRING }], + returnType: type, + locals: [{ id: "s.0", name: "s", type: STRING, mutable: false }], + body: [ + { + kind: "return", + value: { + kind: "ternary", + cond: { + kind: "strEq", + negated: false, + left: s, + right: { kind: "strLit", value: "", type: STRING, loc }, + type: BOOL, + loc, + }, + then: { + kind: "unionWrap", + unionId: type.unionId, + tag: undefinedTag, + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type, + loc, + }, + else_: { + kind: "unionWrap", + unionId: type.unionId, + tag: stringTag, + value: s, + type, + loc, + }, + type, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: helper, args: [text], type, loc }; +} + +/** `x509.publicKey`: the cached KeyObject (a lifted helper fills the + * hidden slot on the first read). */ +export function lowerX509PublicKey( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.name.text !== "publicKey" || access.questionDotToken) return null; + if (!isX509Record(lowerer, access.expression)) return null; + const loc = locOf(access); + const record = lowerer.lowerExpr(access.expression); + if (record.type.kind !== "record") return null; + const shapeId = record.type.shapeId; + const key = `x509.publicKey:${shapeId}`; + let helper = lowerer.valueHelpers.get(key); + if (!helper) { + helper = keyedHelperName(lowerer, "%x509.publicKey", key); + lowerer.valueHelpers.set(key, helper); + const c: IrExpr = { kind: "varRef", localId: "c.0", type: record.type, loc }; + const slot: IrExpr = { + kind: "recordGet", + obj: c, + shapeId, + field: X509_PUBLIC_KEY_FIELD, + type: DYN, + loc, + }; + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "c.0", name: "c", type: record.type }], + returnType: DYN, + locals: [{ id: "c.0", name: "c", type: record.type, mutable: false }], + body: [ + { + kind: "if", + cond: { kind: "dynTest", test: "undefined", value: slot, type: BOOL, loc }, + then: [ + { + kind: "recordSet", + obj: c, + shapeId, + field: X509_PUBLIC_KEY_FIELD, + value: { + kind: "libCall", + fn: "crypto.x509PublicKey", + args: [rawOf(c, loc)], + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + { kind: "return", value: slot, loc }, + ], + loc, + }); + } + return { kind: "call", callee: helper, args: [record], type: DYN, loc }; +} + +type X509Method = { fn: IrLibFn; min: number; max: number; form: string }; + +const METHODS: ReadonlyMap = new Map([ + ["verify", { fn: "crypto.x509Verify", min: 1, max: 1, form: "verify(publicKey)" }], + [ + "checkPrivateKey", + { fn: "crypto.x509CheckPrivateKey", min: 1, max: 1, form: "checkPrivateKey(privateKey)" }, + ], + ["checkIssued", { fn: "crypto.x509CheckIssued", min: 1, max: 1, form: "checkIssued(otherCert)" }], + ["checkHost", { fn: "crypto.x509CheckHost", min: 1, max: 2, form: "checkHost(name[, options])" }], + [ + "checkEmail", + { fn: "crypto.x509CheckEmail", min: 1, max: 2, form: "checkEmail(email[, options])" }, + ], + ["checkIP", { fn: "crypto.x509CheckIP", min: 1, max: 2, form: "checkIP(ip[, options])" }], + ["toLegacyObject", { fn: "crypto.x509LegacyObject", min: 0, max: 0, form: "toLegacyObject()" }], + ["toString", { fn: "crypto.x509Pem", min: 0, max: 0, form: "toString()" }], + ["toJSON", { fn: "crypto.x509Pem", min: 0, max: 0, form: "toJSON()" }], +]); + +/** The X509Certificate methods: verify, checkPrivateKey, checkIssued, + * checkHost, checkEmail, checkIP, toLegacyObject, toString and toJSON. */ +export function lowerX509Call(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 method = METHODS.get(access.name.text); + if (!method) return null; + if (!isX509Record(lowerer, access.expression)) return null; + const loc = locOf(call); + const args = call.arguments.nodes; + if (args.length < method.min || args.length > method.max || args.some(ts.isSpreadElement)) { + lowerer.noLowering( + `X509Certificate.${access.name.text} with ${args.length} argument(s)`, + call, + `the supported form is ${method.form}`, + ); + } + const raw = rawOf(lowerer.lowerExpr(access.expression), loc); + if (method.fn === "crypto.x509LegacyObject") { + return { kind: "libCall", fn: method.fn, args: [raw], type: DYN, loc }; + } + if (method.fn === "crypto.x509Pem") + return { kind: "libCall", fn: method.fn, args: [raw], type: STRING, loc }; + if (method.fn === "crypto.x509CheckIssued") { + if (!isX509Record(lowerer, args[0]!)) { + // Node's validation of otherCert, after both operands evaluate. + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: raw, loc }], + result: { + kind: "libCall", + fn: "error.argTypeThrow", + args: [ + { kind: "strLit", value: "otherCert", type: STRING, loc }, + { kind: "strLit", value: "an instance of X509Certificate", type: STRING, loc }, + lowerer.lowerExprExpecting(args[0]!, DYN), + ], + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } + const other = rawOf(lowerer.lowerExpr(args[0]!), loc); + return { kind: "libCall", fn: method.fn, args: [raw, other], type: BOOL, loc }; + } + const first = lowerer.lowerExprExpecting(args[0]!, DYN); + if (method.fn === "crypto.x509Verify" || method.fn === "crypto.x509CheckPrivateKey") { + return { kind: "libCall", fn: method.fn, args: [raw, first], type: BOOL, loc }; + } + const options = args.length > 1 ? lowerer.lowerExprExpecting(args[1]!, DYN) : dynUndefined(loc); + const text: IrExpr = { + kind: "libCall", + fn: method.fn, + args: [raw, first, options], + type: STRING, + loc, + }; + return optionalString(lowerer, text, lowerer.mapTypeOf(lowerer.typeOf(call)) ?? STRING, loc); +} + +/** The TLS socket certificate accessors. getPeerCertificate([detailed]) + * and getCertificate() answer Node's legacy object (scr_tls_x509.c): + * Node reads `detailed` only when it is true. getPeerX509Certificate() and + * getX509Certificate() build a new X509Certificate from the certificate's + * DER on each call, or answer undefined without one. Null for other + * members. */ +export function lowerTlsCertificateCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + receiver: () => IrExpr, +): IrExpr | null { + const loc = locOf(call); + const args = call.arguments.nodes; + if (name === "getPeerCertificate" || name === "getCertificate") { + const max = name === "getPeerCertificate" ? 1 : 0; + if (args.length > max || args.some(ts.isSpreadElement)) { + lowerer.noLowering( + `${name} with ${args.length} argument(s)`, + call, + name === "getPeerCertificate" + ? "the form is getPeerCertificate(detailed?)" + : "getCertificate() takes no arguments", + ); + } + const socket = receiver(); + const value: IrExpr = + name === "getCertificate" + ? { kind: "libCall", fn: "tls.sockGetCertificate", args: [socket], type: DYN, loc } + : { + kind: "libCall", + fn: "tls.sockGetPeerCertificate", + args: [ + socket, + args.length > 0 ? lowerer.lowerExprExpecting(args[0]!, DYN) : dynUndefined(loc), + ], + type: DYN, + loc, + }; + const declared = lowerer.mapTypeOf(lowerer.typeOf(call)); + return declared === null || declared.kind === "dyn" + ? value + : lowerer.coerceInto(call, value, declared); + } + if (name !== "getPeerX509Certificate" && name !== "getX509Certificate") return null; + if (args.length > 0) { + lowerer.noLowering( + `${name} with ${args.length} argument(s)`, + call, + `${name}() takes no arguments`, + ); + } + const type = lowerer.mapTypeOf(lowerer.typeOf(call)); + const def = type?.kind === "union" ? lowerer.unions.get(type.unionId) : undefined; + const recordTag = def ? def.arms.findIndex((arm) => isX509Type(lowerer, arm)) : -1; + const undefinedTag = def ? def.arms.findIndex((arm) => arm.kind === "undefinedT") : -1; + if (type?.kind !== "union" || !def || recordTag < 0 || undefinedTag < 0) { + lowerer.noLowering( + `tlsSocket.${name} with this result type`, + call, + "its type is X509Certificate | undefined", + ); + } + const record = def.arms[recordTag] as IrType & { kind: "record" }; + const der: IrExpr = { + kind: "libCall", + fn: name === "getPeerX509Certificate" ? "tls.sockPeerCertDer" : "tls.sockOwnCertDer", + args: [receiver()], + type: BYTES_U8, + loc, + }; + const key = `tls.x509:${type.unionId}`; + let helper = lowerer.valueHelpers.get(key); + if (!helper) { + helper = keyedHelperName(lowerer, "%tls.x509", key); + lowerer.valueHelpers.set(key, helper); + const d: IrExpr = { kind: "varRef", localId: "d.0", type: BYTES_U8, loc }; + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "d.0", name: "der", type: BYTES_U8 }], + returnType: type, + locals: [{ id: "d.0", name: "der", type: BYTES_U8, mutable: false }], + body: [ + { + kind: "return", + value: { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: { + kind: "bytesIntrinsic", + method: "length", + receiver: d, + args: [], + type: F64, + loc, + }, + right: { kind: "numLit", value: 0, type: F64, loc }, + type: BOOL, + loc, + }, + then: { + kind: "unionWrap", + unionId: type.unionId, + tag: undefinedTag, + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type, + loc, + }, + else_: { + kind: "unionWrap", + unionId: type.unionId, + tag: recordTag, + // The handshake's DER, read without PEM detection. + value: x509RecordCall(lowerer, d, record, loc, true), + type, + loc, + }, + type, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: helper, args: [der], type, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index b26a3dd9f..ab5ddeab6 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -58,6 +58,7 @@ import { fenceNodeModuleMutation, lowerRequireCacheHas } from "../lower-node-mod import { errorToStringCall } from "../error-methods.js"; import { classToStringConversion, classUnionToString } from "../class-to-primitive.js"; import { isPerfEntryType, perfEntryTag } from "../builtins/performance.js"; +import { isX509Type, x509ToString } from "../builtins/x509.js"; import { guardNullProto, nullProtoRecordShape } from "../null-prototype.js"; import { numLit, varRef } from "../../../ir/build.js"; import { lowerUrlAssignment } from "../builtins/url.js"; @@ -798,6 +799,7 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr // other record is Object.prototype.toString's constant. const shape = lowerer.shapes.get(e.type.shapeId); if (isPerfEntryType(lowerer, e.type)) return perfEntryTag(lowerer, e, e.type, e.loc); + if (isX509Type(lowerer, e.type)) return x509ToString(e, e.loc); if (shape && !shape.tuple && !shape.fields.some((f) => f.name === "toString")) { // A null-prototype record (Object.groupBy) has no toString or // valueOf: ToPrimitive throws Node's TypeError. diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 4cc0bee1a..d6f413dc1 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -28,6 +28,7 @@ import type { IndexMergeContributor } from "../containers/indexed-objects.js"; import type { KeyOrderSegment } from "../key-order-witness.js"; import { PERF_ENTRY_MARKER, + X509_PUBLIC_KEY_FIELD, isGenericCallableMemberType, jsOpenObjectType, } from "../../type-mapper.js"; @@ -501,6 +502,13 @@ function fenceAccessorSpreadSource( "object spread over performance entries (Node keeps their fields as prototype getters, so own-key walks see none; read the fields or call toJSON())", ); } + if (srcShape && srcShape.fields.some((f) => f.name === X509_PUBLIC_KEY_FIELD)) { + lowerer.unsupported( + "SC1090", + prop, + "object spread over X509Certificate values (Node keeps their fields as prototype getters, so own-key walks see none; read the fields)", + ); + } if (srcShape && shapeHasAccessorSlots(srcShape)) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 8148e85f5..b542fa303 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -58,6 +58,7 @@ import { BYTES_ELEMENT_SIZE, } from "../../../ir/ir.js"; import { lowerNavigatorMember } from "../builtins/navigator.js"; +import { lowerX509PublicKey } from "../builtins/x509.js"; import { NODE_COMPAT_VERSION } from "../builtins/process.js"; import { inlineNamespaceRequireOf, @@ -347,6 +348,11 @@ export function lowerPropertyAccessExpr( const reading = lowerHistogramProperty(lowerer, expr); if (reading) return reading; } + // An X509Certificate's publicKey KeyObject. + { + const publicKey = lowerX509PublicKey(lowerer, expr); + if (publicKey) return publicKey; + } // `.length` of a forwarded fixed tuple (a typed `arguments`, a tuple // rest parameter, an alias of either): the signature's arity. if (expr.name.text === "length" && !expr.questionDotToken) { diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 8ac97fd32..87102377c 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -167,6 +167,7 @@ import { lowerPerfHooksCall, perfEntryTag, } from "./builtins/performance.js"; +import { fenceX509KeyWalk } from "./builtins/x509.js"; import { lowerProcStreamMethodCall, lowerProcessBuiltinModuleNamespace, @@ -2957,6 +2958,7 @@ function lowerObjectOwnCall( } else if (type.kind === "record") { const shape = lowerer.shapes.get(type.shapeId); fencePerfEntryKeyWalk(lowerer, type, receiverNode ?? call, `Object.prototype.${method}`); + fenceX509KeyWalk(lowerer, type, receiverNode ?? call, `Object.prototype.${method}`); if (!shape || shape.tuple || shapeHasAccessorSlots(shape)) { lowerer.noLowering( "Object.prototype." + method + " over this record shape", @@ -8921,6 +8923,7 @@ function lowerObjectStaticCall( // RESULTS, invoked in key order. The static field walk models neither // (accessor slots live outside declaredOrder), so the surface fences. fencePerfEntryKeyWalk(lowerer, argIr, call, `Object.${member}`); + fenceX509KeyWalk(lowerer, argIr, call, `Object.${member}`); if (shapeHasAccessorSlots(shape)) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 3e49304d2..2283c7dfb 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -43,6 +43,7 @@ import type { FnCtx, Lowerer } from "./lowerer.js"; import { inlineNamespaceRequireOf, lowerWithInlineRequireInit } from "./cjs-inline-require.js"; import { BOOL, + BYTES_U8, canDynCheckTo, DATE_T, DYN, @@ -99,6 +100,7 @@ import { jsOpenObjectType, typeKey, withUnitArm, + X509_PUBLIC_KEY_FIELD, } from "../type-mapper.js"; import { canonicalBuiltinModule, @@ -10108,6 +10110,52 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const agent = lowerHttpAgentNew(lowerer, expr); if (agent) return agent; } + // `new crypto.X509Certificate(data)` — the Dirent-style data record: + // the certificate's lowered members (fingerprint — the SHA-1 of the + // DER, uppercase colon-separated — plus the validFrom/validTo + // validity window in Node's ASN1_TIME_print shape) compute AT + // CONSTRUCTION, when Node parses too, so unparseable input throws + // Node's exact PEM error here (ERR_OSSL_PEM_NO_START_LINE) and the + // handle never exists. Both import forms (`crypto.X509Certificate` + // through the namespace, named `X509Certificate`); Buffer input only + // — the readFileSync idiom. mapType interns the matching record, so + // locals and the composed member reads all flow. The data argument + // feeds THREE field computations, so construction goes through an + // interned helper whose parameter evaluates it exactly once. + { + const callee = expr.expression; + const isX509 = + (ts.isPropertyAccessExpression(callee) && + callee.name.text === "X509Certificate" && + lowerer.builtinNamespaceModuleOf(callee.expression) === "crypto") || + (ts.isIdentifier(callee) && + (() => { + const bi = lowerer.builtinImportOf(callee); + return bi?.module === "crypto" && bi.member === "X509Certificate"; + })()); + if (isX509) { + const args: readonly ts.Expression[] = expr.arguments?.nodes ?? []; + if (args.length !== 1) { + lowerer.noLowering( + "X509Certificate with this argument shape", + expr, + "the supported form is new X509Certificate(readFileSync(path))", + ); + } + const data = lowerer.lowerExpr(args[0]!); + const isBytes = data.type.kind === "bytes" && data.type.elem === "u8"; + if (!isBytes && data.type.kind !== "string") { + lowerer.noLowering( + `X509Certificate over '${lowerer.fmt(data.type)}' data`, + args[0]!, + "pass the certificate Buffer or PEM string (an fs.readFileSync result)", + ); + } + const t = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (t?.kind !== "record") lowerer.badType(expr, lowerer.typeOf(expr)); + return x509RecordCall(lowerer, data, t, loc); + } + } if ( ts.isIdentifier(expr.expression) || (ts.isPropertyAccessExpression(expr.expression) && @@ -10888,99 +10936,6 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { ); } } - // `new crypto.X509Certificate(data)` — the Dirent-style data record: - // the certificate's lowered members (fingerprint — the SHA-1 of the - // DER, uppercase colon-separated — plus the validFrom/validTo - // validity window in Node's ASN1_TIME_print shape) compute AT - // CONSTRUCTION, when Node parses too, so unparseable input throws - // Node's exact PEM error here (ERR_OSSL_PEM_NO_START_LINE) and the - // handle never exists. Both import forms (`crypto.X509Certificate` - // through the namespace, named `X509Certificate`); Buffer input only - // — the readFileSync idiom. mapType interns the matching record, so - // locals and the composed member reads all flow. The data argument - // feeds THREE field computations, so construction goes through an - // interned helper whose parameter evaluates it exactly once. - { - const callee = expr.expression; - const isX509 = - (ts.isPropertyAccessExpression(callee) && - callee.name.text === "X509Certificate" && - lowerer.builtinNamespaceModuleOf(callee.expression) === "crypto") || - (ts.isIdentifier(callee) && - (() => { - const bi = lowerer.builtinImportOf(callee); - return bi?.module === "crypto" && bi.member === "X509Certificate"; - })()); - if (isX509) { - const args: readonly ts.Expression[] = expr.arguments?.nodes ?? []; - if (args.length !== 1) { - lowerer.noLowering( - "X509Certificate with this argument shape", - expr, - "the supported form is new X509Certificate(readFileSync(path))", - ); - } - const data = lowerer.lowerExpr(args[0]!); - const isBytes = data.type.kind === "bytes" && data.type.elem === "u8"; - if (!isBytes && data.type.kind !== "string") { - lowerer.noLowering( - `X509Certificate over '${lowerer.fmt(data.type)}' data`, - args[0]!, - "pass the certificate Buffer or PEM string (an fs.readFileSync result)", - ); - } - const t = lowerer.mapTypeOf(lowerer.typeOf(expr)); - if (t?.kind !== "record") lowerer.badType(expr, lowerer.typeOf(expr)); - const key = `x509.record:${isBytes ? "bytes" : "str"}`; - let helper = lowerer.valueHelpers.get(key); - if (!helper) { - 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 }; - const field = ( - name: string, - fn: - | "crypto.x509Fingerprint" - | "crypto.x509FingerprintStr" - | "crypto.x509ValidFrom" - | "crypto.x509ValidFromStr" - | "crypto.x509ValidTo" - | "crypto.x509ValidToStr", - ): { name: string; value: IrExpr } => ({ - name, - value: { kind: "libCall", fn, args: [dRef], type: STRING, loc }, - }); - lowerer.liftedFns.push({ - name: helper, - params: [{ localId: "d.0", name: "d", type: dataT }], - returnType: t, - locals: [{ id: "d.0", name: "d", type: dataT, mutable: false }], - body: [ - { - kind: "return", - value: { - kind: "recordLit", - fields: [ - field( - "fingerprint", - isBytes ? "crypto.x509Fingerprint" : "crypto.x509FingerprintStr", - ), - field("validFrom", isBytes ? "crypto.x509ValidFrom" : "crypto.x509ValidFromStr"), - field("validTo", isBytes ? "crypto.x509ValidTo" : "crypto.x509ValidToStr"), - ], - type: t, - loc, - }, - loc, - }, - ], - loc, - }); - } - return { kind: "call", callee: helper, args: [data], type: t, loc }; - } - } // `new X(...)` through a class VALUE (a classval-typed binding, array // element, map read, param): the newValue dispatch through the class // object's construct thunk. Arguments complete against the STATIC @@ -11254,6 +11209,151 @@ function genericFieldFnNodeOf( return null; } +/** The lifted `new crypto.X509Certificate(d)` constructor: raw decodes the + * input once (Node's PEM error throws here), and every other field reads + * raw. subjectAltName, infoAccess and signatureAlgorithm hold undefined + * where the runtime answers "". */ +/** The X509Certificate record `t` built from `data`, a certificate Buffer + * (PEM or DER) or PEM string, through the interned constructor helper. + * `der` marks a Buffer known to hold a certificate's DER (a TLS socket's), + * which is read without looking for PEM armor inside it. */ +export function x509RecordCall( + lowerer: Lowerer, + data: IrExpr, + t: IrType & { kind: "record" }, + loc: SrcLoc, + der = false, +): IrExpr { + const isBytes = data.type.kind === "bytes" && data.type.elem === "u8"; + const key = `x509.record:${der ? "der" : isBytes ? "bytes" : "str"}`; + let helper = lowerer.valueHelpers.get(key); + if (!helper) { + helper = keyedHelperName(lowerer, "%x509.record", key); + lowerer.valueHelpers.set(key, helper); + lowerer.liftedFns.push(x509RecordHelper(lowerer, helper, data.type, t, isBytes, der, loc)); + } + return { kind: "call", callee: helper, args: [data], type: t, loc }; +} + +function x509RecordHelper( + lowerer: Lowerer, + name: string, + dataT: IrType, + t: IrType & { kind: "record" }, + isBytes: boolean, + der: boolean, + loc: SrcLoc, +): IrFunction { + const shape = lowerer.shapes.get(t.shapeId); + if (!shape) throw new InternalCompilerError("lowerer bug: X509Certificate shape unregistered"); + const raw: IrExpr = { kind: "varRef", localId: "raw.0", type: BYTES_U8, loc }; + const call = (fn: IrLibFn, type: IrType): IrExpr => ({ + kind: "libCall", + fn, + args: [raw], + type, + loc, + }); + const locals: IrLocal[] = [ + { id: "d.0", name: "d", type: dataT, mutable: false }, + { id: "raw.0", name: "raw", type: BYTES_U8, mutable: false }, + ]; + const decls: IrStmt[] = [ + { + kind: "varDecl", + localId: "raw.0", + init: { + kind: "libCall", + fn: der ? "crypto.x509RawDer" : isBytes ? "crypto.x509Raw" : "crypto.x509RawStr", + args: [{ kind: "varRef", localId: "d.0", type: dataT, loc }], + type: BYTES_U8, + loc, + }, + loc, + }, + ]; + const validTime = (fn: IrLibFn): IrExpr => ({ + kind: "libCall", + fn: "date.newString", + args: [call(fn, STRING)], + type: DATE_T, + loc, + }); + const values: Record IrExpr> = { + ca: () => call("crypto.x509Ca", BOOL), + fingerprint: () => call("crypto.x509Fingerprint", STRING), + fingerprint256: () => call("crypto.x509Fingerprint256", STRING), + fingerprint512: () => call("crypto.x509Fingerprint512", STRING), + subject: () => call("crypto.x509Subject", STRING), + subjectAltName: (type) => optionalText("san", "crypto.x509SubjectAltName", type), + infoAccess: (type) => optionalText("access", "crypto.x509InfoAccess", type), + issuer: () => call("crypto.x509Issuer", STRING), + raw: () => raw, + serialNumber: () => call("crypto.x509SerialNumber", STRING), + signatureAlgorithm: (type) => optionalText("sigalg", "crypto.x509SignatureAlgorithm", type), + signatureAlgorithmOid: () => call("crypto.x509SignatureAlgorithmOid", STRING), + validFrom: () => call("crypto.x509ValidFrom", STRING), + validFromDate: () => validTime("crypto.x509ValidFrom"), + validTo: () => call("crypto.x509ValidTo", STRING), + validToDate: () => validTime("crypto.x509ValidTo"), + [X509_PUBLIC_KEY_FIELD]: () => ({ + kind: "dynFrom", + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type: DYN, + loc, + }), + }; + /** A string | undefined field: the runtime's text, or undefined for "". */ + function optionalText(local: string, fn: IrLibFn, type: IrType): IrExpr { + if (type.kind !== "union") + throw new InternalCompilerError("lowerer bug: X509Certificate optional field type"); + const def = lowerer.unions.get(type.unionId); + const stringTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; + const undefinedTag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1; + if (stringTag < 0 || undefinedTag < 0) + throw new InternalCompilerError("lowerer bug: X509Certificate optional field arms"); + const id = `${local}.0`; + locals.push({ id, name: local, type: STRING, mutable: false }); + decls.push({ kind: "varDecl", localId: id, init: call(fn, STRING), loc }); + const text: IrExpr = { kind: "varRef", localId: id, type: STRING, loc }; + return { + kind: "ternary", + cond: { + kind: "strEq", + negated: false, + left: text, + right: { kind: "strLit", value: "", type: STRING, loc }, + type: BOOL, + loc, + }, + then: { + kind: "unionWrap", + unionId: type.unionId, + tag: undefinedTag, + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type, + loc, + }, + else_: { kind: "unionWrap", unionId: type.unionId, tag: stringTag, value: text, type, loc }, + type, + loc, + }; + } + const fields = shape.fields.map((f) => { + const value = values[f.name]; + if (!value) throw new InternalCompilerError(`lowerer bug: X509Certificate field ${f.name}`); + return { name: f.name, value: value(f.type) }; + }); + return { + name, + params: [{ localId: "d.0", name: "d", type: dataT }], + returnType: t, + locals, + body: [...decls, { kind: "return", value: { kind: "recordLit", fields, type: t, loc }, loc }], + loc, + }; +} + /** Whether a JavaScript class's own binding is the target of * `Object.defineProperty`/`defineProperties` in its file * (`Object.defineProperty(Definition, "name", { value })`): its constructor diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index cb79accea..4121d13b4 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -1,4 +1,5 @@ import { keyedHelperName } from "./stable-names.js"; +import { lowerTlsCertificateCall } from "./builtins/x509.js"; import { dynUndefinedExpr, nodeThrowExpr, @@ -1765,12 +1766,82 @@ function lowerNetServerMethodCall( ); return { kind: "libCall", fn: "net.serverCloseCb", args: [receiver, cb], type: VOID, loc }; } + if (name === "addContext" || name === "setSecureContext") { + // tls.Server's SNI table and default context (scr_tls.c): addContext + // takes a SecureContext or createSecureContext's options; the runtime + // throws Node's errors and the fence on a non-TLS server. + requireStatementPosition(lowerer, call, `server.${name}(...)`); + const want = name === "addContext" ? 2 : 1; + if (args.length !== want || args.some(ts.isSpreadElement)) { + lowerer.noLowering( + `${name} with ${args.length} arguments`, + call, + name === "addContext" + ? "the form is addContext(servername, context)" + : "the form is setSecureContext(options)", + ); + } + const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T); + if (name === "setSecureContext") { + const options = lowerer.lowerExprExpecting(args[0]!, DYN); + return { + kind: "libCall", + fn: "tls.serverSetSecureContext", + args: [receiver, options], + type: VOID, + loc, + }; + } + const servername = lowerer.lowerExprExpecting(args[0]!, DYN); + const context = lowerer.lowerExpr(args[1]!); + if (context.type.kind === "secureCtx") { + return { + kind: "libCall", + fn: "tls.serverAddContextCtx", + args: [receiver, servername, context], + type: VOID, + loc, + }; + } + return { + kind: "libCall", + fn: "tls.serverAddContext", + args: [receiver, servername, lowerer.coerceInto(args[1]!, context, DYN)], + type: VOID, + loc, + }; + } if ((name === "on" || name === "once" || name === "addListener") && args.length === 2) { requireStatementPosition(lowerer, call, `server.${name}(...)`); const once = boolLit(name === "once", loc); const evT = lowerer.typeOf(args[0]!); const event = evT.isStringLiteralType() ? evT.value : null; const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T); + if (event === "tlsClientError") { + // A failed handshake on a tls or https server: (err, tlsSocket). + const listener = args[1]!; + const { cb } = lowerCallbackArgShared( + lowerer, + listener, + "tlsClientError listeners", + 2, + (p, i) => + i === 0 ? p.kind === "object" && p.className === "%Error" : p.kind === "netSocket", + "use (err, tlsSocket), (err) or ()", + { + dynTuple: [ERROR_T, NETSOCKET_T], + checkAllParams: true, + adapt: (c) => voidizedCallback(lowerer, c, locOf(listener)), + }, + ); + return { + kind: "libCall", + fn: "tls.serverOnClientError", + args: [receiver, cb, once], + type: VOID, + loc, + }; + } if (event === "connection" || event === "secureConnection") { // 'secureConnection' rides the same list: a TLS server's // 'connection' listeners are DEFERRED to handshake completion @@ -2632,6 +2703,12 @@ function lowerNetSocketMethodCall( ? value : lowerer.coerceInto(call, value, declared); } + { + const certificate = lowerTlsCertificateCall(lowerer, call, name, () => + coerceToHandle(lowerer, access.expression, NETSOCKET_T), + ); + if (certificate) return certificate; + } if (name === "isSessionReused" && args.length === 0) { // TLSSocket.isSessionReused(): the client never offers a cached // session (no session resumption, a documented TLS divergence), so @@ -4941,19 +5018,12 @@ function lowerTlsServerOptions( } if (k !== "cert" && k !== "key") { const init = ts.isPropertyAssignment(prop) ? prop.initializer : null; - if ( - (k === "ca" || k === "rejectUnauthorized") && - (init === null || sideEffectFreeOptionValue(init)) - ) { - // Both are inert without requestCert (which fences): the server's - // `ca` verifies CLIENT certificates and rejectUnauthorized only - // gates that verification — Node-observably droppable, matching + if (k === "rejectUnauthorized" && (init === null || sideEffectFreeOptionValue(init))) { + // Without requestCert (a literal with requestCert or ca rides the + // runtime walk) rejectUnauthorized gates nothing — droppable, like // the runtime walk's split (effectful values keep the fence). continue; } - if (k === "requestCert" && (init === null || init.kind === ts.SyntaxKind.FalseKeyword)) { - continue; // Node's false default, spelled out - } fenceOrDropOptionKey( lowerer, prop, @@ -4964,8 +5034,6 @@ function lowerTlsServerOptions( { SNICallback: "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", }, ); continue; // an undocumented key, dropped like Node drops it @@ -5063,12 +5131,19 @@ function tlsLiteralNeedsRuntimeWalk( ); } +/** The client-certificate options (requestCert, and ca, which also + * completes the server's own chain): the runtime walk implements them for + * tls.createServer and https.createServer, and the literal cert/key + * lowering does not. */ +const TLS_CLIENT_AUTH_OPTIONS: ReadonlySet = new Set(["requestCert", "ca"]); + /** tls.createServer options the runtime walk implements and the literal * cert/key lowering does not. */ const TLS_SERVER_RUNTIME_OPTIONS: ReadonlySet = new Set([ "ALPNProtocols", "minVersion", "maxVersion", + ...TLS_CLIENT_AUTH_OPTIONS, ]); function lowerTlsServerOptionsOrDyn( @@ -5092,7 +5167,7 @@ function lowerTlsServerOptionsOrDyn( 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", + "SNICallback lowers with cert and key; ALPNProtocols, minVersion, maxVersion, requestCert, ca and the validated options are not combined with it yet", ); } } @@ -5124,14 +5199,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/socket/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout and throws the catchable + * port/host/socket/rejectUnauthorized/ca/cert/key/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/socket/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/cert/key/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); } @@ -5368,7 +5443,13 @@ function lowerTlsModuleCall( "the supported form is createSecureContext({ cert, key })", ); } - const opts = lowerTlsServerOptionsOrDyn(lowerer, args[0]!, "tls.createSecureContext"); + const opts = lowerTlsServerOptionsOrDyn( + lowerer, + args[0]!, + "tls.createSecureContext", + false, + TLS_CLIENT_AUTH_OPTIONS, + ); if (opts.dyn !== undefined) { return { kind: "libCall", @@ -5625,7 +5706,13 @@ function lowerHttpsModuleCall( "the supported form is createServer({ cert, key }, (req, res) => ...)", ); } - const opts = lowerTlsServerOptionsOrDyn(lowerer, args[0]!, "https.createServer", true); + const opts = lowerTlsServerOptionsOrDyn( + lowerer, + args[0]!, + "https.createServer", + true, + TLS_CLIENT_AUTH_OPTIONS, + ); if (args.length === 1) { // The handler-less form: 'request' listeners arrive later via // server.on("request", ...) — the createSecureServer emission's @@ -6281,7 +6368,9 @@ function runtimeRequestOptions( return ( (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) && - prop.name.text === "socketPath" + (prop.name.text === "socketPath" || + // a client certificate: the runtime request walk reads cert and key + (secureish && (prop.name.text === "cert" || prop.name.text === "key"))) ); }); if (!needsRuntime) return null; diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 82c0dbb3e..a19061fde 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -248,6 +248,7 @@ import { constituentTypes } from "../ts7/checker.js"; import { npmStaticPackageOfPath } from "../npm-static.js"; import { fenceNodeModuleMutation } from "./lower-node-module.js"; import { fencePerfEntryKeyWalk } from "./builtins/performance.js"; +import { fenceX509KeyWalk } from "./builtins/x509.js"; import { classGeneratorIterable } from "./class-iterables.js"; import { iterableUnionOf, lowerForOfIterable } from "./iterable-values.js"; @@ -11935,6 +11936,7 @@ function lowerForIn(lowerer: Lowerer, stmt: ts.ForInStatement): IrStmt { // them (accessor slots live outside declaredOrder), so the loop // fences rather than skip keys. fencePerfEntryKeyWalk(lowerer, receiver.type, stmt.expression, "for-in"); + fenceX509KeyWalk(lowerer, receiver.type, stmt.expression, "for-in"); if (shapeHasAccessorSlots(rShape)) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 35080e8f8..2dd44bf3e 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -63,6 +63,7 @@ import { discoverWasmModules, type WasmModuleReport } from "../../wasm/discover. import { encodeBase64 } from "../../wasm/base64.js"; import { resolveRequireRuntime } from "../runtime-resolve.js"; import { lowerKeyObjectCall } from "./builtins/key-object.js"; +import { lowerX509Call, refuseX509DynValues } from "./builtins/x509.js"; import { isEcdhType, lowerEcdhCall } from "./builtins/ecdh.js"; import { isDiffieHellmanType, lowerDiffieHellmanCall } from "./builtins/dh.js"; import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js"; @@ -7825,6 +7826,7 @@ export class Lowerer { // every producer and consumer agrees. Programs with no unregistered // reference are untouched — byte-stability holds. checkRequireBindingAccesses(this, functions); + refuseX509DynValues(this, functions); const module: IrModule | null = this.diags.length > 0 ? null @@ -13237,6 +13239,7 @@ export class Lowerer { lowerStaticBlobCall(this, expr) ?? lowerWebCryptoCall(this, expr) ?? lowerKeyObjectCall(this, expr) ?? + lowerX509Call(this, expr) ?? lowerEcdhCall(this, expr) ?? lowerDiffieHellmanCall(this, expr) ?? lowerNetAddressCall(this, expr) ?? diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index fc159116f..8a9b59c9a 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -169,9 +169,10 @@ export const WEBCRYPTO_DYN_TYPES: ReadonlySet = new Set([ "AesDerivedKeyParams", ]); -/** 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). */ +/** node:crypto's key, signature and X509Certificate check option + * interfaces: the key functions, KeyObject.export, sign/verify and the + * X509Certificate checks take them as checked-dynamic arguments + * (scr_lib.c, scr_pki.c, scr_x509.c). */ export const CRYPTO_KEY_OPTION_DYN_TYPES: ReadonlySet = new Set([ "AsymmetricKeyExportOptions", "ECKeyPairKeyObjectOptions", @@ -190,6 +191,7 @@ export const CRYPTO_KEY_OPTION_DYN_TYPES: ReadonlySet = new Set([ "SigningOptions", "VerifyKeyObjectInput", "VerifyPublicKeyInput", + "X509CheckOptions", ]); export const FETCH_AMBIENT_TYPES = [ @@ -4457,13 +4459,25 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ), }; } - // 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- - // checked like SecureContext; @types/node declares the class inside - // `declare module "crypto"`, the shipped fallback mirrors it. - // JSON.stringify of one shows the three fields where Node's - // internal-slot object shows {} — the Dirent %dtype edge's cousin. + // X509Certificate.toLegacyObject()'s PeerCertificate (and tls's detailed + // form) is a plain checked-dynamic object. + if ( + psym !== undefined && + (ts.symbolName(psym) === "PeerCertificate" || + ts.symbolName(psym) === "DetailedPeerCertificate") && + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), + ) + ) { + return DYN; + } + // crypto.X509Certificate — the Dirent-style data record: its fields are + // the certificate's lowered members, all computed at construction by the + // new-expression lowering (raw decodes the input once, and the others + // read raw). Module-checked like SecureContext; @types/node declares the + // class inside `declare module "crypto"`, the shipped fallback mirrors + // it. JSON.stringify of one shows the fields where Node's internal-slot + // object shows {} — the Dirent %dtype edge's cousin. if ( psym !== undefined && ts.symbolName(psym) === "X509Certificate" && @@ -4474,17 +4488,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { isDeclaredInAmbientModule(d, "crypto"), ) ) { + const optionalString = withUndefinedArm(STRING, ctx.unions); + if (!optionalString) return null; + const fields = X509_FIELDS.map(([name, type]) => ({ + name, + type: type === "string?" ? optionalString : type, + })); return { kind: "record", shapeId: ctx.shapes.intern( - [ - { name: "fingerprint", type: STRING }, - { name: "validFrom", type: STRING }, - { name: "validTo", type: STRING }, - ], + [...fields].sort((a, b) => (a.name < b.name ? -1 : 1)), false, undefined, - ["fingerprint", "validFrom", "validTo"], + fields.map((f) => f.name), ), }; } @@ -6693,6 +6709,32 @@ function boundVoidReturnType(p: ts.Type, resolveTypeParam?: TypeParamResolver): * result is exactly the nullable-callback shape mapType's union branch * admits, `(() => void) | undefined`) so the interned union is IDENTICAL * to what mapping the checker's own `T | undefined` produces. */ +/** The X509Certificate record's hidden slot for its publicKey KeyObject, + * filled on the first read (builtins/x509.ts). */ +export const X509_PUBLIC_KEY_FIELD = "%publicKey"; + +/** The X509Certificate record's fields in @types/node's declaration order + * ("string?" is string | undefined), then the hidden publicKey slot. */ +export const X509_FIELDS: readonly (readonly [string, IrType | "string?"])[] = [ + ["ca", BOOL], + ["fingerprint", STRING], + ["fingerprint256", STRING], + ["fingerprint512", STRING], + ["subject", STRING], + ["subjectAltName", "string?"], + ["infoAccess", "string?"], + ["issuer", STRING], + ["raw", { kind: "bytes", elem: "u8" }], + ["serialNumber", STRING], + ["signatureAlgorithm", "string?"], + ["signatureAlgorithmOid", STRING], + ["validFrom", STRING], + ["validFromDate", DATE_T], + ["validTo", STRING], + ["validToDate", DATE_T], + [X509_PUBLIC_KEY_FIELD, DYN], +]; + export function withUndefinedArm(t: IrType, unions: UnionRegistry): IrType | null { return withUnitArm(t, "undefinedT", unions); } diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 58dc39c7b..8b88ead00 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -638,6 +638,7 @@ async function compileExecutableNative( dh: features.dh, pki: features.pki, ed25519: features.ed25519, + x509: features.x509, webcrypto: features.webcrypto, perfLoop: features.perfLoop, ...(onArtifactReady === undefined ? {} : { onArtifactReady }), @@ -1869,6 +1870,7 @@ function libraryNativeFeatures(mod: IrModule, backend: "llvm"): EarlyLibraryNati searchParams: features.searchParams, emitter: features.emitter, zlib: features.zlib, + x509: features.x509, copying: features.copying, textDecoderLegacy: features.legacyTextDecoder, dynInvoke: features.dynInvoke, @@ -1964,6 +1966,7 @@ async function compileLibraryNative( searchParams: features.searchParams, emitter: features.emitter, zlib: features.zlib, + x509: features.x509, copying: features.copying, textDecoderLegacy: features.textDecoderLegacy, dynInvoke: features.dynInvoke, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index c8e7536ae..7682fe148 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -247,6 +247,11 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "tls.connectCb", "tls.wrapSocket", "tls.wrapSocketCtx", + // addContext's ERR_TLS_REQUIRED_SERVER_NAME and the context's errors, + // setSecureContext's validateObject. + "tls.serverAddContext", + "tls.serverAddContextCtx", + "tls.serverSetSecureContext", // 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 @@ -1037,6 +1042,16 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "crypto.diffieHellman", "crypto.createSign", "crypto.createVerify", + "crypto.x509Raw", + "crypto.x509RawDer", + "crypto.x509RawStr", + // Node's argument errors (name, options, pkey) and the key parse. + "crypto.x509CheckHost", + "crypto.x509CheckEmail", + "crypto.x509CheckIP", + "crypto.x509PublicKey", + "crypto.x509Verify", + "crypto.x509CheckPrivateKey", ]); /** libCall members that always throw. Their result type is the replaced diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index d563f2951..982550d72 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1216,6 +1216,38 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "crypto.x509ValidFromStr": { argTypes: [STRING], result: STRING }, "crypto.x509ValidTo": { argTypes: [BYTES_U8], result: STRING }, "crypto.x509ValidToStr": { argTypes: [STRING], result: STRING }, + /** The certificate's DER as a fresh Buffer (the raw field), decoded + * once from the constructor's input; the same PEM error (may-throw). */ + "crypto.x509Raw": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "crypto.x509RawDer": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "crypto.x509RawStr": { argTypes: [STRING], result: BYTES_U8 }, + /** The remaining X509Certificate fields over that DER, in Node's + * renderings (scr_lib.c). subjectAltName, infoAccess and + * signatureAlgorithm answer "" where Node answers undefined. Borrowed; + * never throw. */ + "crypto.x509Subject": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509Issuer": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509SubjectAltName": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509InfoAccess": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509SerialNumber": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509Fingerprint256": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509Fingerprint512": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509Ca": { argTypes: [BYTES_U8], result: BOOL }, + "crypto.x509SignatureAlgorithm": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509SignatureAlgorithmOid": { argTypes: [BYTES_U8], result: STRING }, + /** X509Certificate methods over its raw DER. checkHost/checkEmail/checkIP + * answer the matching name or "" (undefined), checkIssued compares two + * certificates, x509Pem is toString's PEM (scr_x509.c); publicKey, + * verify and checkPrivateKey read keys (scr_pki.c). */ + "crypto.x509CheckHost": { argTypes: [BYTES_U8, DYN, DYN], result: STRING }, + "crypto.x509CheckEmail": { argTypes: [BYTES_U8, DYN, DYN], result: STRING }, + "crypto.x509CheckIP": { argTypes: [BYTES_U8, DYN, DYN], result: STRING }, + "crypto.x509CheckIssued": { argTypes: [BYTES_U8, BYTES_U8], result: BOOL }, + "crypto.x509LegacyObject": { argTypes: [BYTES_U8], result: DYN }, + "crypto.x509Pem": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509PublicKey": { argTypes: [BYTES_U8], result: DYN }, + "crypto.x509Verify": { argTypes: [BYTES_U8, DYN], result: BOOL }, + "crypto.x509CheckPrivateKey": { argTypes: [BYTES_U8, DYN], result: BOOL }, "fs.closeSync": { argTypes: [F64], result: VOID }, "stats.isFile": { argTypes: [STATS_T], result: BOOL }, "stats.isDirectory": { argTypes: [STATS_T], result: BOOL }, @@ -2218,6 +2250,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "tls.sockGetProtocol": { argTypes: [NETSOCKET_T], result: DYN }, "tls.sockGetCipher": { argTypes: [NETSOCKET_T], result: DYN }, "tls.sockAlpnProtocol": { argTypes: [NETSOCKET_T], result: DYN }, + /** getPeerCertificate(detailed) and getCertificate() (the legacy object, + * {} or null; scr_tls_x509.c), and the DER behind getPeerX509Certificate() + * and getX509Certificate() (a Buffer, empty without a certificate). */ + "tls.sockGetPeerCertificate": { argTypes: [NETSOCKET_T, DYN], result: DYN }, + "tls.sockGetCertificate": { argTypes: [NETSOCKET_T], result: DYN }, + "tls.sockPeerCertDer": { argTypes: [NETSOCKET_T], result: BYTES_U8 }, + "tls.sockOwnCertDer": { argTypes: [NETSOCKET_T], result: BYTES_U8 }, "tls.sockAuthError": { argTypes: [NETSOCKET_T], result: VOID }, "tls.sockOnSecureConnect": { argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], @@ -2249,6 +2288,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * walk — a validated { cert, key } bag builds the real context. * May-throw. */ "tls.createSecureContextDyn": { argTypes: [DYN], result: SECURECTX_T }, + /** server.addContext(servername, options | SecureContext), + * server.setSecureContext(options) and the 'tlsClientError' listener of + * a tls or https server (scr_tls.c). */ + "tls.serverAddContext": { argTypes: [NETSERVER_T, DYN, DYN], result: VOID }, + "tls.serverAddContextCtx": { argTypes: [NETSERVER_T, DYN, SECURECTX_T], result: VOID }, + "tls.serverSetSecureContext": { argTypes: [NETSERVER_T, DYN], result: VOID }, + "tls.serverOnClientError": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, /** tls.getCACertificates(type): validateString + the documented name * set, then the trailing compiler-rendered fence — ALWAYS THROWS (the * error.nodeThrow polymorphic-result carve-out). May-throw seed. */ diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 2ae19375f..cf1f30a4c 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -738,6 +738,9 @@ export const PKI_LIB_FNS: ReadonlySet = new Set([ "crypto.generateKeyPairSync", "crypto.signOneshot", "crypto.verifyOneshot", + "crypto.x509PublicKey", + "crypto.x509Verify", + "crypto.x509CheckPrivateKey", "crypto.diffieHellman", ]); @@ -1120,6 +1123,34 @@ const RUNTIME_FEATURE_RULES = { /** crypto.subtle importKey/sign/verify/digest and CryptoKey: * scr_webcrypto.c. */ webcrypto: rule({ prefixes: ["webcrypto."] }), + /** scr_x509.c: the X509Certificate fields beyond the validity window and + * fingerprint, which stay in scr_lib.c. */ + x509: rule({ + fns: [ + "crypto.x509Ca", + "crypto.x509Fingerprint256", + "crypto.x509Fingerprint512", + "crypto.x509InfoAccess", + "crypto.x509Issuer", + "crypto.x509Raw", + "crypto.x509RawDer", + "crypto.x509RawStr", + "crypto.x509SerialNumber", + "crypto.x509SignatureAlgorithm", + "crypto.x509SignatureAlgorithmOid", + "crypto.x509Subject", + "crypto.x509SubjectAltName", + "crypto.x509CheckHost", + "crypto.x509CheckEmail", + "crypto.x509CheckIP", + "crypto.x509CheckIssued", + "crypto.x509LegacyObject", + "crypto.x509Pem", + // getPeerCertificate and getCertificate build with scr_x509.c too. + "tls.sockGetPeerCertificate", + "tls.sockGetCertificate", + ], + }), zlib: rule({ prefixes: ["zlib."] }), }; @@ -1151,6 +1182,7 @@ function noRuntimeFeatures(): RuntimeFeatures { dynInvoke: false, dynTier: false, ecdh: false, + x509: false, dh: false, emitter: false, eventTarget: false, diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index ac5d5e47b..fde82f2b6 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -63,6 +63,9 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["crypto.generateKeyPair", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["crypto.signOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["crypto.verifyOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.x509PublicKey", "X509Certificate publicKey, verify and checkPrivateKey"], + ["crypto.x509Verify", "X509Certificate publicKey, verify and checkPrivateKey"], + ["crypto.x509CheckPrivateKey", "X509Certificate publicKey, verify and checkPrivateKey"], ["crypto.diffieHellman", "crypto.diffieHellman"], ["webcrypto.", "WebCrypto (crypto.subtle)"], ["fs.watch", "fs.watch"], diff --git a/packages/compiler/src/library/library-cache.test.ts b/packages/compiler/src/library/library-cache.test.ts index 846d90272..c19275733 100644 --- a/packages/compiler/src/library/library-cache.test.ts +++ b/packages/compiler/src/library/library-cache.test.ts @@ -85,6 +85,7 @@ test("early library cache restores generated artifacts and metadata", async () = searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -138,6 +139,7 @@ test("early library cache publishes after creating a fresh output directory", as searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -168,6 +170,7 @@ test("early library cache hits refresh every payload's LRU time", async () => { searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -213,6 +216,7 @@ test("early library cache misses on source edits and newly-resolved candidates", searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -242,6 +246,7 @@ test("early library cache misses on source edits and newly-resolved candidates", searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -291,6 +296,7 @@ test("semantic library cache restores and rebases IR after a comment-only edit", searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -354,6 +360,7 @@ test("semantic library cache refuses token and directive edits", async () => { searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -402,6 +409,7 @@ test("semantic LLVM cache accepts comment edits that shift source lines", async searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -440,6 +448,7 @@ test("semantic LLVM cache accepts line separator normalization", async () => { searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -501,6 +510,7 @@ test("semantic LLVM cache rebases comment edits in multi-source graphs", async ( searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -540,6 +550,7 @@ test("early library cache is separated by the host Node version", async () => { searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -571,6 +582,7 @@ test("early library cache rejects corrupted artifacts and metadata", async () => searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, @@ -605,6 +617,7 @@ test("disabled early library cache performs no reads or writes", async () => { searchParams: false, emitter: false, zlib: false, + x509: false, copying: false, textDecoderLegacy: false, dynInvoke: false, diff --git a/packages/compiler/src/library/library-cache.ts b/packages/compiler/src/library/library-cache.ts index 3e77ccced..7e61e95b8 100644 --- a/packages/compiler/src/library/library-cache.ts +++ b/packages/compiler/src/library/library-cache.ts @@ -54,6 +54,7 @@ interface EarlyLibraryCacheStamp { searchParams: boolean; emitter: boolean; zlib: boolean; + x509: boolean; copying: boolean; textDecoderLegacy: boolean; }; @@ -74,6 +75,7 @@ export interface EarlyLibraryNativeFeatures { searchParams: boolean; emitter: boolean; zlib: boolean; + x509: boolean; copying: boolean; textDecoderLegacy: boolean; /** Volatile exact-source build identity emitted from the tiny identity TU. */ @@ -132,6 +134,7 @@ const BOOLEAN_NATIVE_KEYS = [ "searchParams", "emitter", "zlib", + "x509", "copying", "textDecoderLegacy", ] as const satisfies readonly (keyof EarlyLibraryNativeFeatures)[]; diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index aa46944f5..b729b2557 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -147,6 +147,7 @@ async function main(): Promise { dh: false, pki: false, ed25519: false, + x509: false, webcrypto: false, perfLoop: false, tlsCa: false, diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index a238029dc..1530e8759 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -250,6 +250,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "dh", "pki", "ed25519", + "x509", "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 33a4c19b9..e039d6404 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -125,6 +125,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( dh: false, pki: false, ed25519: false, + x509: false, webcrypto: false, perfLoop: false, nativeFetch: false, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-x509-certificate-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-x509-certificate-fields.ts.json new file mode 100644 index 000000000..2f7224882 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-x509-certificate-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6936-x509-certificate-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7090-x509-check-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7090-x509-check-names.ts.json new file mode 100644 index 000000000..e375f8d72 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7090-x509-check-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7090-x509-check-names.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7091-x509-certificate-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7091-x509-certificate-keys.ts.json new file mode 100644 index 000000000..d4e9c4a6d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7091-x509-certificate-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7091-x509-certificate-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7092-x509-legacy-object.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7092-x509-legacy-object.ts.json new file mode 100644 index 000000000..fd0b42f36 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7092-x509-legacy-object.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7092-x509-legacy-object.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7093-x509-methods-js.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7093-x509-methods-js.mjs.json new file mode 100644 index 000000000..deeeb0047 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7093-x509-methods-js.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7093-x509-methods-js.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7095-tls-peer-certificates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7095-tls-peer-certificates.ts.json new file mode 100644 index 000000000..b6ebaa3e2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7095-tls-peer-certificates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7095-tls-peer-certificates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7096-tls-peer-certificates.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7096-tls-peer-certificates.mjs.json new file mode 100644 index 000000000..2b85b584a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7096-tls-peer-certificates.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7096-tls-peer-certificates.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7097-tls-client-certificates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7097-tls-client-certificates.ts.json new file mode 100644 index 000000000..ecd6a35d7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7097-tls-client-certificates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7097-tls-client-certificates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7098-tls-client-certificates-paths.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7098-tls-client-certificates-paths.ts.json new file mode 100644 index 000000000..8be352a07 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7098-tls-client-certificates-paths.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7098-tls-client-certificates-paths.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7099-tls-client-certificates.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7099-tls-client-certificates.mjs.json new file mode 100644 index 000000000..9dc6fbc9f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7099-tls-client-certificates.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7099-tls-client-certificates.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7440-tls-server-add-context.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7440-tls-server-add-context.ts.json new file mode 100644 index 000000000..8acc4aade --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7440-tls-server-add-context.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7440-tls-server-add-context.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-tls-client-error.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-tls-client-error.ts.json new file mode 100644 index 000000000..ee5e78748 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-tls-client-error.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7441-tls-client-error.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7442-tls-server-contexts.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7442-tls-server-contexts.mjs.json new file mode 100644 index 000000000..22eda3ac8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7442-tls-server-contexts.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7442-tls-server-contexts.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7443-tls-final-flight.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7443-tls-final-flight.ts.json new file mode 100644 index 000000000..2a8d134cc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7443-tls-final-flight.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7443-tls-final-flight.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7444-tls-final-flight.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7444-tls-final-flight.mjs.json new file mode 100644 index 000000000..98f68721a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7444-tls-final-flight.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7444-tls-final-flight.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7454-tls-peer-embedded-pem.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7454-tls-peer-embedded-pem.ts.json new file mode 100644 index 000000000..e4300cd15 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7454-tls-peer-embedded-pem.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7454-tls-peer-embedded-pem.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7455-tls-als-contexts.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7455-tls-als-contexts.mjs.json new file mode 100644 index 000000000..cd99090a6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7455-tls-als-contexts.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7455-tls-als-contexts.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/x509-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/x509-fences.ts.json new file mode 100644 index 000000000..e2b83a7c2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/x509-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/x509-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-client-certificates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-client-certificates.ts.json new file mode 100644 index 000000000..449e0f3ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-client-certificates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/tls-client-certificates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-peer-certificates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-peer-certificates.ts.json new file mode 100644 index 000000000..c5f8ed40a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-peer-certificates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/tls-peer-certificates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-server-contexts.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-server-contexts.ts.json new file mode 100644 index 000000000..73e294ea2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-server-contexts.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/tls-server-contexts.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/x509-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/x509-methods.ts.json new file mode 100644 index 000000000..c42f8f9cc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/x509-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/x509-methods.ts" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/library-matrix.mjs b/packages/runtime-pack-common/library-matrix.mjs index d9878ec7c..1a621a375 100644 --- a/packages/runtime-pack-common/library-matrix.mjs +++ b/packages/runtime-pack-common/library-matrix.mjs @@ -13,6 +13,7 @@ export function withLibraryRuntimeFlavors(matrix) { "scr_events_emitter.c", "scr_dyn_handle.c", "scr_zlib.c", + "scr_x509.c", "scr_copying.c", "scr_dyn_invoke.c", "scr_bytes_dyn.c", diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 0ab467236..2c02b55c3 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -163,6 +163,9 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_webcrypto.c", "webcrypto"], ["scr_webcrypto_pki.c", all("webcrypto", "pki")], ["scr_ed25519.c", all("pki", "ed25519")], + ["scr_x509.c", "x509"], + // The TLS socket certificate accessors read scr_tls.c and build with scr_x509.c. + ["scr_tls_x509.c", all("tlsEffective", "x509")], // 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-wasm32-wasi/scripts/build.mjs b/packages/runtime-wasm32-wasi/scripts/build.mjs index cbe744e4e..2a2766f4b 100644 --- a/packages/runtime-wasm32-wasi/scripts/build.mjs +++ b/packages/runtime-wasm32-wasi/scripts/build.mjs @@ -26,6 +26,7 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ "scr_pki.c", "scr_webcrypto_pki.c", "scr_ed25519.c", + "scr_tls_x509.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 03a9dd109..15180e880 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -305,5 +305,36 @@ 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); +/* X509Certificate fields (scr_x509.c): raw decodes the constructor's input (+1 Buffer, + * or NULL with Node's PEM error pending); the others read that DER + * (borrowed) and never throw. subjectAltName, infoAccess and + * signatureAlgorithm answer "" where Node answers undefined. */ +ScrBytes *scr_crypto_x509_raw(ScrBytes *data); +ScrBytes *scr_crypto_x509_raw_der(ScrBytes *der); +ScrBytes *scr_crypto_x509_raw_str(ScrStr *pem); +ScrStr *scr_crypto_x509_subject(ScrBytes *der); +ScrStr *scr_crypto_x509_issuer(ScrBytes *der); +ScrStr *scr_crypto_x509_subject_alt_name(ScrBytes *der); +ScrStr *scr_crypto_x509_info_access(ScrBytes *der); +ScrStr *scr_crypto_x509_serial_number(ScrBytes *der); +ScrStr *scr_crypto_x509_fingerprint256(ScrBytes *der); +ScrStr *scr_crypto_x509_fingerprint512(ScrBytes *der); +bool scr_crypto_x509_ca(ScrBytes *der); +ScrStr *scr_crypto_x509_signature_algorithm(ScrBytes *der); +ScrStr *scr_crypto_x509_signature_algorithm_oid(ScrBytes *der); +ScrStr *scr_crypto_x509_pem(ScrBytes *der); +/* X509Certificate methods over the DER (borrowed). checkHost, checkEmail + * and checkIP answer the matching name ("" for none, Node's undefined) or + * NULL with Node's argument error pending (scr_x509.c). publicKey, verify + * and checkPrivateKey read keys (scr_pki.c): publicKey is +1 or NULL, and + * the two checks answer false with the error pending when they throw. */ +ScrStr *scr_crypto_x509_check_host(ScrBytes *der, ScrDyn *name, ScrDyn *options); +ScrStr *scr_crypto_x509_check_email(ScrBytes *der, ScrDyn *email, ScrDyn *options); +ScrStr *scr_crypto_x509_check_ip(ScrBytes *der, ScrDyn *ip, ScrDyn *options); +bool scr_crypto_x509_check_issued(ScrBytes *der, ScrBytes *issuer_der); +ScrDyn *scr_crypto_x509_legacy_object(ScrBytes *der); +ScrDyn *scr_crypto_x509_public_key(ScrBytes *der); +bool scr_crypto_x509_verify(ScrBytes *der, ScrDyn *key); +bool scr_crypto_x509_check_private_key(ScrBytes *der, ScrDyn *key); #endif /* SCR_CRYPTO_H */ diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 18d9c39e5..28e1bda07 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -10522,10 +10522,29 @@ static bool scr_x509_der(const uint8_t *in, size_t n, const uint8_t **der, size_ *der = NULL; *der_len = 0; *decoded = NULL; + /* One whole DER SEQUENCE is the certificate itself: these members read + * the DER the constructor already decoded, whose extensions can carry + * PEM armor as text. */ + if (n >= 2 && in[0] == 0x30) { + size_t len = in[1], head = 2; + if (len & 0x80) { + size_t k = len & 0x7f; + len = 0; + head = 2 + k; + for (size_t i = 0; i < k && k <= 4 && head <= n; i++) len = (len << 8) | in[2 + i]; + if (k == 0 || k > 4) head = n + 1; + } + if (head <= n && len == n - head) { + *der = in; + *der_len = n; + return true; + } + } /* PEM: base64-decode the armor's body. */ const uint8_t *b = NULL; + /* OpenSSL's PEM reader looks for the armor at the start of a line. */ for (size_t i = 0; i + sizeof begin - 1 <= n; i++) { - if (memcmp(in + i, begin, sizeof begin - 1) == 0) { + if ((i == 0 || in[i - 1] == '\n') && memcmp(in + i, begin, sizeof begin - 1) == 0) { b = in + i + sizeof begin - 1; break; } diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 9ff9ba8b0..bd1233978 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -451,6 +451,9 @@ struct ScrNetServer { ScrStr *unix_path; /* listen(path): the bound socket file, removed at close */ size_t nconns; ScrNetLs conn_ls, err_ls, close_ls, listening_cbs, timeout_ls; + /* tls/https servers: 'tlsClientError' (err, tlsSocket), fired by the + * TLS unit for a handshake that fails */ + ScrNetLs client_error_ls; /* server.maxConnections (unset = undefined) and its 'drop' listeners */ bool max_conns_set; double max_conns; @@ -2134,6 +2137,53 @@ void scr_net_server_set_close_override(ScrNetServer *s, ScrClosure *ov /*moves*/ s->close_override = ov; } +void scr_net_server_on_client_error(ScrNetServer *s, ScrClosure *cb /*moves*/, void *fn, bool once) { + if (s->close_emitted) { + scr_closure_release(cb); + return; + } + scr_net_ls_add(&s->client_error_ls, cb, fn, once); +} + +typedef void (*ScrNetClientErrorFn)(ScrClosure *, ScrError *, ScrNetSocket *); + +/* Node emits 'tlsClientError' from the accepted connection's handshake, + * which has no AsyncLocalStorage store, not from the server's listen() + * context the connection's other events run in. */ +void scr_net_server_fire_client_error(ScrNetServer *s, ScrError *err /*borrowed*/, + ScrNetSocket *sock /*borrowed*/) { + if (s->client_error_ls.n == 0) return; + ScrNetL *snap; + size_t n = scr_net_ls_snapshot(&s->client_error_ls, &snap); + ScrAlsCtx *prev = scr_als_dispatch_enter(NULL); + scr_dyn_this_push(s, SCR_DYNH_NET_SERVER); + for (size_t i = 0; i < n; i++) { + if (!scr_exc_pending()) ((ScrNetClientErrorFn)snap[i].fn)(snap[i].cb, err, sock); + scr_closure_release(snap[i].cb); + } + scr_dyn_this_pop(); + scr_als_dispatch_leave(prev); + free(snap); +} + +/* The 'tlsClientError' listener adapters by arity; the arguments arrive + * borrowed and the listener takes them +1. */ +void scr_net_client_error_thunk0(ScrClosure *cb, ScrError *err, ScrNetSocket *sock) { + (void)err; + (void)sock; + ((void (*)(ScrClosure *))cb->fn)(cb); +} + +void scr_net_client_error_thunk1(ScrClosure *cb, ScrError *err, ScrNetSocket *sock) { + (void)sock; + ((void (*)(ScrClosure *, ScrError *))cb->fn)(cb, scr_error_retain(err)); +} + +void scr_net_client_error_thunk2(ScrClosure *cb, ScrError *err, ScrNetSocket *sock) { + ((void (*)(ScrClosure *, ScrError *, ScrNetSocket *))cb->fn)(cb, scr_error_retain(err), + scr_net_sock_retain(sock)); +} + void scr_net_server_on_error(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once) { if (s->close_emitted) { @@ -5318,6 +5368,17 @@ void scr_net_sock_transport_error_code(ScrNetSocket *s, const char *msg, const c scr_net_sock_transport_error(s, msg); } +void scr_net_sock_transport_report(ScrNetSocket *s, const char *msg, const char *code) { + if (s->pending_err) return; + s->pending_err = scr_str_new(msg, strlen(msg)); + s->pending_code = code; +} + +void scr_net_sock_transport_ended(ScrNetSocket *s) { + s->rd_eof = true; + scr_net_fire0_this(&s->end_ls, s, SCR_DYNH_NET_SOCKET); +} + 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)); @@ -5405,6 +5466,7 @@ static void scr_net_server_settle(ScrNetServer *srv) { scr_net_ls_drop(&srv->close_ls); scr_net_ls_drop(&srv->listening_cbs); scr_net_ls_drop(&srv->timeout_ls); + scr_net_ls_drop(&srv->client_error_ls); if (srv->proto_settle && srv->http_ctx) srv->proto_settle(srv->http_ctx); scr_net_server_unregister(srv); } @@ -5450,6 +5512,9 @@ static bool scr_net_sock_settle(ScrNetSocket *sock) { scr_net_fire_arg_this(&sock->close_ls, had_error, sock, SCR_DYNH_NET_SOCKET); scr_dyn_release(had_error); } + /* the transport settles first: a listener its last events add (a TLS + * server's 'tlsClientError') drops with the rest */ + if (sock->tops && sock->tops->settle) sock->tops->settle(sock->tctx); /* settle: listeners drop (cycle story) */ scr_net_ls_drop(&sock->data_ls); scr_net_ls_drop(&sock->end_ls); @@ -5462,7 +5527,6 @@ 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; @@ -5716,6 +5780,7 @@ static bool scr_net_sock_event(ScrNetSocket *s, unsigned events) { if (!scr_exc_pending() && s->fd >= 0) scr_net_sock_update_write(s); } else { s->t_want_write = false; + if (s->tops && s->tops->drain) s->tops->drain(s->tctx); scr_net_sock_flush(s); } } diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index 9e6dd1e0b..95490aaa5 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -186,6 +186,14 @@ void scr_net_server_close_direct(ScrNetServer *s, ScrClosure *cb /*moves, nullab * zero-arg wrapper); reassignment releases the old one. */ void scr_net_server_set_close_override(ScrNetServer *s, ScrClosure *ov /*moves*/); void scr_net_server_on_error(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once); +/* tls/https servers' 'tlsClientError': registration (fn is one of the + * thunks below, by the listener's arity) and the TLS unit's fire. */ +void scr_net_server_on_client_error(ScrNetServer *s, ScrClosure *cb /*moves*/, void *fn, bool once); +void scr_net_server_fire_client_error(ScrNetServer *s, ScrError *err /*borrowed*/, + ScrNetSocket *sock /*borrowed*/); +void scr_net_client_error_thunk0(ScrClosure *cb, ScrError *err, ScrNetSocket *sock); +void scr_net_client_error_thunk1(ScrClosure *cb, ScrError *err, ScrNetSocket *sock); +void scr_net_client_error_thunk2(ScrClosure *cb, ScrError *err, ScrNetSocket *sock); void scr_net_server_on_close(ScrNetServer *s, ScrClosure *cb /*moves*/, bool once); /* server.on('listening', cb): the deferred 'listening' list listen(port, * cb) rides — fires once per successful bind; late registrations on an @@ -362,6 +370,9 @@ typedef struct ScrNetTransportOps { /* nullable: runs when the closed socket settles, after its own * listeners drop; the transport drops the listeners it holds */ void (*settle)(void *tctx); + /* nullable: the established socket's fd is writable; the transport + * writes what it holds back (TLS holds its final handshake flight) */ + void (*drain)(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 @@ -406,7 +417,14 @@ void scr_net_sock_replay(ScrNetSocket *s, const char *data, size_t len); /* Transport-level failure: msg (borrowed, Node's message shape) defers * 'error' + teardown to the sweep; NULL tears down without an 'error' * event, Node's default for an unhandled server-side handshake failure. */ +/* The peer ended the stream before the transport's handshake finished: + * 'end' fires now, ahead of the failure the transport reports next. */ +void scr_net_sock_transport_ended(ScrNetSocket *s); void scr_net_sock_transport_error(ScrNetSocket *s, const char *msg); +/* An error that leaves the socket open: 'error' fires from the sweep with + * `code` (static storage), and the socket ends when the peer closes, as + * Node's TLS socket reports a fatal alert after its handshake. */ +void scr_net_sock_transport_report(ScrNetSocket *s, const char *msg, const char *code); /* 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); diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index a1d5b98a6..72136afdb 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -2804,3 +2804,220 @@ bool scr_pki_verify(ScrPkiKey *key, const char *hash, bool pss, int salt, const 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); } + +/* ── X509Certificate members over keys ───────────────────────────────── */ + +/* The certificate's public key, +1; NULL with the error pending. */ +static SpKey *sp_x509_key(ScrBytes *der) { + ScrBytes *view = der; + SCR_BYTES_SNAPSHOT(view); + const unsigned char *spki; + size_t spki_len; + if (!sp_cert_spki(view->data, (size_t)scr_bytes_byte_len(view), &spki, &spki_len)) { + sp_unsupported_input(); + return NULL; + } + return sp_parse(spki, spki_len, false); +} + +/* x509.publicKey: the certificate's public key as a new KeyObject. */ +ScrDyn *scr_crypto_x509_public_key(ScrBytes *der) { + SpKey *k = sp_x509_key(der); + return k ? sp_key_box(k) : NULL; +} + +/* The pkey argument of verify (a public key) and checkPrivateKey (a + * private key), borrowed; NULL with Node's TypeError pending. */ +static SpKey *sp_x509_pkey(const ScrDyn *value, bool private_key) { + SpKey *k = sp_key_of(value); + size_t len; + if (!k && !scr_key_object_bytes(value, &len)) { + scr_dyn_arg_type_fail("pkey", "an instance of KeyObject", value); + return NULL; + } + if (!k || k->private_key != private_key) { + const char *cls = !k ? "SecretKeyObject" : k->private_key ? "PrivateKeyObject" : "PublicKeyObject"; + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "The argument 'pkey' is invalid. Received %s [KeyObject] {}", + cls); + return NULL; + } + return k; +} + +/* The digest of a signature algorithm OID's last arc (RSA and ECDSA). */ +static mbedtls_md_type_t sp_x509_rsa_md(unsigned char arc) { + switch (arc) { + case 5: return MBEDTLS_MD_SHA1; + case 11: return MBEDTLS_MD_SHA256; + case 12: return MBEDTLS_MD_SHA384; + case 13: return MBEDTLS_MD_SHA512; + case 14: return MBEDTLS_MD_SHA224; + default: return MBEDTLS_MD_NONE; + } +} + +/* A hash AlgorithmIdentifier inside RSASSA-PSS-params. */ +static mbedtls_md_type_t sp_x509_hash_alg(unsigned char *p, const unsigned char *end) { + size_t n; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) + return MBEDTLS_MD_NONE; + mbedtls_asn1_buf oid = {0}; + oid.tag = MBEDTLS_ASN1_OID; + if (mbedtls_asn1_get_tag(&p, end, &oid.len, MBEDTLS_ASN1_OID) != 0) return MBEDTLS_MD_NONE; + oid.p = p; + mbedtls_md_type_t md; + return mbedtls_oid_get_md_alg(&oid, &md) == 0 ? md : MBEDTLS_MD_NONE; +} + +/* x509.verify(publicKey): whether publicKey verifies the certificate's + * signature. An algorithm the key cannot verify answers false. */ +bool scr_crypto_x509_verify(ScrBytes *der, ScrDyn *key) { + SpKey *k = sp_x509_pkey(key, false); + if (!k) return false; + ScrBytes *view = der; + SCR_BYTES_SNAPSHOT(view); + unsigned char *p = view->data; + const unsigned char *end = p + (size_t)scr_bytes_byte_len(view); + size_t n; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) + return false; + end = p + n; + const unsigned char *tbs = p; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) + return false; + p += n; + size_t tbs_len = (size_t)(p - tbs); + const unsigned char *outer_alg = p; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) + return false; + const unsigned char *alg_end = p + n; + /* As in OpenSSL's X509_verify, the TBSCertificate's signature field must + * be the same AlgorithmIdentifier as the outer one. */ + { + unsigned char *q = (unsigned char *)tbs; + const unsigned char *tbs_end = tbs + tbs_len; + size_t len; + if (mbedtls_asn1_get_tag(&q, tbs_end, &len, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) + return false; + if (q < tbs_end && *q == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0)) { + if (mbedtls_asn1_get_tag(&q, tbs_end, &len, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0) != 0) + return false; + q += len; + } + if (mbedtls_asn1_get_tag(&q, tbs_end, &len, MBEDTLS_ASN1_INTEGER) != 0) return false; + q += len; + const unsigned char *inner_alg = q; + if (mbedtls_asn1_get_tag(&q, tbs_end, &len, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) + return false; + size_t inner_len = (size_t)(q + len - inner_alg); + if (inner_len != (size_t)(alg_end - outer_alg) || memcmp(inner_alg, outer_alg, inner_len) != 0) return false; + } + size_t oid_len; + if (mbedtls_asn1_get_tag(&p, alg_end, &oid_len, MBEDTLS_ASN1_OID) != 0) return false; + const unsigned char *oid = p; + unsigned char *params = p + oid_len; + p = (unsigned char *)alg_end; + size_t sig_len; + if (mbedtls_asn1_get_tag(&p, end, &sig_len, MBEDTLS_ASN1_BIT_STRING) != 0 || sig_len < 1 || *p != 0) + return false; + const unsigned char *sig = p + 1; + sig_len -= 1; + static const unsigned char rsa_prefix[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01}; + static const unsigned char ecdsa_prefix[] = {0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04}; + static const unsigned char ed25519_oid[] = {0x2B, 0x65, 0x70}; + if (oid_len == sizeof ed25519_oid && memcmp(oid, ed25519_oid, oid_len) == 0) { + if (!sp_is_ed(k)) return false; + const ScrPkiEd25519 *ed = sp_ed(); + return ed != NULL && sp_ed_verify(ed, k->ed_pub, tbs, tbs_len, sig, sig_len); + } + if (sp_is_ed(k)) return false; + unsigned char hash[MBEDTLS_MD_MAX_SIZE]; + if (oid_len == sizeof rsa_prefix + 1 && memcmp(oid, rsa_prefix, sizeof rsa_prefix) == 0) { + if (!sp_is_rsa(k)) return false; + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(k->pk); + if (oid[sizeof rsa_prefix] == 10) { /* RSASSA-PSS with its parameters */ + mbedtls_md_type_t md = MBEDTLS_MD_SHA1, mgf = MBEDTLS_MD_SHA1; + int salt = 20; + unsigned char *q = params; + size_t plen; + if (mbedtls_asn1_get_tag(&q, alg_end, &plen, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) == 0) { + const unsigned char *pend = q + plen; + while (q < pend) { + unsigned char tag = *q; + size_t len; + if (mbedtls_asn1_get_tag(&q, pend, &len, tag) != 0) return false; + unsigned char *field = q; + q += len; + if (tag == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0)) { + md = sp_x509_hash_alg(field, q); + } else if (tag == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 1)) { + /* MGF1 with its hash: SEQUENCE { id-mgf1, AlgorithmIdentifier } */ + size_t mlen, ilen; + unsigned char *m = field; + if (mbedtls_asn1_get_tag(&m, q, &mlen, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0 || + mbedtls_asn1_get_tag(&m, q, &ilen, MBEDTLS_ASN1_OID) != 0) + return false; + mgf = sp_x509_hash_alg(m + ilen, q); + } else if (tag == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 2)) { + if (mbedtls_asn1_get_int(&field, q, &salt) != 0) return false; + } + } + } + if (md == MBEDTLS_MD_NONE || mgf == MBEDTLS_MD_NONE || sig_len != mbedtls_rsa_get_len(rsa)) + return false; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(md); + mbedtls_md(info, tbs, tbs_len, hash); + return mbedtls_rsa_rsassa_pss_verify_ext(rsa, md, mbedtls_md_get_size(info), hash, mgf, salt, + sig) == 0; + } + mbedtls_md_type_t md = sp_x509_rsa_md(oid[sizeof rsa_prefix]); + if (md == MBEDTLS_MD_NONE) return false; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(md); + mbedtls_md(info, tbs, tbs_len, hash); + /* PKCS#1 v1.5, whatever padding the key's last signature used */ + if (mbedtls_rsa_set_padding(rsa, MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_NONE) != 0) return false; + return mbedtls_pk_verify(&k->pk, md, hash, mbedtls_md_get_size(info), sig, sig_len) == 0; + } + if (oid_len >= sizeof ecdsa_prefix + 1 && memcmp(oid, ecdsa_prefix, sizeof ecdsa_prefix) == 0) { + if (!sp_is_ec(k)) return false; + mbedtls_md_type_t md = MBEDTLS_MD_NONE; + if (oid_len == 7 && oid[6] == 0x01) md = MBEDTLS_MD_SHA1; + else if (oid_len == 8 && oid[6] == 0x03) { + static const mbedtls_md_type_t sha2[] = {MBEDTLS_MD_NONE, MBEDTLS_MD_SHA224, MBEDTLS_MD_SHA256, + MBEDTLS_MD_SHA384, MBEDTLS_MD_SHA512}; + if (oid[7] >= 1 && oid[7] <= 4) md = sha2[oid[7]]; + } + if (md == MBEDTLS_MD_NONE) return false; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(md); + mbedtls_md(info, tbs, tbs_len, hash); + return mbedtls_pk_verify(&k->pk, md, hash, mbedtls_md_get_size(info), sig, sig_len) == 0; + } + return false; +} + +/* x509.checkPrivateKey(privateKey): whether privateKey is the private half + * of the certificate's public key. */ +bool scr_crypto_x509_check_private_key(ScrBytes *der, ScrDyn *key) { + SpKey *k = sp_x509_pkey(key, true); + if (!k) return false; + SpKey *cert = sp_x509_key(der); + if (!cert) return false; + bool same = false; + if (sp_is_ed(k) || sp_is_ed(cert)) { + same = sp_is_ed(k) && sp_is_ed(cert) && memcmp(k->ed_pub, cert->ed_pub, 32) == 0; + } else if (mbedtls_pk_get_type(&k->pk) == mbedtls_pk_get_type(&cert->pk)) { + SpKey *pub = sp_public_of(k); + if (pub) { + 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_write_spki_der(pub, a, SP_DER_MAX); + int nb = sp_write_spki_der(cert, b, SP_DER_MAX); + same = na > 0 && na == nb && memcmp(a + SP_DER_MAX - na, b + SP_DER_MAX - nb, (size_t)na) == 0; + free(a); + free(b); + sp_key_release(pub); + } + } + sp_key_release(cert); + return same; +} diff --git a/packages/runtime/src/scr_tls.c b/packages/runtime/src/scr_tls.c index e7436ab69..2f448a417 100644 --- a/packages/runtime/src/scr_tls.c +++ b/packages/runtime/src/scr_tls.c @@ -64,10 +64,15 @@ * expired") through the socket's pending-error sweep — then 'close', * Node's order, exactly like ECONNREFUSED. * + * Client certificates: servers take requestCert, rejectUnauthorized and + * ca and report socket.authorized/authorizationError as Node does; + * clients send a cert and key when asked. A `ca` also completes the + * chain its owner sends, as OpenSSL's automatic chain does. + * * Bounds (SEMANTICS.md divergence 55): no session resumption or - * tickets, no renegotiation/post-handshake auth, no client - * certificates, and non-verify client handshake failures carry a generic - * message where Node prints an OpenSSL one. + * tickets, no renegotiation/post-handshake auth, and non-verify client + * handshake failures carry a generic message where Node prints an + * OpenSSL one. * * Certificate names: a client always checks the leaf against one name, * the servername when it is non-empty and the host otherwise. SNI carries @@ -82,6 +87,7 @@ #include #include #include +#include #include #include #ifdef _WIN32 @@ -202,6 +208,42 @@ static void scr_tls_bad_material(const char *what, int err) { _Exit(1); } +/* OpenSSL's automatic chain (ssl_add_cert_chain): a certificate option + * that holds only the leaf is sent with the issuers `store` holds, up to + * a self-issued one, as far as they are found. A chain given in the + * option is sent as it is. */ +static void scr_tls_auto_chain(mbedtls_x509_crt *own, const mbedtls_x509_crt *store) { + if (own->raw.len == 0 || own->next != NULL) return; + const mbedtls_x509_crt *at = own; + for (int depth = 0; depth < 8; depth++) { + if (at->issuer_raw.len == at->subject_raw.len && + memcmp(at->issuer_raw.p, at->subject_raw.p, at->subject_raw.len) == 0) + return; + const mbedtls_x509_crt *issuer = NULL; + for (const mbedtls_x509_crt *c = store; c != NULL && c->raw.len > 0 && issuer == NULL; c = c->next) { + if (c->subject_raw.len == at->issuer_raw.len && + memcmp(c->subject_raw.p, at->issuer_raw.p, c->subject_raw.len) == 0) + issuer = c; + } + if (issuer == NULL) return; + for (const mbedtls_x509_crt *c = own; c != NULL; c = c->next) { + if (c->raw.len == issuer->raw.len && memcmp(c->raw.p, issuer->raw.p, c->raw.len) == 0) return; + } + if (mbedtls_x509_crt_parse_der(own, issuer->raw.p, issuer->raw.len) != 0) return; + while (at->next != NULL) at = at->next; + } +} + +/* A copy of a certificate chain, completed from `store` as the automatic + * chain does, for a use that must leave the original as it is. */ +static void scr_tls_chained_copy(mbedtls_x509_crt *out, const mbedtls_x509_crt *chain, + const mbedtls_x509_crt *store) { + for (const mbedtls_x509_crt *c = chain; c != NULL && c->raw.len > 0; c = c->next) { + if (mbedtls_x509_crt_parse_der(out, c->raw.p, c->raw.len) != 0) scr_tls_bad_material("certificate", 0); + } + scr_tls_auto_chain(out, store); +} + /* ── SecureContext (tls.createSecureContext) ─────────────────────────── */ /* An opaque refcounted parsed cert/key pair — what an SNI callback @@ -210,6 +252,9 @@ struct ScrSecureCtx { size_t rc; mbedtls_x509_crt cert; mbedtls_pk_context pk; + /* the ca option: a socket created from this context (a TLSSocket's + * secureContext) sends the chain it completes */ + mbedtls_x509_crt ca; }; #ifdef SCR_RC_AUDIT @@ -224,6 +269,7 @@ ScrSecureCtx *scr_tls_create_secure_context(const char *cert, size_t cert_len, if (!c) scr_tls_oom(); c->rc = 1; mbedtls_x509_crt_init(&c->cert); + mbedtls_x509_crt_init(&c->ca); mbedtls_pk_init(&c->pk); int r = scr_tls_parse_crt(&c->cert, cert, cert_len); if (r != 0) scr_tls_bad_material("certificate", r); @@ -243,6 +289,7 @@ ScrSecureCtx *scr_secure_ctx_retain(ScrSecureCtx *c) { void scr_secure_ctx_release(ScrSecureCtx *c) { if (!c || --c->rc != 0) return; mbedtls_x509_crt_free(&c->cert); + mbedtls_x509_crt_free(&c->ca); mbedtls_pk_free(&c->pk); free(c); #ifdef SCR_RC_AUDIT @@ -372,6 +419,29 @@ typedef struct ScrTlsSrv { * certificate and key this config serves (+1; NULL when cert/key are the * config's own) */ ScrSecureCtx *ctx; + /* client certificates: requestCert asks for one and verifies it against + * `ca` (the default store without one); with rejectUnauthorized a client + * that sends none fails the handshake and one that fails verification + * is dropped before 'secureConnection', as in Node */ + bool request_cert; + bool reject_unauthorized; + bool has_ca; + mbedtls_x509_crt ca; + /* new tls.TLSSocket(socket, { isServer }): Node verifies client + * certificates only in tls.Server, so a standalone socket reports no + * verdict (authorized false, authorizationError null) and drops no + * client for one; rejectUnauthorized there only requires a certificate */ + bool standalone; + /* server.addContext(servername, context): lowercased name patterns + * ('*' matches within one label) with their contexts (+1), newest + * last; server.setSecureContext's replacement default (+1, NULL for the + * server's own). Both serve through scr_tls_f_cert. */ + struct ScrTlsNamedCtx { + char *pattern; + ScrSecureCtx *ctx; + } *contexts; + size_t ncontexts; + ScrSecureCtx *default_ctx; } ScrTlsSrv; /* The ALPN answers (static storage — mbedTLS keeps the pointers). Plain @@ -389,6 +459,7 @@ static ScrTlsSrv *scr_tls_srv_new(const char *cert, size_t cert_len, const char if (!s) scr_tls_oom(); s->rc = 1; mbedtls_x509_crt_init(&s->cert); + mbedtls_x509_crt_init(&s->ca); mbedtls_pk_init(&s->pk); mbedtls_ssl_config_init(&s->conf); int r = scr_tls_parse_crt(&s->cert, cert, cert_len); @@ -401,7 +472,8 @@ static ScrTlsSrv *scr_tls_srv_new(const char *cert, size_t cert_len, const char 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); /* no client certs, Node's default */ + /* no client certificate unless requestCert asks (scr_tls_srv_client_auth) */ + mbedtls_ssl_conf_authmode(&s->conf, MBEDTLS_SSL_VERIFY_NONE); if (mbedtls_ssl_conf_own_cert(&s->conf, &s->cert, &s->pk) != 0) { scr_tls_bad_material("certificate/key pair", 0); } @@ -416,6 +488,7 @@ static ScrTlsSrv *scr_tls_srv_from_ctx(ScrSecureCtx *ctx /*borrowed*/) { if (!s) scr_tls_oom(); s->rc = 1; mbedtls_x509_crt_init(&s->cert); + mbedtls_x509_crt_init(&s->ca); 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, @@ -425,7 +498,13 @@ static ScrTlsSrv *scr_tls_srv_from_ctx(ScrSecureCtx *ctx /*borrowed*/) { 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) { + /* the context's ca completes the chain a socket created from it sends */ + const mbedtls_x509_crt *own = &ctx->cert; + if (ctx->ca.raw.len > 0) { + scr_tls_chained_copy(&s->cert, &ctx->cert, &ctx->ca); + own = &s->cert; + } + if (mbedtls_ssl_conf_own_cert(&s->conf, (mbedtls_x509_crt *)own, &ctx->pk) != 0) { scr_tls_bad_material("certificate/key pair", 0); } return s; @@ -443,14 +522,53 @@ static void scr_tls_srv_release(ScrTlsSrv *s) { scr_closure_release(s->sni_cb); mbedtls_ssl_config_free(&s->conf); mbedtls_x509_crt_free(&s->cert); + mbedtls_x509_crt_free(&s->ca); mbedtls_pk_free(&s->pk); scr_tls_alpn_free(s->alpn); scr_secure_ctx_release(s->ctx); + for (size_t i = 0; i < s->ncontexts; i++) { + free(s->contexts[i].pattern); + scr_secure_ctx_release(s->contexts[i].ctx); + } + free(s->contexts); + scr_secure_ctx_release(s->default_ctx); free(s); } static void scr_tls_srv_release_v(void *p) { scr_tls_srv_release((ScrTlsSrv *)p); } +/* The client-certificate options of a server: requestCert, rejectUnauthorized + * (Node's true default) and ca (+1, NULL for none). */ +typedef struct { + bool request; + bool reject; + ScrBytes *ca; +} ScrTlsClientAuth; + +static mbedtls_x509_crt *scr_tls_system_ca(void); + +/* Applies a server's client-certificate options. A `ca` also completes the + * server's own chain, as OpenSSL's automatic chain sends it. */ +static void scr_tls_srv_client_auth(ScrTlsSrv *s, const ScrTlsClientAuth *auth) { + if (auth == NULL) return; + /* An empty ca Buffer is an empty trust store, not the default one. */ + if (auth->ca != NULL) { + if (auth->ca->len > 0) { + int r = scr_tls_parse_crt(&s->ca, (const char *)auth->ca->data, auth->ca->len); + if (r != 0) scr_tls_bad_material("ca certificate", r); + if (s->ctx == NULL) scr_tls_auto_chain(&s->cert, &s->ca); + } + s->has_ca = true; + } + if (!auth->request) return; + s->request_cert = true; + s->reject_unauthorized = auth->reject; + /* OPTIONAL: the handshake records the verdict and goes on, as Node's + * verify callback does; scr_tls_handshake applies rejectUnauthorized */ + mbedtls_ssl_conf_authmode(&s->conf, MBEDTLS_SSL_VERIFY_OPTIONAL); + mbedtls_ssl_conf_ca_chain(&s->conf, s->has_ca ? &s->ca : scr_tls_system_ca(), NULL); +} + /* ── the client config (one per https.request, owned by its tctx) ────── */ typedef struct ScrTlsCli { @@ -463,6 +581,7 @@ typedef struct ScrTlsCli { * VERIFY_NONE socket (the https client's rejectUnauthorized:false) * never verified, so authorized answers false like Node's. */ bool verify_recorded; + bool reject; /* rejectUnauthorized: a verification failure fails the handshake */ /* The ALPNProtocols option's names, NULL-terminated, in the caller's * order (mbedtls_ssl_conf_alpn_protocols keeps the pointer); NULL when * none are offered. */ @@ -470,6 +589,11 @@ typedef struct ScrTlsCli { /* No TLS version is in the configured range: the handshake fails as * soon as it starts (Node's ERR_SSL_NO_PROTOCOLS_AVAILABLE). */ bool no_protocols; + /* the client certificate (cert and key options), sent when the server + * asks for one */ + bool has_own; + mbedtls_x509_crt own; + mbedtls_pk_context own_pk; } ScrTlsCli; /* The default trust anchors when no `ca` option is given: the host trust @@ -562,6 +686,8 @@ static ScrTlsCli *scr_tls_cli_new2(ScrStr *host, bool reject_unauthorized, const if (!c) scr_tls_oom(); mbedtls_ssl_config_init(&c->conf); mbedtls_x509_crt_init(&c->ca); + mbedtls_x509_crt_init(&c->own); + mbedtls_pk_init(&c->own_pk); if (mbedtls_ssl_config_defaults(&c->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) != 0) { @@ -569,6 +695,7 @@ static ScrTlsCli *scr_tls_cli_new2(ScrStr *host, bool reject_unauthorized, const } mbedtls_ssl_conf_rng(&c->conf, mbedtls_ctr_drbg_random, &scr_tls_drbg); c->verify_recorded = reject_unauthorized || verify_optional; + c->reject = reject_unauthorized; if (ca_len > 0) { int r = scr_tls_parse_crt(&c->ca, ca, ca_len); if (r != 0) scr_tls_bad_material("ca certificate", r); @@ -620,11 +747,27 @@ static void scr_tls_cli_free(ScrTlsCli *c) { mbedtls_ssl_config_free(&c->conf); scr_tls_alpn_free(c->alpn); mbedtls_x509_crt_free(&c->ca); + mbedtls_x509_crt_free(&c->own); + mbedtls_pk_free(&c->own_pk); free(c->hostname); free(c->verify_name); free(c); } +/* The cert and key options of a client: the certificate (with the issuers + * its `ca` holds, OpenSSL's automatic chain) is sent when the server asks + * for one. */ +static void scr_tls_cli_set_own(ScrTlsCli *c, const ScrBytes *cert, const ScrBytes *key) { + int r = scr_tls_parse_crt(&c->own, (const char *)cert->data, cert->len); + if (r != 0) scr_tls_bad_material("certificate", r); + r = scr_tls_parse_key(&c->own_pk, (const char *)key->data, key->len); + if (r != 0) scr_tls_bad_material("private key", r); + if (c->ca.raw.len > 0) scr_tls_auto_chain(&c->own, &c->ca); + if (mbedtls_ssl_conf_own_cert(&c->conf, &c->own, &c->own_pk) != 0) + scr_tls_bad_material("certificate/key pair", 0); + c->has_own = true; +} + /* ── the per-socket transport ────────────────────────────────────────── */ /* The SNI pre-parse states: NONE (no callback on the server — the @@ -665,8 +808,32 @@ typedef struct ScrTlsSock { /* the 'secure' event's listeners */ ScrNetLs secure_ls; bool settled; /* the socket closed: listeners added later never fire */ + /* a TLS 1.3 client sent no certificate to a server that requires one; + * the alert goes out once the handshake is over */ + bool cert_missing; + /* a fatal alert was reported without closing: reads end the stream */ + bool alert_reported; + /* a server socket: the accepting server (+1), which 'tlsClientError' + * fires on, and a hang-up waiting for the socket to close (Node emits + * ECONNRESET 'socket hang up' from the socket's 'close') */ + ScrNetServer *server; + bool hang_up_pending; + /* a client whose verification failed: like Node's, it closes without + * sending the alert, so later writes are dropped */ + bool quiet_failure; + /* the served context's chain completed from the server's ca */ + mbedtls_x509_crt served_chain; + /* what the handshake's last step and the established callbacks write: + * Node flushes it after the nextTick queue and before the immediates + * queued in those callbacks, so a destroy() there drops it. `holding` + * collects; held[held_off..held_len) waits for the fd. */ + bool holding; + unsigned char *held; + size_t held_len, held_off, held_cap; } ScrTlsSock; +static bool scr_tls_alert_error(ScrTlsSock *t, unsigned char alert); + /* checkServerIdentity for an IP literal: the address must be one of the * certificate's IP SANs (no DNS SAN or CN applies). */ static bool scr_tls_leaf_has_ip(const mbedtls_x509_crt *crt, const char *ip) { @@ -711,6 +878,9 @@ static int scr_tls_vrfy_cb(void *ctx, mbedtls_x509_crt *crt, int depth, uint32_t *flags |= name_flags & MBEDTLS_X509_BADCERT_CN_MISMATCH; } } + /* a rejecting client fails the handshake on any flag; Node's client + * then closes without sending an alert (its Finished is discarded) */ + if (*flags != 0 && t->cli != NULL && t->cli->reject) t->quiet_failure = true; bool self = crt->issuer_raw.len == crt->subject_raw.len && memcmp(crt->issuer_raw.p, crt->subject_raw.p, crt->subject_raw.len) == 0; if (*flags & MBEDTLS_X509_BADCERT_EXPIRED) t->vrfy_expired = true; @@ -725,8 +895,52 @@ static int scr_tls_vrfy_cb(void *ctx, mbedtls_x509_crt *crt, int depth, uint32_t /* The BIOs: nonblocking fd I/O in mbedTLS's own convention. A send that * would block arms the socket's write filter so the handshake resumes on * writability. */ +static void scr_tls_hold(ScrTlsSock *t, const unsigned char *buf, size_t len) { + if (t->held_len + len > t->held_cap) { + size_t cap = t->held_cap ? t->held_cap : 4096; + while (cap < t->held_len + len) cap *= 2; + unsigned char *grown = realloc(t->held, cap); + if (grown == NULL) scr_tls_oom(); + t->held = grown; + t->held_cap = cap; + } + memcpy(t->held + t->held_len, buf, len); + t->held_len += len; +} + +/* Writes what is held: 1 once it is all out, 0 when the fd would block + * (the write filter is armed for the rest), -1 when the connection is + * gone (the bytes are dropped). */ +static int scr_tls_flush_held(ScrTlsSock *t) { + int fd = scr_net_sock_fd(t->sock); + while (t->held_off < t->held_len) { + if (fd < 0) break; + ssize_t n = write(fd, t->held + t->held_off, t->held_len - t->held_off); + if (n > 0) { + t->held_off += (size_t)n; + continue; + } + if (n < 0 && errno == EINTR) continue; + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + scr_net_sock_transport_want_write(t->sock); + return 0; + } + break; + } + bool out = t->held_off == t->held_len; + t->held_off = t->held_len = 0; + return out ? 1 : -1; +} + static int scr_tls_bio_send(void *ctx, const unsigned char *buf, size_t len) { ScrTlsSock *t = (ScrTlsSock *)ctx; + if (t->quiet_failure) return (int)len; + if (t->holding) { + scr_tls_hold(t, buf, len); + return (int)len; + } + /* held bytes go first; mbedTLS retries this record once they are out */ + if (t->held_len > t->held_off && scr_tls_flush_held(t) == 0) return MBEDTLS_ERR_SSL_WANT_WRITE; int fd = scr_net_sock_fd(t->sock); if (fd < 0) return MBEDTLS_ERR_NET_CONN_RESET; ssize_t n = write(fd, buf, len); @@ -742,7 +956,7 @@ static int scr_tls_bio_send(void *ctx, const unsigned char *buf, size_t len) { /* 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; + if (t->srv == NULL || 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); @@ -770,6 +984,21 @@ static int scr_tls_bio_recv(void *ctx, unsigned char *buf, size_t len) { return MBEDTLS_ERR_NET_RECV_FAILED; } +/* Node reads what the kernel holds (one 64 KiB libuv read) before + * OpenSSL sees the records, so a failed handshake closes with nothing + * unread and the peer gets a FIN. Closing with the rest of, say, an HTTP + * request still queued would make the kernel send a reset instead. */ +static void scr_tls_drain_unread(ScrTlsSock *t) { + int fd = scr_net_sock_fd(t->sock); + if (fd < 0) return; + unsigned char sink[4096]; + for (size_t total = 0; total < 65536; ) { + ssize_t n = read(fd, sink, sizeof sink); + if (n <= 0) break; + total += (size_t)n; + } +} + /* ── the transport ops ───────────────────────────────────────────────── */ static void scr_tls_fire_session(ScrTlsSock *t); @@ -793,6 +1022,14 @@ static ssize_t scr_tls_xread(void *tctx, char *buf, size_t n) { errno = EAGAIN; return -1; } + if (t->alert_reported) return 0; /* the server closing after its alert */ + /* a TLS 1.3 server rejects a missing client certificate after the + * client finished its handshake */ + if (r == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE && t->cli != NULL && + t->ssl.MBEDTLS_PRIVATE(in_msg) != NULL && scr_tls_alert_error(t, t->ssl.MBEDTLS_PRIVATE(in_msg)[1])) { + errno = EAGAIN; + return -1; + } errno = ECONNRESET; /* protocol-level death reads as a reset */ return -1; } @@ -1081,6 +1318,99 @@ void scr_tls_sni_answer(ScrClosure *self, bool has_err, ScrSecureCtx *ctx /*move * 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). */ +/* The OpenSSL reason (without its per-process prefix and source location, + * SEMANTICS.md divergence 55) and Node's code for a server handshake that + * failed with mbedTLS error `r`; false when the peer closed or reset the + * connection instead. A failure mbedTLS reports without a reason OpenSSL + * would give answers a coded SC2020 refusal. */ +static bool scr_tls_server_reason(ScrTlsSock *t, int r, const char **msg, const char **code) { + if (r == MBEDTLS_ERR_SSL_CONN_EOF || r == MBEDTLS_ERR_NET_CONN_RESET) return false; + if (r == MBEDTLS_ERR_SSL_NO_CLIENT_CERTIFICATE) { + *msg = "error:0A0000C7:SSL routines:tls_process_client_certificate:peer did not return a certificate"; + *code = "ERR_SSL_PEER_DID_NOT_RETURN_A_CERTIFICATE"; + return true; + } + 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 }; + *msg = "error:0A00010B:SSL routines:tls_validate_record_header:wrong version number"; + *code = "ERR_SSL_WRONG_VERSION_NUMBER"; + for (size_t i = 0; http[i] != NULL; i++) { + if (memcmp(h, http[i], strlen(http[i])) == 0) { + *msg = "error:0A00009C:SSL routines:tls_validate_record_header:http request"; + *code = "ERR_SSL_HTTP_REQUEST"; + } + } + if (memcmp(h, "CONNE", 5) == 0) { + *msg = "error:0A00009B:SSL routines:tls_validate_record_header:https proxy request"; + *code = "ERR_SSL_HTTPS_PROXY_REQUEST"; + } + return true; + } + if (r == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE && t->ssl.MBEDTLS_PRIVATE(in_msg) != NULL) { + /* an alert the client sent: OpenSSL's reason is 1000 + its number */ + static const struct { unsigned char alert; const char *msg, *code; } alerts[] = { + { 40, "error:0A000410:SSL routines:ssl3_read_bytes:ssl/tls alert handshake failure", + "ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE" }, + { 42, "error:0A000412:SSL routines:ssl3_read_bytes:sslv3 alert bad certificate", + "ERR_SSL_SSLV3_ALERT_BAD_CERTIFICATE" }, + { 45, "error:0A000415:SSL routines:ssl3_read_bytes:sslv3 alert certificate expired", + "ERR_SSL_SSLV3_ALERT_CERTIFICATE_EXPIRED" }, + { 46, "error:0A000416:SSL routines:ssl3_read_bytes:sslv3 alert certificate unknown", + "ERR_SSL_SSLV3_ALERT_CERTIFICATE_UNKNOWN" }, + { 48, "error:0A000418:SSL routines:ssl3_read_bytes:tlsv1 alert unknown ca", + "ERR_SSL_TLSV1_ALERT_UNKNOWN_CA" }, + { 49, "error:0A000419:SSL routines:ssl3_read_bytes:tlsv1 alert access denied", + "ERR_SSL_TLSV1_ALERT_ACCESS_DENIED" }, + { 51, "error:0A00041B:SSL routines:ssl3_read_bytes:tlsv1 alert decrypt error", + "ERR_SSL_TLSV1_ALERT_DECRYPT_ERROR" }, + { 70, "error:0A00042E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version", + "ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION" }, + { 80, "error:0A000438:SSL routines:ssl3_read_bytes:tlsv1 alert internal error", + "ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR" }, + }; + unsigned char alert = t->ssl.MBEDTLS_PRIVATE(in_msg)[1]; + for (size_t i = 0; i < sizeof alerts / sizeof alerts[0]; i++) { + if (alerts[i].alert == alert) { + *msg = alerts[i].msg; + *code = alerts[i].code; + return true; + } + } + } + if (r == MBEDTLS_ERR_SSL_BAD_PROTOCOL_VERSION) { + *msg = "error:0A000102:SSL routines:tls_early_post_process_client_hello:unsupported protocol"; + *code = "ERR_SSL_UNSUPPORTED_PROTOCOL"; + return true; + } + if (r == MBEDTLS_ERR_SSL_NO_APPLICATION_PROTOCOL) { + *msg = "error:0A0000EB:SSL routines:tls_handle_alpn:no application protocol"; + *code = "ERR_SSL_NO_APPLICATION_PROTOCOL"; + return true; + } + static SCR_TL char refusal[128]; + snprintf(refusal, sizeof refusal, + "a TLS handshake failure (mbedTLS error -0x%04X) without the reason OpenSSL gives is not supported yet", + (unsigned)-r); + *msg = refusal; + *code = "SC2020"; + return true; +} + +/* 'tlsClientError' on the accepting server: Node's Error with `code`, and + * the socket. Without a listener the socket is only destroyed. */ +static void scr_tls_client_error(ScrTlsSock *t, const char *msg, const char *code) { + if (t->server == NULL) return; + ScrStr *m = scr_str_new(msg, strlen(msg)); + ScrError *e = scr_error_new(SCR_ERR_ERROR, m); + scr_str_release(m); + scr_error_set_code(e, code); + scr_net_server_fire_client_error(t->server, e, t->sock); + scr_error_release(e); +} + static void scr_tls_wrapped_failure(ScrTlsSock *t, int r) { if (r == MBEDTLS_ERR_SSL_CONN_EOF) { scr_net_sock_transport_eof(t->sock); @@ -1090,27 +1420,86 @@ static void scr_tls_wrapped_failure(ScrTlsSock *t, int r) { 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; + const char *msg, *code; + (void)scr_tls_server_reason(t, r, &msg, &code); + scr_net_sock_transport_error_code(t->sock, msg, code); +} + +/* A fatal alert a server sends a client without the certificate it + * requires: the client's error is OpenSSL's reason (without its + * per-process prefix and source location, SEMANTICS.md divergence 55) and + * Node's code. False for other alerts. */ +static bool scr_tls_alert_error(ScrTlsSock *t, unsigned char alert) { + if (alert == MBEDTLS_SSL_ALERT_MSG_CERT_REQUIRED) { + /* after the client's handshake: Node emits 'error' and the socket + * then ends when the server closes, with close(false) */ + scr_net_sock_transport_report(t->sock, + "error:0A00045C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required", + "ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED"); + t->alert_reported = true; + return true; } - scr_net_sock_transport_error(t->sock, "server TLS handshake failed"); + if (alert == MBEDTLS_SSL_ALERT_MSG_HANDSHAKE_FAILURE) { + scr_net_sock_transport_error_code(t->sock, + "error:0A000410:SSL routines:ssl3_read_bytes:ssl/tls alert handshake failure", + "ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE"); + return true; + } + return false; +} + +/* A server that requires a client certificate (requestCert with + * rejectUnauthorized) runs its handshake a step at a time, so a client + * that sends none fails as soon as its empty Certificate is read, before + * the server's Finished in TLS 1.2, with OpenSSL's alert: certificate + * required in TLS 1.3, handshake failure in TLS 1.2. */ +static int scr_tls_handshake_requiring_cert(ScrTlsSock *t) { + while (!mbedtls_ssl_is_handshake_over(&t->ssl)) { + int r = mbedtls_ssl_handshake_step(&t->ssl); + if (r != 0) return r; + if (t->cert_missing) continue; + /* all ones until the client's Certificate is read */ + uint32_t verdict = mbedtls_ssl_get_verify_result(&t->ssl); + if (verdict == UINT32_MAX || !(verdict & MBEDTLS_X509_BADCERT_MISSING)) continue; + if (mbedtls_ssl_get_version_number(&t->ssl) != MBEDTLS_SSL_VERSION_TLS1_3) { + (void)mbedtls_ssl_send_alert_message(&t->ssl, MBEDTLS_SSL_ALERT_LEVEL_FATAL, + MBEDTLS_SSL_ALERT_MSG_HANDSHAKE_FAILURE); + return MBEDTLS_ERR_SSL_NO_CLIENT_CERTIFICATE; + } + /* TLS 1.3: the client has finished and reads under the server's + * application keys, which mbedTLS writes with only once the handshake + * is over, so the alert waits for that */ + t->cert_missing = true; + } + if (t->cert_missing) { + (void)mbedtls_ssl_send_alert_message(&t->ssl, MBEDTLS_SSL_ALERT_LEVEL_FATAL, + MBEDTLS_SSL_ALERT_MSG_CERT_REQUIRED); + return MBEDTLS_ERR_SSL_NO_CLIENT_CERTIFICATE; + } + return 0; +} + +static ScrTlsSock *scr_tls_sock_of(ScrNetSocket *s); + +/* The immediate that ends the hold: queued as the handshake finishes, + * before the established callbacks run, so it comes ahead of any + * immediate they queue. A socket destroyed by then has no fd, and what + * was held is dropped. */ +static void scr_tls_release_hold(ScrClosure *self) { + ScrNetSocket *s = scr_box_get_ref(self->caps[0]); /* +1 */ + ScrTlsSock *t = scr_tls_sock_of(s); + if (t != NULL && t->holding) { + t->holding = false; + if (t->held_len > t->held_off) (void)scr_tls_flush_held(t); + } + scr_net_sock_release(s); +} + +static void scr_tls_hold_until_immediate(ScrTlsSock *t) { + ScrClosure *cb = scr_closure_new((void *)&scr_tls_release_hold, 1); + cb->caps[0] = scr_box_new_obj(&scr_net_sock_retain_v, &scr_net_sock_release_v, NULL); + scr_box_set_ref(cb->caps[0], scr_net_sock_retain(t->sock)); + (void)scr_set_immediate(cb); } static int scr_tls_handshake(void *tctx) { @@ -1129,8 +1518,27 @@ static int scr_tls_handshake(void *tctx) { } if (t->sni_state == SCR_SNI_WAIT) return 0; /* the callback is deciding */ scr_tls_current = t; - int r = mbedtls_ssl_handshake(&t->ssl); + t->holding = true; + int r = t->srv != NULL && t->srv->request_cert && t->srv->reject_unauthorized + ? scr_tls_handshake_requiring_cert(t) + : mbedtls_ssl_handshake(&t->ssl); scr_tls_current = NULL; + if (r == 0) { + scr_tls_hold_until_immediate(t); + } else { + /* a step that did not finish the handshake sends what it wrote now */ + t->holding = false; + if (t->held_len > t->held_off) (void)scr_tls_flush_held(t); + } + if (r == 0 && t->srv != NULL && t->srv->request_cert && t->srv->reject_unauthorized && + !t->srv->standalone && mbedtls_ssl_get_verify_result(&t->ssl) != 0) { + /* a client certificate that fails verification: Node's server + * destroys the socket before 'secureConnection', and its + * 'tlsClientError' is the hang-up */ + scr_net_sock_transport_error(t->sock, NULL); + t->hang_up_pending = true; + return -1; + } 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) { @@ -1138,21 +1546,38 @@ static int scr_tls_handshake(void *tctx) { 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 - * listener) sees. */ + /* the socket is destroyed without an error event, and the server's + * 'tlsClientError' gets the failure: now, before the socket closes, + * for an OpenSSL reason; once it closes for a hang-up, as Node + * emits ECONNRESET from the socket's 'close' */ + const char *msg, *code; + bool reason = scr_tls_server_reason(t, r, &msg, &code); + /* Node's socket still has its handle when an OpenSSL reason is + * emitted, so remoteAddress answers; the address is cached before + * the fd closes */ + if (reason) scr_str_release(scr_net_sock_remote_address(t->sock)); + scr_tls_drain_unread(t); scr_net_sock_transport_error(t->sock, NULL); + if (reason) scr_tls_client_error(t, msg, code); + else t->hang_up_pending = true; return -1; } char msg[160]; 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 closed or reset the connection mid-handshake: Node's - * ECONNRESET for tls.connect and https.request alike. */ + } else if (r == MBEDTLS_ERR_SSL_CONN_EOF) { + /* The peer closed the connection mid-handshake: Node's socket ends, + * then fails with ECONNRESET, for tls.connect and https.request + * alike. */ + scr_net_sock_transport_ended(t->sock); + if (scr_exc_pending()) return -1; scr_net_sock_transport_error( t->sock, "Client network socket disconnected before secure TLS connection was established"); return -1; + } else if (r == MBEDTLS_ERR_NET_CONN_RESET) { + /* a reset is the read's own error, as in Node */ + scr_net_sock_transport_error(t->sock, "read ECONNRESET"); + return -1; } else if (r == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE && t->ssl.MBEDTLS_PRIVATE(in_msg) != NULL && t->ssl.MBEDTLS_PRIVATE(in_msg)[1] == MBEDTLS_SSL_ALERT_MSG_NO_APPLICATION_PROTOCOL) { @@ -1167,6 +1592,9 @@ static int scr_tls_handshake(void *tctx) { t->ssl.MBEDTLS_PRIVATE(in_msg)[1] == MBEDTLS_SSL_ALERT_MSG_PROTOCOL_VERSION) { /* The server accepts none of the offered versions. */ snprintf(msg, sizeof msg, "error:0A00042E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version"); + } else if (r == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE && t->ssl.MBEDTLS_PRIVATE(in_msg) != NULL && + scr_tls_alert_error(t, t->ssl.MBEDTLS_PRIVATE(in_msg)[1])) { + return -1; } else { /* non-verify handshake failures: honest but not Node's exact openssl * string (SEMANTICS.md divergence 55) */ @@ -1215,6 +1643,9 @@ static void scr_tls_shutdown_write(void *tctx) { ScrTlsSock *t = (ScrTlsSock *)tctx; if (t->notify_sent) return; t->notify_sent = true; + /* end() sends what is held, then the close_notify, ahead of the FIN */ + t->holding = false; + if (t->held_len > t->held_off) (void)scr_tls_flush_held(t); (void)mbedtls_ssl_close_notify(&t->ssl); /* best effort; EAGAIN drops it */ } @@ -1227,9 +1658,18 @@ static void scr_tls_free(void *tctx) { scr_net_ls_drop(&t->secure_ls); free(t->hello); scr_secure_ctx_release(t->sni_ctx); + mbedtls_x509_crt_free(&t->served_chain); + if (t->server) scr_net_server_release(t->server); + free(t->held); free(t); } +/* The fd is writable again: the rest of what was held goes out. */ +static void scr_tls_drain(void *tctx) { + ScrTlsSock *t = (ScrTlsSock *)tctx; + if (!t->holding && t->held_len > t->held_off) (void)scr_tls_flush_held(t); +} + /* The 'secure' event: after a client's secureConnect listeners, as Node * emits it. */ static void scr_tls_after_established(void *tctx) { @@ -1247,6 +1687,10 @@ static void scr_tls_settle(void *tctx) { t->settled = true; scr_net_ls_drop(&t->secure_ls); scr_net_ls_drop(&t->session_ls); + if (t->hang_up_pending) { + t->hang_up_pending = false; + scr_tls_client_error(t, "socket hang up", "ECONNRESET"); + } } static const ScrNetTransportOps SCR_TLS_OPS = { @@ -1259,6 +1703,7 @@ static const ScrNetTransportOps SCR_TLS_OPS = { .free = scr_tls_free, .settle = scr_tls_settle, .after_established = scr_tls_after_established, + .drain = scr_tls_drain, }; /* ── the server surface ──────────────────────────────────────────────── */ @@ -1272,8 +1717,11 @@ static void scr_tls_srv_on_conn(void *ctx, ScrNetSocket *sock) { if (!t) scr_tls_oom(); mbedtls_ssl_init(&t->ssl); if (mbedtls_ssl_setup(&t->ssl, &srv->conf) != 0) scr_tls_oom(); + if (srv->request_cert) mbedtls_ssl_set_verify(&t->ssl, &scr_tls_vrfy_cb, t); t->sock = sock; t->srv = scr_tls_srv_retain(srv); + ScrNetServer *server = scr_net_sock_server(sock); + t->server = server != NULL ? scr_net_server_retain(server) : NULL; if (srv->sni_cb != NULL || srv->max_tls12) t->sni_state = SCR_SNI_COLLECT; /* pre-parse the hello */ mbedtls_ssl_set_bio(&t->ssl, t, scr_tls_bio_send, scr_tls_bio_recv, NULL); scr_net_sock_set_transport(sock, &SCR_TLS_OPS, t); @@ -1285,8 +1733,10 @@ static ScrNetServer *scr_tls_create_server_alpn(const char *cert, size_t cert_le ScrNetConnFn fn, char **alpn /*moves, nullable*/, int min_version, int max_version, ScrClosure *sni_cb /*moves, nullable*/, - void *sni_answer_fn) { + void *sni_answer_fn, + const ScrTlsClientAuth *auth /*nullable*/) { ScrTlsSrv *srv = scr_tls_srv_new(cert, cert_len, key, key_len, false); + scr_tls_srv_client_auth(srv, auth); if (sni_cb != NULL) { srv->sni_cb = sni_cb; /* moves; released with srv */ srv->sni_answer_fn = sni_answer_fn; @@ -1319,7 +1769,7 @@ ScrNetServer *scr_tls_create_server(const char *cert, size_t cert_len, const cha 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, NULL, - NULL); + NULL, NULL); } /* tls.createServer({ cert, key, SNICallback }): the pre-parse machinery @@ -1332,14 +1782,15 @@ ScrNetServer *scr_tls_create_server_sni(const char *cert, size_t cert_len, const 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); + sni_answer_fn, NULL); } -ScrNetServer *scr_https_create_server(const char *cert, size_t cert_len, const char *key, - size_t key_len, ScrClosure *handler /*moves*/, - ScrHttpReqFn fn) { +static ScrNetServer *scr_https_create_server_auth(const char *cert, size_t cert_len, const char *key, + size_t key_len, ScrClosure *handler /*moves*/, + ScrHttpReqFn fn, const ScrTlsClientAuth *auth /*nullable*/) { ScrNetServer *s = scr_http_create_server(handler, fn); ScrTlsSrv *srv = scr_tls_srv_new(cert, cert_len, key, key_len, true /* ALPN http/1.1 */); + scr_tls_srv_client_auth(srv, auth); /* wrap the parser hook: TLS first, parser at establishment */ scr_net_server_get_native_conn(s, &srv->inner_conn, &srv->inner_ctx, &srv->inner_ctx_free); scr_net_server_set_native_conn(s, &scr_tls_srv_on_conn, srv, &scr_tls_srv_release_v); @@ -1347,6 +1798,12 @@ ScrNetServer *scr_https_create_server(const char *cert, size_t cert_len, const c return s; } +ScrNetServer *scr_https_create_server(const char *cert, size_t cert_len, const char *key, + size_t key_len, ScrClosure *handler /*moves*/, + ScrHttpReqFn fn) { + return scr_https_create_server_auth(cert, cert_len, key, key_len, handler, fn, NULL); +} + /* The SNI-callback form of the allowHTTP1 compatibility server: the same * handler-less https server, plus the pre-parse machinery (design note * atop the SNI section). A NULL sni_cb — the conditional spread's @@ -1505,12 +1962,22 @@ static int scr_tls_tunnel_bio_recv(void *ctx, unsigned char *buf, size_t len) { return scr_tls_tunnel_read((ScrTlsTunnel *)ctx, buf, len); } +/* A tunnel leg's handshake has finished. The tunnel runs no established + * callbacks before it writes the CONNECT request or the request itself, so + * the last flight goes out now instead of waiting for the immediate that + * ends a socket's hold (which does not see the tunnel's legs). */ +static void scr_tls_tunnel_end_hold(ScrTlsSock *t) { + t->holding = false; + if (t->held_len > t->held_off) (void)scr_tls_flush_held(t); +} + static int scr_tls_tunnel_handshake(void *tctx) { ScrTlsTunnel *tun = (ScrTlsTunnel *)tctx; ScrNetSocket *sock = tun->outer->sock; if (tun->stage == SCR_TUN_OUTER) { int r = scr_tls_handshake(tun->outer); if (r <= 0) return r; + scr_tls_tunnel_end_hold(tun->outer); tun->outer->established = true; tun->stage = SCR_TUN_SEND; } @@ -1579,6 +2046,7 @@ static int scr_tls_tunnel_handshake(void *tctx) { if (tun->stage == SCR_TUN_INNER) { int r = scr_tls_handshake(tun->inner); if (r <= 0) return r; + scr_tls_tunnel_end_hold(tun->inner); tun->stage = SCR_TUN_UP; } return 1; @@ -1625,12 +2093,16 @@ static void scr_tls_tunnel_free(void *tctx) { free(tun); } +/* The fd is writable again: the rest of what the outer leg held. */ +static void scr_tls_tunnel_drain(void *tctx) { scr_tls_drain(((ScrTlsTunnel *)tctx)->outer); } + static const ScrNetTransportOps SCR_TLS_TUNNEL_OPS = { .xread = scr_tls_tunnel_xread, .xwrite = scr_tls_tunnel_xwrite, .handshake = scr_tls_tunnel_handshake, .on_established = scr_tls_tunnel_on_established, .pending = scr_tls_tunnel_pending, + .drain = scr_tls_tunnel_drain, .shutdown_write = scr_tls_tunnel_shutdown_write, .free = scr_tls_tunnel_free, }; @@ -1822,10 +2294,11 @@ static ScrBytes *scr_tls_pem_dyn(const ScrDyn *v, const char *what, bool concat) /* https.request/get with a runtime options record: the http walk, then * the TLS options the literal https path lowers (rejectUnauthorized, ca, - * servername). Other tls.connect options fence when they carry a value. */ + * servername) and a client certificate (cert and key). Other tls.connect + * options fence when they carry a value. */ static const char *const SCR_HTTPS_REQ_FENCED_OPTIONS[] = { - "ALPNProtocols", "cert", "checkServerIdentity", "ciphers", "crl", "dhparam", "ecdhCurve", - "honorCipherOrder", "key", "maxVersion", "minVersion", "passphrase", "pfx", "secureContext", + "ALPNProtocols", "checkServerIdentity", "ciphers", "crl", "dhparam", "ecdhCurve", + "honorCipherOrder", "maxVersion", "minVersion", "passphrase", "pfx", "secureContext", "secureOptions", "secureProtocol", "session", "sessionIdContext", "sigalgs", NULL, }; @@ -1840,6 +2313,7 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au const ScrDyn *rec = o.view != NULL ? o.view : opts; bool reject = true; ScrBytes *ca = NULL; + ScrBytes *own_cert = NULL, *own_key = NULL; ScrStr *servername = NULL; bool ok = true; if (rec != NULL && rec->kind == SCR_DYN_OBJ) { @@ -1848,7 +2322,7 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au const ScrDyn *v = scr_dyn_obj_get(rec, key, strlen(key)); if (v != NULL && scr_tls_dyn_truthy(v)) { scr_http_req_opts_fence("https.request", key, - "rejectUnauthorized, ca, and servername are the supported TLS options"); + "rejectUnauthorized, ca, servername, cert, and key are the supported TLS options"); ok = false; } } @@ -1859,6 +2333,21 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au ca = scr_tls_pem_dyn(c, "an https.request 'ca' option", true); if (ca == NULL) ok = false; } + const ScrDyn *oc = ok ? scr_dyn_obj_get(rec, "cert", 4) : NULL; + if (oc != NULL && oc->kind != SCR_DYN_UNDEF && oc->kind != SCR_DYN_NULL) { + own_cert = scr_tls_pem_dyn(oc, "an https.request 'cert' option", false); + if (own_cert == NULL) ok = false; + } + const ScrDyn *ok_ = ok ? scr_dyn_obj_get(rec, "key", 3) : NULL; + if (ok_ != NULL && ok_->kind != SCR_DYN_UNDEF && ok_->kind != SCR_DYN_NULL) { + own_key = scr_tls_pem_dyn(ok_, "an https.request 'key' option", false); + if (own_key == NULL) ok = false; + } + if (ok && (own_cert == NULL) != (own_key == NULL)) { + scr_http_req_opts_fence("https.request", own_cert != NULL ? "cert" : "key", + "a client certificate lowers with both cert and key"); + ok = false; + } const ScrDyn *sn = ok ? scr_dyn_obj_get(rec, "servername", 10) : NULL; if (sn != NULL && sn->kind != SCR_DYN_UNDEF && sn->kind != SCR_DYN_NULL) { if (sn->kind != SCR_DYN_STR) { @@ -1871,6 +2360,8 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au } if (!ok) { if (ca) scr_bytes_release(ca); + scr_bytes_release(own_cert); + scr_bytes_release(own_key); scr_http_req_opts_free(&o); if (cb) scr_closure_release(cb); return NULL; @@ -1879,6 +2370,22 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au size_t ca_len = ca ? ca->len : 0; ScrTlsCli *cli = scr_tls_cli_for(o.host, reject, ca_data, ca_len, servername); ScrStr *key = scr_tls_pool_key(o.host, reject, ca_data, ca_len, servername); + if (own_cert != NULL) { + /* Node's agent keys pooled sockets by the client certificate too */ + scr_tls_cli_set_own(cli, own_cert, own_key); + uint64_t h = 1469598103934665603ull; + for (size_t i = 0; i < own_cert->len; i++) h = (h ^ own_cert->data[i]) * 1099511628211ull; + for (size_t i = 0; i < own_key->len; i++) h = (h ^ own_key->data[i]) * 1099511628211ull; + char suffix[24]; + int n = snprintf(suffix, sizeof suffix, ":c%016llx", (unsigned long long)h); + ScrStr *tail = scr_str_new(suffix, (size_t)n); + ScrStr *keyed = scr_str_concat(key, tail); + scr_str_release(tail); + scr_str_release(key); + key = keyed; + scr_bytes_release(own_cert); + scr_bytes_release(own_key); + } scr_str_release(servername); ScrHttpClientReq *req = scr_http_req_opts_send(&o, auto_end, cb, fn, add_abort, &scr_tls_cli_wrap, cli, key); @@ -2014,9 +2521,22 @@ static bool scr_tls_opts_validate(const ScrDyn *opts) { /* `server_ok`: tls.createServer reads ALPNProtocols, minVersion and * maxVersion itself; the walk's other callers keep their fences. */ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrBytes **cert_out, - ScrBytes **key_out, bool server_ok) { + ScrBytes **key_out, bool server_ok, ScrTlsClientAuth *auth) { + if (opts != NULL && opts->kind == SCR_DYN_TYPED_REF) { + /* a typed record (an options binding): its fields, as a plain object */ + ScrDyn *plain = scr_dyn_typed_ref_materialize(opts); + if (plain == NULL) { + *cert_out = NULL; + *key_out = NULL; + return false; + } + bool ok = scr_tls_srv_opts_walk_ex(plain, api, cert_out, key_out, server_ok, auth); + scr_dyn_release(plain); + return ok; + } *cert_out = NULL; *key_out = NULL; + if (auth != NULL) *auth = (ScrTlsClientAuth){.request = false, .reject = true, .ca = NULL}; if (opts == NULL || opts->kind != SCR_DYN_OBJ) { scr_dyn_arg_type_fail("options", "of type object", opts ? opts : scr_dyn_undefined()); return false; @@ -2038,20 +2558,39 @@ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrByt continue; } if (strcmp(e->key, "ca") == 0) { - /* the server's `ca` verifies CLIENT certificates — inert without - * requestCert (which fences below), so Node-observably droppable */ + /* the server's `ca` verifies client certificates and completes the + * server's own chain (scr_tls_srv_client_auth); a walk without + * client certificates drops it */ + if (auth != NULL) { + char what[96]; + snprintf(what, sizeof what, "a %s 'ca' option", api); + scr_bytes_release(auth->ca); + /* An empty string is no ca, as Node reads it; an empty Buffer is + * an empty trust store. */ + if (v->kind == SCR_DYN_STR && v->v.str->len == 0) { + auth->ca = NULL; + } else { + auth->ca = scr_tls_pem_dyn(v, what, true); + if (auth->ca == NULL) ok = false; + } + } continue; } if (strcmp(e->key, "requestCert") == 0) { + if (auth != NULL) { + auth->request = scr_tls_dyn_truthy(v); + continue; + } if (scr_tls_dyn_truthy(v)) { scr_tls_opt_fence(api, "requestCert", - "client-certificate handshakes are not modeled (SEMANTICS.md divergence 55) — requestCert stays at Node's false default"); + "requestCert lowers on tls.createServer, https.createServer and new tls.TLSSocket"); ok = false; } continue; } if (strcmp(e->key, "rejectUnauthorized") == 0) { - continue; /* meaningful only with requestCert, which fences */ + if (auth != NULL) auth->reject = scr_tls_dyn_truthy(v); + continue; /* without client certificates it has no effect */ } if (server_ok && (strcmp(e->key, "ALPNProtocols") == 0 || strcmp(e->key, "minVersion") == 0 || strcmp(e->key, "maxVersion") == 0)) @@ -2085,13 +2624,17 @@ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrByt scr_bytes_release(*key_out); *cert_out = NULL; *key_out = NULL; + if (auth != NULL) { + scr_bytes_release(auth->ca); + auth->ca = NULL; + } } return ok; } bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes **cert_out, ScrBytes **key_out) { - return scr_tls_srv_opts_walk_ex(opts, api, cert_out, key_out, false); + return scr_tls_srv_opts_walk_ex(opts, api, cert_out, key_out, false, NULL); } /* createSecureContext over a RUNTIME options record: Node's typed @@ -2101,9 +2644,22 @@ bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes **cert_ * +1, or NULL with the exception pending. */ ScrSecureCtx *scr_tls_create_secure_context_dyn(const ScrDyn *opts /*borrowed*/) { ScrBytes *cert, *key; - if (!scr_tls_srv_opts_walk(opts, "tls.createSecureContext", &cert, &key)) return NULL; + /* requestCert and rejectUnauthorized are no context options: Node's + * createSecureContext ignores them */ + ScrTlsClientAuth auth; + if (!scr_tls_srv_opts_walk_ex(opts, "tls.createSecureContext", &cert, &key, false, &auth)) return NULL; ScrSecureCtx *c = scr_tls_create_secure_context((const char *)cert->data, cert->len, (const char *)key->data, key->len); + if (auth.ca != NULL) { + /* kept for sockets created from the context (a TLSSocket's + * secureContext); an SNI answer's chain comes from the server's + * context in OpenSSL, so the answer's ca is not used there */ + if (auth.ca->len > 0) { + int r = scr_tls_parse_crt(&c->ca, (const char *)auth.ca->data, auth.ca->len); + if (r != 0) scr_tls_bad_material("ca certificate", r); + } + scr_bytes_release(auth.ca); + } scr_bytes_release(cert); scr_bytes_release(key); return c; @@ -2142,7 +2698,8 @@ ScrNetServer *scr_tls_create_server_dyn(const ScrDyn *opts /*borrowed*/, return NULL; } ScrBytes *cert, *key; - if (!scr_tls_srv_opts_walk_ex(opts, "tls.createServer", &cert, &key, true)) { + ScrTlsClientAuth auth; + if (!scr_tls_srv_opts_walk_ex(opts, "tls.createServer", &cert, &key, true, &auth)) { scr_closure_release(handler); return NULL; } @@ -2153,12 +2710,14 @@ ScrNetServer *scr_tls_create_server_dyn(const ScrDyn *opts /*borrowed*/, if (protocols != NULL && !scr_tls_alpn_dyn(protocols, &alpn)) { scr_bytes_release(cert); scr_bytes_release(key); + scr_bytes_release(auth.ca); scr_closure_release(handler); return NULL; } 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, NULL, NULL); + alpn, min_version, max_version, NULL, NULL, &auth); + scr_bytes_release(auth.ca); scr_bytes_release(cert); scr_bytes_release(key); return s; @@ -2189,18 +2748,146 @@ ScrNetServer *scr_https_create_server_dyn(const ScrDyn *opts /*borrowed*/, } } ScrBytes *cert, *key; - if (!scr_tls_srv_opts_walk(opts, "https.createServer", &cert, &key)) { + ScrTlsClientAuth auth; + if (!scr_tls_srv_opts_walk_ex(opts, "https.createServer", &cert, &key, false, &auth)) { scr_closure_release(handler); return NULL; } - ScrNetServer *s = scr_https_create_server((const char *)cert->data, cert->len, - (const char *)key->data, key->len, handler, fn); + ScrNetServer *s = scr_https_create_server_auth((const char *)cert->data, cert->len, + (const char *)key->data, key->len, handler, fn, &auth); + scr_bytes_release(auth.ca); if (has_timeout_buffer) scr_net_server_timeout_set(s, 4, timeout_buffer); scr_bytes_release(cert); scr_bytes_release(key); return s; } +/* ── server.addContext and server.setSecureContext ───────────────────── */ + +/* Node's addContext pattern: the name with '*' as [^.]* (any run within a + * label, empty too), matched without regard to case. */ +static bool scr_tls_name_match(const char *pattern, const unsigned char *name, size_t len) { + if (*pattern == '\0') return len == 0; + if (*pattern == '*') { + for (size_t k = 0;; k++) { + if (scr_tls_name_match(pattern + 1, name + k, len - k)) return true; + if (k == len || name[k] == '.') return false; + } + } + return len > 0 && (unsigned char)tolower(name[0]) == (unsigned char)*pattern && + scr_tls_name_match(pattern + 1, name + 1, len - 1); +} + +/* The certificate a server handshake serves when the server has contexts + * (mbedTLS's certificate callback, after the SNI name is read): the + * newest addContext entry whose pattern matches the name, else the + * setSecureContext default, else the server's own. An SNICallback answer + * (or a server with an SNICallback, whose contexts Node does not read) + * keeps its own path. OpenSSL completes the served chain from the store + * of the connection's context, the server's current default, and verifies + * client certificates against the served context's. */ +static int scr_tls_f_cert(mbedtls_ssl_context *ssl) { + ScrTlsSock *t = scr_tls_current; + if (t == NULL || t->srv == NULL || t->sni_ctx != NULL) return 0; + ScrTlsSrv *s = t->srv; + ScrSecureCtx *pick = NULL; + if (s->sni_cb == NULL) { + size_t len = 0; + const unsigned char *name = mbedtls_ssl_get_hs_sni(ssl, &len); + for (size_t i = s->ncontexts; name != NULL && i-- > 0;) { + if (scr_tls_name_match(s->contexts[i].pattern, name, len)) { + pick = s->contexts[i].ctx; + break; + } + } + } + if (pick == NULL) pick = s->default_ctx; + if (pick == NULL) return 0; + t->sni_ctx = scr_secure_ctx_retain(pick); + const mbedtls_x509_crt *store = s->default_ctx != NULL ? &s->default_ctx->ca : &s->ca; + mbedtls_x509_crt *own = &pick->cert; + if (store->raw.len > 0 && pick->cert.next == NULL) { + scr_tls_chained_copy(&t->served_chain, &pick->cert, store); + own = &t->served_chain; + } + if (s->request_cert) + mbedtls_ssl_set_hs_ca_chain(ssl, pick->ca.raw.len > 0 ? &pick->ca : scr_tls_system_ca(), NULL); + return mbedtls_ssl_set_hs_own_cert(ssl, own, &pick->pk); +} + +/* The TLS config of a tls or https server, or NULL with the fence thrown. */ +static ScrTlsSrv *scr_tls_srv_of_server(ScrNetServer *server, const char *what) { + ScrNetNativeConnFn fn = NULL; + void *ctx = NULL; + void (*ctx_free)(void *) = NULL; + scr_net_server_get_native_conn(server, &fn, &ctx, &ctx_free); + if (fn == &scr_tls_srv_on_conn && ctx != NULL) return (ScrTlsSrv *)ctx; + char msg[160]; + int n = snprintf(msg, sizeof msg, "%s on a server that is not a tls or https server is not supported yet", what); + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)(n < (int)sizeof msg ? n : (int)sizeof msg - 1), "SC2020"); + return NULL; +} + +/* server.addContext(servername, context): `context` is a SecureContext + * (borrowed, nullable) or the options createSecureContext reads. Node + * builds a RegExp from the name, so a name with regular-expression groups + * or alternation throws the fence. */ +void scr_tls_server_add_context(ScrNetServer *server, const ScrDyn *servername, + const ScrDyn *options /*nullable*/, ScrSecureCtx *ctx /*nullable*/) { + ScrTlsSrv *s = scr_tls_srv_of_server(server, "server.addContext"); + if (s == NULL) return; + if (!scr_tls_dyn_truthy(servername)) { + static const char msg[] = "\"servername\" is required parameter for Server.addContext"; + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "ERR_TLS_REQUIRED_SERVER_NAME"); + return; + } + if (servername->kind != SCR_DYN_STR) { + scr_tls_opt_fence("server.addContext", "servername", "the servername lowers as a string"); + return; + } + const ScrStr *name = servername->v.str; + for (size_t i = 0; i < name->len; i++) { + if (name->data[i] == '(' || name->data[i] == ')' || name->data[i] == '|') { + scr_tls_opt_fence("server.addContext", "servername", + "a servername with regular-expression groups or alternation has no lowering"); + return; + } + /* the RegExp's case folding beyond ASCII differs from tolower's */ + if ((unsigned char)name->data[i] >= 0x80) { + scr_tls_opt_fence("server.addContext", "servername", "a servername outside ASCII has no lowering"); + return; + } + } + ScrSecureCtx *c = ctx != NULL ? scr_secure_ctx_retain(ctx) : scr_tls_create_secure_context_dyn(options); + if (c == NULL) return; + char *pattern = malloc(name->len + 1); + struct ScrTlsNamedCtx *grown = realloc(s->contexts, (s->ncontexts + 1) * sizeof *s->contexts); + if (pattern == NULL || grown == NULL) scr_tls_oom(); + for (size_t i = 0; i < name->len; i++) pattern[i] = (char)tolower((unsigned char)name->data[i]); + pattern[name->len] = '\0'; + s->contexts = grown; + s->contexts[s->ncontexts].pattern = pattern; + s->contexts[s->ncontexts].ctx = c; + s->ncontexts++; + mbedtls_ssl_conf_cert_cb(&s->conf, &scr_tls_f_cert); +} + +/* server.setSecureContext(options): new connections serve a context made + * from `options`; open ones keep theirs. */ +void scr_tls_server_set_secure_context(ScrNetServer *server, const ScrDyn *options) { + ScrTlsSrv *s = scr_tls_srv_of_server(server, "server.setSecureContext"); + if (s == NULL) return; + if (options == NULL || (options->kind != SCR_DYN_OBJ && options->kind != SCR_DYN_TYPED_REF)) { + scr_dyn_arg_type_fail("options", "of type object", options != NULL ? options : scr_dyn_undefined()); + return; + } + ScrSecureCtx *c = scr_tls_create_secure_context_dyn(options); + if (c == NULL) return; + scr_secure_ctx_release(s->default_ctx); + s->default_ctx = c; + mbedtls_ssl_conf_cert_cb(&s->conf, &scr_tls_f_cert); +} + /* ── new tls.TLSSocket(socket, options) ───────────────────────────────── */ /* The server-mode wrap: a fresh TLS socket takes over raw's connection @@ -2226,7 +2913,7 @@ static bool scr_tls_wrap_socket_opts_check(const ScrDyn *record, const char *api 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"); + "requestCert lowers with cert and key options, not beside a secureContext"); return false; } if (strcmp(e->key, "SNICallback") == 0 || strcmp(e->key, "ALPNCallback") == 0) { @@ -2271,13 +2958,17 @@ ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrow /* Node serves a secureContext as it is: the record's cert, key and * 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. */ + * requestCert lowers with cert and key, whose ca the walk reads. */ int min_version = 12, max_version = 13; ScrBytes *cert = NULL, *key = NULL; + ScrTlsClientAuth auth = {.request = false, .reject = false, .ca = 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; + if (!scr_tls_srv_opts_walk_ex(record, api, &cert, &key, true, &auth)) return NULL; + /* a standalone socket requires a certificate only for a truthy + * rejectUnauthorized; tls.Server's true default does not apply */ + const ScrDyn *reject = scr_dyn_obj_get(record, "rejectUnauthorized", 18); + auth.reject = reject != NULL && scr_tls_dyn_truthy(reject); } else if (!scr_tls_wrap_socket_opts_check(record, api)) { return NULL; } @@ -2286,6 +2977,7 @@ ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrow if (protocols != NULL && protocols->kind != SCR_DYN_UNDEF && !scr_tls_alpn_dyn(protocols, &alpn)) { scr_bytes_release(cert); scr_bytes_release(key); + scr_bytes_release(auth.ca); return NULL; } ScrTlsSrv *srv = ctx != NULL ? scr_tls_srv_from_ctx(ctx) @@ -2293,6 +2985,9 @@ ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrow (const char *)key->data, key->len, false); scr_bytes_release(cert); scr_bytes_release(key); + srv->standalone = true; + scr_tls_srv_client_auth(srv, ctx == NULL ? &auth : NULL); + scr_bytes_release(auth.ca); 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)) { @@ -2314,6 +3009,7 @@ ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrow if (!ts) scr_tls_oom(); mbedtls_ssl_init(&ts->ssl); if (mbedtls_ssl_setup(&ts->ssl, &srv->conf) != 0) scr_tls_oom(); + if (srv->request_cert) mbedtls_ssl_set_verify(&ts->ssl, &scr_tls_vrfy_cb, ts); ts->sock = t; ts->srv = srv; /* moves */ ts->wrapped = true; @@ -2468,10 +3164,10 @@ static bool scr_tls_alpn_dyn(const ScrDyn *v, char ***out) { * throws (the server walk's stance). */ static const char *const SCR_TLS_CONNECT_FENCED_OPTIONS[] = { "allowHalfOpen", "autoSelectFamily", - "autoSelectFamilyAttemptTimeout", "cert", "certificateCompression", "checkServerIdentity", + "autoSelectFamilyAttemptTimeout", "certificateCompression", "checkServerIdentity", "ciphers", "clientCertEngine", "crl", "dhparam", "ecdhCurve", "enableTrace", "family", "fd", "hints", "highWaterMark", - "honorCipherOrder", "keepAlive", "keepAliveInitialDelay", "key", + "honorCipherOrder", "keepAlive", "keepAliveInitialDelay", "localAddress", "localPort", "lookup", "minDHSize", "noDelay", "onread", "passphrase", "path", "pfx", "privateKeyEngine", "privateKeyIdentifier", "pskCallback", "readable", "secureContext", "secureOptions", "secureProtocol", "session", @@ -2479,12 +3175,32 @@ static const char *const SCR_TLS_CONNECT_FENCED_OPTIONS[] = { "writable", NULL, }; +static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, nullable*/, + const ScrDyn *opts /*borrowed, nullable*/, + ScrClosure *cb /*moves, nullable*/); + ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable*/, const ScrDyn *opts /*borrowed, nullable*/, ScrClosure *cb /*moves, nullable*/) { + if (opts == NULL || opts->kind != SCR_DYN_TYPED_REF) return scr_tls_connect_plain(port, host, opts, cb); + /* a typed record (an options binding): its fields, as a plain object */ + ScrDyn *plain = scr_dyn_typed_ref_materialize(opts); + if (plain == NULL) { + scr_closure_release(cb); + return NULL; + } + ScrNetSocket *s = scr_tls_connect_plain(port, host, plain, cb); + scr_dyn_release(plain); + return s; +} + +static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, nullable*/, + const ScrDyn *opts /*borrowed, nullable*/, + ScrClosure *cb /*moves, nullable*/) { scr_tls_rng_init(); bool reject = true; ScrBytes *ca = NULL; + ScrBytes *own_cert = NULL, *own_key = NULL; /* the client certificate */ const ScrStr *servername = NULL; const ScrStr *opt_host = NULL; char **alpn = NULL; @@ -2549,6 +3265,14 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable if (ca == NULL) ok = false; continue; } + if (strcmp(e->key, "cert") == 0 || strcmp(e->key, "key") == 0) { + ScrBytes **slot = e->key[0] == 'c' ? &own_cert : &own_key; + scr_bytes_release(*slot); + *slot = scr_tls_pem_dyn(v, e->key[0] == 'c' ? "a tls.connect 'cert' option" : "a tls.connect 'key' option", + false); + if (*slot == NULL) ok = false; + continue; + } if (strcmp(e->key, "ALPNProtocols") == 0) { alpn_value = v; /* converted after the context options, as TLSSocket does */ continue; @@ -2607,6 +3331,11 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable timeout_ms = timeout_value->v.num; } } + if (ok && (own_cert == NULL) != (own_key == NULL)) { + scr_tls_opt_fence("tls.connect", own_cert != NULL ? "cert" : "key", + "a client certificate lowers with both cert and key"); + ok = false; + } if (ok && port < 0 && wrap == NULL) { scr_dyn_arg_type_fail("options.port", "of type number", scr_dyn_undefined()); ok = false; @@ -2616,6 +3345,8 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable if (!ok) { scr_tls_alpn_free(alpn); scr_bytes_release(ca); + scr_bytes_release(own_cert); + scr_bytes_release(own_key); scr_closure_release(cb); return NULL; } @@ -2639,6 +3370,8 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable if (s == NULL) { scr_tls_alpn_free(alpn); scr_bytes_release(ca); + scr_bytes_release(own_cert); + scr_bytes_release(own_key); scr_closure_release(cb); scr_str_release(localhost_fallback); scr_str_release(socket_host); @@ -2656,6 +3389,9 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable ca != NULL ? (const char *)ca->data : NULL, ca != NULL ? ca->len : 0, !reject); if (servername != NULL) scr_tls_cli_set_servername(cli, servername); + if (own_cert != NULL) scr_tls_cli_set_own(cli, own_cert, own_key); + scr_bytes_release(own_cert); + scr_bytes_release(own_key); if (alpn != NULL) { cli->alpn = alpn; /* moves */ mbedtls_ssl_conf_alpn_protocols(&cli->conf, (const char **)cli->alpn); @@ -2731,6 +3467,20 @@ static const char *scr_tls_auth_code(ScrTlsSock *t) { return "UNABLE_TO_VERIFY_LEAF_SIGNATURE"; } +/* A server's authorizationError for the client certificate. OpenSSL + * keeps the last error its verification met: the validity period is + * checked last, the purpose (clientAuth) before it, the chain first. A + * client that sent none is UNABLE_TO_GET_ISSUER_CERT, as Node reports it. */ +static const char *scr_tls_client_cert_code(ScrTlsSock *t) { + uint32_t vr = mbedtls_ssl_get_verify_result(&t->ssl); + if (vr & MBEDTLS_X509_BADCERT_MISSING) return "UNABLE_TO_GET_ISSUER_CERT"; + if (t->vrfy_expired) return "CERT_HAS_EXPIRED"; + if (vr & (MBEDTLS_X509_BADCERT_EXT_KEY_USAGE | MBEDTLS_X509_BADCERT_KEY_USAGE)) return "INVALID_PURPOSE"; + if (t->vrfy_leaf_self) return "DEPTH_ZERO_SELF_SIGNED_CERT"; + if (t->vrfy_chain_self) return "SELF_SIGNED_CERT_IN_CHAIN"; + return "UNABLE_TO_VERIFY_LEAF_SIGNATURE"; +} + static ScrTlsSock *scr_tls_sock_of(ScrNetSocket *s) { return (ScrTlsSock *)scr_net_sock_transport_ctx_for(s, &SCR_TLS_OPS); } @@ -2775,22 +3525,106 @@ static void scr_tls_fire_session(ScrTlsSock *t) { /* The STATIC-lane TLSSocket members (the lowered socket surface). */ +/* Whether the socket verified its peer: a client against its trust + * anchors, a server that requested a client certificate against its. */ +static bool scr_tls_sock_verifies(const ScrTlsSock *t) { + return t->established && ((t->cli != NULL && t->cli->verify_recorded) || + (t->srv != NULL && t->srv->request_cert && !t->srv->standalone)); +} + bool scr_tls_sock_authorized(ScrNetSocket *s) { ScrTlsSock *t = scr_tls_sock_of(s); - return t != NULL && t->established && t->cli != NULL && t->cli->verify_recorded && - mbedtls_ssl_get_verify_result(&t->ssl) == 0; + return t != NULL && scr_tls_sock_verifies(t) && mbedtls_ssl_get_verify_result(&t->ssl) == 0; } ScrStr *scr_tls_sock_auth_error(ScrNetSocket *s) { ScrTlsSock *t = scr_tls_sock_of(s); - if (t == NULL || !t->established || t->cli == NULL || !t->cli->verify_recorded || - mbedtls_ssl_get_verify_result(&t->ssl) == 0) { + if (t == NULL || !scr_tls_sock_verifies(t) || mbedtls_ssl_get_verify_result(&t->ssl) == 0) { return NULL; /* the null arm — authorized, or never verified */ } - const char *code = scr_tls_auth_code(t); + const char *code = t->srv != NULL ? scr_tls_client_cert_code(t) : scr_tls_auth_code(t); return scr_str_new(code, strlen(code)); } +/* ── certificates for the X509Certificate accessors ────────────────── + * + * getPeerCertificate, getCertificate and their X509Certificate forms read + * the DER below (scr_tls_x509.c builds the objects, so this unit needs no + * X.509 code). Node answers from the socket's handle, which a destroyed + * socket no longer has. */ + +bool scr_tls_sock_has_handle(ScrNetSocket *s) { + return scr_tls_sock_of(s) != NULL && !scr_net_sock_destroyed(s); +} + +/* The certificates the peer sent, leaf first (borrowed DER), up to `max`; + * none before the handshake. Answers the count. */ +size_t scr_tls_sock_peer_chain(ScrNetSocket *s, const uint8_t **der, size_t *len, size_t max) { + ScrTlsSock *t = scr_tls_sock_of(s); + if (t == NULL || !t->established) return 0; + size_t n = 0; + for (const mbedtls_x509_crt *c = mbedtls_ssl_get_peer_cert(&t->ssl); c != NULL && c->raw.len > 0 && n < max; + c = c->next) { + der[n] = c->raw.p; + len[n] = c->raw.len; + n++; + } + return n; +} + +/* The certificate the socket serves (borrowed DER), or NULL: a server's + * SNI answer, secureContext or own certificate. Clients send none. */ +const uint8_t *scr_tls_sock_own_cert(ScrNetSocket *s, size_t *len) { + ScrTlsSock *t = scr_tls_sock_of(s); + if (t != NULL && t->cli != NULL && t->cli->has_own) { + *len = t->cli->own.raw.len; + return t->cli->own.raw.p; + } + if (t == NULL || t->srv == NULL) return NULL; + const mbedtls_x509_crt *c = t->sni_ctx != NULL ? &t->sni_ctx->cert + : t->srv->ctx != NULL ? &t->srv->ctx->cert + : &t->srv->cert; + if (c->raw.len == 0) return NULL; + *len = c->raw.len; + return c->raw.p; +} + +/* Visits the trust anchors the socket verified its peer against (its + * `ca`, or the default store) until `visit` answers true. */ +void scr_tls_sock_each_trusted(ScrNetSocket *s, bool (*visit)(void *ctx, const uint8_t *der, size_t len), + void *ctx) { + ScrTlsSock *t = scr_tls_sock_of(s); + if (t == NULL || (t->cli == NULL && t->srv == NULL)) return; + const mbedtls_x509_crt *c = t->cli != NULL ? (t->cli->ca.raw.len > 0 ? &t->cli->ca : scr_tls_system_ca()) + : t->srv->has_ca ? &t->srv->ca + : scr_tls_system_ca(); + for (; c != NULL && c->raw.len > 0; c = c->next) + if (visit(ctx, c->raw.p, c->raw.len)) return; +} + +static ScrBytes *scr_tls_der_buffer(const uint8_t *der, size_t len) { + ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)len); + b->is_buffer = true; + if (len) memcpy(b->data, der, len); + return b; +} + +/* getPeerX509Certificate() and getX509Certificate(): the certificate's + * DER (+1 Buffer), empty when there is none (undefined in Node). */ +ScrBytes *scr_tls_sock_peer_cert_der(ScrNetSocket *s) { + const uint8_t *der; + size_t len; + if (!scr_tls_sock_has_handle(s) || scr_tls_sock_peer_chain(s, &der, &len, 1) == 0) + return scr_tls_der_buffer(NULL, 0); + return scr_tls_der_buffer(der, len); +} + +ScrBytes *scr_tls_sock_own_cert_der(ScrNetSocket *s) { + size_t len = 0; + const uint8_t *der = scr_tls_sock_has_handle(s) ? scr_tls_sock_own_cert(s, &len) : NULL; + return scr_tls_der_buffer(der, der ? len : 0); +} + /* tlsSocket.getProtocol(): the negotiated version ("TLSv1.2", * "TLSv1.3"); before the handshake the highest version on offer, as * OpenSSL answers, and null after close. +1. */ diff --git a/packages/runtime/src/scr_tls.h b/packages/runtime/src/scr_tls.h index cc8e3336e..94bf41d33 100644 --- a/packages/runtime/src/scr_tls.h +++ b/packages/runtime/src/scr_tls.h @@ -58,6 +58,19 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable bool scr_tls_sock_authorized(ScrNetSocket *s); ScrStr *scr_tls_sock_auth_error(ScrNetSocket *s); /* +1 or NULL */ ScrDyn *scr_tls_sock_get_protocol(ScrNetSocket *s); /* +1: string or null */ +/* The certificates behind getPeerCertificate, getCertificate and their + * X509Certificate forms (scr_tls_x509.c reads them). */ +bool scr_tls_sock_has_handle(ScrNetSocket *s); +size_t scr_tls_sock_peer_chain(ScrNetSocket *s, const uint8_t **der, size_t *len, size_t max); +const uint8_t *scr_tls_sock_own_cert(ScrNetSocket *s, size_t *len); +void scr_tls_sock_each_trusted(ScrNetSocket *s, bool (*visit)(void *ctx, const uint8_t *der, size_t len), + void *ctx); +ScrBytes *scr_tls_sock_peer_cert_der(ScrNetSocket *s); /* +1 Buffer, empty for none */ +ScrBytes *scr_tls_sock_own_cert_der(ScrNetSocket *s); /* +1 Buffer, empty for none */ +/* scr_tls_x509.c: getPeerCertificate([detailed]) and getCertificate(), +1 + * (an object, {} or null). */ +ScrDyn *scr_tls_sock_get_peer_certificate(ScrNetSocket *s, const ScrDyn *detailed); +ScrDyn *scr_tls_sock_get_certificate(ScrNetSocket *s); ScrDyn *scr_tls_sock_get_cipher(ScrNetSocket *s); /* +1: object, undefined or null */ 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); @@ -97,6 +110,12 @@ 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); +/* server.addContext(servername, context) (a SecureContext, or options when + * NULL) and server.setSecureContext(options) on a tls or https server; + * Node's errors pending on failure. */ +void scr_tls_server_add_context(ScrNetServer *server, const ScrDyn *servername, + const ScrDyn *options /*nullable*/, ScrSecureCtx *ctx /*nullable*/); +void scr_tls_server_set_secure_context(ScrNetServer *server, const ScrDyn *options); /* 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 diff --git a/packages/runtime/src/scr_tls_x509.c b/packages/runtime/src/scr_tls_x509.c new file mode 100644 index 000000000..d1dfd819b --- /dev/null +++ b/packages/runtime/src/scr_tls_x509.c @@ -0,0 +1,119 @@ +/* tlsSocket.getPeerCertificate([detailed]) and getCertificate(): Node's + * legacy certificate objects, built by scr_x509.c's toLegacyObject over + * the certificates scr_tls.c keeps. Linked into programs that use both + * units, so neither unit depends on the other. */ +#include "scr_runtime.h" + +#include +#include + +/* The peer chain entries read; a longer chain is cut, as no real chain is + * this long. */ +enum { SCR_TLS_X509_MAX_CHAIN = 16 }; + +static ScrBytes *scr_tls_x509_bytes(const uint8_t *der, size_t len) { + ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)len); + if (len) memcpy(b->data, der, len); + return b; +} + +static bool scr_tls_x509_same(const ScrBytes *a, const ScrBytes *b) { + return a->len == b->len && memcmp(a->data, b->data, a->len) == 0; +} + +/* tlsSocket.getCertificate(): the certificate the socket serves, {} + * without one, null once the socket is destroyed. +1. */ +ScrDyn *scr_tls_sock_get_certificate(ScrNetSocket *s) { + if (!scr_tls_sock_has_handle(s)) return scr_dyn_new_null(); + size_t len = 0; + const uint8_t *der = scr_tls_sock_own_cert(s, &len); + if (der == NULL) return scr_dyn_new_obj(); + ScrBytes *cert = scr_tls_x509_bytes(der, len); + ScrDyn *result = scr_crypto_x509_legacy_object(cert); + scr_bytes_release(cert); + return result; +} + +typedef struct { + ScrBytes *subject; /* borrowed */ + ScrBytes *found; /* +1, NULL until found */ +} ScrTlsX509Lookup; + +/* SSL_CTX_get_issuer: the first trust anchor that issued the subject. */ +static bool scr_tls_x509_issuer_visit(void *ctx, const uint8_t *der, size_t len) { + ScrTlsX509Lookup *lookup = ctx; + ScrBytes *candidate = scr_tls_x509_bytes(der, len); + if (scr_crypto_x509_check_issued(lookup->subject, candidate)) { + lookup->found = candidate; + return true; + } + scr_bytes_release(candidate); + return false; +} + +/* Sets `chain`'s issuerCertificate to the legacy object of `issuer` + * (borrowed) and answers that object (borrowed; `chain` holds it). */ +static ScrDyn *scr_tls_x509_link(ScrDyn *chain, ScrBytes *issuer) { + ScrDyn *object = scr_crypto_x509_legacy_object(issuer); + scr_dyn_obj_set(chain, "issuerCertificate", 17, object); + return object; +} + +/* tlsSocket.getPeerCertificate([detailed]): the peer's certificate, {} + * before the handshake or without one, null once the socket is destroyed. + * With `detailed`, issuerCertificate links each certificate to its issuer + * as Node's GetPeerCert does: first from the certificates the peer sent, + * in their order of issue, then from the trust anchors; a self-issued + * last certificate is its own issuerCertificate. +1. */ +ScrDyn *scr_tls_sock_get_peer_certificate(ScrNetSocket *s, const ScrDyn *detailed_arg) { + /* Node reads the argument with IsTrue(): only true itself. */ + bool detailed = detailed_arg->kind == SCR_DYN_BOOL && detailed_arg->v.b; + if (!scr_tls_sock_has_handle(s)) return scr_dyn_new_null(); + const uint8_t *der[SCR_TLS_X509_MAX_CHAIN]; + size_t len[SCR_TLS_X509_MAX_CHAIN]; + size_t n = scr_tls_sock_peer_chain(s, der, len, SCR_TLS_X509_MAX_CHAIN); + if (n == 0) return scr_dyn_new_obj(); + ScrBytes *cert = scr_tls_x509_bytes(der[0], len[0]); + ScrDyn *result = scr_crypto_x509_legacy_object(cert); + if (!detailed) { + scr_bytes_release(cert); + return result; + } + ScrBytes *pool[SCR_TLS_X509_MAX_CHAIN]; + size_t pooled = 0; + for (size_t i = 1; i < n; i++) pool[pooled++] = scr_tls_x509_bytes(der[i], len[i]); + ScrDyn *chain = result; + /* AddIssuerChainToObject: take the issuer out of the peer's list, and + * stop when none issued the current certificate or the issuer taken was + * the list's last entry. */ + for (;;) { + size_t i; + for (i = 0; i < pooled; i++) { + if (!scr_crypto_x509_check_issued(cert, pool[i])) continue; + chain = scr_tls_x509_link(chain, pool[i]); + scr_bytes_release(cert); + cert = pool[i]; + memmove(pool + i, pool + i + 1, (pooled - i - 1) * sizeof pool[0]); + pooled--; + break; + } + if (i == pooled) break; + } + for (size_t i = 0; i < pooled; i++) scr_bytes_release(pool[i]); + /* GetLastIssuedCert: then the trust anchors, until a self-issued one + * (bounded, for anchors that issued each other). */ + for (size_t step = 0; step < SCR_TLS_X509_MAX_CHAIN && !scr_crypto_x509_check_issued(cert, cert); step++) { + ScrTlsX509Lookup lookup = {cert, NULL}; + scr_tls_sock_each_trusted(s, scr_tls_x509_issuer_visit, &lookup); + if (lookup.found == NULL) break; + chain = scr_tls_x509_link(chain, lookup.found); + bool same = scr_tls_x509_same(cert, lookup.found); + scr_bytes_release(cert); + cert = lookup.found; + if (same) break; + } + if (scr_crypto_x509_check_issued(cert, cert)) + scr_dyn_obj_set(chain, "issuerCertificate", 17, scr_dyn_retain(chain)); + scr_bytes_release(cert); + return result; +} diff --git a/packages/runtime/src/scr_x509.c b/packages/runtime/src/scr_x509.c new file mode 100644 index 000000000..7b853d68a --- /dev/null +++ b/packages/runtime/src/scr_x509.c @@ -0,0 +1,1820 @@ +/* X509Certificate fields (raw, subject, issuer, subjectAltName, + * infoAccess, serialNumber, fingerprint256, fingerprint512, ca, + * signatureAlgorithm, signatureAlgorithmOid), linked only into programs + * that read them (the x509 runtime feature). */ +#include "scr_runtime.h" + +#include +#include +#include + +/* Copies of scr_lib.c's PEM front half and DER header reader for the + * X509Certificate members it keeps, so that unit is unchanged for + * programs that do not read these fields. */ + +/* PEM armor → DER (or raw-DER passthrough), the shared front half of + * every X509Certificate member. On success `*der`/`*der_len` hold the + * certificate bytes and `*decoded` the malloc'd base64 buffer to free + * (NULL for raw-DER input); unparseable input throws Node's exact PEM + * error and answers false with the exception pending. */ +static bool scr_x509_der(const uint8_t *in, size_t n, const uint8_t **der, size_t *der_len, + uint8_t **decoded) { + static const char begin[] = "-----BEGIN CERTIFICATE-----"; + static const char end[] = "-----END CERTIFICATE-----"; + *der = NULL; + *der_len = 0; + *decoded = NULL; + /* PEM: base64-decode the armor's body. */ + const uint8_t *b = NULL; + /* OpenSSL's PEM reader looks for the armor at the start of a line. */ + for (size_t i = 0; i + sizeof begin - 1 <= n; i++) { + if ((i == 0 || in[i - 1] == '\n') && memcmp(in + i, begin, sizeof begin - 1) == 0) { + b = in + i + sizeof begin - 1; + break; + } + } + if (b != NULL) { + const uint8_t *stop = in + n; + for (const uint8_t *p = b; p + sizeof end - 1 <= in + n; p++) { + if (memcmp(p, end, sizeof end - 1) == 0) { + stop = p; + break; + } + } + uint8_t *out = malloc(((size_t)(stop - b) / 4 + 1) * 3 + 3); + if (!out) { + scr_trap("scriptc: out of memory\n"); + } + size_t o = 0; + unsigned acc = 0; + int have = 0; + for (const uint8_t *p = b; p < stop; p++) { + int v; + uint8_t c = *p; + if (c >= 'A' && c <= 'Z') v = c - 'A'; + else if (c >= 'a' && c <= 'z') v = c - 'a' + 26; + else if (c >= '0' && c <= '9') v = c - '0' + 52; + else if (c == '+') v = 62; + else if (c == '/') v = 63; + else continue; /* whitespace, '=' */ + acc = (acc << 6) | (unsigned)v; + if (++have == 4) { + out[o++] = (uint8_t)(acc >> 16); + out[o++] = (uint8_t)(acc >> 8); + out[o++] = (uint8_t)acc; + acc = 0; + have = 0; + } + } + if (have == 2) out[o++] = (uint8_t)(acc >> 4); + else if (have == 3) { + out[o++] = (uint8_t)(acc >> 10); + out[o++] = (uint8_t)(acc >> 2); + } + *decoded = out; + *der = out; + *der_len = o; + return true; + } + if (n > 0 && in[0] == 0x30) { + /* Raw DER: the certificate SEQUENCE, without bytes after it, as + * d2i_X509 reads it. */ + *der = in; + *der_len = n; + if (n >= 2) { + size_t len = in[1], head = 2; + if (len & 0x80) { + size_t k = len & 0x7f; + len = 0; + head = 2 + k; + for (size_t i = 0; i < k && k <= 4 && head <= n; i++) len = (len << 8) | in[2 + i]; + if (k == 0 || k > 4) head = n + 1; + } + if (head <= n && len <= n - head) *der_len = head + len; + } + return true; + } + scr_throw_error_msg_code(SCR_ERR_ERROR, + "error:0480006C:PEM routines::no start line", + 42, "ERR_OSSL_PEM_NO_START_LINE"); + return false; /* exception pending */ +} + +/* Reads one DER TL header at p (before end): tag to *tag, content length + * to *len, and answers the content pointer (NULL on malformed/overlong). */ +static const uint8_t *scr_der_tl(const uint8_t *p, const uint8_t *end, uint8_t *tag, size_t *len) { + if (p == NULL || end - p < 2) return NULL; + *tag = p[0]; + uint8_t l0 = p[1]; + const uint8_t *content = p + 2; + size_t l; + if (l0 < 0x80) { + l = l0; + } else { + size_t nb = l0 & 0x7f; + if (nb == 0 || nb > sizeof(size_t) || end - content < (ptrdiff_t)nb) return NULL; + l = 0; + for (size_t i = 0; i < nb; i++) l = (l << 8) | content[i]; + content += nb; + } + if ((size_t)(end - content) < l) return NULL; + *tag = p[0]; + *len = l; + return content; +} + +/* ── X509Certificate fields over the DER ───────────────────────────────── + * + * `raw` decodes the constructor's input once (scr_x509_der); every other + * field reads those DER bytes. The walks follow RFC 5280's Certificate ::= + * SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue } and + * render each value as Node does: names through X509_NAME_print_ex (one + * attribute per line, RFC 2253 escaping), alternative names and access + * descriptions through Node's PrintGeneralName, the serial number through + * BN_bn2hex. A field whose part of the certificate is missing or malformed + * answers Node's value for an absent part. */ + +typedef struct { + char *data; + size_t len, cap; +} ScrX509Buf; + +static void scr_x509_put(ScrX509Buf *b, const void *p, size_t n) { + if (b->len + n > b->cap) { + size_t cap = b->cap ? b->cap * 2 : 128; + while (cap < b->len + n) cap *= 2; + char *grown = realloc(b->data, cap); + if (!grown) scr_trap("scriptc: out of memory\n"); + b->data = grown; + b->cap = cap; + } + memcpy(b->data + b->len, p, n); + b->len += n; +} + +static void scr_x509_puts(ScrX509Buf *b, const char *s) { scr_x509_put(b, s, strlen(s)); } + +static void scr_x509_putc(ScrX509Buf *b, char c) { scr_x509_put(b, &c, 1); } + +static ScrStr *scr_x509_buf_str(ScrX509Buf *b) { + ScrStr *s = scr_str_new(b->data ? b->data : "", b->len); + free(b->data); + return s; +} + +/* The DER of a certificate field's input (raw or the bytes constructor + * input). */ +#define SCR_X509_DER(bytes) \ + ((const uint8_t *)(bytes)->data), ((bytes)->len * scr_bytes_elem_size((bytes)->elem)) + +typedef struct { + const uint8_t *tbs, *tbs_end; /* tbsCertificate contents */ + const uint8_t *sig_alg; /* outer signatureAlgorithm contents */ + size_t sig_alg_len; + const uint8_t *serial; /* serialNumber contents */ + size_t serial_len; + const uint8_t *issuer, *subject; /* Name contents */ + size_t issuer_len, subject_len; + const uint8_t *validity; /* Validity contents */ + size_t validity_len; + const uint8_t *exts; /* Extensions SEQUENCE contents, or NULL */ + size_t exts_len; + const uint8_t *spki; /* SubjectPublicKeyInfo contents, or NULL */ + size_t spki_len; + const uint8_t *spki_tlv; /* the whole SubjectPublicKeyInfo DER */ + size_t spki_tlv_len; +} ScrX509Parts; + +/* Splits the certificate into the parts the fields read; false when the + * outer structure does not parse. */ +static bool scr_x509_parts(const uint8_t *der, size_t n, ScrX509Parts *out) { + memset(out, 0, sizeof *out); + const uint8_t *end = der + n; + uint8_t tag; + size_t len; + const uint8_t *cert = scr_der_tl(der, end, &tag, &len); + if (!cert || tag != 0x30) return false; + const uint8_t *cert_end = cert + len; + const uint8_t *tbs = scr_der_tl(cert, cert_end, &tag, &len); + if (!tbs || tag != 0x30) return false; + out->tbs = tbs; + out->tbs_end = tbs + len; + const uint8_t *alg = scr_der_tl(tbs + len, cert_end, &tag, &len); + if (alg && tag == 0x30) { + out->sig_alg = alg; + out->sig_alg_len = len; + } + const uint8_t *p = tbs; + const uint8_t *c = scr_der_tl(p, out->tbs_end, &tag, &len); + if (c && tag == 0xA0) { /* [0] version */ + p = c + len; + c = scr_der_tl(p, out->tbs_end, &tag, &len); + } + if (!c || tag != 0x02) return true; + out->serial = c; + out->serial_len = len; + p = c + len; + /* signature AlgorithmIdentifier */ + c = scr_der_tl(p, out->tbs_end, &tag, &len); + if (!c) return true; + p = c + len; + c = scr_der_tl(p, out->tbs_end, &tag, &len); + if (!c || tag != 0x30) return true; + out->issuer = c; + out->issuer_len = len; + p = c + len; + c = scr_der_tl(p, out->tbs_end, &tag, &len); + if (!c || tag != 0x30) return true; + out->validity = c; + out->validity_len = len; + p = c + len; + c = scr_der_tl(p, out->tbs_end, &tag, &len); + if (!c || tag != 0x30) return true; + out->subject = c; + out->subject_len = len; + p = c + len; + c = scr_der_tl(p, out->tbs_end, &tag, &len); + if (c && tag == 0x30) { + out->spki = c; + out->spki_len = len; + out->spki_tlv = p; + out->spki_tlv_len = (size_t)(c + len - p); + } + /* subjectPublicKeyInfo, then the optional [1] [2] unique IDs and [3] + * extensions */ + while (p < out->tbs_end) { + c = scr_der_tl(p, out->tbs_end, &tag, &len); + if (!c) break; + if (tag == 0xA3) { + const uint8_t *seq = scr_der_tl(c, c + len, &tag, &len); + if (seq && tag == 0x30) { + out->exts = seq; + out->exts_len = len; + } + break; + } + p = c + len; + } + return true; +} + +/* Finds the extension with the DER-encoded OID `oid` (contents only): + * its extnValue contents, or NULL. */ +static const uint8_t *scr_x509_ext(const ScrX509Parts *parts, const uint8_t *oid, size_t oid_len, + size_t *value_len) { + if (!parts->exts) return NULL; + const uint8_t *p = parts->exts; + const uint8_t *end = parts->exts + parts->exts_len; + uint8_t tag; + size_t len; + while (p < end) { + const uint8_t *ext = scr_der_tl(p, end, &tag, &len); + if (!ext || tag != 0x30) return NULL; + const uint8_t *ext_end = ext + len; + p = ext_end; + const uint8_t *id = scr_der_tl(ext, ext_end, &tag, &len); + if (!id || tag != 0x06) continue; + bool match = len == oid_len && memcmp(id, oid, len) == 0; + const uint8_t *q = id + len; + const uint8_t *v = scr_der_tl(q, ext_end, &tag, &len); + if (v && tag == 0x01) { /* critical */ + q = v + len; + v = scr_der_tl(q, ext_end, &tag, &len); + } + if (match && v && tag == 0x04) { + *value_len = len; + return v; + } + } + return NULL; +} + +/* An OID's dotted text (OBJ_obj2txt with no_name). */ +static void scr_x509_oid_dotted(ScrX509Buf *b, const uint8_t *oid, size_t n) { + char tmp[24]; + uint64_t v = 0; + bool first = true; + for (size_t i = 0; i < n; i++) { + v = (v << 7) | (oid[i] & 0x7f); + if (oid[i] & 0x80) continue; + if (first) { + unsigned top = v < 40 ? 0 : v < 80 ? 1 : 2; + snprintf(tmp, sizeof tmp, "%u.%llu", top, (unsigned long long)(v - top * 40)); + first = false; + } else { + snprintf(tmp, sizeof tmp, ".%llu", (unsigned long long)v); + } + scr_x509_puts(b, tmp); + v = 0; + } +} + +typedef struct { + const char *oid; /* dotted */ + const char *name; +} ScrX509OidName; + +/* OpenSSL's short names for the attribute types names use (XN_FLAG_FN_SN). */ +static const ScrX509OidName scr_x509_attr_names[] = { + {"2.5.4.3", "CN"}, + {"2.5.4.4", "SN"}, + {"2.5.4.5", "serialNumber"}, + {"2.5.4.6", "C"}, + {"2.5.4.7", "L"}, + {"2.5.4.8", "ST"}, + {"2.5.4.9", "street"}, + {"2.5.4.10", "O"}, + {"2.5.4.11", "OU"}, + {"2.5.4.12", "title"}, + {"2.5.4.13", "description"}, + {"2.5.4.15", "businessCategory"}, + {"2.5.4.16", "postalAddress"}, + {"2.5.4.17", "postalCode"}, + {"2.5.4.18", "postOfficeBox"}, + {"2.5.4.20", "telephoneNumber"}, + {"2.5.4.41", "name"}, + {"2.5.4.42", "GN"}, + {"2.5.4.43", "initials"}, + {"2.5.4.44", "generationQualifier"}, + {"2.5.4.45", "x500UniqueIdentifier"}, + {"2.5.4.46", "dnQualifier"}, + {"2.5.4.65", "pseudonym"}, + {"2.5.4.72", "role"}, + {"2.5.4.97", "organizationIdentifier"}, + {"1.2.840.113549.1.9.1", "emailAddress"}, + {"1.2.840.113549.1.9.2", "unstructuredName"}, + {"0.9.2342.19200300.100.1.1", "UID"}, + {"0.9.2342.19200300.100.1.25", "DC"}, + {"1.3.6.1.4.1.311.60.2.1.1", "jurisdictionL"}, + {"1.3.6.1.4.1.311.60.2.1.2", "jurisdictionST"}, + {"1.3.6.1.4.1.311.60.2.1.3", "jurisdictionC"}, +}; + +/* OpenSSL's long names for the signature algorithms certificates use. */ +static const ScrX509OidName scr_x509_sig_names[] = { + {"1.2.840.113549.1.1.4", "md5WithRSAEncryption"}, + {"1.2.840.113549.1.1.5", "sha1WithRSAEncryption"}, + {"1.2.840.113549.1.1.10", "rsassaPss"}, + {"1.2.840.113549.1.1.11", "sha256WithRSAEncryption"}, + {"1.2.840.113549.1.1.12", "sha384WithRSAEncryption"}, + {"1.2.840.113549.1.1.13", "sha512WithRSAEncryption"}, + {"1.2.840.113549.1.1.14", "sha224WithRSAEncryption"}, + {"1.2.840.10045.4.1", "ecdsa-with-SHA1"}, + {"1.2.840.10045.4.3.1", "ecdsa-with-SHA224"}, + {"1.2.840.10045.4.3.2", "ecdsa-with-SHA256"}, + {"1.2.840.10045.4.3.3", "ecdsa-with-SHA384"}, + {"1.2.840.10045.4.3.4", "ecdsa-with-SHA512"}, + {"1.2.840.10040.4.3", "dsaWithSHA1"}, + {"2.16.840.1.101.3.4.3.1", "dsa_with_SHA224"}, + {"2.16.840.1.101.3.4.3.2", "dsa_with_SHA256"}, + {"1.3.101.112", "ED25519"}, + {"1.3.101.113", "ED448"}, +}; + +/* OpenSSL's long names for the access methods infoAccess prints. */ +static const ScrX509OidName scr_x509_access_names[] = { + {"1.3.6.1.5.5.7.48.1", "OCSP"}, + {"1.3.6.1.5.5.7.48.2", "CA Issuers"}, + {"1.3.6.1.5.5.7.48.3", "AD Time Stamping"}, + {"1.3.6.1.5.5.7.48.5", "CA Repository"}, +}; + +/* Appends the table name of an OID, or its dotted text; answers whether + * the table named it. */ +static bool scr_x509_oid_name(ScrX509Buf *b, const uint8_t *oid, size_t n, + const ScrX509OidName *table, size_t count) { + ScrX509Buf dotted = {0}; + scr_x509_oid_dotted(&dotted, oid, n); + for (size_t i = 0; i < count; i++) { + if (strlen(table[i].oid) == dotted.len && memcmp(table[i].oid, dotted.data, dotted.len) == 0) { + scr_x509_puts(b, table[i].name); + free(dotted.data); + return true; + } + } + scr_x509_put(b, dotted.data, dotted.len); + free(dotted.data); + return false; +} + +/* Appends one code point as UTF-8. */ +static void scr_x509_put_utf8(ScrX509Buf *b, uint32_t c) { + char u[4]; + size_t n; + if (c < 0x80) { + u[0] = (char)c; + n = 1; + } else if (c < 0x800) { + u[0] = (char)(0xC0 | (c >> 6)); + u[1] = (char)(0x80 | (c & 0x3F)); + n = 2; + } else if (c < 0x10000) { + u[0] = (char)(0xE0 | (c >> 12)); + u[1] = (char)(0x80 | ((c >> 6) & 0x3F)); + u[2] = (char)(0x80 | (c & 0x3F)); + n = 3; + } else { + u[0] = (char)(0xF0 | (c >> 18)); + u[1] = (char)(0x80 | ((c >> 12) & 0x3F)); + u[2] = (char)(0x80 | ((c >> 6) & 0x3F)); + u[3] = (char)(0x80 | (c & 0x3F)); + n = 4; + } + scr_x509_put(b, u, n); +} + +/* An attribute value as UTF-8 bytes (ASN1_STRFLGS_UTF8_CONVERT): a + * UTF8String as stored, BMPString and UniversalString decoded, every other + * type read as Latin-1. */ +static void scr_x509_value_utf8(ScrX509Buf *out, uint8_t tag, const uint8_t *v, size_t n) { + if (tag == 0x0C) { + scr_x509_put(out, v, n); + } else if (tag == 0x1E) { + for (size_t i = 0; i + 1 < n; i += 2) scr_x509_put_utf8(out, ((uint32_t)v[i] << 8) | v[i + 1]); + } else if (tag == 0x1C) { + for (size_t i = 0; i + 3 < n; i += 4) + scr_x509_put_utf8(out, ((uint32_t)v[i] << 24) | ((uint32_t)v[i + 1] << 16) | + ((uint32_t)v[i + 2] << 8) | v[i + 3]); + } else { + for (size_t i = 0; i < n; i++) scr_x509_put_utf8(out, v[i]); + } +} + +/* Appends a value with ASN1_STRFLGS_ESC_2253 escaping: the characters + * `"+,;<>\` get a backslash, as do a leading '#' or space and a trailing + * space; with `esc_ctrl`, control characters print as \XX. Bytes above + * 0x7f pass through. */ +static void scr_x509_put_escaped(ScrX509Buf *b, const uint8_t *s, size_t n, bool esc_ctrl) { + static const char hex[] = "0123456789ABCDEF"; + for (size_t i = 0; i < n; i++) { + uint8_t c = s[i]; + bool first = i == 0; + bool last = i + 1 == n; + if (c == '"' || c == '+' || c == ',' || c == ';' || c == '<' || c == '>' || c == '\\' || + (first && (c == '#' || c == ' ')) || (last && c == ' ')) { + scr_x509_putc(b, '\\'); + scr_x509_putc(b, (char)c); + } else if (esc_ctrl && (c < 0x20 || c == 0x7f)) { + char e[3] = {'\\', hex[c >> 4], hex[c & 0x0f]}; + scr_x509_put(b, e, 3); + } else { + scr_x509_putc(b, (char)c); + } + } +} + +/* Prints a Name. `multiline` is Node's subject/issuer form (attributes in + * order, one RDN per line, ESC_CTRL); otherwise the RFC 2253 form DirName + * alternative names use (RDNs reversed and comma-separated, unknown + * attribute types dumped as #hex of the value's DER). RDNs with several + * attributes join them with " + " or "+". */ +static void scr_x509_name_print(ScrX509Buf *b, const uint8_t *name, size_t n, bool multiline) { + const uint8_t *end = name + n; + const uint8_t *rdns[64]; + size_t rdn_lens[64]; + size_t count = 0; + uint8_t tag; + size_t len; + for (const uint8_t *p = name; p < end && count < 64;) { + const uint8_t *set = scr_der_tl(p, end, &tag, &len); + if (!set || tag != 0x31) break; + rdns[count] = set; + rdn_lens[count++] = len; + p = set + len; + } + for (size_t k = 0; k < count; k++) { + size_t r = multiline ? k : count - 1 - k; + if (k > 0) scr_x509_puts(b, multiline ? "\n" : ","); + const uint8_t *set_end = rdns[r] + rdn_lens[r]; + bool first_attr = true; + for (const uint8_t *q = rdns[r]; q < set_end;) { + const uint8_t *atv = scr_der_tl(q, set_end, &tag, &len); + if (!atv || tag != 0x30) break; + const uint8_t *atv_end = atv + len; + q = atv_end; + const uint8_t *oid = scr_der_tl(atv, atv_end, &tag, &len); + if (!oid || tag != 0x06) continue; + size_t oid_len = len; + const uint8_t *value_tl = oid + len; + uint8_t vtag; + size_t vlen; + const uint8_t *value = scr_der_tl(value_tl, atv_end, &vtag, &vlen); + if (!value) continue; + if (!first_attr) scr_x509_puts(b, multiline ? " + " : "+"); + first_attr = false; + bool known = scr_x509_oid_name(b, oid, oid_len, scr_x509_attr_names, + sizeof scr_x509_attr_names / sizeof scr_x509_attr_names[0]); + scr_x509_putc(b, '='); + if (!multiline && !known) { + static const char hex[] = "0123456789ABCDEF"; + scr_x509_putc(b, '#'); + for (const uint8_t *d = value_tl; d < value + vlen; d++) { + scr_x509_putc(b, hex[*d >> 4]); + scr_x509_putc(b, hex[*d & 0x0f]); + } + continue; + } + ScrX509Buf text = {0}; + scr_x509_value_utf8(&text, vtag, value, vlen); + scr_x509_put_escaped(b, (const uint8_t *)text.data, text.len, multiline); + free(text.data); + } + } +} + +/* Node's PrintAltName: a name with none of `"\,'` and only printable + * characters (in UTF-8 names, only non-control bytes) prints as is; + * otherwise it prints as a JSON string, `prefix:` inside the quotes, with + * `\\`, `\"` and \u00XX for commas, control characters and (outside + * UTF-8 names) bytes above 0x7e. */ +static void scr_x509_put_alt_name(ScrX509Buf *b, const uint8_t *s, size_t n, bool utf8, + const char *prefix) { + bool safe = true; + for (size_t i = 0; i < n && safe; i++) { + uint8_t c = s[i]; + if (c == '"' || c == '\\' || c == ',' || c == '\'') safe = false; + else if (utf8 ? (c < ' ' || c == 0x7f) : (c < ' ' || c > '~')) safe = false; + } + if (safe) { + if (prefix) { + scr_x509_puts(b, prefix); + scr_x509_putc(b, ':'); + } + scr_x509_put(b, s, n); + return; + } + static const char hex[] = "0123456789abcdef"; + scr_x509_putc(b, '"'); + if (prefix) { + scr_x509_puts(b, prefix); + scr_x509_putc(b, ':'); + } + for (size_t i = 0; i < n; i++) { + uint8_t c = s[i]; + if (c == '\\') { + scr_x509_puts(b, "\\\\"); + } else if (c == '"') { + scr_x509_puts(b, "\\\""); + } else if ((c >= ' ' && c != ',' && c <= '~') || (utf8 && (c & 0x80))) { + scr_x509_putc(b, (char)c); + } else { + char u[6] = {'\\', 'u', '0', '0', hex[c >> 4], hex[c & 0x0f]}; + scr_x509_put(b, u, 6); + } + } + scr_x509_putc(b, '"'); +} + +static const uint8_t SCR_X509_OID_UPN[] = {0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x14, 0x02, 0x03}; +static const uint8_t SCR_X509_OID_XMPP[] = {0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x08, 0x05}; +static const uint8_t SCR_X509_OID_SRV[] = {0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x08, 0x07}; +static const uint8_t SCR_X509_OID_NAI[] = {0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x08, 0x08}; +static const uint8_t SCR_X509_OID_SMTP_UTF8[] = {0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x08, 0x09}; + +/* Node's PrintGeneralName for one GeneralName element (tag and contents). */ +static void scr_x509_general_name(ScrX509Buf *b, uint8_t tag, const uint8_t *v, size_t n) { + switch (tag) { + case 0x81: /* rfc822Name */ + scr_x509_puts(b, "email:"); + scr_x509_put_alt_name(b, v, n, false, NULL); + return; + case 0x82: /* dNSName */ + scr_x509_puts(b, "DNS:"); + scr_x509_put_alt_name(b, v, n, false, NULL); + return; + case 0x86: /* uniformResourceIdentifier */ + scr_x509_puts(b, "URI:"); + scr_x509_put_alt_name(b, v, n, false, NULL); + return; + case 0x87: { /* iPAddress */ + char tmp[48]; + scr_x509_puts(b, "IP Address:"); + if (n == 4) { + snprintf(tmp, sizeof tmp, "%d.%d.%d.%d", v[0], v[1], v[2], v[3]); + scr_x509_puts(b, tmp); + } else if (n == 16) { + for (unsigned j = 0; j < 8; j++) { + snprintf(tmp, sizeof tmp, j == 0 ? "%X" : ":%X", (unsigned)((v[2 * j] << 8) | v[2 * j + 1])); + scr_x509_puts(b, tmp); + } + } else { + snprintf(tmp, sizeof tmp, "", n); + scr_x509_puts(b, tmp); + } + return; + } + case 0x88: /* registeredID */ + scr_x509_puts(b, "Registered ID:"); + scr_x509_oid_dotted(b, v, n); + return; + case 0xA4: { /* directoryName: [4] EXPLICIT Name */ + uint8_t ntag; + size_t nlen; + const uint8_t *name = scr_der_tl(v, v + n, &ntag, &nlen); + ScrX509Buf text = {0}; + if (name && ntag == 0x30) scr_x509_name_print(&text, name, nlen, false); + scr_x509_puts(b, "DirName:"); + scr_x509_put_alt_name(b, (const uint8_t *)(text.data ? text.data : ""), text.len, true, NULL); + free(text.data); + return; + } + case 0xA0: { /* otherName: type-id OID, [0] EXPLICIT value */ + uint8_t t; + size_t len; + const uint8_t *end = v + n; + const uint8_t *oid = scr_der_tl(v, end, &t, &len); + const char *prefix = NULL; + bool unicode = true; + if (oid && t == 0x06) { +#define SCR_X509_OID_IS(o) (len == sizeof(o) && memcmp(oid, (o), sizeof(o)) == 0) + if (SCR_X509_OID_IS(SCR_X509_OID_SMTP_UTF8)) prefix = "SmtpUTF8Mailbox"; + else if (SCR_X509_OID_IS(SCR_X509_OID_XMPP)) prefix = "XmppAddr"; + else if (SCR_X509_OID_IS(SCR_X509_OID_SRV)) { + prefix = "SRVName"; + unicode = false; + } else if (SCR_X509_OID_IS(SCR_X509_OID_UPN)) prefix = "UPN"; + else if (SCR_X509_OID_IS(SCR_X509_OID_NAI)) prefix = "NAIRealm"; +#undef SCR_X509_OID_IS + } + const uint8_t *value = NULL; + size_t value_len = 0; + uint8_t vtag = 0; + if (oid && prefix) { + const uint8_t *wrap = scr_der_tl(oid + len, end, &t, &len); + if (wrap && t == 0xA0) value = scr_der_tl(wrap, wrap + len, &vtag, &value_len); + } + if (!value || vtag != (unicode ? 0x0C : 0x16)) { + scr_x509_puts(b, "othername:"); + } else { + scr_x509_puts(b, "othername:"); + scr_x509_put_alt_name(b, value, value_len, unicode, prefix); + } + return; + } + case 0xA3: + scr_x509_puts(b, "X400Name:"); + return; + case 0xA5: + scr_x509_puts(b, "EdiPartyName:"); + return; + default: + return; + } +} + +/* The DER contents of the TBSCertificate parts, or an empty string when + * the certificate does not parse. */ +static ScrStr *scr_x509_name_field(ScrBytes *der, bool subject) { + ScrX509Parts parts; + ScrX509Buf b = {0}; + if (scr_x509_parts(SCR_X509_DER(der), &parts)) { + const uint8_t *name = subject ? parts.subject : parts.issuer; + if (name) scr_x509_name_print(&b, name, subject ? parts.subject_len : parts.issuer_len, true); + } + return scr_x509_buf_str(&b); +} + +/* new X509Certificate(data).raw: the certificate's DER as a fresh Buffer. + * Unparseable input throws Node's PEM error (exception pending, NULL). */ +static ScrBytes *scr_x509_raw(const uint8_t *in, size_t n) { + const uint8_t *der; + size_t der_len; + uint8_t *decoded; + if (!scr_x509_der(in, n, &der, &der_len, &decoded)) return NULL; + ScrBytes *out = scr_buffer_alloc_unsafe_slow((double)der_len); + memcpy(out->data, der, der_len); + free(decoded); + return out; +} + +ScrBytes *scr_crypto_x509_raw(ScrBytes *data) { + SCR_BYTES_SNAPSHOT(data); + return scr_x509_raw(SCR_X509_DER(data)); +} + +/* raw of a certificate whose DER a TLS handshake delivered: a copy, with + * no search for PEM armor that its extensions could carry. */ +ScrBytes *scr_crypto_x509_raw_der(ScrBytes *der) { + SCR_BYTES_SNAPSHOT(der); + size_t n = (size_t)scr_bytes_byte_len(der); + ScrBytes *out = scr_buffer_alloc_unsafe_slow((double)n); + memcpy(out->data, der->data, n); + return out; +} + +ScrBytes *scr_crypto_x509_raw_str(ScrStr *pem) { + return scr_x509_raw((const uint8_t *)pem->data, pem->len); +} + +ScrStr *scr_crypto_x509_subject(ScrBytes *der) { return scr_x509_name_field(der, true); } + +ScrStr *scr_crypto_x509_issuer(ScrBytes *der) { return scr_x509_name_field(der, false); } + +static const uint8_t SCR_X509_OID_SAN[] = {0x55, 0x1D, 0x11}; +static const uint8_t SCR_X509_OID_BASIC_CONSTRAINTS[] = {0x55, 0x1D, 0x13}; +static const uint8_t SCR_X509_OID_KEY_USAGE[] = {0x55, 0x1D, 0x0F}; +static const uint8_t SCR_X509_OID_AIA[] = {0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x01}; + +/* subjectAltName: the GeneralNames joined with ", "; an empty string when + * the certificate has no subjectAltName extension (Node's undefined). */ +ScrStr *scr_crypto_x509_subject_alt_name(ScrBytes *der) { + ScrX509Parts parts; + ScrX509Buf b = {0}; + size_t n; + const uint8_t *ext = scr_x509_parts(SCR_X509_DER(der), &parts) + ? scr_x509_ext(&parts, SCR_X509_OID_SAN, sizeof SCR_X509_OID_SAN, &n) + : NULL; + uint8_t tag; + size_t len; + const uint8_t *names = ext ? scr_der_tl(ext, ext + n, &tag, &len) : NULL; + if (names && tag == 0x30) { + const uint8_t *end = names + len; + bool first = true; + for (const uint8_t *p = names; p < end;) { + const uint8_t *v = scr_der_tl(p, end, &tag, &len); + if (!v) break; + if (!first) scr_x509_puts(&b, ", "); + first = false; + scr_x509_general_name(&b, tag, v, len); + p = v + len; + } + } + return scr_x509_buf_str(&b); +} + +/* infoAccess: one " - " line per AccessDescription; + * an empty string when the extension is absent (Node's undefined). */ +ScrStr *scr_crypto_x509_info_access(ScrBytes *der) { + ScrX509Parts parts; + ScrX509Buf b = {0}; + size_t n; + const uint8_t *ext = scr_x509_parts(SCR_X509_DER(der), &parts) + ? scr_x509_ext(&parts, SCR_X509_OID_AIA, sizeof SCR_X509_OID_AIA, &n) + : NULL; + uint8_t tag; + size_t len; + const uint8_t *descs = ext ? scr_der_tl(ext, ext + n, &tag, &len) : NULL; + if (descs && tag == 0x30) { + const uint8_t *end = descs + len; + bool first = true; + for (const uint8_t *p = descs; p < end;) { + const uint8_t *desc = scr_der_tl(p, end, &tag, &len); + if (!desc || tag != 0x30) break; + const uint8_t *desc_end = desc + len; + p = desc_end; + const uint8_t *method = scr_der_tl(desc, desc_end, &tag, &len); + if (!method || tag != 0x06) continue; + if (!first) scr_x509_putc(&b, '\n'); + first = false; + scr_x509_oid_name(&b, method, len, scr_x509_access_names, + sizeof scr_x509_access_names / sizeof scr_x509_access_names[0]); + scr_x509_puts(&b, " - "); + const uint8_t *location = scr_der_tl(method + len, desc_end, &tag, &len); + if (location) scr_x509_general_name(&b, tag, location, len); + } + } + return scr_x509_buf_str(&b); +} + +/* serialNumber: BN_bn2hex of the INTEGER ("0" for zero, whole bytes in + * uppercase hex without leading zero bytes, '-' before a negative). */ +ScrStr *scr_crypto_x509_serial_number(ScrBytes *der) { + ScrX509Parts parts; + ScrX509Buf b = {0}; + if (scr_x509_parts(SCR_X509_DER(der), &parts) && parts.serial_len > 0) { + static const char hex[] = "0123456789ABCDEF"; + size_t n = parts.serial_len; + uint8_t *mag = malloc(n); + if (!mag) scr_trap("scriptc: out of memory\n"); + memcpy(mag, parts.serial, n); + bool neg = (mag[0] & 0x80) != 0; + if (neg) { /* two's complement magnitude */ + for (size_t i = 0; i < n; i++) mag[i] = (uint8_t)~mag[i]; + for (size_t i = n; i-- > 0;) { + if (++mag[i] != 0) break; + } + } + size_t i = 0; + while (i < n && mag[i] == 0) i++; + if (i == n) { + scr_x509_putc(&b, '0'); + } else { + if (neg) scr_x509_putc(&b, '-'); + for (; i < n; i++) { + scr_x509_putc(&b, hex[mag[i] >> 4]); + scr_x509_putc(&b, hex[mag[i] & 0x0f]); + } + } + free(mag); + } + return scr_x509_buf_str(&b); +} + +static ScrStr *scr_x509_fingerprint_alg(ScrBytes *der, const char *alg) { + unsigned char digest[64]; + size_t dn = scr_crypto_digest_raw(alg, SCR_X509_DER(der), digest); + static const char hex[] = "0123456789ABCDEF"; + char buf[192]; + size_t o = 0; + for (size_t i = 0; i < dn; i++) { + if (i > 0) buf[o++] = ':'; + buf[o++] = hex[digest[i] >> 4]; + buf[o++] = hex[digest[i] & 0x0f]; + } + return scr_str_new(buf, o); +} + +ScrStr *scr_crypto_x509_fingerprint256(ScrBytes *der) { + return scr_x509_fingerprint_alg(der, "sha256"); +} + +ScrStr *scr_crypto_x509_fingerprint512(ScrBytes *der) { + return scr_x509_fingerprint_alg(der, "sha512"); +} + +/* ca: X509_check_ca() == 1 — basicConstraints with cA TRUE, and a + * keyUsage extension, when present, that allows keyCertSign. */ +bool scr_crypto_x509_ca(ScrBytes *der) { + ScrX509Parts parts; + if (!scr_x509_parts(SCR_X509_DER(der), &parts)) return false; + uint8_t tag; + size_t len, n; + const uint8_t *ku = scr_x509_ext(&parts, SCR_X509_OID_KEY_USAGE, sizeof SCR_X509_OID_KEY_USAGE, &n); + if (ku) { + const uint8_t *bits = scr_der_tl(ku, ku + n, &tag, &len); + if (!bits || tag != 0x03 || len < 2 || (bits[1] & 0x04) == 0) return false; + } + const uint8_t *bc = scr_x509_ext(&parts, SCR_X509_OID_BASIC_CONSTRAINTS, + sizeof SCR_X509_OID_BASIC_CONSTRAINTS, &n); + if (!bc) return false; + const uint8_t *seq = scr_der_tl(bc, bc + n, &tag, &len); + if (!seq || tag != 0x30) return false; + const uint8_t *flag = scr_der_tl(seq, seq + len, &tag, &len); + return flag && tag == 0x01 && len == 1 && flag[0] != 0; +} + +/* The signature AlgorithmIdentifier's OID contents, or NULL. */ +static const uint8_t *scr_x509_sig_oid(ScrBytes *der, size_t *n) { + ScrX509Parts parts; + if (!scr_x509_parts(SCR_X509_DER(der), &parts) || !parts.sig_alg) return NULL; + uint8_t tag; + const uint8_t *oid = scr_der_tl(parts.sig_alg, parts.sig_alg + parts.sig_alg_len, &tag, n); + return oid && tag == 0x06 ? oid : NULL; +} + +/* signatureAlgorithm: OpenSSL's long name for the signature algorithm; an + * empty string for an algorithm OpenSSL does not name (Node's + * undefined). */ +ScrStr *scr_crypto_x509_signature_algorithm(ScrBytes *der) { + size_t n; + const uint8_t *oid = scr_x509_sig_oid(der, &n); + ScrX509Buf b = {0}; + if (oid && !scr_x509_oid_name(&b, oid, n, scr_x509_sig_names, + sizeof scr_x509_sig_names / sizeof scr_x509_sig_names[0])) + b.len = 0; + return scr_x509_buf_str(&b); +} + +/* signatureAlgorithmOid: the signature algorithm's dotted OID. */ +ScrStr *scr_crypto_x509_signature_algorithm_oid(ScrBytes *der) { + size_t n; + const uint8_t *oid = scr_x509_sig_oid(der, &n); + ScrX509Buf b = {0}; + if (oid) scr_x509_oid_dotted(&b, oid, n); + return scr_x509_buf_str(&b); +} + +/* toString() and toJSON(): PEM_write_bio_X509's armor over the DER, 64 + * base64 characters a line. */ +ScrStr *scr_crypto_x509_pem(ScrBytes *der) { + static const char digits[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + const uint8_t *d = (const uint8_t *)der->data; + size_t n = der->len * scr_bytes_elem_size(der->elem); + ScrX509Buf b = {0}; + scr_x509_puts(&b, "-----BEGIN CERTIFICATE-----\n"); + size_t col = 0; + for (size_t i = 0; i < n; i += 3) { + uint32_t v = (uint32_t)d[i] << 16; + if (i + 1 < n) v |= (uint32_t)d[i + 1] << 8; + if (i + 2 < n) v |= d[i + 2]; + char q[4] = {digits[(v >> 18) & 63], digits[(v >> 12) & 63], i + 1 < n ? digits[(v >> 6) & 63] : '=', + i + 2 < n ? digits[v & 63] : '='}; + for (int k = 0; k < 4; k++) { + scr_x509_putc(&b, q[k]); + if (++col == 64) { + scr_x509_putc(&b, '\n'); + col = 0; + } + } + } + if (col) scr_x509_putc(&b, '\n'); + scr_x509_puts(&b, "-----END CERTIFICATE-----\n"); + return scr_x509_buf_str(&b); +} + +/* ── checkHost, checkEmail and checkIP ───────────────────────────────── */ + +/* X509_CHECK_FLAG_* as Node's getFlags sets them. */ +enum { + SCR_X509_ALWAYS_CHECK_SUBJECT = 0x1, + SCR_X509_NO_WILDCARDS = 0x2, + SCR_X509_NO_PARTIAL_WILDCARDS = 0x4, + SCR_X509_MULTI_LABEL_WILDCARDS = 0x8, + SCR_X509_SINGLE_LABEL_SUBDOMAINS = 0x10, + SCR_X509_NEVER_CHECK_SUBJECT = 0x20, +}; + +/* Node's getFlags(options): validateObject, then the subject string and + * the four booleans, in Node's order. -1 with the error pending. */ +static int scr_x509_check_flags(const ScrDyn *options) { + if (options == NULL || options->kind == SCR_DYN_UNDEF) return 0; + if (options->kind == SCR_DYN_TYPED_REF) { + ScrDyn *value = scr_dyn_typed_ref_materialize((ScrDyn *)options); + if (!value) return -1; + int flags = scr_x509_check_flags(value); + scr_dyn_release(value); + return flags; + } + if (options->kind != SCR_DYN_OBJ) { + scr_dyn_arg_type_fail("options", "of type object", options); + return -1; + } + const ScrDyn *subject = scr_dyn_obj_get(options, "subject", 7); + static const char *const bool_names[] = {"wildcards", "partialWildcards", "multiLabelWildcards", + "singleLabelSubdomains"}; + static const bool bool_defaults[] = {true, true, false, false}; + bool values[4]; + if (subject != NULL && subject->kind != SCR_DYN_UNDEF && subject->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("options.subject", "of type string", subject); + return -1; + } + for (size_t i = 0; i < 4; i++) { + const ScrDyn *v = scr_dyn_obj_get(options, bool_names[i], strlen(bool_names[i])); + if (v == NULL || v->kind == SCR_DYN_UNDEF) { + values[i] = bool_defaults[i]; + continue; + } + if (v->kind != SCR_DYN_BOOL) { + char name[48]; + snprintf(name, sizeof name, "options.%s", bool_names[i]); + scr_dyn_prop_type_fail(name, "of type boolean", v); + return -1; + } + values[i] = v->v.b; + } + int flags = 0; + if (subject != NULL && subject->kind == SCR_DYN_STR) { + const ScrStr *s = subject->v.str; + if (s->len == 6 && memcmp(s->data, "always", 6) == 0) flags |= SCR_X509_ALWAYS_CHECK_SUBJECT; + else if (s->len == 5 && memcmp(s->data, "never", 5) == 0) flags |= SCR_X509_NEVER_CHECK_SUBJECT; + else if (!(s->len == 7 && memcmp(s->data, "default", 7) == 0)) { + scr_dyn_arg_value_fail("options.subject", NULL, subject); + return -1; + } + } + if (!values[0]) flags |= SCR_X509_NO_WILDCARDS; + if (!values[1]) flags |= SCR_X509_NO_PARTIAL_WILDCARDS; + if (values[2]) flags |= SCR_X509_MULTI_LABEL_WILDCARDS; + if (values[3]) flags |= SCR_X509_SINGLE_LABEL_SUBDOMAINS; + return flags; +} + +static bool scr_x509_ieq(const uint8_t *a, const uint8_t *b, size_t n) { + for (size_t i = 0; i < n; i++) { + uint8_t x = a[i], y = b[i]; + if (x >= 'A' && x <= 'Z') x = (uint8_t)(x + 32); + if (y >= 'A' && y <= 'Z') y = (uint8_t)(y + 32); + if (x != y) return false; + } + return true; +} + +/* OpenSSL's equal_nocase after skip_prefix: a host that starts with '.' + * compares with the name's equal-length suffix when the characters + * skipped hold no NUL (and, under singleLabelSubdomains, no dot). */ +static bool scr_x509_host_equal(const uint8_t *pattern, size_t plen, const uint8_t *host, + size_t hlen, int flags) { + if (hlen > 1 && host[0] == '.') { + const uint8_t *p = pattern; + size_t n = plen; + while (n > hlen && *p) { + if ((flags & SCR_X509_SINGLE_LABEL_SUBDOMAINS) && *p == '.') break; + p++; + n--; + } + if (n == hlen) { + pattern = p; + plen = n; + } + } + return plen == hlen && memchr(pattern, 0, plen) == NULL && scr_x509_ieq(pattern, host, hlen); +} + +static bool scr_x509_label_char(uint8_t c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); +} + +/* OpenSSL's valid_star: the '*' of a wildcard pattern, or NULL when the + * pattern is not a valid wildcard (the star outside the first label, in + * an IDNA label, partial when partial wildcards are off, or fewer than two + * labels after it). */ +static const uint8_t *scr_x509_valid_star(const uint8_t *p, size_t len, int flags) { + const uint8_t *star = NULL; + int dots = 0; + bool atstart = true, idna = false, first = true; + for (size_t i = 0; i < len; i++) { + if (p[i] == '*') { + bool atend = i == len - 1 || p[i + 1] == '.'; + if (star != NULL || !first || idna) return NULL; + if ((flags & SCR_X509_NO_PARTIAL_WILDCARDS) && (!atstart || !atend)) return NULL; + if (!atstart && !atend) return NULL; + star = &p[i]; + atstart = false; + } else if (p[i] == '.') { + if (atstart) return NULL; + idna = false; + dots++; + atstart = true; + first = false; + } else if (p[i] == '-' || scr_x509_label_char(p[i])) { + if (atstart && len - i >= 4 && scr_x509_ieq(&p[i], (const uint8_t *)"xn--", 4)) idna = true; + atstart = false; + } else { + return NULL; + } + } + if (star == NULL || dots < 2 || atstart) return NULL; + return star; +} + +/* OpenSSL's wildcard_match: prefix and suffix around the star match the + * host case-insensitively, the star covers at least one character, never + * a dot unless multi-label wildcards are on, and never an IDNA label. */ +static bool scr_x509_wildcard_match(const uint8_t *prefix, size_t prefix_len, + const uint8_t *suffix, size_t suffix_len, + const uint8_t *host, size_t hlen, int flags) { + if (hlen < prefix_len + suffix_len) return false; + if (!scr_x509_ieq(prefix, host, prefix_len)) return false; + const uint8_t *wild_start = host + prefix_len; + const uint8_t *wild_end = host + hlen - suffix_len; + if (!scr_x509_ieq(wild_end, suffix, suffix_len)) return false; + if (prefix_len == 0 && suffix_len > 0 && *suffix == '.') { + if (wild_start == wild_end) return false; + if (wild_end - wild_start > 4 && scr_x509_ieq(wild_start, (const uint8_t *)"xn--", 4)) + return false; + } + bool allow_multi = (flags & SCR_X509_MULTI_LABEL_WILDCARDS) != 0; + for (const uint8_t *p = wild_start; p != wild_end; p++) { + if (!scr_x509_label_char(*p) && *p != '-' && !(*p == '.' && allow_multi)) return false; + } + return true; +} + +static bool scr_x509_host_match(const uint8_t *pattern, size_t plen, const uint8_t *host, + size_t hlen, int flags) { + /* a host that starts with '.' matches a wildcard only as a suffix */ + if (!(flags & SCR_X509_NO_WILDCARDS) && !(hlen > 1 && host[0] == '.')) { + const uint8_t *star = scr_x509_valid_star(pattern, plen, flags); + if (star != NULL) + return scr_x509_wildcard_match(pattern, (size_t)(star - pattern), star + 1, + plen - (size_t)(star - pattern) - 1, host, hlen, flags); + } + return scr_x509_host_equal(pattern, plen, host, hlen, flags); +} + +/* OpenSSL's equal_email: the local part as is, the domain case- + * insensitively. */ +static bool scr_x509_email_match(const uint8_t *pattern, size_t plen, const uint8_t *email, + size_t elen) { + if (plen != elen || memchr(pattern, 0, plen) != NULL) return false; + size_t at = elen; + while (at > 0 && email[at - 1] != '@') at--; + if (at == 0) return scr_x509_ieq(pattern, email, elen); + return memcmp(pattern, email, at) == 0 && scr_x509_ieq(pattern + at, email + at, elen - at); +} + +enum { SCR_X509_GEN_EMAIL = 0x81, SCR_X509_GEN_DNS = 0x82, SCR_X509_GEN_IP = 0x87 }; + +/* do_x509_check over the subjectAltName entries of one type, then the + * subject's CN (hosts) or emailAddress (emails) entries when the flags + * allow. Writes the matching certificate name into *out. */ +static bool scr_x509_check(ScrBytes *der, uint8_t gen, const uint8_t *chk, size_t chk_len, + int flags, ScrX509Buf *out) { + ScrX509Parts parts; + if (!scr_x509_parts(SCR_X509_DER(der), &parts)) return false; + bool san_present = false; + size_t n; + const uint8_t *ext = scr_x509_ext(&parts, SCR_X509_OID_SAN, sizeof SCR_X509_OID_SAN, &n); + uint8_t tag; + size_t len; + const uint8_t *names = ext ? scr_der_tl(ext, ext + n, &tag, &len) : NULL; + if (names && tag == 0x30) { + const uint8_t *end = names + len; + for (const uint8_t *p = names; p < end;) { + const uint8_t *v = scr_der_tl(p, end, &tag, &len); + if (!v) break; + p = v + len; + if (tag != gen) continue; + san_present = true; + bool hit = gen == SCR_X509_GEN_IP ? len == chk_len && memcmp(v, chk, len) == 0 + : gen == SCR_X509_GEN_DNS ? scr_x509_host_match(v, len, chk, chk_len, flags) + : scr_x509_email_match(v, len, chk, chk_len); + if (hit) { + scr_x509_put(out, v, len); + return true; + } + } + } + if (gen == SCR_X509_GEN_IP || (flags & SCR_X509_NEVER_CHECK_SUBJECT) || + (san_present && !(flags & SCR_X509_ALWAYS_CHECK_SUBJECT)) || !parts.subject) + return false; + /* the subject's CN (2.5.4.3) or emailAddress (1.2.840.113549.1.9.1) */ + static const uint8_t cn[] = {0x55, 0x04, 0x03}; + static const uint8_t email[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x09, 0x01}; + const uint8_t *want = gen == SCR_X509_GEN_DNS ? cn : email; + size_t want_len = gen == SCR_X509_GEN_DNS ? sizeof cn : sizeof email; + const uint8_t *end = parts.subject + parts.subject_len; + for (const uint8_t *p = parts.subject; p < end;) { + const uint8_t *set = scr_der_tl(p, end, &tag, &len); + if (!set || tag != 0x31) break; + const uint8_t *set_end = set + len; + p = set_end; + for (const uint8_t *q = set; q < set_end;) { + const uint8_t *atv = scr_der_tl(q, set_end, &tag, &len); + if (!atv || tag != 0x30) break; + const uint8_t *atv_end = atv + len; + q = atv_end; + const uint8_t *oid = scr_der_tl(atv, atv_end, &tag, &len); + if (!oid || tag != 0x06 || len != want_len || memcmp(oid, want, len) != 0) continue; + uint8_t vtag; + size_t vlen; + const uint8_t *value = scr_der_tl(oid + len, atv_end, &vtag, &vlen); + if (!value) continue; + ScrX509Buf text = {0}; + scr_x509_value_utf8(&text, vtag, value, vlen); + bool hit = gen == SCR_X509_GEN_DNS + ? scr_x509_host_match((const uint8_t *)text.data, text.len, chk, chk_len, flags) + : scr_x509_email_match((const uint8_t *)text.data, text.len, chk, chk_len); + if (hit) scr_x509_put(out, text.data, text.len); + free(text.data); + if (hit) return true; + } + } + return false; +} + +/* x509.checkHost(name[, options]) and x509.checkEmail(email[, options]): + * the certificate name that matched (checkHost) or the argument + * (checkEmail), "" for none (Node's undefined), or NULL with the + * validation error pending. */ +static ScrStr *scr_x509_check_name(ScrBytes *der, const ScrDyn *value, const ScrDyn *options, + const char *arg, uint8_t gen) { + if (value->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail(arg, "of type string", value); + return NULL; + } + int flags = scr_x509_check_flags(options); + if (flags < 0) return NULL; + ScrX509Buf b = {0}; + const ScrStr *s = value->v.str; + bool hit = s->len > 0 && scr_x509_check(der, gen, (const uint8_t *)s->data, s->len, flags, &b); + if (hit && gen == SCR_X509_GEN_EMAIL) { + /* checkEmail answers its argument (X509_check_email reports no name) */ + free(b.data); + return scr_str_retain((ScrStr *)s); + } + return scr_x509_buf_str(&b); +} + +ScrStr *scr_crypto_x509_check_host(ScrBytes *der, ScrDyn *name, ScrDyn *options) { + return scr_x509_check_name(der, name, options, "name", SCR_X509_GEN_DNS); +} + +ScrStr *scr_crypto_x509_check_email(ScrBytes *der, ScrDyn *email, ScrDyn *options) { + return scr_x509_check_name(der, email, options, "email", SCR_X509_GEN_EMAIL); +} + +/* inet_pton's IPv4 form, which Node's checkIP parses: four decimal parts, + * each at most 255, without leading zeros. */ +static bool scr_x509_ipv4(const char *s, size_t n, uint8_t out[4]) { + size_t part = 0, i = 0; + while (part < 4) { + if (i >= n || s[i] < '0' || s[i] > '9') return false; + unsigned v = 0; + size_t digits = 0; + if (s[i] == '0' && i + 1 < n && s[i + 1] >= '0' && s[i + 1] <= '9') return false; + while (i < n && s[i] >= '0' && s[i] <= '9') { + v = v * 10 + (unsigned)(s[i++] - '0'); + if (++digits > 3 || v > 255) return false; + } + out[part++] = (uint8_t)v; + if (part < 4) { + if (i >= n || s[i] != '.') return false; + i++; + } + } + return i == n; +} + +/* inet_pton's IPv6 form: hex groups with at most one "::", and an IPv4 + * address in place of the last two groups. */ +static bool scr_x509_ipv6(const char *s, size_t n, uint8_t out[16]) { + uint8_t head[16], tail[16]; + size_t head_len = 0, tail_len = 0; + bool compressed = false; + size_t i = 0; + if (n >= 2 && s[0] == ':' && s[1] == ':') { + compressed = true; + i = 2; + if (i == n) { + memset(out, 0, 16); + return true; + } + } else if (n > 0 && s[0] == ':') { + return false; + } + while (i < n) { + uint8_t *dst = compressed ? tail : head; + size_t *dst_len = compressed ? &tail_len : &head_len; + /* an IPv4 tail */ + const char *dot = memchr(s + i, '.', n - i); + const char *colon = memchr(s + i, ':', n - i); + if (dot != NULL && colon == NULL) { + if (*dst_len + 4 > 16 || !scr_x509_ipv4(s + i, n - i, dst + *dst_len)) return false; + *dst_len += 4; + i = n; + break; + } + unsigned v = 0; + size_t digits = 0; + while (i < n && s[i] != ':') { + char c = s[i++]; + unsigned d = c >= '0' && c <= '9' ? (unsigned)(c - '0') + : c >= 'a' && c <= 'f' ? (unsigned)(c - 'a' + 10) + : c >= 'A' && c <= 'F' ? (unsigned)(c - 'A' + 10) + : 16; + if (d == 16 || ++digits > 4) return false; + v = (v << 4) | d; + } + if (digits == 0 || *dst_len + 2 > 16) return false; + dst[(*dst_len)++] = (uint8_t)(v >> 8); + dst[(*dst_len)++] = (uint8_t)v; + if (i == n) break; + i++; /* ':' */ + if (i < n && s[i] == ':') { + if (compressed) return false; + compressed = true; + i++; + if (i == n) break; + } else if (i == n) { + return false; + } + } + if (compressed) { + if (head_len + tail_len > 14) return false; + } else if (head_len != 16) { + return false; + } + memset(out, 0, 16); + memcpy(out, head, head_len); + memcpy(out + 16 - tail_len, tail, tail_len); + return true; +} + +/* x509.checkIP(ip[, options]): the argument itself when an iPAddress + * alternative name equals it, "" for none, NULL with the error pending. + * The options are validated like checkHost's and have no effect. */ +ScrStr *scr_crypto_x509_check_ip(ScrBytes *der, ScrDyn *ip, ScrDyn *options) { + if (ip->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("ip", "of type string", ip); + return NULL; + } + if (scr_x509_check_flags(options) < 0) return NULL; + const ScrStr *s = ip->v.str; + uint8_t addr[16]; + size_t addr_len = 0; + if (memchr(s->data, ':', s->len) != NULL) { + if (scr_x509_ipv6(s->data, s->len, addr)) addr_len = 16; + } else if (scr_x509_ipv4(s->data, s->len, addr)) { + addr_len = 4; + } + if (addr_len == 0) { + static const char msg[] = "Invalid name"; + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_VALUE"); + return NULL; + } + ScrX509Buf b = {0}; + if (scr_x509_check(der, SCR_X509_GEN_IP, addr, addr_len, 0, &b)) { + free(b.data); + return scr_str_retain((ScrStr *)s); + } + return scr_x509_buf_str(&b); +} + +/* ── checkIssued ──────────────────────────────────────────────────────── */ + +/* The string types OpenSSL canonicalizes before comparing names + * (ASN1_MASK_CANON). */ +static bool scr_x509_canon_type(uint8_t tag) { + return tag == 0x0C || tag == 0x1E || tag == 0x1C || tag == 0x13 || tag == 0x14 || tag == 0x16 || + tag == 0x1A; +} + +static bool scr_x509_space(uint8_t c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r'; +} + +/* asn1_string_canon: UTF-8, leading and trailing whitespace dropped, + * inner runs of whitespace as one space, ASCII in lower case. */ +static void scr_x509_canon(ScrX509Buf *out, uint8_t tag, const uint8_t *v, size_t n) { + ScrX509Buf text = {0}; + scr_x509_value_utf8(&text, tag, v, n); + const uint8_t *p = (const uint8_t *)text.data; + size_t len = text.len; + while (len > 0 && scr_x509_space(*p)) p++, len--; + while (len > 0 && scr_x509_space(p[len - 1])) len--; + for (size_t i = 0; i < len; i++) { + uint8_t c = p[i]; + if (scr_x509_space(c)) { + scr_x509_putc(out, ' '); + while (i + 1 < len && scr_x509_space(p[i + 1])) i++; + } else { + scr_x509_putc(out, (char)(c >= 'A' && c <= 'Z' ? c + 32 : c)); + } + } + free(text.data); +} + +/* One Name as comparable text: per RDN, each attribute's OID and its + * canonical value (or raw DER for non-string types), with separators that + * keep the RDN grouping. */ +static void scr_x509_name_canon(ScrX509Buf *out, const uint8_t *name, size_t n) { + const uint8_t *end = name + n; + uint8_t tag; + size_t len; + for (const uint8_t *p = name; p < end;) { + const uint8_t *set = scr_der_tl(p, end, &tag, &len); + if (!set || tag != 0x31) break; + const uint8_t *set_end = set + len; + p = set_end; + scr_x509_putc(out, '\x01'); + for (const uint8_t *q = set; q < set_end;) { + const uint8_t *atv = scr_der_tl(q, set_end, &tag, &len); + if (!atv || tag != 0x30) break; + const uint8_t *atv_end = atv + len; + q = atv_end; + const uint8_t *oid = scr_der_tl(atv, atv_end, &tag, &len); + if (!oid || tag != 0x06) continue; + scr_x509_putc(out, '\x02'); + scr_x509_put(out, oid, len); + uint8_t vtag; + size_t vlen; + const uint8_t *value_tl = oid + len; + const uint8_t *value = scr_der_tl(value_tl, atv_end, &vtag, &vlen); + if (!value) continue; + scr_x509_putc(out, '\x03'); + if (scr_x509_canon_type(vtag)) scr_x509_canon(out, vtag, value, vlen); + else scr_x509_put(out, value_tl, (size_t)(value + vlen - value_tl)); + } + } +} + +static bool scr_x509_name_eq(const uint8_t *a, size_t an, const uint8_t *b, size_t bn) { + ScrX509Buf x = {0}, y = {0}; + scr_x509_name_canon(&x, a, an); + scr_x509_name_canon(&y, b, bn); + bool eq = x.len == y.len && (x.len == 0 || memcmp(x.data, y.data, x.len) == 0); + free(x.data); + free(y.data); + return eq; +} + +static const uint8_t SCR_X509_OID_SKID[] = {0x55, 0x1D, 0x0E}; +static const uint8_t SCR_X509_OID_AKID[] = {0x55, 0x1D, 0x23}; + +/* The public key algorithm a signature algorithm needs: 1 RSA, 2 EC, + * 3 Ed25519, 4 Ed448, 0 unknown. */ +static int scr_x509_sig_key_kind(const uint8_t *oid, size_t n) { + static const uint8_t rsa_prefix[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01}; + static const uint8_t ecdsa_prefix[] = {0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04}; + if (n == sizeof rsa_prefix + 1 && memcmp(oid, rsa_prefix, sizeof rsa_prefix) == 0) { + uint8_t last = oid[sizeof rsa_prefix]; + return last == 4 || last == 5 || (last >= 10 && last <= 14) ? 1 : 0; + } + if (n >= sizeof ecdsa_prefix + 1 && memcmp(oid, ecdsa_prefix, sizeof ecdsa_prefix) == 0) + return 2; + if (n == 3 && oid[0] == 0x2B && oid[1] == 0x65 && oid[2] == 0x70) return 3; + if (n == 3 && oid[0] == 0x2B && oid[1] == 0x65 && oid[2] == 0x71) return 4; + return 0; +} + +/* The kind (as above) of a SubjectPublicKeyInfo's algorithm. */ +static int scr_x509_spki_kind(const ScrX509Parts *parts) { + if (!parts->spki) return 0; + uint8_t tag; + size_t len; + const uint8_t *alg = scr_der_tl(parts->spki, parts->spki + parts->spki_len, &tag, &len); + if (!alg || tag != 0x30) return 0; + const uint8_t *oid = scr_der_tl(alg, alg + len, &tag, &len); + if (!oid || tag != 0x06) return 0; + static const uint8_t rsa[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01}; + static const uint8_t pss[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0A}; + static const uint8_t ec[] = {0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01}; + if ((len == sizeof rsa && memcmp(oid, rsa, len) == 0) || + (len == sizeof pss && memcmp(oid, pss, len) == 0)) + return 1; + if (len == sizeof ec && memcmp(oid, ec, len) == 0) return 2; + if (len == 3 && oid[0] == 0x2B && oid[1] == 0x65 && oid[2] == 0x70) return 3; + if (len == 3 && oid[0] == 0x2B && oid[1] == 0x65 && oid[2] == 0x71) return 4; + return 0; +} + +/* X509_check_akid: the subject's authorityKeyIdentifier against the + * issuer's subjectKeyIdentifier, serial number and issuer name. */ +static bool scr_x509_akid_ok(const ScrX509Parts *issuer, const ScrX509Parts *subject) { + size_t n; + const uint8_t *akid = scr_x509_ext(subject, SCR_X509_OID_AKID, sizeof SCR_X509_OID_AKID, &n); + if (!akid) return true; + uint8_t tag; + size_t len; + const uint8_t *seq = scr_der_tl(akid, akid + n, &tag, &len); + if (!seq || tag != 0x30) return true; + const uint8_t *end = seq + len; + for (const uint8_t *p = seq; p < end;) { + const uint8_t *v = scr_der_tl(p, end, &tag, &len); + if (!v) break; + p = v + len; + if (tag == 0x80) { /* keyIdentifier */ + size_t sn; + const uint8_t *skid = scr_x509_ext(issuer, SCR_X509_OID_SKID, sizeof SCR_X509_OID_SKID, &sn); + uint8_t stag; + size_t slen; + const uint8_t *id = skid ? scr_der_tl(skid, skid + sn, &stag, &slen) : NULL; + if (id && stag == 0x04 && (slen != len || memcmp(id, v, len) != 0)) return false; + } else if (tag == 0x82) { /* authorityCertSerialNumber */ + if (issuer->serial && (issuer->serial_len != len || memcmp(issuer->serial, v, len) != 0)) + return false; + } else if (tag == 0xA1 && issuer->issuer) { /* authorityCertIssuer */ + bool found = false; + const uint8_t *names_end = v + len; + for (const uint8_t *q = v; q < names_end && !found;) { + uint8_t gtag; + size_t glen; + const uint8_t *g = scr_der_tl(q, names_end, >ag, &glen); + if (!g) break; + q = g + glen; + if (gtag != 0xA4) continue; + uint8_t ntag; + size_t nlen; + const uint8_t *name = scr_der_tl(g, g + glen, &ntag, &nlen); + if (name && ntag == 0x30 && scr_x509_name_eq(name, nlen, issuer->issuer, issuer->issuer_len)) + found = true; + } + if (!found) return false; + } + } + return true; +} + +/* x509.checkIssued(otherCert): X509_check_issued(otherCert, x509) — the + * names match, the authority key identifier fits, the signature algorithm + * suits the issuer's key, and the issuer's keyUsage, when present, allows + * certificate signing. */ +bool scr_crypto_x509_check_issued(ScrBytes *der, ScrBytes *issuer_der) { + ScrX509Parts subject, issuer; + if (!scr_x509_parts(SCR_X509_DER(der), &subject) || + !scr_x509_parts(SCR_X509_DER(issuer_der), &issuer)) + return false; + if (!subject.issuer || !issuer.subject || + !scr_x509_name_eq(issuer.subject, issuer.subject_len, subject.issuer, subject.issuer_len)) + return false; + if (!scr_x509_akid_ok(&issuer, &subject)) return false; + uint8_t tag; + size_t len; + const uint8_t *sig_oid = subject.sig_alg + ? scr_der_tl(subject.sig_alg, subject.sig_alg + subject.sig_alg_len, + &tag, &len) + : NULL; + if (!sig_oid || tag != 0x06) return false; + int need = scr_x509_sig_key_kind(sig_oid, len); + if (need == 0 || need != scr_x509_spki_kind(&issuer)) return false; + size_t n; + const uint8_t *ku = scr_x509_ext(&issuer, SCR_X509_OID_KEY_USAGE, sizeof SCR_X509_OID_KEY_USAGE, &n); + if (ku) { + const uint8_t *bits = scr_der_tl(ku, ku + n, &tag, &len); + if (!bits || tag != 0x03 || len < 2 || (bits[1] & 0x04) == 0) return false; + } + return true; +} + +/* ── toLegacyObject ───────────────────────────────────────────────────── */ + +static ScrDyn *scr_x509_dyn_text(const char *s, size_t n) { + ScrStr *str = scr_str_new(s, n); + ScrDyn *d = scr_dyn_new_str(str); + scr_str_release(str); + return d; +} + +/* A fresh Buffer holding n bytes. */ +static ScrDyn *scr_x509_dyn_buffer(const uint8_t *p, size_t n) { + ScrBytes *b = scr_buffer_alloc_unsafe_slow((double)n); + if (n) memcpy(b->data, p, n); + ScrDyn *d = scr_dyn_new_bytes(b); + scr_bytes_release(b); + return d; +} + +/* A Name as Node's legacy object: one property per attribute, keyed by + * OpenSSL's short name (the dotted OID otherwise), its value the UTF-8 + * text; a repeated key collects its values in an array. */ +static ScrDyn *scr_x509_name_object(const uint8_t *name, size_t n) { + ScrDyn *obj = scr_dyn_new_obj_null_proto(); + const uint8_t *end = name + n; + uint8_t tag; + size_t len; + for (const uint8_t *p = name; p < end;) { + const uint8_t *set = scr_der_tl(p, end, &tag, &len); + if (!set || tag != 0x31) break; + const uint8_t *set_end = set + len; + p = set_end; + for (const uint8_t *q = set; q < set_end;) { + const uint8_t *atv = scr_der_tl(q, set_end, &tag, &len); + if (!atv || tag != 0x30) break; + const uint8_t *atv_end = atv + len; + q = atv_end; + const uint8_t *oid = scr_der_tl(atv, atv_end, &tag, &len); + if (!oid || tag != 0x06) continue; + size_t oid_len = len; + uint8_t vtag; + size_t vlen; + const uint8_t *value = scr_der_tl(oid + len, atv_end, &vtag, &vlen); + if (!value) continue; + ScrX509Buf key = {0}, text = {0}; + scr_x509_oid_name(&key, oid, oid_len, scr_x509_attr_names, + sizeof scr_x509_attr_names / sizeof scr_x509_attr_names[0]); + scr_x509_value_utf8(&text, vtag, value, vlen); + ScrDyn *v = scr_x509_dyn_text(text.data ? text.data : "", text.len); + const ScrDyn *prior = scr_dyn_obj_get(obj, key.data, key.len); + if (prior == NULL) { + scr_dyn_obj_set(obj, key.data, key.len, v); + } else if (prior->kind == SCR_DYN_ARR) { + scr_dyn_arr_push((ScrDyn *)prior, v); + } else { + ScrDyn *arr = scr_dyn_new_arr(); + scr_dyn_arr_push(arr, scr_dyn_retain((ScrDyn *)prior)); + scr_dyn_arr_push(arr, v); + scr_dyn_obj_set(obj, key.data, key.len, arr); + } + free(key.data); + free(text.data); + } + } + return obj; +} + +/* The JSON string literal at s (a quoted alternative name), decoded. */ +static void scr_x509_json_unquote(ScrX509Buf *out, const char *s, size_t n) { + for (size_t i = 1; i < n; i++) { + char c = s[i]; + if (c == '"') return; + if (c != '\\' || i + 1 >= n) { + scr_x509_putc(out, c); + continue; + } + c = s[++i]; + if (c == 'u' && i + 4 < n) { + unsigned v = 0; + for (int k = 1; k <= 4; k++) { + char h = s[i + k]; + v = v * 16 + (unsigned)(h <= '9' ? h - '0' : (h | 0x20) - 'a' + 10); + } + scr_x509_put_utf8(out, v); + i += 4; + } else { + scr_x509_putc(out, c == 'n' ? '\n' : c == 't' ? '\t' : c); + } + } +} + +/* Node's legacy infoAccess: the infoAccess lines split at their first + * colon into " - " keys, each holding its values in an + * array (a quoted value is JSON-decoded). */ +static ScrDyn *scr_x509_info_access_object(ScrBytes *der) { + ScrStr *text = scr_crypto_x509_info_access(der); + if (text->len == 0) { + scr_str_release(text); + return scr_dyn_undefined(); + } + ScrDyn *obj = scr_dyn_new_obj_null_proto(); + const char *s = text->data; + size_t n = text->len; + for (size_t start = 0; start < n;) { + size_t eol = start; + while (eol < n && s[eol] != '\n') eol++; + size_t colon = start; + while (colon < eol && s[colon] != ':') colon++; + if (colon < eol) { + const char *val = s + colon + 1; + size_t val_len = eol - colon - 1; + ScrX509Buf v = {0}; + if (val_len > 0 && val[0] == '"') scr_x509_json_unquote(&v, val, val_len); + else scr_x509_put(&v, val, val_len); + ScrDyn *item = scr_x509_dyn_text(v.data ? v.data : "", v.len); + free(v.data); + const ScrDyn *prior = scr_dyn_obj_get(obj, s + start, colon - start); + if (prior != NULL && prior->kind == SCR_DYN_ARR) { + scr_dyn_arr_push((ScrDyn *)prior, item); + } else { + ScrDyn *arr = scr_dyn_new_arr(); + scr_dyn_arr_push(arr, item); + scr_dyn_obj_set(obj, s + start, colon - start, arr); + } + } + start = eol + 1; + } + scr_str_release(text); + return obj; +} + +/* Hex of a big-endian magnitude: BN_bn2hex (whole bytes, upper case, no + * leading zero bytes) or, with `lower_trim`, %llx-style text. */ +static void scr_x509_put_hex(ScrX509Buf *b, const uint8_t *p, size_t n, bool lower_trim) { + const char *hex = lower_trim ? "0123456789abcdef" : "0123456789ABCDEF"; + while (n > 0 && *p == 0) p++, n--; + if (n == 0) { + scr_x509_putc(b, '0'); + return; + } + for (size_t i = 0; i < n; i++) { + if (!(lower_trim && i == 0 && (p[i] >> 4) == 0)) scr_x509_putc(b, hex[p[i] >> 4]); + scr_x509_putc(b, hex[p[i] & 0x0f]); + } +} + +typedef struct { + const char *oid; + const char *name; + const char *nist; + int bits; +} ScrX509Curve; + +static const ScrX509Curve scr_x509_curves[] = { + {"1.2.840.10045.3.1.1", "prime192v1", "P-192", 192}, + {"1.3.132.0.33", "secp224r1", "P-224", 224}, + {"1.2.840.10045.3.1.7", "prime256v1", "P-256", 256}, + {"1.3.132.0.34", "secp384r1", "P-384", 384}, + {"1.3.132.0.35", "secp521r1", "P-521", 521}, + {"1.3.132.0.31", "secp192k1", NULL, 192}, + {"1.3.132.0.32", "secp224k1", NULL, 225}, + {"1.3.132.0.10", "secp256k1", NULL, 256}, + {"1.3.36.3.3.2.8.1.1.7", "brainpoolP256r1", NULL, 256}, + {"1.3.36.3.3.2.8.1.1.11", "brainpoolP384r1", NULL, 384}, + {"1.3.36.3.3.2.8.1.1.13", "brainpoolP512r1", NULL, 512}, +}; + +/* The public key members of the legacy object: modulus, exponent, + * pubkey, bits, asn1Curve and nistCurve (each undefined where the key + * type has none). */ +static void scr_x509_key_fields(const ScrX509Parts *parts, ScrDyn *slots[6]) { + for (int i = 0; i < 6; i++) slots[i] = scr_dyn_undefined(); + if (!parts->spki) return; + int kind = scr_x509_spki_kind(parts); + uint8_t tag; + size_t len; + const uint8_t *spki_end = parts->spki + parts->spki_len; + const uint8_t *alg = scr_der_tl(parts->spki, spki_end, &tag, &len); + if (!alg || tag != 0x30) return; + const uint8_t *alg_end = alg + len; + const uint8_t *oid = scr_der_tl(alg, alg_end, &tag, &len); + if (!oid) return; + const uint8_t *params = oid + len; + uint8_t ptag; + size_t plen; + const uint8_t *param = scr_der_tl(params, alg_end, &ptag, &plen); + const uint8_t *bits = scr_der_tl(alg_end, spki_end, &tag, &len); + if (!bits || tag != 0x03 || len < 1) return; + const uint8_t *key = bits + 1; + size_t key_len = len - 1; + static const uint8_t rsa_oid[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01}; + bool rsa = kind == 1 && (size_t)(params - oid) == sizeof rsa_oid && + memcmp(oid, rsa_oid, sizeof rsa_oid) == 0; + if (rsa) { + const uint8_t *seq = scr_der_tl(key, key + key_len, &tag, &len); + if (!seq || tag != 0x30) return; + const uint8_t *seq_end = seq + len; + size_t nlen, elen; + const uint8_t *nmod = scr_der_tl(seq, seq_end, &tag, &nlen); + if (!nmod || tag != 0x02) return; + const uint8_t *nexp = scr_der_tl(nmod + nlen, seq_end, &tag, &elen); + if (!nexp || tag != 0x02) return; + ScrX509Buf m = {0}, e = {0}; + scr_x509_put_hex(&m, nmod, nlen, false); + scr_x509_puts(&e, "0x"); + scr_x509_put_hex(&e, nexp, elen, true); + slots[0] = scr_x509_dyn_text(m.data, m.len); + slots[1] = scr_x509_dyn_text(e.data, e.len); + free(m.data); + free(e.data); + slots[2] = scr_x509_dyn_buffer(parts->spki_tlv, parts->spki_tlv_len); + size_t i = 0; + while (i < nlen && nmod[i] == 0) i++; + int nbits = 0; + if (i < nlen) { + nbits = (int)((nlen - i - 1) * 8); + for (uint8_t top = nmod[i]; top; top >>= 1) nbits++; + } + slots[3] = scr_dyn_new_num(nbits); + return; + } + if (kind == 2 && param && ptag == 0x06) { + ScrX509Buf dotted = {0}; + scr_x509_oid_dotted(&dotted, param, plen); + for (size_t c = 0; c < sizeof scr_x509_curves / sizeof scr_x509_curves[0]; c++) { + const ScrX509Curve *curve = &scr_x509_curves[c]; + if (strlen(curve->oid) != dotted.len || memcmp(curve->oid, dotted.data, dotted.len) != 0) + continue; + slots[2] = scr_x509_dyn_buffer(key, key_len); + slots[3] = scr_dyn_new_num(curve->bits); + slots[4] = scr_x509_dyn_text(curve->name, strlen(curve->name)); + if (curve->nist) slots[5] = scr_x509_dyn_text(curve->nist, strlen(curve->nist)); + break; + } + free(dotted.data); + } +} + +static const uint8_t SCR_X509_OID_EKU[] = {0x55, 0x1D, 0x25}; + +/* x509.toLegacyObject(): Node's X509ToObject, member order included. */ +ScrDyn *scr_crypto_x509_legacy_object(ScrBytes *der) { + ScrX509Parts parts; + ScrDyn *obj = scr_dyn_new_obj(); + bool ok = scr_x509_parts(SCR_X509_DER(der), &parts); + if (!ok) memset(&parts, 0, sizeof parts); + scr_dyn_obj_set(obj, "subject", 7, + parts.subject ? scr_x509_name_object(parts.subject, parts.subject_len) + : scr_dyn_new_obj_null_proto()); + scr_dyn_obj_set(obj, "issuer", 6, + parts.issuer ? scr_x509_name_object(parts.issuer, parts.issuer_len) + : scr_dyn_new_obj_null_proto()); + ScrStr *san = scr_crypto_x509_subject_alt_name(der); + scr_dyn_obj_set(obj, "subjectaltname", 14, san->len ? scr_dyn_new_str(san) : scr_dyn_undefined()); + scr_str_release(san); + scr_dyn_obj_set(obj, "infoAccess", 10, scr_x509_info_access_object(der)); + scr_dyn_obj_set(obj, "ca", 2, scr_dyn_new_bool(scr_crypto_x509_ca(der))); + ScrDyn *key[6]; + scr_x509_key_fields(&parts, key); + scr_dyn_obj_set(obj, "modulus", 7, key[0]); + scr_dyn_obj_set(obj, "exponent", 8, key[1]); + scr_dyn_obj_set(obj, "pubkey", 6, key[2]); + scr_dyn_obj_set(obj, "bits", 4, key[3]); + static const struct { + const char *key; + ScrStr *(*field)(ScrBytes *); + } texts[] = { + {"valid_from", scr_crypto_x509_valid_from}, + {"valid_to", scr_crypto_x509_valid_to}, + {"fingerprint", scr_crypto_x509_fingerprint}, + {"fingerprint256", scr_crypto_x509_fingerprint256}, + {"fingerprint512", scr_crypto_x509_fingerprint512}, + }; + for (size_t i = 0; i < sizeof texts / sizeof texts[0]; i++) { + ScrStr *s = texts[i].field(der); + scr_dyn_obj_set(obj, texts[i].key, strlen(texts[i].key), scr_dyn_new_str(s)); + scr_str_release(s); + } + size_t n; + const uint8_t *eku = ok ? scr_x509_ext(&parts, SCR_X509_OID_EKU, sizeof SCR_X509_OID_EKU, &n) : NULL; + uint8_t tag; + size_t len; + const uint8_t *purposes = eku ? scr_der_tl(eku, eku + n, &tag, &len) : NULL; + if (purposes && tag == 0x30) { + ScrDyn *arr = scr_dyn_new_arr(); + const uint8_t *end = purposes + len; + for (const uint8_t *p = purposes; p < end;) { + const uint8_t *oid = scr_der_tl(p, end, &tag, &len); + if (!oid) break; + p = oid + len; + if (tag != 0x06) continue; + ScrX509Buf dotted = {0}; + scr_x509_oid_dotted(&dotted, oid, len); + scr_dyn_arr_push(arr, scr_x509_dyn_text(dotted.data, dotted.len)); + free(dotted.data); + } + scr_dyn_obj_set(obj, "ext_key_usage", 13, arr); + } else { + scr_dyn_obj_set(obj, "ext_key_usage", 13, scr_dyn_undefined()); + } + ScrStr *serial = scr_crypto_x509_serial_number(der); + scr_dyn_obj_set(obj, "serialNumber", 12, scr_dyn_new_str(serial)); + scr_str_release(serial); + { + ScrBytes *view = der; + SCR_BYTES_SNAPSHOT(view); + scr_dyn_obj_set(obj, "raw", 3, scr_x509_dyn_buffer(SCR_X509_DER(view))); + } + scr_dyn_obj_set(obj, "asn1Curve", 9, key[4]); + scr_dyn_obj_set(obj, "nistCurve", 9, key[5]); + return obj; +} diff --git a/tests/corpus/6936-x509-certificate-fields.ts b/tests/corpus/6936-x509-certificate-fields.ts new file mode 100644 index 000000000..de4b78f9a --- /dev/null +++ b/tests/corpus/6936-x509-certificate-fields.ts @@ -0,0 +1,189 @@ +// X509Certificate fields match Node: subject and issuer (one attribute +// per line, RFC 2253 escaping), subjectAltName and infoAccess (undefined +// without the extension), serialNumber, the SHA-1/256/512 fingerprints, ca, +// the signature algorithm, the validity window as strings and Dates, and +// raw, over PEM Buffer, PEM string and DER input. +import * as crypto from "node:crypto"; + +// An RSA CA with a multi-attribute subject. +const CA = `-----BEGIN CERTIFICATE----- +MIIEITCCAwmgAwIBAgIIASNFZ4mrze8wDQYJKoZIhvcNAQELBQAwgZwxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp +c2NvMRQwEgYDVQQKDAtFeGFtcGxlIE9yZzERMA8GA1UECwwIRGV2IFRlYW0xGDAW +BgNVBAMMD0V4YW1wbGUgUm9vdCBDQTEdMBsGCSqGSIb3DQEJARYOY2FAZXhhbXBs +ZS5jb20wIBcNMjYxMDIxMTYzMzUyWhgPMjEyNjA5MjcxNjMzNTJaMIGcMQswCQYD +VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j +aXNjbzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxETAPBgNVBAsMCERldiBUZWFtMRgw +FgYDVQQDDA9FeGFtcGxlIFJvb3QgQ0ExHTAbBgkqhkiG9w0BCQEWDmNhQGV4YW1w +bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YGcCr0pxR8d +6qDdLuILcoT7vhtAjyHwfkoChnH6N3TLWzDUugnBp9NbDb7gDLqbRT4uTDoNb6zS +tEJZ4Q7/hhd0jDvoPmOy0Y3Kyj9uNsdf25GdbvwwEnlddELJCZprTZ4OhEg6w2s0 +Yy4SlviLzDXLmsggTypxeS+K8JHT9onMipi5L4GFu8n1QZbzXvSS4BkRgMXEyrtr +FOE9qSo2qkubFyd/WNKRTRIpTSc5/GGsqJ0FKDDJnN5adotcUucbJFn4noNnFUWh +M8mhBEj7ENDdJsD2EVFsI6H958jA2SbNNu4ngzAOs2AKNBYBfQK02ZQAdRNOSL3K +5FkfbII5LwIDAQABo2MwYTAdBgNVHQ4EFgQUP+yQyItpNYEls26Jfv+NnciGaK8w +HwYDVR0jBBgwFoAUP+yQyItpNYEls26Jfv+NnciGaK8wDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBABDw3Va/C+RFc/Hs +TJnHtQJtxFL0ZhnHUqg4DcDA3m+JDdIDNL8sNpLEbtn/pr0VeEWgG+MImWAQlrUW +IAZkLj9/Zg1wSO0oZ86ZyZJZmp54lUYXgG17CNl4g+F7VHt3bBwUJHTbWye/FKM6 +5OVZetCYKVj7Z+5BYuf7cumzvRqcn32fXidB/kjRX14SJ4JDPn2yFZWCr2ygIqWQ +SOuEmWbNAbYGaVZkQr2Ma8U28DWY2jb0f6nP8jLt49t9o6N/e6Fwd6I5t/mx9Mfj +0403omkxmr9vqN2kBamzVnun+2mDkRZcKPCr72Q4sfbvmDi/W5Nf1Iu2cQW8VfDO +SqpyT54= +-----END CERTIFICATE----- +`; + +// An EC leaf with alternative names, access descriptions and extended key usage, signed by CA. +const LEAF = `-----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----- +`; + +// Escaped subject characters, UTF-8 and a comma in a DNS name. +const ODD = `-----BEGIN CERTIFICATE----- +MIICXzCCAeWgAwIBAgIBATAKBggqhkjOPQQDAjBvMQ0wCwYDVQQDDARhLCBiMQ4w +DAYDVQQHDAUjbGVhZDESMBAGA1UECAwJICBzcGFjZSAgMSYwJAYDVQQKDB1RdW90 +ZSBxIGJhY2tcc2xhc2g7IHNlbWkgPGx0PjESMBAGA1UECwwJQ2Fmw6kg4oKsMB4X +DTI2MTAyMTE2MzM1MloXDTI3MTAyMTE2MzM1MlowbzENMAsGA1UEAwwEYSwgYjEO +MAwGA1UEBwwFI2xlYWQxEjAQBgNVBAgMCSAgc3BhY2UgIDEmMCQGA1UECgwdUXVv +dGUgcSBiYWNrXHNsYXNoOyBzZW1pIDxsdD4xEjAQBgNVBAsMCUNhZsOpIOKCrDB2 +MBAGByqGSM49AgEGBSuBBAAiA2IABMLLzwDsSeezCzScnC9xTIFl35dFAX+1Uagk +0y4Y72rOOgVFA13SMIbMfv2InIyg2vXOdqn0y7RqiaaqqnAYZB2RHHL0SD7t2MVL +K3BQajy7Oi6IeDDRGK1jM4D6nk/1AKNVMFMwMgYDVR0RBCswKYILYSxiLmV4YW1w +bGWCDXBsYWluLmV4YW1wbGWBBXhAeS56hwQKAAABMB0GA1UdDgQWBBSR4U6nIpTt +5rLoIZZxfxSyx9Uc9zAKBggqhkjOPQQDAgNoADBlAjEAlznFL0Nrt1jjZrT3/GN0 +yN/8gxO3SawyqoWrfP3uk1GScTh0PlrTEOrpSyuq3wl5AjBRntVI1ZoUM46pCwIw +0Vl3c3lns7OljcQFiR7o8yRhEW9ejdiR2W6PLBrjCly6U3I= +-----END CERTIFICATE----- +`; + +// A multi-valued RDN and serial number 0. +const MULTI = `-----BEGIN CERTIFICATE----- +MIICNzCCAaCgAwIBAgIBADANBgkqhkiG9w0BAQsFADA3MSgwCgYDVQQLDANyZG4w +DAYDVQQDDAVtdWx0aTAMBgNVBAoMBXRocmVlMQswCQYDVQQGEwJERTAeFw0yNjEw +MjExNjM0MDVaFw0yNjEwMzExNjM0MDVaMDcxKDAKBgNVBAsMA3JkbjAMBgNVBAMM +BW11bHRpMAwGA1UECgwFdGhyZWUxCzAJBgNVBAYTAkRFMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDdv64EOPdpcWdjVMAlEQvNzm3ZbQYXrhOwRF9yzQp25SuA +3uKvStZOt8hyTifAd30oDXIfgyYI39xKHEkHfARTV1E2Jwyiz86/pDHmFypPFJuI +v/vOjYLLeisiVi8TFfHgmoGqGvySR83fwiUnoOOWj9Bqt/5EVBQbBeWrU9qH6QID +AQABo1MwUTAdBgNVHQ4EFgQUZrGU0gkgqCkmegRtu6vQ3h7kpPUwHwYDVR0jBBgw +FoAUZrGU0gkgqCkmegRtu6vQ3h7kpPUwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQsFAAOBgQAMzFkzJElc9VJYhGx7AngO8Yve22iCTOVLO3/be8w55klb/HJX +vfESQz+EJLYVaP5Q/uChoE0I27NytkeUSpJFwmjyqoQq2j6kJWGKONqhQSnXDrms +HMPA7iik/C5hOqpwzDZ98wNU/e86TrqZxzpFkwbnZx9MSfiGr690s5BfpA== +-----END CERTIFICATE----- +`; + +// An Ed25519 certificate with otherName, directoryName, registeredID and IPv6 names, a BMPString subject, a negative serial and keyUsage without keyCertSign. +const NAMES = `-----BEGIN CERTIFICATE----- +MIICSDCCAfqgAwIBAgIB+zAFBgMrZXAwLDEVMBMGA1UEAx4MAGIAbQBwACAA6U4t +MRMwEQYDVQQKHgoAcABsAGEAaQBuMB4XDTI2MTAyMTE2NDQzM1oXDTI2MTEyMDE2 +NDQzM1owLDEVMBMGA1UEAx4MAGIAbQBwACAA6U4tMRMwEQYDVQQKHgoAcABsAGEA +aQBuMCowBQYDK2VwAyEAP+2Vcp8nwu2AhFvSczYTxGs1jV/ckA76Jqvwj9QDGqij +ggE/MIIBOzCBtwYDVR0RBIGvMIGsoBsGCisGAQQBgjcUAgOgDQwLdXNlckBkb21h +aW6gEAYDKgMEoAkMB3Vua25vd26kRTBDMQswCQYDVQQGEwJVSzEZMBcGA1UECh4Q +AEQAaQByACwAIABPAHIAZzEZMBcGA1UEAx4QAGQAaQByACAAbgBhAG0AZYgEKgME +BYcQ/oAAAAAAAAAAAAAAAAEAAocQIAENuAAAAAAAAP8AAEKDKYYKaHR0cDovL3gv +cTAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIHgDBFBggrBgEFBQcBAQQ5MDcwIAYI +KwYBBQUHMAWGFGh0dHA6Ly9yZXBvLmV4YW1wbGUvMBMGBCoDBAWCC29kZC5leGFt +cGxlMB0GA1UdDgQWBBQYMJ0fQJ1Nx0PmatIfTJusAEA9WTAFBgMrZXADQQCixubE +nuunBZYYPXF52+joQSX/a5DFkOEDmzRTuH7IIpPFWPo37rmJOSdPA/PV8v9clzYF +LP3EjM4YOAatcbsO +-----END CERTIFICATE----- +`; + +// An RSA-PSS signature and a control character in the subject. +const PSS = `-----BEGIN CERTIFICATE----- +MIIDdTCCAiigAwIBAgICAIAwQgYJKoZIhvcNAQEKMDWgDzANBglghkgBZQMEAgEF +AKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIEAgIA3jAeMQwwCgYDVQQD +DANwc3MxDjAMBgNVBAoMBWN0bAF4MB4XDTI2MTAyMTE2NDQzM1oXDTI2MTEyMDE2 +NDQzM1owHjEMMAoGA1UEAwwDcHNzMQ4wDAYDVQQKDAVjdGwBeDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL2pno8Ybwihdv3hnMhWPPt85SXQmFAJpFoS +E+sT9H6FbwFCIyPkY+SWk8H7y8V+D+388tNvJZmGjX3uvWpK2Wx6anSB3/WMpkUR +2ExP4cpSxZNr9EFzC/cdqLvsxJcHMHkc/uJuFbtjsuiH1ygOe3nyhWSNiPwGdwHE +cKkBFkzW3j2C/4Jkgt4k+sMe3gy5litKtMi4zZB8ixCm5oGhveGhUZy0Kb01twgZ +RP4FEd32sSRqq3NCx4cB0kyreTuzmk6U7BkxcdPzaj6DifjRznTQmjMdoCNZQRCj +G3KBOMHHyvmV2B4lNGDuMlcSap/TpYq7n32AH5sDvELrUnPwVzsCAwEAAaNTMFEw +HQYDVR0OBBYEFC/QJvAQsbQ25sErc1To2G1VuNZ0MB8GA1UdIwQYMBaAFC/QJvAQ +sbQ25sErc1To2G1VuNZ0MA8GA1UdEwEB/wQFMAMBAf8wQgYJKoZIhvcNAQEKMDWg +DzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIE +AgIA3gOCAQEAu3lP2Q3Bb5fWUQtgFr/82Oke1ZRW/GRbj03mZ30izADHC7yk0OYm +02f7B5sub04Jk6yuRVmEBjJHSPNwAo0RJWtwvD77KLhG8aObr7Z3+pkfFvpGzqeS +MQ4FtK2MPps3kA1a5D4XLwkwDJtHWZVuVyXpPgxkptfLzZgrieKrpNyOKR8Pz/PO +Wlwv6bF7SXvAV8Vrd7KDvTqwQ5iq9UFhP6GeMDWezYtzLdjlvje7MhmZ96zgSY6m +qumNWOPHmiN4eoOE4M69B+0fa3WyMzubW0nVGM/sDhtS0k5zHs6Y4RLn11HolhXH +LvbvDgqtl/m5i0uHxEld+Z1uhzJscqFQRg== +-----END CERTIFICATE----- +`; + +// A version 1 certificate without extensions. +const V1 = `-----BEGIN CERTIFICATE----- +MIIBSTCB7wIUI3kYI5NpR07YqZe6RwOx22je9IowCgYIKoZIzj0EAwIwJzERMA8G +A1UECgwITGVhZiBPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yNjEwMjExNjMz +NTJaFw0yNjExMjAxNjMzNTJaMCcxETAPBgNVBAoMCExlYWYgT3JnMRIwEAYDVQQD +DAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASVX1cvRB/1R0Lk +aqRRZ15NBdNJ3A4begD4FTw6MmyvBz5XDOE+R9cgDjB55R9vSytY7TKlNWgDookE +cz3Qr6qLMAoGCCqGSM49BAMCA0kAMEYCIQCFfFOw9qzzfSOQO1zRnT0sOXibjLWl +hHDyqecxhY7gtQIhAPMgqg+p1JXV5bgGpqgDfP3sE53luxMGlnAC39uhwBUh +-----END CERTIFICATE----- +`; + + +function show(label: string, cert: crypto.X509Certificate): void { + console.log(label, "subject", JSON.stringify(cert.subject)); + console.log(label, "issuer", JSON.stringify(cert.issuer)); + console.log(label, "subjectAltName", JSON.stringify(cert.subjectAltName), typeof cert.subjectAltName); + console.log(label, "infoAccess", JSON.stringify(cert.infoAccess), cert.infoAccess === undefined); + console.log(label, "serialNumber", cert.serialNumber); + console.log(label, "fingerprints", cert.fingerprint, cert.fingerprint256); + console.log(label, "fingerprint512", cert.fingerprint512); + console.log(label, "ca", cert.ca); + console.log(label, "signature", cert.signatureAlgorithm, cert.signatureAlgorithmOid); + console.log(label, "validity", cert.validFrom, "|", cert.validTo); + console.log(label, "dates", cert.validFromDate.toISOString(), cert.validToDate.getTime()); + console.log(label, "raw", cert.raw.length, Buffer.isBuffer(cert.raw), cert.raw.subarray(0, 4).toString("hex")); +} + +const pems: [string, string][] = [ + ["ca", CA], + ["leaf", LEAF], + ["odd", ODD], + ["multi", MULTI], + ["names", NAMES], + ["pss", PSS], + ["v1", V1], +]; +for (const [label, pem] of pems) show(label, new crypto.X509Certificate(Buffer.from(pem))); + +// A PEM string, and the DER bytes of raw. +const fromString = new crypto.X509Certificate(LEAF); +show("string", fromString); +const fromDer = new crypto.X509Certificate(fromString.raw); +console.log("der", fromDer.subject === fromString.subject, fromDer.fingerprint256 === fromString.fingerprint256); +console.log("der raw copy", fromDer.raw !== fromString.raw, fromDer.raw.equals(fromString.raw)); + +try { + new crypto.X509Certificate(Buffer.from("not a certificate")); +} catch (e) { + const err = e as NodeJS.ErrnoException; + console.log("garbage", err.name, err.code); +} diff --git a/tests/corpus/7090-x509-check-names.ts b/tests/corpus/7090-x509-check-names.ts new file mode 100644 index 000000000..31ec7244f --- /dev/null +++ b/tests/corpus/7090-x509-check-names.ts @@ -0,0 +1,141 @@ +// X509Certificate.checkHost, checkEmail and checkIP match Node: the +// subject alternative names of the asked kind first, then the subject's +// common name or emailAddress when the certificate has none (or as the +// subject option says), the wildcard rules and their options, case rules, +// IPv4 and IPv6 parsing, and an invalid subject option. +import { X509Certificate, type X509CheckOptions } from "node:crypto"; + +// DNS names with every wildcard form, an IP address and an email name. +const WILD = `-----BEGIN CERTIFICATE----- +MIIB/TCCAaOgAwIBAgIBCTAKBggqhkjOPQQDAjAXMRUwEwYDVQQDDAwqLmNuLmV4 +YW1wbGUwIBcNMjYxMDIyMTAwMTMxWhgPMjEyNjA5MjgxMDAxMzFaMBcxFTATBgNV +BAMMDCouY24uZXhhbXBsZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGJ1KKs0 +givD5gd5LXXbPc37WfT64D0ePORDm7dB9mxEYJ6K8IQxBKzn5xQT+e/lZZ1raRBZ +q/E8aW4cW1iZLXSjgd0wgdowgbgGA1UdEQSBsDCBrYIOZiouZXhhbXBsZS5jb22C +Dypvby5leGFtcGxlLm5ldIIXKi54bi0tYmNoZXIta3ZhLmV4YW1wbGWCDXhuLS0q +LmV4YW1wbGWCBSouY29tgg9hLiouZXhhbXBsZS5vcmeCASqCDyouTWl4ZWQuRXhh +bXBsZYIMLmRvdC5leGFtcGxlgg9iKnouZXhhbXBsZS5lZHWHBMCoAQGBEU1peGVk +QEV4YW1wbGUuQ09NMB0GA1UdDgQWBBR8lJ0Uw8GcVD1cGUSprHt346HjDDAKBggq +hkjOPQQDAgNIADBFAiALkTbNe6bIavd8T9qKlAqzmEPQrFRZUVoOHSQPI/zV5QIh +AM0et8JtbWP+4ySiUPPqCXtlup00GfhcqGXy+lSl32vL +-----END CERTIFICATE----- +`; + +// No alternative names; the subject has a wildcard CN and an emailAddress. +const CN = `-----BEGIN CERTIFICATE----- +MIIBVDCB+gIBCjAKBggqhkjOPQQDAjA1MRcwFQYDVQQDDA5zZWNvbmQuZXhhbXBs +ZTEaMBgGCSqGSIb3DQEJARYLYUBiLmV4YW1wbGUwIBcNMjYxMDIyMTAwMTMxWhgP +MjEyNjA5MjgxMDAxMzFaMDUxFzAVBgNVBAMMDnNlY29uZC5leGFtcGxlMRowGAYJ +KoZIhvcNAQkBFgthQGIuZXhhbXBsZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA +BGJ1KKs0givD5gd5LXXbPc37WfT64D0ePORDm7dB9mxEYJ6K8IQxBKzn5xQT+e/l +ZZ1raRBZq/E8aW4cW1iZLXQwCgYIKoZIzj0EAwIDSQAwRgIhAIE0IFtuCNllBodV +go5WWgb8h1McrS/o31V5K0JrpF7QAiEAil8AEHuMiVtef/txZsEKPPojDKfPTYC8 +zplMuFd9gQA= +-----END CERTIFICATE----- +`; + +// A leaf with DNS, email, IPv4, IPv6 and URI names. +const LEAF = `-----BEGIN CERTIFICATE----- +MIIDqjCCApKgAwIBAgICCwswDQYJKoZIhvcNAQELBQAwPjELMAkGA1UEBhMCVVMx +FDASBgNVBAoMC0V4YW1wbGUgT3JnMRkwFwYDVQQDDBBFeGFtcGxlIFJTQSBSb290 +MCAXDTI2MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjBfMQswCQYDVQQGEwJV +UzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNv +bTEgMB4GCSqGSIb3DQEJARYRYWRtaW5AZXhhbXBsZS5jb20wWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARidSirNIIrw+YHeS112z3N+1n0+uA9HjzkQ5u3QfZsRGCe +ivCEMQSs5+cUE/nv5WWda2kQWavxPGluHFtYmS10o4IBWDCCAVQwCQYDVR0TBAIw +ADALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHsG +A1UdEQR0MHKCDSouZXhhbXBsZS5jb22CC2V4YW1wbGUuY29tghV4bi0tYmNoZXIt +a3ZhLmV4YW1wbGWBD29wc0BleGFtcGxlLmNvbYcECgAAAYcQIAENuAAAAAAAAAAA +AAAAAYYUaHR0cHM6Ly9leGFtcGxlLmNvbS8wXgYIKwYBBQUHAQEEUjBQMCQGCCsG +AQUFBzABhhhodHRwOi8vb2NzcC5leGFtcGxlLmNvbS8wKAYIKwYBBQUHMAKGHGh0 +dHA6Ly9jYS5leGFtcGxlLmNvbS9jYS5jcnQwHQYDVR0OBBYEFHyUnRTDwZxUPVwZ +RKmse3fjoeMMMB8GA1UdIwQYMBaAFAgaWVveRbA7VKqcHzjMqBPzi1/LMA0GCSqG +SIb3DQEBCwUAA4IBAQAfMTcpidoH47PXOQVxRttfrG7K9BcntyXP4+5hUAoSfMr6 +Rxy7X/9ba7R0yCGlNsUCusObaBpbdvwX8v9TiTYKVL+C9rEVGrTHnh6kbgZaaCQc +L3oWbfRrYnEBYuSlFuadjVvX42aqzHV28E4ITgywCxwubu2hAbacbcUHpnRSqLDq +lV+VZSQA/6cPNct7PR6AkJU80D+5wVOk7RVi4rsfbw1wFDeksCHu+0EThs9zjzL+ +S7HTCjB6AOHpbu1Sx+0d5YEKSE7EPF+dSFATNbhjTHzYQySnHK+Lm8nON8wmY1rC +7opOhvUlid1jA1d2BKO2ZxlxLir4pyIehTC1ddG2 +-----END CERTIFICATE----- +`; + +// A self-signed certificate without alternative names. +const PLAIN = `-----BEGIN CERTIFICATE----- +MIIC8jCCAdoCAQcwDQYJKoZIhvcNAQELBQAwPjEaMBgGA1UEAwwRcGxhaW4uZXhh +bXBsZS5vcmcxIDAeBgkqhkiG9w0BCQEWEXBsYWluQGV4YW1wbGUub3JnMCAXDTI2 +MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjA+MRowGAYDVQQDDBFwbGFpbi5l +eGFtcGxlLm9yZzEgMB4GCSqGSIb3DQEJARYRcGxhaW5AZXhhbXBsZS5vcmcwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq01NcNAy9Ng04mVuAFvmZUvsm +rY7aOaMgtODmptulgZ2qw25E09RgGYCn1S+4IYDu9hH9X4s+28Ag7Khmj4nmeoXN +of5095wche8DR2AAUElqyr/Nmgm2kTLdIJX4wl0eWXHIlmSPdOUNovA6JjGKH99b +8W7a2TqY/TsSadpPSvvN2esr7Jr4KgW7MXPUOnwNbcBOVcQP9rtFrduAWz01q0HM +02LboclnUnk/e58BXAj8vgdVojljDcc0bhbdz/Hh1/5gaaNILjJNOd4AgwZaCT02 +KojGz4Nb6MSgFmON2iMgAZjY1fXXkJ7wsfVPytjW73e7uV/RFuqSHQZenxupAgMB +AAEwDQYJKoZIhvcNAQELBQADggEBAJX1DGuiSLoCKBDI3oCgAu7X86G01ovEgZJr +8JrmGHW5q7ujjo53t46h8XPm3LJ8UV9/MZ+Yry43xB2txXYv3b3g/i+7JdTWLgUD +INA5VWvVkEZfm0lBEpU1Agpy4lV5uUCKZTgP/3MY5hnJ0YhQIs4BOjbwueMRFshL +yyHVb73MMbpWaZ7+T368seafa+ekeuY6gQly/HacuszjoYd6KKV3n8eDewenClCF +HoOF1Y4I+CvmR+a6B3iBczJ+a4tceFIIWsZUIeqzvGHpM7G9YwlmbvGprfLgR8Nv +igUib/D9tU3hdL6VWN5nMjdXV/8Le4nDnBjVBjYDkVbsnmT91AQ= +-----END CERTIFICATE----- +`; + +const wild = new X509Certificate(WILD); +const cn = new X509Certificate(Buffer.from(CN)); +const leaf = new X509Certificate(LEAF); +const plain = new X509Certificate(PLAIN); + +const hosts = [ + "foo.example.com", "f.example.com", "fx.example.com", "a.b.example.com", "foo.example.net", + "oo.example.net", "x.xn--bcher-kva.example", "xn--abc.example", "example.com", + "a.b.example.org", "x", "a.mixed.example", "A.MIXED.EXAMPLE", "a.dot.example", ".dot.example", + "dot.example", "bz.example.edu", "baz.example.edu", "b.z.example.edu", "x.cn.example", + "second.example", "192.168.1.1", ".example.com", ".mixed.example", "a.b.mixed.example", +]; +const opts: [string, X509CheckOptions | undefined][] = [ + ["default", undefined], + ["nowild", { wildcards: false }], + ["nopartial", { partialWildcards: false }], + ["multi", { multiLabelWildcards: true }], + ["single", { singleLabelSubdomains: true }], + ["always", { subject: "always" }], + ["never", { subject: "never" }], +]; +for (const [label, o] of opts) { + console.log(label, hosts.map((h) => `${h}=${wild.checkHost(h, o)}`).join(" ")); + console.log(label, "cn", hosts.map((h) => `${h}=${cn.checkHost(h, o)}`).join(" ")); +} + +for (const h of ["www.example.com", "example.com", "EXAMPLE.COM", "xn--bcher-kva.example", "", "foo.example.com."]) { + console.log("leaf", JSON.stringify(h), leaf.checkHost(h), leaf.checkHost(h, { subject: "always" })); +} +console.log("plain", plain.checkHost("plain.example.org"), plain.checkHost("plain.example.org", { subject: "never" })); +const found: string | undefined = leaf.checkHost("api.example.com"); +console.log("typed", found === undefined ? "none" : found.toUpperCase()); + +for (const e of ["Mixed@example.com", "mixed@example.com", "Mixed@EXAMPLE.COM", "a@b.example", "a@B.EXAMPLE", "A@b.example"]) { + console.log("email", e, wild.checkEmail(e), cn.checkEmail(e), cn.checkEmail(e, { subject: "never" })); +} +for (const e of ["ops@example.com", "OPS@EXAMPLE.COM", "admin@example.com"]) console.log("email", e, leaf.checkEmail(e)); +console.log("email plain", plain.checkEmail("plain@example.org"), plain.checkEmail("plain@example.org", { subject: "always" })); + +const ips = [ + "192.168.1.1", "192.168.001.001", "192.168.1.1.", "1.2.3", "::", "::ffff:192.168.1.1", + "0:0:0:0:0:0:0:0", "1::2::3", "fe80::1%eth0", " 192.168.1.1", "192.168.1.256", + "10.0.0.1", "2001:db8::1", "2001:DB8:0:0:0:0:0:1", "2001:db8:0::0:1", "::ffff:10.0.0.1", "", +]; +for (const ip of ips) { + try { + console.log("ip", JSON.stringify(ip), wild.checkIP(ip), leaf.checkIP(ip)); + } catch (e) { + const err = e as NodeJS.ErrnoException; + console.log("ip", JSON.stringify(ip), err.name, err.code, err.message); + } +} + +try { + leaf.checkHost("www.example.com", { subject: "sometimes" as "always" }); +} catch (e) { + const err = e as NodeJS.ErrnoException; + console.log("subject", err.name, err.code, err.message); +} diff --git a/tests/corpus/7091-x509-certificate-keys.ts b/tests/corpus/7091-x509-certificate-keys.ts new file mode 100644 index 000000000..b9b89ae1e --- /dev/null +++ b/tests/corpus/7091-x509-certificate-keys.ts @@ -0,0 +1,332 @@ +// X509Certificate.publicKey, verify, checkIssued and checkPrivateKey match +// Node: the public key as a cached KeyObject, signature checks for RSA +// PKCS#1, RSA-PSS, ECDSA and Ed25519 signatures, issuer checks by name, +// key identifier and key type, private key matches, and the pkey errors. +import { + X509Certificate, + constants, + createPrivateKey, + createPublicKey, + createSecretKey, + generateKeyPairSync, + sign, + verify, + type KeyObject, +} from "node:crypto"; + +// An RSA root and the EC leaf it signed. +const ROOT = `-----BEGIN CERTIFICATE----- +MIIDPTCCAiWgAwIBAgIDChHOMA0GCSqGSIb3DQEBCwUAMD4xCzAJBgNVBAYTAlVT +MRQwEgYDVQQKDAtFeGFtcGxlIE9yZzEZMBcGA1UEAwwQRXhhbXBsZSBSU0EgUm9v +dDAgFw0yNjEwMjIwOTQ2NDFaGA8yMTI2MDkyODA5NDY0MVowPjELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0V4YW1wbGUgT3JnMRkwFwYDVQQDDBBFeGFtcGxlIFJTQSBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwsfidupnXQcn8PUF +XaCKRYw8XKI1GUn29U5/IUGjAnJVaRmcB6BPTmblbKt4y7gLvU4h1Hxuypx8FZSZ +30eZOS1ixyrymDthG5zUHqzXk5zAGsNvZeXjN3YxTC9vX5CpQ9Uz2i+bLOi3PJsE +qyKVZ+GGT9fpIaALG11YrWmXTUko/a0AKGot0w8O/RaMpXQpkbeyTBF6f4pHNMqx +QGy+KlpC/+yIJ9psT6qyU33HF3Wa+DOy+0txAFEQNw9m1j7adOLnU0zBpkM/bnP2 +fSwxMkDB2Di2P5H6PkdqLT4zBf/BLdg7WqpxxSqqgVmaSXhEqUQ6xjf51yLR3wy/ +xBWhMQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQUCBpZW95FsDtUqpwfOMyoE/OLX8swDQYJKoZIhvcNAQELBQADggEB +AChUtpNi8dPPxA4sf4EpZhEcYBN53X3WUcG3KJGtSf8JbPKru9lK9O+sZtJADbph +WoMPdWh8g+fEMk5D6l8Y5pDk/OR++oWNY7XCLFNyigmUGn+4SopJynBQiDk2dYPV +b6swDPPiV85TuAHVn79l766rtFdHdYwRe5HvfEkiXax7Egonlx7NR73xxm/XgvHQ +jhKbTZN9k/DEjkQuo5rt9ldVupUMM0iMhDMKX5oWEx9nvRwHPbjESS+Shy/FA4nA +2WHWO++4IU+imO40an1B/8SmFF+X9hj3UE6qaZLynVZRUiouBZibwZCcdCpibFM3 +kmB2s+NZXRvzhCjIxO97Ib4= +-----END CERTIFICATE----- +`; +const LEAF = `-----BEGIN CERTIFICATE----- +MIIDqjCCApKgAwIBAgICCwswDQYJKoZIhvcNAQELBQAwPjELMAkGA1UEBhMCVVMx +FDASBgNVBAoMC0V4YW1wbGUgT3JnMRkwFwYDVQQDDBBFeGFtcGxlIFJTQSBSb290 +MCAXDTI2MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjBfMQswCQYDVQQGEwJV +UzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNv +bTEgMB4GCSqGSIb3DQEJARYRYWRtaW5AZXhhbXBsZS5jb20wWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARidSirNIIrw+YHeS112z3N+1n0+uA9HjzkQ5u3QfZsRGCe +ivCEMQSs5+cUE/nv5WWda2kQWavxPGluHFtYmS10o4IBWDCCAVQwCQYDVR0TBAIw +ADALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHsG +A1UdEQR0MHKCDSouZXhhbXBsZS5jb22CC2V4YW1wbGUuY29tghV4bi0tYmNoZXIt +a3ZhLmV4YW1wbGWBD29wc0BleGFtcGxlLmNvbYcECgAAAYcQIAENuAAAAAAAAAAA +AAAAAYYUaHR0cHM6Ly9leGFtcGxlLmNvbS8wXgYIKwYBBQUHAQEEUjBQMCQGCCsG +AQUFBzABhhhodHRwOi8vb2NzcC5leGFtcGxlLmNvbS8wKAYIKwYBBQUHMAKGHGh0 +dHA6Ly9jYS5leGFtcGxlLmNvbS9jYS5jcnQwHQYDVR0OBBYEFHyUnRTDwZxUPVwZ +RKmse3fjoeMMMB8GA1UdIwQYMBaAFAgaWVveRbA7VKqcHzjMqBPzi1/LMA0GCSqG +SIb3DQEBCwUAA4IBAQAfMTcpidoH47PXOQVxRttfrG7K9BcntyXP4+5hUAoSfMr6 +Rxy7X/9ba7R0yCGlNsUCusObaBpbdvwX8v9TiTYKVL+C9rEVGrTHnh6kbgZaaCQc +L3oWbfRrYnEBYuSlFuadjVvX42aqzHV28E4ITgywCxwubu2hAbacbcUHpnRSqLDq +lV+VZSQA/6cPNct7PR6AkJU80D+5wVOk7RVi4rsfbw1wFDeksCHu+0EThs9zjzL+ +S7HTCjB6AOHpbu1Sx+0d5YEKSE7EPF+dSFATNbhjTHzYQySnHK+Lm8nON8wmY1rC +7opOhvUlid1jA1d2BKO2ZxlxLir4pyIehTC1ddG2 +-----END CERTIFICATE----- +`; +// A self-signed RSA certificate and a self-signed Ed25519 certificate. +const PLAIN = `-----BEGIN CERTIFICATE----- +MIIC8jCCAdoCAQcwDQYJKoZIhvcNAQELBQAwPjEaMBgGA1UEAwwRcGxhaW4uZXhh +bXBsZS5vcmcxIDAeBgkqhkiG9w0BCQEWEXBsYWluQGV4YW1wbGUub3JnMCAXDTI2 +MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjA+MRowGAYDVQQDDBFwbGFpbi5l +eGFtcGxlLm9yZzEgMB4GCSqGSIb3DQEJARYRcGxhaW5AZXhhbXBsZS5vcmcwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq01NcNAy9Ng04mVuAFvmZUvsm +rY7aOaMgtODmptulgZ2qw25E09RgGYCn1S+4IYDu9hH9X4s+28Ag7Khmj4nmeoXN +of5095wche8DR2AAUElqyr/Nmgm2kTLdIJX4wl0eWXHIlmSPdOUNovA6JjGKH99b +8W7a2TqY/TsSadpPSvvN2esr7Jr4KgW7MXPUOnwNbcBOVcQP9rtFrduAWz01q0HM +02LboclnUnk/e58BXAj8vgdVojljDcc0bhbdz/Hh1/5gaaNILjJNOd4AgwZaCT02 +KojGz4Nb6MSgFmON2iMgAZjY1fXXkJ7wsfVPytjW73e7uV/RFuqSHQZenxupAgMB +AAEwDQYJKoZIhvcNAQELBQADggEBAJX1DGuiSLoCKBDI3oCgAu7X86G01ovEgZJr +8JrmGHW5q7ujjo53t46h8XPm3LJ8UV9/MZ+Yry43xB2txXYv3b3g/i+7JdTWLgUD +INA5VWvVkEZfm0lBEpU1Agpy4lV5uUCKZTgP/3MY5hnJ0YhQIs4BOjbwueMRFshL +yyHVb73MMbpWaZ7+T368seafa+ekeuY6gQly/HacuszjoYd6KKV3n8eDewenClCF +HoOF1Y4I+CvmR+a6B3iBczJ+a4tceFIIWsZUIeqzvGHpM7G9YwlmbvGprfLgR8Nv +igUib/D9tU3hdL6VWN5nMjdXV/8Le4nDnBjVBjYDkVbsnmT91AQ= +-----END CERTIFICATE----- +`; +const ED = `-----BEGIN CERTIFICATE----- +MIIBljCCAUigAwIBAgIUNFuRt3zKS19UAUMdLWewqHrGBigwBQYDK2VwMC8xFDAS +BgNVBAoMC0V4YW1wbGUgT3JnMRcwFQYDVQQDDA5lZC5leGFtcGxlLm5ldDAgFw0y +NjEwMjIxMDEwMTJaGA8yMTI2MDkyODEwMTAxMlowLzEUMBIGA1UECgwLRXhhbXBs +ZSBPcmcxFzAVBgNVBAMMDmVkLmV4YW1wbGUubmV0MCowBQYDK2VwAyEAOCSLKkel +kOH/5aqUwI3PNJVUUNyhgu2s19LFC254fNCjdDByMB0GA1UdDgQWBBR+50zUcqL8 +739jh6/rEYojULDmlTAfBgNVHSMEGDAWgBR+50zUcqL8739jh6/rEYojULDmlTAP +BgNVHRMBAf8EBTADAQH/MB8GA1UdEQQYMBaCDmVkLmV4YW1wbGUubmV0hwTAAAIH +MAUGAytlcANBAC/c0Bi0SVUnHCg/1pn7MaCgVivSHuoISi2SCq/0EnLefRdgsIir +0QxjVnxeuKN2Y1yfEO/R1W8Nwovo1l2M1gQ= +-----END CERTIFICATE----- +`; +// A second RSA root with its EC leaf, a self-signed RSA-PSS certificate +// and a self-signed v1 certificate with the second leaf's subject. +const ROOT2 = `-----BEGIN CERTIFICATE----- +MIIEITCCAwmgAwIBAgIIASNFZ4mrze8wDQYJKoZIhvcNAQELBQAwgZwxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp +c2NvMRQwEgYDVQQKDAtFeGFtcGxlIE9yZzERMA8GA1UECwwIRGV2IFRlYW0xGDAW +BgNVBAMMD0V4YW1wbGUgUm9vdCBDQTEdMBsGCSqGSIb3DQEJARYOY2FAZXhhbXBs +ZS5jb20wIBcNMjYxMDIxMTYzMzUyWhgPMjEyNjA5MjcxNjMzNTJaMIGcMQswCQYD +VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j +aXNjbzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxETAPBgNVBAsMCERldiBUZWFtMRgw +FgYDVQQDDA9FeGFtcGxlIFJvb3QgQ0ExHTAbBgkqhkiG9w0BCQEWDmNhQGV4YW1w +bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YGcCr0pxR8d +6qDdLuILcoT7vhtAjyHwfkoChnH6N3TLWzDUugnBp9NbDb7gDLqbRT4uTDoNb6zS +tEJZ4Q7/hhd0jDvoPmOy0Y3Kyj9uNsdf25GdbvwwEnlddELJCZprTZ4OhEg6w2s0 +Yy4SlviLzDXLmsggTypxeS+K8JHT9onMipi5L4GFu8n1QZbzXvSS4BkRgMXEyrtr +FOE9qSo2qkubFyd/WNKRTRIpTSc5/GGsqJ0FKDDJnN5adotcUucbJFn4noNnFUWh +M8mhBEj7ENDdJsD2EVFsI6H958jA2SbNNu4ngzAOs2AKNBYBfQK02ZQAdRNOSL3K +5FkfbII5LwIDAQABo2MwYTAdBgNVHQ4EFgQUP+yQyItpNYEls26Jfv+NnciGaK8w +HwYDVR0jBBgwFoAUP+yQyItpNYEls26Jfv+NnciGaK8wDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBABDw3Va/C+RFc/Hs +TJnHtQJtxFL0ZhnHUqg4DcDA3m+JDdIDNL8sNpLEbtn/pr0VeEWgG+MImWAQlrUW +IAZkLj9/Zg1wSO0oZ86ZyZJZmp54lUYXgG17CNl4g+F7VHt3bBwUJHTbWye/FKM6 +5OVZetCYKVj7Z+5BYuf7cumzvRqcn32fXidB/kjRX14SJ4JDPn2yFZWCr2ygIqWQ +SOuEmWbNAbYGaVZkQr2Ma8U28DWY2jb0f6nP8jLt49t9o6N/e6Fwd6I5t/mx9Mfj +0403omkxmr9vqN2kBamzVnun+2mDkRZcKPCr72Q4sfbvmDi/W5Nf1Iu2cQW8VfDO +SqpyT54= +-----END CERTIFICATE----- +`; +const LEAF2 = `-----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----- +`; +const PSS = `-----BEGIN CERTIFICATE----- +MIIDdTCCAiigAwIBAgICAIAwQgYJKoZIhvcNAQEKMDWgDzANBglghkgBZQMEAgEF +AKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIEAgIA3jAeMQwwCgYDVQQD +DANwc3MxDjAMBgNVBAoMBWN0bAF4MB4XDTI2MTAyMTE2NDQzM1oXDTI2MTEyMDE2 +NDQzM1owHjEMMAoGA1UEAwwDcHNzMQ4wDAYDVQQKDAVjdGwBeDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL2pno8Ybwihdv3hnMhWPPt85SXQmFAJpFoS +E+sT9H6FbwFCIyPkY+SWk8H7y8V+D+388tNvJZmGjX3uvWpK2Wx6anSB3/WMpkUR +2ExP4cpSxZNr9EFzC/cdqLvsxJcHMHkc/uJuFbtjsuiH1ygOe3nyhWSNiPwGdwHE +cKkBFkzW3j2C/4Jkgt4k+sMe3gy5litKtMi4zZB8ixCm5oGhveGhUZy0Kb01twgZ +RP4FEd32sSRqq3NCx4cB0kyreTuzmk6U7BkxcdPzaj6DifjRznTQmjMdoCNZQRCj +G3KBOMHHyvmV2B4lNGDuMlcSap/TpYq7n32AH5sDvELrUnPwVzsCAwEAAaNTMFEw +HQYDVR0OBBYEFC/QJvAQsbQ25sErc1To2G1VuNZ0MB8GA1UdIwQYMBaAFC/QJvAQ +sbQ25sErc1To2G1VuNZ0MA8GA1UdEwEB/wQFMAMBAf8wQgYJKoZIhvcNAQEKMDWg +DzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIE +AgIA3gOCAQEAu3lP2Q3Bb5fWUQtgFr/82Oke1ZRW/GRbj03mZ30izADHC7yk0OYm +02f7B5sub04Jk6yuRVmEBjJHSPNwAo0RJWtwvD77KLhG8aObr7Z3+pkfFvpGzqeS +MQ4FtK2MPps3kA1a5D4XLwkwDJtHWZVuVyXpPgxkptfLzZgrieKrpNyOKR8Pz/PO +Wlwv6bF7SXvAV8Vrd7KDvTqwQ5iq9UFhP6GeMDWezYtzLdjlvje7MhmZ96zgSY6m +qumNWOPHmiN4eoOE4M69B+0fa3WyMzubW0nVGM/sDhtS0k5zHs6Y4RLn11HolhXH +LvbvDgqtl/m5i0uHxEld+Z1uhzJscqFQRg== +-----END CERTIFICATE----- +`; +const V1 = `-----BEGIN CERTIFICATE----- +MIIBSTCB7wIUI3kYI5NpR07YqZe6RwOx22je9IowCgYIKoZIzj0EAwIwJzERMA8G +A1UECgwITGVhZiBPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yNjEwMjExNjMz +NTJaFw0yNjExMjAxNjMzNTJaMCcxETAPBgNVBAoMCExlYWYgT3JnMRIwEAYDVQQD +DAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASVX1cvRB/1R0Lk +aqRRZ15NBdNJ3A4begD4FTw6MmyvBz5XDOE+R9cgDjB55R9vSytY7TKlNWgDookE +cz3Qr6qLMAoGCCqGSM49BAMCA0kAMEYCIQCFfFOw9qzzfSOQO1zRnT0sOXibjLWl +hHDyqecxhY7gtQIhAPMgqg+p1JXV5bgGpqgDfP3sE53luxMGlnAC39uhwBUh +-----END CERTIFICATE----- +`; + +const ROOT_KEY = `-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDCx+J26mddByfw +9QVdoIpFjDxcojUZSfb1Tn8hQaMCclVpGZwHoE9OZuVsq3jLuAu9TiHUfG7KnHwV +lJnfR5k5LWLHKvKYO2EbnNQerNeTnMAaw29l5eM3djFML29fkKlD1TPaL5ss6Lc8 +mwSrIpVn4YZP1+khoAsbXVitaZdNSSj9rQAoai3TDw79FoyldCmRt7JMEXp/ikc0 +yrFAbL4qWkL/7Ign2mxPqrJTfccXdZr4M7L7S3EAURA3D2bWPtp04udTTMGmQz9u +c/Z9LDEyQMHYOLY/kfo+R2otPjMF/8Et2DtaqnHFKqqBWZpJeESpRDrGN/nXItHf +DL/EFaExAgMBAAECggEADsvlabgryvbYs2bBlhe0eU0KZtURqI6kWoUwhiO40N5D +m7zMzA2DCMcrRYKPJhagmaxeSIh9WBnkGTWVLM+BFcVbXBrom+Ti3xieSohBxh5h +v4a6Jyj8BkINw6ZU1RMlsJf5HOmB5br19H4oMCHr7JzGubkCpkHEpjsEmgPL8Rxs +LYb9palDyTD1z5xVAQDAkvnCYQMX2DsiH8RuO4Ih0OoWMIsAGasS1dTQCkOzN9wF +cMg+hmJvQLN5xLYI+VPy/iZn+bmerCx8/NQBrbo+zibmnv7cDK1U9emVD70/Nd1d +B7ZEm+a4gReaxWVQGEkFMvs29u/TDWXYRnw0PGjUYwKBgQDrmKEx+ZiZwNNk/S3l +s8FgVrLFDr/+t5X33jaD5nVuSN6/AoR3qfh9piIyCrdNKdHicAcCnWP6skUFYLq3 +/tyMBATX3j+f3PHW3i2ZUAK26hgfp66F/3IyowePZbBDlGSmGsffVlc8on5fCk1Q +k3G0znktbrMCNxeeY9GSi1CHewKBgQDTpled6pZepZ54OGnKVJFo6DbQC0l4HQ6y +Rwv9BMtAz+AxWXda1oYK5bbJZrCmHyVP85VxkzRHOMxicv7M2kR9y2zZsJD7INqr +Nvt+lo2yGtIGv7kOdwWzvs4Pn7aCWSApzYBjuKrpP1xDzaMHhusAQtkk77YGCZCu +cCAQ/FjEQwKBgBq/1KWI8lOauiXAPULUWwNmNJv9Jb10+LkQ6N2m//RKn8fOMF4p +1UXm5AEMiG1hbywnjhQ07JIdRYrrxad4b3ASUL51xIGRI4BC76h1lilQgPavIjii +nUO0kPouEIpLwfW+Gq137yW6SWDfGZIpe4KM4uNOq1J4EN1x9eyOltEbAoGAP4bj +L5QVFE2T1jPglQlzJ9DjeuiYYcQfACXRFF3XABolCcr3xz0yuJygk60OAp7AnVHY +nmF3xJToy/gGQMiW3Ect4d1cdvQyjkePjzxzRW1Fcx8i1ZOmE4CG6yueJVvJCqMT +EVuBeUc6U2Oqtnyplsd509tbgE5oreMbzN1ha28CgYAHpG10ZQRDeqWFIo/ttk6Q +Q1Gf53eqZFCgh8GO1hBJ7+Fp+Ec5S9UPMBDjZb6GET7ifF8Zit/lR4gN2PuELwJA +YaXTIehCugH7Q3oTjNcQtls6tW+/+jEGjbjSHyZvwk8EFHc0J7PWRszB36s+qM+O +YPorNipExSeWti1506ljLQ== +-----END PRIVATE KEY----- +`; +const LEAF_KEY = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEINqbifA6kN/DSN0cMD2s7fXiNtPXZqipFrbCs/3mCGbtoAoGCCqGSM49 +AwEHoUQDQgAEYnUoqzSCK8PmB3ktdds9zftZ9PrgPR485EObt0H2bERgnorwhDEE +rOfnFBP57+VlnWtpEFmr8TxpbhxbWJktdA== +-----END EC PRIVATE KEY----- +`; +const PLAIN_KEY = `-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCq01NcNAy9Ng04 +mVuAFvmZUvsmrY7aOaMgtODmptulgZ2qw25E09RgGYCn1S+4IYDu9hH9X4s+28Ag +7Khmj4nmeoXNof5095wche8DR2AAUElqyr/Nmgm2kTLdIJX4wl0eWXHIlmSPdOUN +ovA6JjGKH99b8W7a2TqY/TsSadpPSvvN2esr7Jr4KgW7MXPUOnwNbcBOVcQP9rtF +rduAWz01q0HM02LboclnUnk/e58BXAj8vgdVojljDcc0bhbdz/Hh1/5gaaNILjJN +Od4AgwZaCT02KojGz4Nb6MSgFmON2iMgAZjY1fXXkJ7wsfVPytjW73e7uV/RFuqS +HQZenxupAgMBAAECggEAAxY5ACZHoaanpAFvbBfeMEVV2PvTZEGc30MtejWDLBE0 +yGhqJm++e3MShlXdnCHs2sSTDCVNFphxdxbXryzCVOobX10hRDtSNJi9+izwwOiy +fu3FfAFf0g77IJBNbXaTqK/8lJ+VFxuJZQ7v8NkOVZzgVCC/fFBnFOskhV9ozLx6 +Uk+5Rg/UFenUhhHqrzwxCLKR3TAX8pBdidmbJF61ypxt6Cn3pOA/pB1gUBi76nau +w4kefdNj8AXzSjzOfVEg0quywfeoOq6ESdy3p4RQMt6NZ3wnCgzr/uuWjVQzJbJU +Mg2TVlrKzbiinel2ZqKV8lpymhFTlRaWNzK7PzD91QKBgQDaSDpdQIk2+CJMAVzU +LsI5tdZa0cMDbAZzxBmg1W6kwIrMzYVeMo7RPBarhCuQ/3gJFVvA7LdvXR87BbYa +UuDXbvO4Gv+2wqxxX961sWK85VqhwAqXhFbkS9KNJ0twmlmAcX9lvl0Zn4ciO3c+ +AMJT4JN4WR6wI/dt1TCHLa1wtQKBgQDIV9bgKinpUzhSy9byTH0CwghwudAPx2CA +9gJ6VcjyUKeBdgJ0PQMTwppyrPs0fz+NgJ6+LiTvp9BS9vrXmtnjbD1oAxFcC9uU +NL7BJSRY+wgmnPgbldA25xq1TRJmB8YMr1CO4gYKGSNM7I1HlNv4dvMcS295RhTG +cdDkl4T7pQKBgQCzJmygdIMplmWvs76Ydvf5nYAcceR4PoTdJqlSE1oH5e8FP/ih +4yTMHWENzCBllQbTjbAMmzvCLs+fBYT7/eGDbxCgnagaO5SESIbk4H2FQ0grUQ5g +ZMFcBc79Aq8r78JdZMmGm8KVcRTbx6qKPN1BMiRWte416o78meiGSbr7sQKBgQCJ +2AY8TkZwB5ZlQWMva3lIAIEWLoL9mEzJ7npd+dA9CUl3Rd8CY0hOvELQTDy7LiJo +FesjJ1S6oFgx/UUyO5VXaQ4HFrVHA9Gc832cHxYXMwG0r6P2NuoAHJq/y7uxAABA +kg93gARvxkQHlWrPqZiAKDe1bCHS62k38O+tmKMjEQKBgHF7a9tqZVoc8CqUL/I8 +FXRMRmdzC4RoWC2NDqMazW9qxXpFZmPux1q9ds6ikoG0v3pAxZmRH0eMTIujtLPn +chtAnvTvkg5Q+/xa429PFThJ2b04zsHCmnzfdMaIPvqhBqoPmyAmptjUeMB+yzQU +d3AigXwTBk4l4yPleDD4qUbO +-----END PRIVATE KEY----- +`; +const ED_KEY = `-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEID8VP7HLPAW9FXkVxo3eYbqZ8zkXsPFi3c2RwdDdwy/O +-----END PRIVATE KEY----- +`; + +const certs: [string, X509Certificate][] = [ + ["root", new X509Certificate(ROOT)], + ["leaf", new X509Certificate(Buffer.from(LEAF))], + ["plain", new X509Certificate(PLAIN)], + ["ed", new X509Certificate(ED)], + ["root2", new X509Certificate(ROOT2)], + ["leaf2", new X509Certificate(LEAF2)], + ["pss", new X509Certificate(PSS)], + ["v1", new X509Certificate(V1)], +]; + +function describe(key: KeyObject): string { + const details = key.asymmetricKeyDetails ?? {}; + const size = key.asymmetricKeyType === "ed25519" ? "32 bytes" : `${details.modulusLength ?? details.namedCurve}`; + return `${key.type} ${key.asymmetricKeyType} ${size}`; +} + +for (const [name, cert] of certs) { + const key = cert.publicKey; + const spki = key.export({ type: "spki", format: "der" }); + console.log(name, describe(key), key === cert.publicKey, spki.length, createPublicKey(cert.toString()).equals(key)); +} + +for (const [name, cert] of certs) { + const by: string[] = []; + const issued: string[] = []; + for (const [other, signer] of certs) { + if (cert.verify(signer.publicKey)) by.push(other); + if (cert.checkIssued(signer)) issued.push(other); + } + console.log(name, "verifies with", by.join(",") || "-", "issued by", issued.join(",") || "-"); +} + +const keys: [string, KeyObject][] = [ + ["root", createPrivateKey(ROOT_KEY)], + ["leaf", createPrivateKey(Buffer.from(LEAF_KEY))], + ["plain", createPrivateKey({ key: PLAIN_KEY, format: "pem" })], + ["ed", createPrivateKey(ED_KEY)], + ["fresh", generateKeyPairSync("ec", { namedCurve: "prime256v1" }).privateKey], +]; +for (const [name, cert] of certs) { + console.log(name, "private", keys.filter(([, key]) => cert.checkPrivateKey(key)).map(([k]) => k).join(",") || "-"); +} + +const leaf = certs[1]![1]; +function attempt(label: string, f: () => boolean): void { + try { + console.log(label, f()); + } catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(label, err.name, err.code, err.message); + } +} +attempt("verify private", () => leaf.verify(keys[0]![1])); +attempt("verify secret", () => leaf.verify(createSecretKey(Buffer.alloc(16)))); +attempt("check public", () => leaf.checkPrivateKey(leaf.publicKey)); +attempt("check secret", () => leaf.checkPrivateKey(createSecretKey(Buffer.alloc(16)))); + +// A PSS signature check on the root's public key leaves no padding behind +// for the certificate checks that follow. +const root = certs[0]![1]; +const message = Buffer.from("pss"); +const pssSignature = sign("sha256", message, { key: keys[0]![1], padding: constants.RSA_PKCS1_PSS_PADDING }); +const pssValid = verify("sha256", message, { key: root.publicKey, padding: constants.RSA_PKCS1_PSS_PADDING }, pssSignature); +console.log("after pss", pssValid, root.verify(root.publicKey), leaf.verify(root.publicKey)); + +// An outer signatureAlgorithm that differs from the TBSCertificate's, here +// without its NULL parameters, does not verify. +function header(der: Buffer, at: number): [number, number] { + const first = der[at + 1]!; + if (first < 0x80) return [2, first]; + let length = 0; + for (let i = 0; i < (first & 0x7f); i++) length = length * 256 + der[at + 2 + i]!; + return [2 + (first & 0x7f), length]; +} +const rootDer = root.raw; +const [outerHeader] = header(rootDer, 0); +const [tbsHeader, tbsLength] = header(rootDer, outerHeader); +const algAt = outerHeader + tbsHeader + tbsLength; +const [algHeader, algLength] = header(rootDer, algAt); +const algorithm = rootDer.subarray(algAt, algAt + algHeader + algLength); +const oid = algorithm.subarray(2, 2 + 2 + algorithm[3]!); +const bare = Buffer.concat([Buffer.from([0x30, oid.length]), oid]); +const body = Buffer.concat([rootDer.subarray(outerHeader, algAt), bare, rootDer.subarray(algAt + algHeader + algLength)]); +const altered = new X509Certificate(Buffer.concat([Buffer.from([0x30, 0x82, body.length >> 8, body.length & 0xff]), body])); +console.log("outer algorithm", algorithm.toString("hex"), "->", bare.toString("hex"), altered.subject.split("\n").join("/"), altered.verify(root.publicKey)); diff --git a/tests/corpus/7092-x509-legacy-object.ts b/tests/corpus/7092-x509-legacy-object.ts new file mode 100644 index 000000000..6b423860a --- /dev/null +++ b/tests/corpus/7092-x509-legacy-object.ts @@ -0,0 +1,158 @@ +// X509Certificate.toLegacyObject and toString match Node: the +// legacy object's members in Node's order (subject and issuer objects +// with repeated attributes as arrays, alternative names, access +// descriptions by method, the RSA modulus, exponent and SPKI or the EC +// point and curve names, the validity window, fingerprints, extended key +// usages, serial number and raw bytes), its inspect output, and the PEM +// text of toString, String() and template literals. +import { X509Certificate } from "node:crypto"; + +// An EC leaf with every alternative name kind, and the RSA root that signed it. +const LEAF = `-----BEGIN CERTIFICATE----- +MIIDqjCCApKgAwIBAgICCwswDQYJKoZIhvcNAQELBQAwPjELMAkGA1UEBhMCVVMx +FDASBgNVBAoMC0V4YW1wbGUgT3JnMRkwFwYDVQQDDBBFeGFtcGxlIFJTQSBSb290 +MCAXDTI2MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjBfMQswCQYDVQQGEwJV +UzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNv +bTEgMB4GCSqGSIb3DQEJARYRYWRtaW5AZXhhbXBsZS5jb20wWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARidSirNIIrw+YHeS112z3N+1n0+uA9HjzkQ5u3QfZsRGCe +ivCEMQSs5+cUE/nv5WWda2kQWavxPGluHFtYmS10o4IBWDCCAVQwCQYDVR0TBAIw +ADALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHsG +A1UdEQR0MHKCDSouZXhhbXBsZS5jb22CC2V4YW1wbGUuY29tghV4bi0tYmNoZXIt +a3ZhLmV4YW1wbGWBD29wc0BleGFtcGxlLmNvbYcECgAAAYcQIAENuAAAAAAAAAAA +AAAAAYYUaHR0cHM6Ly9leGFtcGxlLmNvbS8wXgYIKwYBBQUHAQEEUjBQMCQGCCsG +AQUFBzABhhhodHRwOi8vb2NzcC5leGFtcGxlLmNvbS8wKAYIKwYBBQUHMAKGHGh0 +dHA6Ly9jYS5leGFtcGxlLmNvbS9jYS5jcnQwHQYDVR0OBBYEFHyUnRTDwZxUPVwZ +RKmse3fjoeMMMB8GA1UdIwQYMBaAFAgaWVveRbA7VKqcHzjMqBPzi1/LMA0GCSqG +SIb3DQEBCwUAA4IBAQAfMTcpidoH47PXOQVxRttfrG7K9BcntyXP4+5hUAoSfMr6 +Rxy7X/9ba7R0yCGlNsUCusObaBpbdvwX8v9TiTYKVL+C9rEVGrTHnh6kbgZaaCQc +L3oWbfRrYnEBYuSlFuadjVvX42aqzHV28E4ITgywCxwubu2hAbacbcUHpnRSqLDq +lV+VZSQA/6cPNct7PR6AkJU80D+5wVOk7RVi4rsfbw1wFDeksCHu+0EThs9zjzL+ +S7HTCjB6AOHpbu1Sx+0d5YEKSE7EPF+dSFATNbhjTHzYQySnHK+Lm8nON8wmY1rC +7opOhvUlid1jA1d2BKO2ZxlxLir4pyIehTC1ddG2 +-----END CERTIFICATE----- +`; +const ROOT = `-----BEGIN CERTIFICATE----- +MIIDPTCCAiWgAwIBAgIDChHOMA0GCSqGSIb3DQEBCwUAMD4xCzAJBgNVBAYTAlVT +MRQwEgYDVQQKDAtFeGFtcGxlIE9yZzEZMBcGA1UEAwwQRXhhbXBsZSBSU0EgUm9v +dDAgFw0yNjEwMjIwOTQ2NDFaGA8yMTI2MDkyODA5NDY0MVowPjELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0V4YW1wbGUgT3JnMRkwFwYDVQQDDBBFeGFtcGxlIFJTQSBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwsfidupnXQcn8PUF +XaCKRYw8XKI1GUn29U5/IUGjAnJVaRmcB6BPTmblbKt4y7gLvU4h1Hxuypx8FZSZ +30eZOS1ixyrymDthG5zUHqzXk5zAGsNvZeXjN3YxTC9vX5CpQ9Uz2i+bLOi3PJsE +qyKVZ+GGT9fpIaALG11YrWmXTUko/a0AKGot0w8O/RaMpXQpkbeyTBF6f4pHNMqx +QGy+KlpC/+yIJ9psT6qyU33HF3Wa+DOy+0txAFEQNw9m1j7adOLnU0zBpkM/bnP2 +fSwxMkDB2Di2P5H6PkdqLT4zBf/BLdg7WqpxxSqqgVmaSXhEqUQ6xjf51yLR3wy/ +xBWhMQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQUCBpZW95FsDtUqpwfOMyoE/OLX8swDQYJKoZIhvcNAQELBQADggEB +AChUtpNi8dPPxA4sf4EpZhEcYBN53X3WUcG3KJGtSf8JbPKru9lK9O+sZtJADbph +WoMPdWh8g+fEMk5D6l8Y5pDk/OR++oWNY7XCLFNyigmUGn+4SopJynBQiDk2dYPV +b6swDPPiV85TuAHVn79l766rtFdHdYwRe5HvfEkiXax7Egonlx7NR73xxm/XgvHQ +jhKbTZN9k/DEjkQuo5rt9ldVupUMM0iMhDMKX5oWEx9nvRwHPbjESS+Shy/FA4nA +2WHWO++4IU+imO40an1B/8SmFF+X9hj3UE6qaZLynVZRUiouBZibwZCcdCpibFM3 +kmB2s+NZXRvzhCjIxO97Ib4= +-----END CERTIFICATE----- +`; +// An EC leaf with access descriptions and extended key usages. +const ACCESS = `-----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----- +`; +// A multi-valued RDN, an Ed25519 key and names that need escaping. +const MULTI = `-----BEGIN CERTIFICATE----- +MIICNzCCAaCgAwIBAgIBADANBgkqhkiG9w0BAQsFADA3MSgwCgYDVQQLDANyZG4w +DAYDVQQDDAVtdWx0aTAMBgNVBAoMBXRocmVlMQswCQYDVQQGEwJERTAeFw0yNjEw +MjExNjM0MDVaFw0yNjEwMzExNjM0MDVaMDcxKDAKBgNVBAsMA3JkbjAMBgNVBAMM +BW11bHRpMAwGA1UECgwFdGhyZWUxCzAJBgNVBAYTAkRFMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDdv64EOPdpcWdjVMAlEQvNzm3ZbQYXrhOwRF9yzQp25SuA +3uKvStZOt8hyTifAd30oDXIfgyYI39xKHEkHfARTV1E2Jwyiz86/pDHmFypPFJuI +v/vOjYLLeisiVi8TFfHgmoGqGvySR83fwiUnoOOWj9Bqt/5EVBQbBeWrU9qH6QID +AQABo1MwUTAdBgNVHQ4EFgQUZrGU0gkgqCkmegRtu6vQ3h7kpPUwHwYDVR0jBBgw +FoAUZrGU0gkgqCkmegRtu6vQ3h7kpPUwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQsFAAOBgQAMzFkzJElc9VJYhGx7AngO8Yve22iCTOVLO3/be8w55klb/HJX +vfESQz+EJLYVaP5Q/uChoE0I27NytkeUSpJFwmjyqoQq2j6kJWGKONqhQSnXDrms +HMPA7iik/C5hOqpwzDZ98wNU/e86TrqZxzpFkwbnZx9MSfiGr690s5BfpA== +-----END CERTIFICATE----- +`; +const ED = `-----BEGIN CERTIFICATE----- +MIICSDCCAfqgAwIBAgIB+zAFBgMrZXAwLDEVMBMGA1UEAx4MAGIAbQBwACAA6U4t +MRMwEQYDVQQKHgoAcABsAGEAaQBuMB4XDTI2MTAyMTE2NDQzM1oXDTI2MTEyMDE2 +NDQzM1owLDEVMBMGA1UEAx4MAGIAbQBwACAA6U4tMRMwEQYDVQQKHgoAcABsAGEA +aQBuMCowBQYDK2VwAyEAP+2Vcp8nwu2AhFvSczYTxGs1jV/ckA76Jqvwj9QDGqij +ggE/MIIBOzCBtwYDVR0RBIGvMIGsoBsGCisGAQQBgjcUAgOgDQwLdXNlckBkb21h +aW6gEAYDKgMEoAkMB3Vua25vd26kRTBDMQswCQYDVQQGEwJVSzEZMBcGA1UECh4Q +AEQAaQByACwAIABPAHIAZzEZMBcGA1UEAx4QAGQAaQByACAAbgBhAG0AZYgEKgME +BYcQ/oAAAAAAAAAAAAAAAAEAAocQIAENuAAAAAAAAP8AAEKDKYYKaHR0cDovL3gv +cTAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIHgDBFBggrBgEFBQcBAQQ5MDcwIAYI +KwYBBQUHMAWGFGh0dHA6Ly9yZXBvLmV4YW1wbGUvMBMGBCoDBAWCC29kZC5leGFt +cGxlMB0GA1UdDgQWBBQYMJ0fQJ1Nx0PmatIfTJusAEA9WTAFBgMrZXADQQCixubE +nuunBZYYPXF52+joQSX/a5DFkOEDmzRTuH7IIpPFWPo37rmJOSdPA/PV8v9clzYF +LP3EjM4YOAatcbsO +-----END CERTIFICATE----- +`; +const ODD = `-----BEGIN CERTIFICATE----- +MIICXzCCAeWgAwIBAgIBATAKBggqhkjOPQQDAjBvMQ0wCwYDVQQDDARhLCBiMQ4w +DAYDVQQHDAUjbGVhZDESMBAGA1UECAwJICBzcGFjZSAgMSYwJAYDVQQKDB1RdW90 +ZSBxIGJhY2tcc2xhc2g7IHNlbWkgPGx0PjESMBAGA1UECwwJQ2Fmw6kg4oKsMB4X +DTI2MTAyMTE2MzM1MloXDTI3MTAyMTE2MzM1MlowbzENMAsGA1UEAwwEYSwgYjEO +MAwGA1UEBwwFI2xlYWQxEjAQBgNVBAgMCSAgc3BhY2UgIDEmMCQGA1UECgwdUXVv +dGUgcSBiYWNrXHNsYXNoOyBzZW1pIDxsdD4xEjAQBgNVBAsMCUNhZsOpIOKCrDB2 +MBAGByqGSM49AgEGBSuBBAAiA2IABMLLzwDsSeezCzScnC9xTIFl35dFAX+1Uagk +0y4Y72rOOgVFA13SMIbMfv2InIyg2vXOdqn0y7RqiaaqqnAYZB2RHHL0SD7t2MVL +K3BQajy7Oi6IeDDRGK1jM4D6nk/1AKNVMFMwMgYDVR0RBCswKYILYSxiLmV4YW1w +bGWCDXBsYWluLmV4YW1wbGWBBXhAeS56hwQKAAABMB0GA1UdDgQWBBSR4U6nIpTt +5rLoIZZxfxSyx9Uc9zAKBggqhkjOPQQDAgNoADBlAjEAlznFL0Nrt1jjZrT3/GN0 +yN/8gxO3SawyqoWrfP3uk1GScTh0PlrTEOrpSyuq3wl5AjBRntVI1ZoUM46pCwIw +0Vl3c3lns7OljcQFiR7o8yRhEW9ejdiR2W6PLBrjCly6U3I= +-----END CERTIFICATE----- +`; + +const certs: [string, X509Certificate][] = [ + ["leaf", new X509Certificate(LEAF)], + ["root", new X509Certificate(Buffer.from(ROOT))], + ["access", new X509Certificate(ACCESS)], + ["multi", new X509Certificate(MULTI)], + ["ed", new X509Certificate(ED)], + ["odd", new X509Certificate(ODD)], +]; + +for (const [name, cert] of certs) { + const legacy = cert.toLegacyObject(); + const shown = JSON.stringify(legacy, (key: string, value: unknown) => { + if (value === undefined) return ""; + if (key === "raw" || key === "pubkey") return `<${(value as { data: number[] }).data.length} bytes>`; + return value; + }); + console.log(name, shown); + const absent = Object.keys(legacy).filter((key) => (legacy as unknown as Record)[key] === undefined); + console.log(name, "absent", absent.join(","), Buffer.isBuffer(legacy.raw), legacy.raw.equals(cert.raw), legacy.raw === cert.raw); + const spki: Buffer = cert.publicKey.export({ type: "spki", format: "der" }); + if (legacy.pubkey !== undefined) console.log(name, "pubkey", legacy.pubkey.equals(spki)); +} + +console.log(certs[0]![1].toLegacyObject()); +console.log(certs[3]![1].toLegacyObject()); + +for (const [name, cert] of certs) { + const pem = cert.toString(); + console.log(name, pem.length, pem === String(cert), `${cert}` === pem, new X509Certificate(pem).fingerprint256 === cert.fingerprint256); +} +console.log(certs[3]![1].toString()); diff --git a/tests/corpus/7093-x509-methods-js.mjs b/tests/corpus/7093-x509-methods-js.mjs new file mode 100644 index 000000000..a8144d071 --- /dev/null +++ b/tests/corpus/7093-x509-methods-js.mjs @@ -0,0 +1,158 @@ +// X509Certificate's methods from JavaScript match Node: publicKey, +// verify, checkIssued, checkPrivateKey, checkHost, checkEmail, checkIP, +// toLegacyObject, toString and toJSON, including the TypeErrors for +// arguments of the wrong type. +import { X509Certificate, createPrivateKey, createPublicKey, createSecretKey } from "node:crypto"; + +// An RSA root, the EC leaf it signed, and a self-signed RSA certificate. +const ROOT = `-----BEGIN CERTIFICATE----- +MIIDPTCCAiWgAwIBAgIDChHOMA0GCSqGSIb3DQEBCwUAMD4xCzAJBgNVBAYTAlVT +MRQwEgYDVQQKDAtFeGFtcGxlIE9yZzEZMBcGA1UEAwwQRXhhbXBsZSBSU0EgUm9v +dDAgFw0yNjEwMjIwOTQ2NDFaGA8yMTI2MDkyODA5NDY0MVowPjELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0V4YW1wbGUgT3JnMRkwFwYDVQQDDBBFeGFtcGxlIFJTQSBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwsfidupnXQcn8PUF +XaCKRYw8XKI1GUn29U5/IUGjAnJVaRmcB6BPTmblbKt4y7gLvU4h1Hxuypx8FZSZ +30eZOS1ixyrymDthG5zUHqzXk5zAGsNvZeXjN3YxTC9vX5CpQ9Uz2i+bLOi3PJsE +qyKVZ+GGT9fpIaALG11YrWmXTUko/a0AKGot0w8O/RaMpXQpkbeyTBF6f4pHNMqx +QGy+KlpC/+yIJ9psT6qyU33HF3Wa+DOy+0txAFEQNw9m1j7adOLnU0zBpkM/bnP2 +fSwxMkDB2Di2P5H6PkdqLT4zBf/BLdg7WqpxxSqqgVmaSXhEqUQ6xjf51yLR3wy/ +xBWhMQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQUCBpZW95FsDtUqpwfOMyoE/OLX8swDQYJKoZIhvcNAQELBQADggEB +AChUtpNi8dPPxA4sf4EpZhEcYBN53X3WUcG3KJGtSf8JbPKru9lK9O+sZtJADbph +WoMPdWh8g+fEMk5D6l8Y5pDk/OR++oWNY7XCLFNyigmUGn+4SopJynBQiDk2dYPV +b6swDPPiV85TuAHVn79l766rtFdHdYwRe5HvfEkiXax7Egonlx7NR73xxm/XgvHQ +jhKbTZN9k/DEjkQuo5rt9ldVupUMM0iMhDMKX5oWEx9nvRwHPbjESS+Shy/FA4nA +2WHWO++4IU+imO40an1B/8SmFF+X9hj3UE6qaZLynVZRUiouBZibwZCcdCpibFM3 +kmB2s+NZXRvzhCjIxO97Ib4= +-----END CERTIFICATE----- +`; +const LEAF = `-----BEGIN CERTIFICATE----- +MIIDqjCCApKgAwIBAgICCwswDQYJKoZIhvcNAQELBQAwPjELMAkGA1UEBhMCVVMx +FDASBgNVBAoMC0V4YW1wbGUgT3JnMRkwFwYDVQQDDBBFeGFtcGxlIFJTQSBSb290 +MCAXDTI2MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjBfMQswCQYDVQQGEwJV +UzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNv +bTEgMB4GCSqGSIb3DQEJARYRYWRtaW5AZXhhbXBsZS5jb20wWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARidSirNIIrw+YHeS112z3N+1n0+uA9HjzkQ5u3QfZsRGCe +ivCEMQSs5+cUE/nv5WWda2kQWavxPGluHFtYmS10o4IBWDCCAVQwCQYDVR0TBAIw +ADALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMHsG +A1UdEQR0MHKCDSouZXhhbXBsZS5jb22CC2V4YW1wbGUuY29tghV4bi0tYmNoZXIt +a3ZhLmV4YW1wbGWBD29wc0BleGFtcGxlLmNvbYcECgAAAYcQIAENuAAAAAAAAAAA +AAAAAYYUaHR0cHM6Ly9leGFtcGxlLmNvbS8wXgYIKwYBBQUHAQEEUjBQMCQGCCsG +AQUFBzABhhhodHRwOi8vb2NzcC5leGFtcGxlLmNvbS8wKAYIKwYBBQUHMAKGHGh0 +dHA6Ly9jYS5leGFtcGxlLmNvbS9jYS5jcnQwHQYDVR0OBBYEFHyUnRTDwZxUPVwZ +RKmse3fjoeMMMB8GA1UdIwQYMBaAFAgaWVveRbA7VKqcHzjMqBPzi1/LMA0GCSqG +SIb3DQEBCwUAA4IBAQAfMTcpidoH47PXOQVxRttfrG7K9BcntyXP4+5hUAoSfMr6 +Rxy7X/9ba7R0yCGlNsUCusObaBpbdvwX8v9TiTYKVL+C9rEVGrTHnh6kbgZaaCQc +L3oWbfRrYnEBYuSlFuadjVvX42aqzHV28E4ITgywCxwubu2hAbacbcUHpnRSqLDq +lV+VZSQA/6cPNct7PR6AkJU80D+5wVOk7RVi4rsfbw1wFDeksCHu+0EThs9zjzL+ +S7HTCjB6AOHpbu1Sx+0d5YEKSE7EPF+dSFATNbhjTHzYQySnHK+Lm8nON8wmY1rC +7opOhvUlid1jA1d2BKO2ZxlxLir4pyIehTC1ddG2 +-----END CERTIFICATE----- +`; +const PLAIN = `-----BEGIN CERTIFICATE----- +MIIC8jCCAdoCAQcwDQYJKoZIhvcNAQELBQAwPjEaMBgGA1UEAwwRcGxhaW4uZXhh +bXBsZS5vcmcxIDAeBgkqhkiG9w0BCQEWEXBsYWluQGV4YW1wbGUub3JnMCAXDTI2 +MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjA+MRowGAYDVQQDDBFwbGFpbi5l +eGFtcGxlLm9yZzEgMB4GCSqGSIb3DQEJARYRcGxhaW5AZXhhbXBsZS5vcmcwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq01NcNAy9Ng04mVuAFvmZUvsm +rY7aOaMgtODmptulgZ2qw25E09RgGYCn1S+4IYDu9hH9X4s+28Ag7Khmj4nmeoXN +of5095wche8DR2AAUElqyr/Nmgm2kTLdIJX4wl0eWXHIlmSPdOUNovA6JjGKH99b +8W7a2TqY/TsSadpPSvvN2esr7Jr4KgW7MXPUOnwNbcBOVcQP9rtFrduAWz01q0HM +02LboclnUnk/e58BXAj8vgdVojljDcc0bhbdz/Hh1/5gaaNILjJNOd4AgwZaCT02 +KojGz4Nb6MSgFmON2iMgAZjY1fXXkJ7wsfVPytjW73e7uV/RFuqSHQZenxupAgMB +AAEwDQYJKoZIhvcNAQELBQADggEBAJX1DGuiSLoCKBDI3oCgAu7X86G01ovEgZJr +8JrmGHW5q7ujjo53t46h8XPm3LJ8UV9/MZ+Yry43xB2txXYv3b3g/i+7JdTWLgUD +INA5VWvVkEZfm0lBEpU1Agpy4lV5uUCKZTgP/3MY5hnJ0YhQIs4BOjbwueMRFshL +yyHVb73MMbpWaZ7+T368seafa+ekeuY6gQly/HacuszjoYd6KKV3n8eDewenClCF +HoOF1Y4I+CvmR+a6B3iBczJ+a4tceFIIWsZUIeqzvGHpM7G9YwlmbvGprfLgR8Nv +igUib/D9tU3hdL6VWN5nMjdXV/8Le4nDnBjVBjYDkVbsnmT91AQ= +-----END CERTIFICATE----- +`; +const ROOT_KEY = `-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDCx+J26mddByfw +9QVdoIpFjDxcojUZSfb1Tn8hQaMCclVpGZwHoE9OZuVsq3jLuAu9TiHUfG7KnHwV +lJnfR5k5LWLHKvKYO2EbnNQerNeTnMAaw29l5eM3djFML29fkKlD1TPaL5ss6Lc8 +mwSrIpVn4YZP1+khoAsbXVitaZdNSSj9rQAoai3TDw79FoyldCmRt7JMEXp/ikc0 +yrFAbL4qWkL/7Ign2mxPqrJTfccXdZr4M7L7S3EAURA3D2bWPtp04udTTMGmQz9u +c/Z9LDEyQMHYOLY/kfo+R2otPjMF/8Et2DtaqnHFKqqBWZpJeESpRDrGN/nXItHf +DL/EFaExAgMBAAECggEADsvlabgryvbYs2bBlhe0eU0KZtURqI6kWoUwhiO40N5D +m7zMzA2DCMcrRYKPJhagmaxeSIh9WBnkGTWVLM+BFcVbXBrom+Ti3xieSohBxh5h +v4a6Jyj8BkINw6ZU1RMlsJf5HOmB5br19H4oMCHr7JzGubkCpkHEpjsEmgPL8Rxs +LYb9palDyTD1z5xVAQDAkvnCYQMX2DsiH8RuO4Ih0OoWMIsAGasS1dTQCkOzN9wF +cMg+hmJvQLN5xLYI+VPy/iZn+bmerCx8/NQBrbo+zibmnv7cDK1U9emVD70/Nd1d +B7ZEm+a4gReaxWVQGEkFMvs29u/TDWXYRnw0PGjUYwKBgQDrmKEx+ZiZwNNk/S3l +s8FgVrLFDr/+t5X33jaD5nVuSN6/AoR3qfh9piIyCrdNKdHicAcCnWP6skUFYLq3 +/tyMBATX3j+f3PHW3i2ZUAK26hgfp66F/3IyowePZbBDlGSmGsffVlc8on5fCk1Q +k3G0znktbrMCNxeeY9GSi1CHewKBgQDTpled6pZepZ54OGnKVJFo6DbQC0l4HQ6y +Rwv9BMtAz+AxWXda1oYK5bbJZrCmHyVP85VxkzRHOMxicv7M2kR9y2zZsJD7INqr +Nvt+lo2yGtIGv7kOdwWzvs4Pn7aCWSApzYBjuKrpP1xDzaMHhusAQtkk77YGCZCu +cCAQ/FjEQwKBgBq/1KWI8lOauiXAPULUWwNmNJv9Jb10+LkQ6N2m//RKn8fOMF4p +1UXm5AEMiG1hbywnjhQ07JIdRYrrxad4b3ASUL51xIGRI4BC76h1lilQgPavIjii +nUO0kPouEIpLwfW+Gq137yW6SWDfGZIpe4KM4uNOq1J4EN1x9eyOltEbAoGAP4bj +L5QVFE2T1jPglQlzJ9DjeuiYYcQfACXRFF3XABolCcr3xz0yuJygk60OAp7AnVHY +nmF3xJToy/gGQMiW3Ect4d1cdvQyjkePjzxzRW1Fcx8i1ZOmE4CG6yueJVvJCqMT +EVuBeUc6U2Oqtnyplsd509tbgE5oreMbzN1ha28CgYAHpG10ZQRDeqWFIo/ttk6Q +Q1Gf53eqZFCgh8GO1hBJ7+Fp+Ec5S9UPMBDjZb6GET7ifF8Zit/lR4gN2PuELwJA +YaXTIehCugH7Q3oTjNcQtls6tW+/+jEGjbjSHyZvwk8EFHc0J7PWRszB36s+qM+O +YPorNipExSeWti1506ljLQ== +-----END PRIVATE KEY----- +`; +const LEAF_KEY = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEINqbifA6kN/DSN0cMD2s7fXiNtPXZqipFrbCs/3mCGbtoAoGCCqGSM49 +AwEHoUQDQgAEYnUoqzSCK8PmB3ktdds9zftZ9PrgPR485EObt0H2bERgnorwhDEE +rOfnFBP57+VlnWtpEFmr8TxpbhxbWJktdA== +-----END EC PRIVATE KEY----- +`; + +const ca = new X509Certificate(ROOT); +const leaf = new X509Certificate(Buffer.from(LEAF)); +const plain = new X509Certificate(PLAIN); +const caKey = createPrivateKey(ROOT_KEY); +const leafKey = createPrivateKey(LEAF_KEY); +function t(label, f) { + try { + const v = f(); + console.log(label, typeof v === "string" ? JSON.stringify(v) : v); + } catch (e) { + console.log(label, "THROW", e.name, e.code, e.message); + } +} +t("pub", () => [leaf.publicKey.asymmetricKeyType, ca.publicKey.asymmetricKeyType, ca.publicKey.asymmetricKeyDetails.modulusLength]); +t("pub same", () => leaf.publicKey === leaf.publicKey); +t("pub equals", () => leaf.publicKey.equals(createPublicKey(leafKey))); +t("verify ca", () => leaf.verify(ca.publicKey)); +t("verify self", () => leaf.verify(leaf.publicKey)); +t("verify ca self", () => ca.verify(ca.publicKey)); +t("verify private", () => leaf.verify(caKey)); +t("verify string", () => leaf.verify("x")); +t("verify secret", () => leaf.verify(createSecretKey(Buffer.alloc(16)))); +t("issued", () => [leaf.checkIssued(ca), ca.checkIssued(leaf), ca.checkIssued(ca), plain.checkIssued(plain), leaf.checkIssued(plain)]); +t("issued bad", () => leaf.checkIssued({})); +t("pk", () => [leaf.checkPrivateKey(leafKey), leaf.checkPrivateKey(caKey), ca.checkPrivateKey(caKey)]); +t("pk public", () => leaf.checkPrivateKey(leaf.publicKey)); +t("pk string", () => leaf.checkPrivateKey("x")); +for (const h of ["www.example.com", "foo.example.com", "a.b.example.com", "example.com", "EXAMPLE.COM", "xn--bcher-kva.example", "other.org", "10.0.0.1", "", "foo.example.com."]) t("host " + h, () => leaf.checkHost(h)); +t("host plain", () => plain.checkHost("plain.example.org")); +t("host plain never", () => plain.checkHost("plain.example.org", { subject: "never" })); +t("host leaf always", () => leaf.checkHost("www.example.com", { subject: "always" })); +t("host nowild", () => leaf.checkHost("foo.example.com", { wildcards: false })); +t("host partial", () => leaf.checkHost("foo.example.com", { partialWildcards: false })); +t("host multi", () => leaf.checkHost("a.b.example.com", { multiLabelWildcards: true })); +t("host single", () => leaf.checkHost("example.com", { singleLabelSubdomains: true })); +t("host num", () => leaf.checkHost(1)); +t("host opts bad", () => leaf.checkHost("x", { subject: "sometimes" })); +t("host opts type", () => leaf.checkHost("x", 5)); +t("host wild type", () => leaf.checkHost("x", { wildcards: "yes" })); +for (const e of ["ops@example.com", "OPS@EXAMPLE.COM", "admin@example.com", "nobody@example.com"]) t("email " + e, () => leaf.checkEmail(e)); +t("email plain", () => plain.checkEmail("plain@example.org")); +t("email plain never", () => plain.checkEmail("plain@example.org", { subject: "never" })); +t("email num", () => leaf.checkEmail(1)); +for (const ip of ["10.0.0.1", "2001:db8::1", "2001:DB8:0:0:0:0:0:1", "10.0.0.2", "::ffff:10.0.0.1", "bogus", ""]) t("ip " + ip, () => leaf.checkIP(ip)); +t("ip num", () => leaf.checkIP(1)); +t("ip opts", () => leaf.checkIP("10.0.0.1", {})); +for (const cert of [leaf, ca, plain]) { + const legacy = cert.toLegacyObject(); + console.log(Object.keys(legacy).join(","), legacy.subject.CN, legacy.bits, legacy.raw.length); +} +t("pem", () => [leaf.toString() === leaf.toJSON(), String(ca) === `${ca}`, ca.toString().split("\n").length]); +t("pem round trip", () => new X509Certificate(plain.toJSON()).fingerprint === plain.fingerprint); diff --git a/tests/corpus/7095-tls-peer-certificates.ts b/tests/corpus/7095-tls-peer-certificates.ts new file mode 100644 index 000000000..eee3c9ac8 --- /dev/null +++ b/tests/corpus/7095-tls-peer-certificates.ts @@ -0,0 +1,188 @@ +// tlsSocket.getPeerCertificate([detailed]), getCertificate(), +// getPeerX509Certificate() and getX509Certificate() match Node: the +// legacy certificate objects, {} before a handshake or without a +// certificate and null once the socket is destroyed; the detailed +// issuerCertificate chain from the certificates the peer sent and then +// the trust anchors, in any order, ending at a self-issued root that is its +// own issuer; and the certificates served by SNI answers, secureContext +// wraps and https servers. +import { X509Certificate } from "node:crypto"; +import { readFileSync } from "node:fs"; +import * as https from "node:https"; +import * as net from "node:net"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const d = "tests/fixtures/server/certs"; +const read = (f: string): Buffer => readFileSync(`${d}/${f}`); +const ca = read("ca.pem"), ca2 = read("ca2.pem"), cert = read("localhost.pem"), key = read("localhost-key.pem"); +const self = read("selfsigned.pem"), selfKey = read("selfsigned-key.pem"), alt = read("alt.pem"), altKey = read("alt-key.pem"); + +function describe(label: string, o: tls.PeerCertificate | tls.DetailedPeerCertificate | object | null): string { + if (o === null) return `${label} null`; + const keys = Object.keys(o); + if (keys.length === 0) return `${label} {}`; + const p = o as tls.DetailedPeerCertificate; + const issuer = p.issuerCertificate; + const link = issuer ? ` issuerCert:${issuer.subject.CN} self:${issuer === issuer.issuerCertificate} same:${issuer === p}` : ""; + return `${label} ${keys.join(",")} ${p.subject.CN} ${p.issuer.CN}${link}`; +} +function show(label: string, o: tls.PeerCertificate | tls.DetailedPeerCertificate | object | null): void { + console.log(describe(label, o)); +} +// A server's lines print once its client has the reply, so the output +// order does not depend on which side's callback runs first. +const serverLines: string[] = []; +function flushServer(): void { + for (const line of serverLines.splice(0)) console.log(line); +} +await new Promise((done) => { +const server = tls.createServer({ cert, key }, (s) => { + serverLines.push(describe("server peer", s.getPeerCertificate())); + serverLines.push(describe("server peer detailed", s.getPeerCertificate(true))); + serverLines.push(`server peerX509 ${s.getPeerX509Certificate()?.subject}`); + serverLines.push(describe("server own", s.getCertificate())); + serverLines.push(`server ownX509 ${s.getX509Certificate()?.subject}`); + s.end("hi"); +}); +server.listen(0, () => { + const c = tls.connect({ port: (server.address() as AddressInfo).port, ca, servername: "localhost" }, () => { + show("client peer", c.getPeerCertificate()); + const det = c.getPeerCertificate(true); + show("client peer detailed", det); + show("client issuer", det.issuerCertificate); + const x = c.getPeerX509Certificate(); + if (x) console.log("client peerX509", x.subject, x.issuer, x === c.getPeerX509Certificate(), x.checkIssued(new X509Certificate(ca)), x.checkHost("localhost")); + show("client own", c.getCertificate()); + console.log("client ownX509", c.getX509Certificate()?.subject); + console.log("peer raw eq", c.getPeerCertificate().raw.equals(x!.raw), c.getPeerCertificate() === c.getPeerCertificate()); + }); + c.on("data", () => {}); + c.on("end", () => { + flushServer(); + show("after end peer", c.getPeerCertificate()); + c.destroy(); + }); + c.on("close", () => { + show("after close peer", c.getPeerCertificate()); + console.log("after close X509", c.getPeerX509Certificate()?.subject, c.getCertificate()); + server.close(() => done()); + }); + show("before handshake", c.getPeerCertificate()); + console.log("before X509", c.getPeerX509Certificate()?.subject, c.getCertificate(), c.getX509Certificate()?.subject); +}); +}); + +function chain(o: tls.DetailedPeerCertificate | null): string { + if (o === null) return "null"; + if (Object.keys(o).length === 0) return "{}"; + const names: string[] = []; + let at: tls.DetailedPeerCertificate | undefined = o; + for (let i = 0; at !== undefined && i < 6; i++) { + names.push(at.subject.CN as string); + const next: tls.DetailedPeerCertificate | undefined = at.issuerCertificate; + if (next === at) { + names.push("(self)"); + break; + } + at = next; + } + return names.join(" -> "); +} +async function run(label: string, served: Buffer, servedKey: Buffer, trust: Buffer | null): Promise { + await new Promise((done) => { + const server = tls.createServer({ cert: served, key: servedKey }, (s) => s.end()); + server.listen(0, () => { + const port = (server.address() as AddressInfo).port; + const socket = + trust === null + ? tls.connect({ port, servername: "localhost", rejectUnauthorized: false }) + : tls.connect({ port, servername: "localhost", ca: trust, rejectUnauthorized: false }); + socket.on("secureConnect", () => { + console.log(label, socket.authorized, chain(socket.getPeerCertificate(true))); + console.log(label, "abbreviated", "issuerCertificate" in socket.getPeerCertificate(), "issuerCertificate" in socket.getPeerCertificate(false)); + socket.destroy(); + console.log(label, "destroyed", socket.getPeerCertificate(), socket.getPeerX509Certificate()?.subject, socket.getCertificate()); + server.close(() => done()); + }); + socket.on("error", (e) => console.log(label, "error", e.message)); + }); + }); +} +await run("leaf", cert, key, ca); +await run("sent chain", Buffer.concat([cert, ca]), key, ca); +await run("sent chain, default store", Buffer.concat([cert, ca]), key, null); +await run("out of order", Buffer.concat([cert, ca2, ca]), key, ca); +await run("self-signed", self, selfKey, null); +await run("wrong ca", cert, key, ca2); +await run("leaf, default store", cert, key, null); + +// SNI and secureContext: the certificate the server socket serves. +await new Promise((done) => { + const altCtx = tls.createSecureContext({ cert: alt, key: altKey }); + const server = tls.createServer({ cert, key, SNICallback: (name: string, cb: (err: Error | null, ctx?: tls.SecureContext) => void) => cb(null, name === "alt.localhost" ? altCtx : undefined) }, (s) => { + serverLines.push(`sni own ${(s.getCertificate() as tls.PeerCertificate).subject.CN} ${s.getX509Certificate()?.subject}`); + s.end("x"); + }); + server.listen(0, () => { + const port = (server.address() as AddressInfo).port; + const a = tls.connect({ port, ca, servername: "alt.localhost" }, () => { + console.log("sni peer", a.getPeerCertificate().subject.CN, a.getPeerX509Certificate()?.subjectAltName); + }); + a.resume(); + a.on("end", () => { + flushServer(); + a.destroy(); + const b = tls.connect({ port, ca, servername: "localhost" }, () => { + console.log("sni default peer", b.getPeerCertificate().subject.CN); + }); + b.resume(); + b.on("end", () => { + flushServer(); + b.destroy(); + server.close(() => done()); + }); + }); + }); +}); + +// new tls.TLSSocket over a net connection, with a secureContext. +await new Promise((done) => { + const context = tls.createSecureContext({ cert: alt, key: altKey }); + const server = net.createServer((raw) => { + const secure = new tls.TLSSocket(raw, { isServer: true, secureContext: context }); + console.log("wrap before", (secure.getCertificate() as tls.PeerCertificate).subject?.CN, secure.getX509Certificate()?.subject, JSON.stringify(secure.getPeerCertificate())); + secure.on("secure", () => { + serverLines.push(`wrap secure ${(secure.getCertificate() as tls.PeerCertificate).subject.CN} ${secure.getPeerX509Certificate()?.subject}`); + secure.end("x"); + }); + }); + server.listen(0, () => { + const c = tls.connect({ port: (server.address() as AddressInfo).port, ca, servername: "alt.localhost" }, () => { + console.log("wrap client", chain(c.getPeerCertificate(true))); + }); + c.resume(); + c.on("end", () => { + flushServer(); + c.destroy(); + server.close(() => done()); + }); + }); +}); + +// https: both sides of a request. +await new Promise((done) => { + const server = https.createServer({ cert, key }, (req, res) => { + const s = req.socket as tls.TLSSocket; + console.log("https server", (s.getCertificate() as tls.PeerCertificate).subject.CN, JSON.stringify(s.getPeerCertificate())); + res.end("ok"); + }); + server.listen(0, () => { + https.get({ port: (server.address() as AddressInfo).port, ca, host: "localhost", path: "/" }, (res) => { + const s = res.socket as tls.TLSSocket; + console.log("https client", chain(s.getPeerCertificate(true)), s.getPeerX509Certificate()?.issuer); + res.resume(); + res.on("end", () => server.close(() => done())); + }); + }); +}); diff --git a/tests/corpus/7096-tls-peer-certificates.mjs b/tests/corpus/7096-tls-peer-certificates.mjs new file mode 100644 index 000000000..ef1bfeb2a --- /dev/null +++ b/tests/corpus/7096-tls-peer-certificates.mjs @@ -0,0 +1,35 @@ +// tlsSocket.getPeerCertificate from JavaScript matches Node: only a +// detailed argument that is true itself adds the issuerCertificate chain, +// and the legacy object is a plain object with a Buffer raw and arrays for +// extended key usages. +import * as tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const d = "tests/fixtures/server/certs"; +const ca = readFileSync(`${d}/ca.pem`); +const cert = readFileSync(`${d}/localhost.pem`); +const key = readFileSync(`${d}/localhost-key.pem`); + +const server = tls.createServer({ cert, key }, (s) => s.end("x")); +server.listen(0, () => { + const c = tls.connect({ port: server.address().port, ca, servername: "localhost" }, () => { + for (const arg of [true, false, 1, "yes", null, undefined, {}]) { + const peer = c.getPeerCertificate(arg); + console.log(String(arg), "issuerCertificate" in peer, peer.issuer.CN); + } + const legacy = c.getPeerCertificate(); + console.log(Object.keys(legacy).length, Buffer.isBuffer(legacy.raw), legacy.ext_key_usage, JSON.stringify(legacy.subject)); + const detailed = c.getPeerCertificate(true); + const root = detailed.issuerCertificate; + console.log(root.subject.CN, root.issuerCertificate === root, root.ca, Buffer.isBuffer(root.raw)); + const x509 = c.getPeerX509Certificate(); + if (x509) console.log(x509.subject, x509.checkHost("localhost"), x509.checkIssued(x509), legacy.raw.equals(x509.raw)); + console.log(c.getCertificate(), c.getX509Certificate() === undefined); + }); + c.resume(); + c.on("end", () => { + c.destroy(); + console.log(c.getPeerCertificate(), c.getPeerX509Certificate() === undefined, c.getCertificate()); + server.close(); + }); +}); diff --git a/tests/corpus/7097-tls-client-certificates.ts b/tests/corpus/7097-tls-client-certificates.ts new file mode 100644 index 000000000..b45560084 --- /dev/null +++ b/tests/corpus/7097-tls-client-certificates.ts @@ -0,0 +1,84 @@ +// Client certificates match Node: tls.createServer's requestCert, +// rejectUnauthorized and ca against clients that send no certificate, a +// valid one, one without the clientAuth purpose and a self-signed one +// (socket.authorized and authorizationError on the server, the peer +// certificate, the client's own certificate), the client's chain completed +// from its ca, and a server that requires a certificate rejecting a client +// without one in TLS 1.3 and TLS 1.2 (the client's events and codes). +import { readFileSync } from "node:fs"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f: string): Buffer => readFileSync(`${D}/${f}`); +const ca = r("ca.pem"), ca2 = r("ca2.pem"), cert = r("localhost.pem"), key = r("localhost-key.pem"); +const client = r("client.pem"), clientKey = r("client-key.pem"); +const self = r("selfsigned.pem"), selfKey = r("selfsigned-key.pem"); + +type Handler = (s: tls.TLSSocket) => void; +const servers: [string, (h: Handler) => tls.Server][] = [ + ["request", (h) => tls.createServer({ cert, key, requestCert: true, rejectUnauthorized: false, ca }, h)], + ["reject", (h) => tls.createServer({ cert, key, requestCert: true, ca }, h)], + ["request-noca", (h) => tls.createServer({ cert, key, requestCert: true, rejectUnauthorized: false }, h)], + ["request-ca2", (h) => tls.createServer({ cert, key, requestCert: true, rejectUnauthorized: false, ca: ca2 }, h)], + // an empty Buffer is an empty trust store; an empty string is no ca + ["request-empty", (h) => tls.createServer({ cert, key, requestCert: true, rejectUnauthorized: false, ca: Buffer.alloc(0) }, h)], + ["request-emptystr", (h) => tls.createServer({ cert, key, requestCert: true, rejectUnauthorized: false, ca: "" }, h)], + ["plain", (h) => tls.createServer({ cert, key }, h)], +]; +const clients: [string, Buffer | null, Buffer | null][] = [ + ["none", null, null], + ["valid", client, clientKey], + ["serverEku", cert, key], + ["self", self, selfKey], +]; +async function run(label: string, make: (h: Handler) => tls.Server, name: string, clientCert: Buffer | null, clientKeyPem: Buffer | null): Promise { + await new Promise((done) => { + const events: string[] = []; + const server = make((s) => { + events.push(`server secureConnection authorized=${s.authorized} error=${s.authorizationError} peer=${(s.getPeerCertificate() as tls.PeerCertificate).subject?.CN} x509=${s.getPeerX509Certificate()?.subject}`); + s.end("ok"); + }); + server.listen(0, () => { + const port = (server.address() as AddressInfo).port; + const c = + clientCert === null || clientKeyPem === null + ? tls.connect({ port, ca, servername: "localhost" }) + : tls.connect({ port, ca, servername: "localhost", cert: clientCert, key: clientKeyPem }); + c.on("secureConnect", () => events.push(`client secureConnect own=${(c.getCertificate() as tls.PeerCertificate | null)?.subject?.CN}`)); + c.on("data", (d: Buffer) => events.push(`client data ${d}`)); + c.on("end", () => events.push("client end")); + c.on("error", (e: NodeJS.ErrnoException) => events.push(`client error ${e.code}`)); + c.on("close", () => { + setTimeout(() => { + console.log(label, name, "|", events.join(" ; ")); + server.close(() => done()); + }, 50); + }); + }); + }); +} +for (const [label, make] of servers) { + for (const [name, c, k] of clients) await run(label, make, name, c, k); +} + +// A server that requires a certificate, per protocol version: TLS 1.3 +// rejects after the client's handshake, TLS 1.2 during it. +for (const maxVersion of ["TLSv1.3", "TLSv1.2"] as const) { + await new Promise((done) => { + const server = tls.createServer({ cert, key, requestCert: true, ca, maxVersion }, (s) => s.end("ok")); + server.listen(0, () => { + const events: string[] = []; + const c = tls.connect({ port: (server.address() as AddressInfo).port, ca, servername: "localhost" }); + c.on("secureConnect", () => events.push("secureConnect")); + c.on("data", () => events.push("data")); + c.on("end", () => events.push("end")); + c.on("error", (e: NodeJS.ErrnoException) => events.push(`error ${e.code}`)); + c.on("close", (hadError: boolean) => { + events.push(`close ${hadError}`); + console.log(maxVersion, events.join(" ; ")); + server.close(() => done()); + }); + }); + }); +} diff --git a/tests/corpus/7098-tls-client-certificates-paths.ts b/tests/corpus/7098-tls-client-certificates-paths.ts new file mode 100644 index 000000000..b71e72b6f --- /dev/null +++ b/tests/corpus/7098-tls-client-certificates-paths.ts @@ -0,0 +1,161 @@ +// Client certificates on the other paths match Node: a server's (and a +// SecureContext's) ca completes the certificate chain it sends, as +// OpenSSL's automatic chain does; new tls.TLSSocket with requestCert and +// rejectUnauthorized, where Node reports no verdict; and https on both +// sides, including an agent that pools sockets per client certificate. +import { readFileSync } from "node:fs"; +import * as http from "node:http"; +import * as https from "node:https"; +import * as net from "node:net"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f: string): Buffer => readFileSync(`${D}/${f}`); +const ca = r("ca.pem"), ca2 = r("ca2.pem"), cert = r("localhost.pem"), key = r("localhost-key.pem"); +const client = r("client.pem"), clientKey = r("client-key.pem"); + +// A server's ca completes the chain it sends. +for (const [label, make] of [ + ["no ca", () => tls.createServer({ cert, key }, (s) => s.end())], + ["ca", () => tls.createServer({ cert, key, ca }, (s) => s.end())], + ["other ca", () => tls.createServer({ cert, key, ca: ca2 }, (s) => s.end())], + // An SNI answer's own ca does not complete its chain. + ["sni ctx ca, server no ca", () => { + const context = tls.createSecureContext({ cert, key, ca }); + return tls.createServer({ cert, key, SNICallback: (name: string, cb: (err: Error | null, ctx?: tls.SecureContext) => void) => cb(null, context) }, (s) => s.end()); + }], +] as [string, () => tls.Server][]) { + await new Promise((done) => { + const server = make(); + server.listen(0, () => { + const c = tls.connect({ port: (server.address() as AddressInfo).port, ca: ca2, servername: "localhost", rejectUnauthorized: false }, () => { + const names: string[] = []; + let p: tls.DetailedPeerCertificate | undefined = c.getPeerCertificate(true); + for (let i = 0; p !== undefined && i < 4; i++) { + names.push(p.subject.CN as string); + if (p.issuerCertificate === p) break; + p = p.issuerCertificate; + } + console.log(label, c.authorizationError, names.join(" -> ")); + c.destroy(); + server.close(() => done()); + }); + }); + }); +} + +// A TLSSocket's secureContext sends the chain its ca completes. +for (const [label, context] of [ + ["wrap ctx ca", tls.createSecureContext({ cert, key, ca })], + ["wrap ctx", tls.createSecureContext({ cert, key })], +] as [string, tls.SecureContext][]) { + await new Promise((done) => { + const server = net.createServer((raw) => { + const s = new tls.TLSSocket(raw, { isServer: true, secureContext: context }); + s.on("secure", () => s.end()); + }); + server.listen(0, () => { + const c = tls.connect({ port: (server.address() as AddressInfo).port, ca: ca2, servername: "localhost", rejectUnauthorized: false }, () => { + const names: string[] = []; + let p: tls.DetailedPeerCertificate | undefined = c.getPeerCertificate(true); + for (let i = 0; p !== undefined && i < 4; i++) { + names.push(p.subject.CN as string); + if (p.issuerCertificate === p) break; + p = p.issuerCertificate; + } + console.log(label, c.authorizationError, names.join(" -> ")); + c.destroy(); + server.close(() => done()); + }); + }); + }); +} + +// new tls.TLSSocket with client certificates: Node reports no verdict there. +for (const [label, wrap] of [ + ["wrap request", (raw: net.Socket) => new tls.TLSSocket(raw, { isServer: true, cert, key, requestCert: true, ca })], + ["wrap reject", (raw: net.Socket) => new tls.TLSSocket(raw, { isServer: true, cert, key, requestCert: true, rejectUnauthorized: true, ca })], +] as [string, (raw: net.Socket) => tls.TLSSocket][]) { + for (const withCert of [false, true]) { + await new Promise((done) => { + const events: string[] = []; + const server = net.createServer((raw) => { + const s = wrap(raw); + s.on("secure", () => { + events.push(`wrap secure authorized=${s.authorized} error=${s.authorizationError} peer=${(s.getPeerCertificate() as tls.PeerCertificate).subject?.CN}`); + s.end("ok"); + }); + s.on("error", (e: NodeJS.ErrnoException) => events.push(`wrap error ${e.code}`)); + }); + server.listen(0, () => { + const port = (server.address() as AddressInfo).port; + const c = withCert + ? tls.connect({ port, ca, servername: "localhost", cert: client, key: clientKey }) + : tls.connect({ port, ca, servername: "localhost" }); + c.on("secureConnect", () => events.push("client secureConnect")); + c.on("data", (d: Buffer) => events.push(`client data ${d}`)); + c.on("error", (e: NodeJS.ErrnoException) => events.push(`client error ${e.code}`)); + c.on("close", () => { + setTimeout(() => { + console.log(label, withCert ? "cert" : "none", "|", events.join(" ; ")); + server.close(() => done()); + }, 50); + }); + }); + }); + } +} + +// https with client certificates, on both sides. +await new Promise((done) => { + const server = https.createServer({ cert, key, requestCert: true, rejectUnauthorized: false, ca }, (req, res) => { + const s = req.socket as tls.TLSSocket; + res.end(`${s.authorized} ${s.authorizationError} ${(s.getPeerCertificate() as tls.PeerCertificate).subject?.CN}`); + }); + server.listen(0, () => { + const port = (server.address() as AddressInfo).port; + https.get({ port, ca, host: "localhost", path: "/", cert: client, key: clientKey }, (res) => { + let body = ""; + res.on("data", (d: Buffer) => (body += d)); + res.on("end", () => { + console.log("https with cert", body); + https.get({ port, ca, host: "localhost", path: "/", agent: false }, (res2) => { + let body2 = ""; + res2.on("data", (d: Buffer) => (body2 += d)); + res2.on("end", () => { + console.log("https without cert", body2); + server.close(() => done()); + }); + }); + }); + }); + }); +}); + +// An agent pools sockets per client certificate. +let connections = 0; +const server = https.createServer({ cert, key, requestCert: true, rejectUnauthorized: false, ca }, (req, res) => { + const s = req.socket as tls.TLSSocket; + res.end(`${s.authorized} ${(s.getPeerCertificate() as tls.PeerCertificate).subject?.CN}`); +}); +server.on("secureConnection", () => connections++); +function get(port: number, withCert: boolean): Promise { + return new Promise((resolve) => { + const done = (res: http.IncomingMessage) => { + let body = ""; + res.on("data", (d: Buffer) => (body += d)); + res.on("end", () => resolve(body)); + }; + if (withCert) https.get({ port, ca, host: "localhost", path: "/", cert: client, key: clientKey }, done); + else https.get({ port, ca, host: "localhost", path: "/" }, done); + }); +} +server.listen(0, async () => { + // the agent keeps one socket per client certificate + const port = (server.address() as AddressInfo).port; + for (const withCert of [true, true, false, false, true]) console.log(withCert, await get(port, withCert)); + console.log("connections", connections); + server.close(); + https.globalAgent.destroy(); +}); diff --git a/tests/corpus/7099-tls-client-certificates.mjs b/tests/corpus/7099-tls-client-certificates.mjs new file mode 100644 index 000000000..0c9eeed2f --- /dev/null +++ b/tests/corpus/7099-tls-client-certificates.mjs @@ -0,0 +1,34 @@ +// Client certificates from JavaScript match Node: a server that requests +// one reads socket.authorized, authorizationError and the peer +// certificate's chain, and a client sends its cert and key. +import * as tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const D = "tests/fixtures/server/certs"; +const ca = readFileSync(`${D}/ca.pem`); +const cert = readFileSync(`${D}/localhost.pem`); +const key = readFileSync(`${D}/localhost-key.pem`); +const clientCert = readFileSync(`${D}/client.pem`); +const clientKey = readFileSync(`${D}/client-key.pem`); + +const lines = []; +const server = tls.createServer({ cert, key, ca, requestCert: true, rejectUnauthorized: false }, (socket) => { + const peer = socket.getPeerCertificate(true); + lines.push(`server ${socket.authorized} ${socket.authorizationError} ${peer.subject?.CN} ${peer.issuerCertificate?.subject.CN} ${peer.ext_key_usage}`); + socket.end("hello"); +}); +server.listen(0, () => { + const port = server.address().port; + const first = tls.connect({ port, ca, servername: "localhost", cert: clientCert, key: clientKey }, () => { + lines.push(`client ${first.authorized} ${first.getCertificate().subject.CN} ${first.getX509Certificate()?.subject}`); + }); + first.resume(); + first.on("end", () => { + const second = tls.connect({ port, ca, servername: "localhost" }, () => lines.push(`anonymous ${second.authorized}`)); + second.resume(); + second.on("end", () => { + for (const line of lines) console.log(line); + server.close(); + }); + }); +}); diff --git a/tests/corpus/7440-tls-server-add-context.ts b/tests/corpus/7440-tls-server-add-context.ts new file mode 100644 index 000000000..3cb29ebb3 --- /dev/null +++ b/tests/corpus/7440-tls-server-add-context.ts @@ -0,0 +1,119 @@ +// tls.Server's addContext and setSecureContext match Node: SNI names +// matched against exact and wildcard patterns (a "*" covers one label, +// case-insensitively, the newest match winning), the default context when +// nothing matches or an SNICallback is set, setSecureContext replacing the +// default context, the argument errors, the chain each context sends +// (completed from the default context's ca), and the store that verifies +// client certificates after each change. +import { readFileSync } from "node:fs"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f: string): Buffer => readFileSync(`${D}/${f}`); +const ca = r("ca.pem"), ca2 = r("ca2.pem"), cert = r("localhost.pem"), key = r("localhost-key.pem"); +const alt = r("alt.pem"), altKey = r("alt-key.pem"), self = r("selfsigned.pem"), selfKey = r("selfsigned-key.pem"); +const client = r("client.pem"), clientKey = r("client-key.pem"); + +const listen = (server: tls.Server): Promise => + new Promise((d) => server.listen(0, () => d((server.address() as AddressInfo).port))); + +function peer(port: number, servername: string): Promise { + return new Promise((res) => { + const c = tls.connect({ port, servername, rejectUnauthorized: false }, () => { + const p = c.getPeerCertificate(); + res(`${p.subject.CN}${p.subject.CN === "localhost" && p.issuer.CN === "localhost" ? "(self)" : ""}`); + c.destroy(); + }); + c.on("error", (e: NodeJS.ErrnoException) => res(`error ${e.code}`)); + }); +} + +function chain(c: tls.TLSSocket): string { + const names: string[] = []; + let p: tls.DetailedPeerCertificate | undefined = c.getPeerCertificate(true); + for (let i = 0; p !== undefined && i < 4; i++) { + names.push(p.subject.CN as string); + if (p.issuerCertificate === p) break; + p = p.issuerCertificate; + } + return names.join(" -> "); +} + +function look(port: number, servername: string): Promise { + return new Promise((res) => { + const c = tls.connect({ port, servername, ca: ca2, rejectUnauthorized: false }, () => { + const line = `${c.authorizationError} ${chain(c)}`; + c.destroy(); + res(line); + }); + }); +} + +function attempt(label: string, run: () => void): void { + try { + run(); + console.log(label, "ok"); + } catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(label, err.name, err.code, err.message.split("\n")[0]); + } +} + +{ + const server = tls.createServer({ cert, key }); + server.addContext("alt.localhost", { cert: alt, key: altKey }); + server.addContext("*.wild.test", tls.createSecureContext({ cert: self, key: selfKey })); + server.addContext("a.b.wild.test", { cert: alt, key: altKey }); + const port = await listen(server); + const names = ["alt.localhost", "ALT.localhost", "x.wild.test", "a.b.wild.test", "c.b.wild.test", "wild.test", ".wild.test", "other", "alt.localhostx"]; + for (const name of names) console.log("sni", name, await peer(port, name)); + attempt("addContext empty", () => server.addContext("", { cert, key })); + attempt("addContext string ctx", () => server.addContext("x.test", "nope" as unknown as tls.SecureContextOptions)); + server.addContext("alt.localhost", { cert: self, key: selfKey }); + console.log("override", await peer(port, "alt.localhost")); + server.setSecureContext({ cert: alt, key: altKey }); + console.log("setSecureContext default", await peer(port, "nomatch"), await peer(port, "alt.localhost")); + attempt("setSecureContext string", () => server.setSecureContext("x" as unknown as tls.SecureContextOptions)); + server.close(); +} + +{ + const server = tls.createServer({ cert, key, SNICallback: (n: string, cb: (err: Error | null, ctx?: tls.SecureContext) => void) => cb(null, undefined) }); + server.addContext("alt.localhost", { cert: alt, key: altKey }); + console.log("with SNICallback", await peer(await listen(server), "alt.localhost")); + server.close(); +} + +{ + const server = tls.createServer({ cert, key, ca }); + server.addContext("alt.localhost", { cert: alt, key: altKey }); + const port = await listen(server); + console.log("server ca, sni", await look(port, "alt.localhost")); + server.setSecureContext({ cert, key }); + console.log("after set without ca, sni", await look(port, "alt.localhost")); + console.log("after set without ca, default", await look(port, "localhost")); + server.close(); +} + +{ + const lines: string[] = []; + const server = tls.createServer({ cert, key, ca, requestCert: true, rejectUnauthorized: false }, (s) => { + lines.push(`server ${s.authorized} ${s.authorizationError}`); + s.end(); + }); + const port = await listen(server); + const once = (servername: string): Promise => + new Promise((res) => { + const c = tls.connect({ port, servername, ca, cert: client, key: clientKey }); + c.on("data", () => {}); + c.on("close", () => res()); + }); + await once("localhost"); + server.setSecureContext({ cert, key, ca: ca2 }); + await once("localhost"); + server.addContext("alt.localhost", { cert: alt, key: altKey, ca }); + await once("alt.localhost"); + console.log(lines.join(" | ")); + server.close(); +} diff --git a/tests/corpus/7441-tls-client-error.ts b/tests/corpus/7441-tls-client-error.ts new file mode 100644 index 000000000..431ea9019 --- /dev/null +++ b/tests/corpus/7441-tls-client-error.ts @@ -0,0 +1,93 @@ +// tls.Server's 'tlsClientError' event matches Node: the error a failed +// handshake reports (OpenSSL's codes and messages for an HTTP or proxy +// request, a client that rejects the certificate, a missing required +// certificate, an unsupported version and no common ALPN protocol; a +// "socket hang up" ECONNRESET for a client that leaves), the socket it +// passes, what the client sees, and the same event on https.Server. +import * as https from "node:https"; +import * as net from "node:net"; +import type { AddressInfo } from "node:net"; +import { readFileSync } from "node:fs"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f: string): Buffer => readFileSync(`${D}/${f}`); +const ca = r("ca.pem"), ca2 = r("ca2.pem"), cert = r("localhost.pem"), key = r("localhost-key.pem"); +const clean = (m: string): string => m.replace(/^[0-9A-F]+:/, "").split(":../")[0]!; + +type Drive = (port: number, client: string[], fin: () => void) => void; + +async function scenario(label: string, make: (h: (s: tls.TLSSocket) => void) => tls.Server, drive: Drive): Promise { + await new Promise((done) => { + const events: string[] = []; + const client: string[] = []; + const server = make((s) => { + events.push("secureConnection"); + s.end("ok"); + }); + server.on("tlsClientError", (err: Error, socket: tls.TLSSocket) => { + events.push(`tlsClientError ${err.name} ${(err as NodeJS.ErrnoException).code} [${clean(err.message)}] destroyed=${socket.destroyed} remote=${typeof socket.remoteAddress}`); + socket.on("close", () => events.push("server socket close")); + }); + server.listen(0, () => { + drive((server.address() as AddressInfo).port, client, () => + setTimeout(() => { + console.log(label, "|", events.join(" ; "), "| client:", client.join(" ; ")); + server.close(() => done()); + }, 100), + ); + }); + }); +} + +const raw = (bytes: Buffer | null, endFirst: boolean): Drive => (port, client, fin) => { + const s = net.connect(port, "127.0.0.1", () => { + if (bytes !== null) s.write(bytes); + if (endFirst) s.end(); + }); + s.on("data", (d: Buffer) => client.push(`data ${d.length}`)); + s.on("end", () => client.push("end")); + s.on("error", (e: NodeJS.ErrnoException) => client.push(`error ${e.code}`)); + s.on("close", (hadError: boolean) => { + client.push(`close ${hadError}`); + fin(); + }); +}; + +const tlsc = (trust: Buffer, maxVersion: "TLSv1.2" | "TLSv1.3", alpn: boolean): Drive => (port, client, fin) => { + const c = alpn + ? tls.connect({ port, servername: "localhost", ca: trust, maxVersion, ALPNProtocols: ["http/1.1"] }) + : tls.connect({ port, servername: "localhost", ca: trust, maxVersion }); + c.on("secureConnect", () => client.push("secureConnect")); + c.on("error", (e: NodeJS.ErrnoException) => client.push(`error ${e.code}`)); + c.on("data", () => {}); + c.on("close", (hadError: boolean) => { + client.push(`close ${hadError}`); + fin(); + }); +}; + +const plain = (h: (s: tls.TLSSocket) => void): tls.Server => tls.createServer({ cert, key }, h); +await scenario("close before hello", plain, raw(null, true)); +await scenario("http request", plain, raw(Buffer.from("GET / HTTP/1.1\r\nHost: x\r\n\r\n"), false)); +await scenario("connect request", plain, raw(Buffer.from("CONNECT x:443 HTTP/1.1\r\n\r\n"), false)); +await scenario("half hello", plain, raw(Buffer.from([0x16, 0x03, 0x01, 0x02, 0x00, 0x01]), true)); +await scenario("client rejects cert", plain, tlsc(ca2, "TLSv1.3", false)); +await scenario("no cert, required", (h) => tls.createServer({ cert, key, requestCert: true, ca }, h), tlsc(ca, "TLSv1.3", false)); +await scenario("version", (h) => tls.createServer({ cert, key, minVersion: "TLSv1.3" }, h), tlsc(ca, "TLSv1.2", false)); +await scenario("alpn", (h) => tls.createServer({ cert, key, ALPNProtocols: ["h2"] }, h), tlsc(ca, "TLSv1.3", true)); +await scenario("ok", plain, tlsc(ca, "TLSv1.3", false)); + +{ + const server = https.createServer({ cert, key }, (req, res) => res.end("ok")); + server.on("tlsClientError", (err: Error, socket: tls.TLSSocket) => console.log("https tlsClientError", (err as NodeJS.ErrnoException).code, socket.destroyed)); + await new Promise((d) => server.listen(0, () => d())); + await new Promise((res) => { + const s = net.connect((server.address() as AddressInfo).port, "127.0.0.1", () => s.write("GET / HTTP/1.1\r\nHost: x\r\n\r\n")); + s.on("data", () => {}); + s.on("error", (e: NodeJS.ErrnoException) => console.log("https client error", e.code)); + s.on("close", () => res()); + }); + await new Promise((d) => setTimeout(d, 50)); + server.close(); +} diff --git a/tests/corpus/7442-tls-server-contexts.mjs b/tests/corpus/7442-tls-server-contexts.mjs new file mode 100644 index 000000000..97f797129 --- /dev/null +++ b/tests/corpus/7442-tls-server-contexts.mjs @@ -0,0 +1,51 @@ +// tls.Server's addContext, setSecureContext and 'tlsClientError' from +// JavaScript match Node: a wildcard SNI context, the default context +// replaced, and the errors a plain-text client and one that leaves +// before its hello report. +import * as net from "node:net"; +import * as tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const D = "tests/fixtures/server/certs"; +const cert = readFileSync(`${D}/localhost.pem`); +const key = readFileSync(`${D}/localhost-key.pem`); +const alt = readFileSync(`${D}/alt.pem`); +const altKey = readFileSync(`${D}/alt-key.pem`); + +const server = tls.createServer({ cert, key }, (socket) => socket.end()); +server.addContext("*.localhost", { cert: alt, key: altKey }); +server.on("tlsClientError", (err, socket) => { + const reason = err.message.replace(/^[0-9A-F]+:/, "").split(":../")[0]; + console.log("tlsClientError", err.code, reason, socket.destroyed); +}); + +function peer(port, servername) { + return new Promise((resolve) => { + const c = tls.connect({ port, servername, rejectUnauthorized: false }, () => { + resolve(c.getPeerCertificate().subject.CN); + c.end(); + }); + }); +} + +function raw(port, bytes) { + return new Promise((resolve) => { + const s = net.connect(port, "127.0.0.1", () => { + if (bytes) s.write(bytes); + else s.end(); + }); + s.on("data", () => {}); + s.on("error", (err) => console.log("client error", err.code)); + s.on("close", () => setTimeout(resolve, 50)); + }); +} + +server.listen(0, async () => { + const { port } = server.address(); + console.log("sni", await peer(port, "alt.localhost"), await peer(port, "localhost")); + server.setSecureContext({ cert: alt, key: altKey }); + console.log("after setSecureContext", await peer(port, "example.test")); + await raw(port, "GET / HTTP/1.1\r\nHost: x\r\n\r\n"); + await raw(port, null); + server.close(); +}); diff --git a/tests/corpus/7443-tls-final-flight.ts b/tests/corpus/7443-tls-final-flight.ts new file mode 100644 index 000000000..b4de4b4d4 --- /dev/null +++ b/tests/corpus/7443-tls-final-flight.ts @@ -0,0 +1,114 @@ +// The last handshake flight is flushed when Node flushes it: after the +// callbacks that see the handshake finish and their nextTick callbacks, +// before the immediates they queue. A destroy() in secureConnect or a tick +// drops a TLS 1.3 client's Finished (the server reports a hang-up), one in +// secureConnection drops a TLS 1.2 server's Finished (the client ends, then +// fails), and data written beside it is dropped too; end() and a destroy +// in an immediate let it go out. A client whose server closes or resets +// mid-handshake gets Node's events, from tls.connect and https.request. +import { readFileSync } from "node:fs"; +import * as https from "node:https"; +import * as net from "node:net"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const cert = readFileSync(`${D}/localhost.pem`), key = readFileSync(`${D}/localhost-key.pem`); +type Version = "TLSv1.2" | "TLSv1.3"; +const hows = ["destroy", "tick", "immediate", "write+destroy", "end"]; + +function act(s: tls.TLSSocket, how: string): void { + if (how === "destroy") s.destroy(); + else if (how === "tick") process.nextTick(() => s.destroy()); + else if (how === "immediate") setImmediate(() => s.destroy()); + else if (how === "write+destroy") { + s.write("hi"); + s.destroy(); + } else s.end("bye"); +} + +const listen = (server: net.Server): Promise => + new Promise((d) => server.listen(0, "127.0.0.1", () => d((server.address() as AddressInfo).port))); + +// the client acts in secureConnect; the server reports what arrived +for (const v of ["TLSv1.2", "TLSv1.3"] as Version[]) { + for (const how of hows) { + const ev: string[] = []; + const server = tls.createServer({ cert, key, maxVersion: v }, (s) => { + ev.push("secureConnection"); + s.on("data", (d: Buffer) => ev.push(`data ${d}`)); + // A client that closes before it reads the server's TLS 1.3 session + // tickets resets the connection. Whether the tickets are read first + // depends on the platform's timing, so that reset is not printed. + s.on("error", (e: NodeJS.ErrnoException) => { + if (e.code !== "ECONNRESET") ev.push(`server socket error ${e.code}`); + }); + }); + server.on("tlsClientError", (e: Error) => ev.push(`tlsClientError ${(e as NodeJS.ErrnoException).code}`)); + const port = await listen(server); + await new Promise((res) => { + const c = tls.connect({ port, host: "127.0.0.1", rejectUnauthorized: false }, () => act(c, how)); + c.on("close", () => setTimeout(res, 100)); + }); + console.log("client", v, how, "|", ev.join(" ; ")); + server.close(); + } +} + +// the server acts in secureConnection; the client reports what arrived +for (const v of ["TLSv1.2", "TLSv1.3"] as Version[]) { + for (const how of hows) { + const ev: string[] = []; + const server = tls.createServer({ cert, key, maxVersion: v }, (s) => { + s.on("error", () => {}); + act(s, how); + }); + const port = await listen(server); + await new Promise((res) => { + const c = tls.connect({ port, host: "127.0.0.1", rejectUnauthorized: false }, () => ev.push("secureConnect")); + c.on("data", (d: Buffer) => ev.push(`data ${d}`)); + c.on("end", () => ev.push("end")); + c.on("error", (e: NodeJS.ErrnoException) => ev.push(`error ${e.code} ${e.message}`)); + c.on("close", (hadError: boolean) => { + ev.push(`close ${hadError}`); + setTimeout(res, 50); + }); + }); + console.log("server", v, how, "|", ev.join(" ; ")); + server.close(); + } +} + +// a peer that closes or resets after the client's hello +for (const how of ["fin", "rst"]) { + for (const via of ["tls.connect", "https.request"]) { + const ev: string[] = []; + const server = net.createServer((s) => { + s.once("data", () => { + if (how === "rst") s.resetAndDestroy(); + else s.end(); + }); + }); + const port = await listen(server); + await new Promise((res) => { + if (via === "tls.connect") { + const c = tls.connect({ port, host: "127.0.0.1", rejectUnauthorized: false }); + c.on("end", () => ev.push("end")); + c.on("error", (e: NodeJS.ErrnoException) => ev.push(`error ${e.code} ${e.message}`)); + c.on("close", (hadError: boolean) => { + ev.push(`close ${hadError}`); + res(); + }); + } else { + const req = https.request({ host: "127.0.0.1", port, path: "/", rejectUnauthorized: false, agent: false }); + req.on("error", (e: NodeJS.ErrnoException) => { + ev.push(`request error ${e.code} ${e.message}`); + res(); + }); + req.end(); + } + }); + console.log("peer", how, via, "|", ev.join(" ; ")); + server.close(); + } +} diff --git a/tests/corpus/7444-tls-final-flight.mjs b/tests/corpus/7444-tls-final-flight.mjs new file mode 100644 index 000000000..e21de782c --- /dev/null +++ b/tests/corpus/7444-tls-final-flight.mjs @@ -0,0 +1,28 @@ +// The last handshake flight from JavaScript: a TLS 1.3 client that +// destroys its socket in secureConnect or a tick never sends its Finished, +// so the server reports a hang-up; one that waits for an immediate, or +// ends the socket, completes the server's handshake. +import * as tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const D = "tests/fixtures/server/certs"; +const cert = readFileSync(`${D}/localhost.pem`); +const key = readFileSync(`${D}/localhost-key.pem`); + +for (const how of ["destroy", "tick", "immediate", "end"]) { + const events = []; + const server = tls.createServer({ cert, key }, () => events.push("secureConnection")); + server.on("tlsClientError", (err) => events.push(`tlsClientError ${err.code}`)); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + await new Promise((resolve) => { + const client = tls.connect({ port: server.address().port, host: "127.0.0.1", rejectUnauthorized: false }, () => { + if (how === "destroy") client.destroy(); + else if (how === "tick") process.nextTick(() => client.destroy()); + else if (how === "immediate") setImmediate(() => client.destroy()); + else client.end(); + }); + client.on("close", () => setTimeout(resolve, 100)); + }); + console.log(how, events.join(" ; ")); + server.close(); +} diff --git a/tests/corpus/7454-tls-peer-embedded-pem.ts b/tests/corpus/7454-tls-peer-embedded-pem.ts new file mode 100644 index 000000000..1937320e9 --- /dev/null +++ b/tests/corpus/7454-tls-peer-embedded-pem.ts @@ -0,0 +1,46 @@ +// A peer certificate whose extension carries another certificate's PEM as +// text: getPeerX509Certificate() and getX509Certificate() give the +// certificate the handshake delivered, and its fields, not the embedded +// one. new X509Certificate() of the same DER reads the PEM inside it first, +// as Node's PEM-before-DER parse does. +import { X509Certificate } from "node:crypto"; +import { readFileSync } from "node:fs"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const cert = readFileSync(`${D}/embedded.pem`), key = readFileSync(`${D}/embedded-key.pem`); +const pem = new X509Certificate(cert); + +function describe(label: string, x: X509Certificate | undefined): void { + if (!x) return console.log(label, "undefined"); + console.log(label, x.subject, x.serialNumber, x.fingerprint256 === pem.fingerprint256, x.fingerprint === pem.fingerprint, x.validTo, x.raw.length); +} + +// raw is the certificate SEQUENCE, without bytes after it, and PEM armor +// counts only at the start of a line +const trailing = new X509Certificate(Buffer.concat([pem.raw, Buffer.from([1, 2, 3])])); +console.log("trailing bytes", trailing.raw.length === pem.raw.length, trailing.fingerprint256 === pem.fingerprint256, trailing.fingerprint === pem.fingerprint, trailing.validFrom === pem.validFrom); +for (const [label, input] of [["after a line", Buffer.concat([Buffer.from("text\n"), cert])], ["inside a line", Buffer.concat([Buffer.from("text"), cert])]] as const) { + try { + console.log(label, new X509Certificate(input).subject); + } catch (e) { + console.log(label, (e as NodeJS.ErrnoException).code); + } +} + +const server = tls.createServer({ cert, key }, (s) => { + describe("server own", s.getX509Certificate()); + s.end(); +}); +server.listen(0, "127.0.0.1", () => { + const c = tls.connect({ port: (server.address() as AddressInfo).port, host: "127.0.0.1", rejectUnauthorized: false }, () => { + const peer = c.getPeerX509Certificate(); + describe("client peer", peer); + const legacy = c.getPeerCertificate(); + console.log("legacy", legacy.subject.CN, legacy.fingerprint256 === pem.fingerprint256); + if (peer) describe("rebuilt from raw", new X509Certificate(peer.raw)); + }); + c.resume(); + c.on("close", () => server.close()); +}); diff --git a/tests/corpus/7455-tls-als-contexts.mjs b/tests/corpus/7455-tls-als-contexts.mjs new file mode 100644 index 000000000..38783fdf9 --- /dev/null +++ b/tests/corpus/7455-tls-als-contexts.mjs @@ -0,0 +1,48 @@ +// The AsyncLocalStorage store in TLS server and client events: a server's +// connections run in its listen() context, except 'tlsClientError', which +// Node emits with no store; a client's events run in its connect() context. +import * as tls from "node:tls"; +import { AsyncLocalStorage } from "node:async_hooks"; +import { readFileSync } from "node:fs"; + +const D = "tests/fixtures/server/certs"; +const cert = readFileSync(`${D}/localhost.pem`); +const key = readFileSync(`${D}/localhost-key.pem`); +const als = new AsyncLocalStorage(); +const serverEvents = []; +const clientEvents = []; +const server = tls.createServer({ cert, key }, (socket) => { + serverEvents.push(`secureConnection ${als.getStore()}`); + socket.on("data", () => { + serverEvents.push(`data ${als.getStore()}`); + socket.end("pong"); + }); +}); +server.on("tlsClientError", (err) => serverEvents.push(`tlsClientError ${err.code} ${als.getStore()}`)); +await als.run("listen", () => new Promise((resolve) => server.listen(0, "127.0.0.1", () => { + serverEvents.push(`listening ${als.getStore()}`); + resolve(); +}))); +const { port } = server.address(); +await als.run("client", () => new Promise((resolve) => { + const client = tls.connect({ port, host: "127.0.0.1", rejectUnauthorized: false }, () => { + clientEvents.push(`secureConnect ${als.getStore()}`); + client.write("ping"); + }); + client.on("data", () => clientEvents.push(`data ${als.getStore()}`)); + client.on("close", () => { + clientEvents.push(`close ${als.getStore()}`); + resolve(); + }); +})); +await als.run("untrusted", () => new Promise((resolve) => { + const client = tls.connect({ port, host: "127.0.0.1" }); + client.on("error", (err) => { + clientEvents.push(`error ${err.code} ${als.getStore()}`); + setTimeout(resolve, 100); + }); +})); +server.close(() => { + console.log("server:", serverEvents.join(" ; ")); + console.log("client:", clientEvents.join(" ; ")); +}); diff --git a/tests/diagnostics/x509-fences.ts b/tests/diagnostics/x509-fences.ts new file mode 100644 index 000000000..416a88b59 --- /dev/null +++ b/tests/diagnostics/x509-fences.ts @@ -0,0 +1,21 @@ +// X509Certificate keeps its fields as prototype getters, so Node's own-key +// walks over a certificate see none of them. Those walks are refused by +// name instead of listing the fields. +import { X509Certificate } from "node:crypto"; +import { readFileSync } from "node:fs"; + +const cert = new X509Certificate(readFileSync("cert.pem")); +console.log(Object.keys(cert)); +console.log(Object.entries(cert)); +console.log({ ...cert }); +console.log(Object.prototype.hasOwnProperty.call(cert, "subject")); +for (const key in cert) console.log(key); + +// A certificate in an unknown or any slot would cross as a plain object of +// its fields, which Node's certificate is not. +const held: unknown = cert; +console.log(held); +function describe(value: unknown): string { + return typeof value; +} +console.log(describe([cert])); diff --git a/tests/fixtures/node-types/tls-client-certificates.ts b/tests/fixtures/node-types/tls-client-certificates.ts new file mode 100644 index 000000000..3268369f9 --- /dev/null +++ b/tests/fixtures/node-types/tls-client-certificates.ts @@ -0,0 +1,32 @@ +// Under @types/node: a mutual-TLS server that requires a client +// certificate signed by its CA, and a client that presents one. +import { readFileSync } from "node:fs"; +import type { AddressInfo } 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`); +let serverLine = ""; +const server = tls.createServer({ cert, key, ca, requestCert: true, rejectUnauthorized: true }, (socket: tls.TLSSocket) => { + const peer: tls.PeerCertificate = socket.getPeerCertificate(); + serverLine = `server ${socket.authorized} ${String(socket.authorizationError)} ${peer.subject.CN}`; + socket.end("welcome"); +}); +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as AddressInfo).port; + const clientCert = readFileSync(`${certs}/client.pem`); + const clientKey = readFileSync(`${certs}/client-key.pem`); + const client = tls.connect({ port, host: "127.0.0.1", servername: "localhost", ca, cert: clientCert, key: clientKey }, () => { + console.log("client", client.authorized, client.getX509Certificate()?.subject); + }); + let body = ""; + client.on("data", (chunk: Buffer) => (body += chunk.toString())); + client.on("end", () => { + console.log(serverLine); + console.log("body", body); + server.close(); + }); +}); diff --git a/tests/fixtures/node-types/tls-peer-certificates.ts b/tests/fixtures/node-types/tls-peer-certificates.ts new file mode 100644 index 000000000..d082cfead --- /dev/null +++ b/tests/fixtures/node-types/tls-peer-certificates.ts @@ -0,0 +1,37 @@ +// Under @types/node: a TLS client reads the server's certificate as the +// legacy object, its detailed issuer chain and an X509Certificate, and the +// server reads the certificate it serves. +import { readFileSync } from "node:fs"; +import type { AddressInfo } 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`); + +// The server's line prints once the client has its reply, so the output +// order does not depend on which side's callback runs first. +let serverLine = ""; +const server = tls.createServer({ cert, key }, (socket: tls.TLSSocket) => { + const own = socket.getCertificate() as tls.PeerCertificate; + serverLine = `server ${own.subject.CN} ${socket.getX509Certificate()?.subject} ${Object.keys(socket.getPeerCertificate()).length}`; + socket.end("done"); +}); +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as AddressInfo).port; + const client = tls.connect({ port, host: "127.0.0.1", ca, servername: "localhost" }, () => { + const peer: tls.PeerCertificate = client.getPeerCertificate(); + const detailed: tls.DetailedPeerCertificate = client.getPeerCertificate(true); + console.log("client", peer.subject.CN, peer.subjectaltname, peer.bits, detailed.issuerCertificate.subject.CN); + console.log("root", detailed.issuerCertificate.issuerCertificate === detailed.issuerCertificate); + const x509 = client.getPeerX509Certificate(); + if (x509 !== undefined) console.log("x509", x509.subject, x509.checkIP("127.0.0.1"), x509.fingerprint256 === peer.fingerprint256); + }); + client.resume(); + client.on("end", () => { + console.log(serverLine); + client.destroy(); + server.close(); + }); +}); diff --git a/tests/fixtures/node-types/tls-server-contexts.ts b/tests/fixtures/node-types/tls-server-contexts.ts new file mode 100644 index 000000000..ff0425a56 --- /dev/null +++ b/tests/fixtures/node-types/tls-server-contexts.ts @@ -0,0 +1,38 @@ +// Under @types/node: a tls.Server that answers an SNI name from +// addContext, replaces its default context with setSecureContext, and +// reports a plain-text client through 'tlsClientError'. +import { readFileSync } from "node:fs"; +import * as net from "node:net"; +import type { AddressInfo } 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 alt = readFileSync(`${certs}/alt.pem`); +const altKey = readFileSync(`${certs}/alt-key.pem`); + +const server: tls.Server = tls.createServer({ cert, key }, (socket: tls.TLSSocket) => socket.end()); +server.addContext("*.localhost", tls.createSecureContext({ cert: alt, key: altKey })); +server.on("tlsClientError", (err: Error, socket: tls.TLSSocket) => { + console.log("tlsClientError", (err as NodeJS.ErrnoException).code, socket.destroyed); +}); + +function subject(port: number, servername: string): Promise { + return new Promise((resolve) => { + const client = tls.connect({ port, host: "127.0.0.1", servername, rejectUnauthorized: false }, () => { + resolve(String(client.getPeerCertificate().subject.CN)); + client.end(); + }); + }); +} + +server.listen(0, "127.0.0.1", async () => { + const port = (server.address() as AddressInfo).port; + console.log("sni", await subject(port, "alt.localhost"), await subject(port, "localhost")); + server.setSecureContext({ cert: alt, key: altKey }); + console.log("default", await subject(port, "localhost")); + const raw = net.connect(port, "127.0.0.1", () => raw.write("GET / HTTP/1.1\r\nHost: x\r\n\r\n")); + raw.on("data", () => {}); + raw.on("close", () => setTimeout(() => server.close(), 50)); +}); diff --git a/tests/fixtures/node-types/x509-methods.ts b/tests/fixtures/node-types/x509-methods.ts new file mode 100644 index 000000000..8736ec990 --- /dev/null +++ b/tests/fixtures/node-types/x509-methods.ts @@ -0,0 +1,20 @@ +// Under @types/node: X509Certificate's methods over the test certificates: +// the issuer, the signature, the host and IP names, the private key, the +// legacy object and the PEM text. +import { X509Certificate, createPrivateKey } from "node:crypto"; +import { readFileSync } from "node:fs"; +import type { PeerCertificate } from "node:tls"; + +const certs = "tests/fixtures/server/certs"; +const ca = new X509Certificate(readFileSync(`${certs}/ca.pem`)); +const leaf = new X509Certificate(readFileSync(`${certs}/localhost.pem`)); +const key = createPrivateKey(readFileSync(`${certs}/localhost-key.pem`)); + +console.log(leaf.checkIssued(ca), leaf.verify(ca.publicKey), ca.verify(leaf.publicKey)); +const host: string | undefined = leaf.checkHost("localhost"); +console.log(host, leaf.checkHost("example.com"), leaf.checkIP("127.0.0.1"), leaf.checkIP("::1")); +console.log(leaf.checkEmail("root@localhost", { subject: "always" }), leaf.checkPrivateKey(key)); +const legacy: PeerCertificate = leaf.toLegacyObject(); +console.log(legacy.subject.CN, legacy.subjectaltname, legacy.bits, legacy.exponent, legacy.raw.equals(leaf.raw)); +console.log(leaf.publicKey.asymmetricKeyType, leaf.publicKey === leaf.publicKey); +console.log(leaf.toString() === readFileSync(`${certs}/localhost.pem`, "latin1"), String(ca).split("\n")[0]); diff --git a/tests/fixtures/server/certs/README.md b/tests/fixtures/server/certs/README.md index dd383f028..2ab5c1839 100644 --- a/tests/fixtures/server/certs/README.md +++ b/tests/fixtures/server/certs/README.md @@ -13,3 +13,15 @@ Minted ONCE with the openssl recipe below (see san.cnf) and committed with a 100 (ca2 repeats the ca recipe with its own key and CN.) `alt.pem`/`alt-key.pem` (the SNI fixtures' second identity: CN=alt.localhost, SAN DNS:alt.localhost) repeats the leaf recipe against the same `ca.pem`, with an ext section whose subjectAltName is `DNS:alt.localhost`. + +`client.pem`/`client-key.pem` (CN=scriptc test client) is a client certificate for the mutual-TLS fixtures: a P-256 leaf signed by `ca.pem` whose extended key usage is clientAuth (`client.cnf`, section `ext`): + + openssl ecparam -genkey -name prime256v1 -noout -out client-key.pem + openssl req -new -key client-key.pem -out client.csr -subj "/CN=scriptc test client" -config client.cnf + openssl x509 -req -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client.pem -days 36500 -extfile client.cnf -extensions ext + +`embedded.pem`/`embedded-key.pem` (CN=embedded pem) is a localhost leaf signed by `ca.pem` whose Netscape comment extension holds `alt.pem` as PEM text on one line (`embedded.cnf`, section `ext`). It checks that a peer certificate's DER is read as DER: + + openssl ecparam -genkey -name prime256v1 -noout -out embedded-key.pem + openssl req -new -key embedded-key.pem -out embedded.csr -subj "/CN=embedded pem" -config embedded.cnf + openssl x509 -req -in embedded.csr -CA ca.pem -CAkey ca-key.pem -set_serial 4242 -out embedded.pem -days 36500 -extfile embedded.cnf -extensions ext diff --git a/tests/fixtures/server/certs/client-key.pem b/tests/fixtures/server/certs/client-key.pem new file mode 100644 index 000000000..90f95c680 --- /dev/null +++ b/tests/fixtures/server/certs/client-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIE6TPp76NrDFeBw1tQSN7lygt9zixQ5xaGb0Xug5/aNVoAoGCCqGSM49 +AwEHoUQDQgAE9iN6ruUQ8IneIyb280iqEfRKAusDKrssfCJh7717mNISg8U7TKvG +XXxgsHD/8NKQjw1Z5SZye7FfNv0WetLRvg== +-----END EC PRIVATE KEY----- diff --git a/tests/fixtures/server/certs/client.cnf b/tests/fixtures/server/certs/client.cnf new file mode 100644 index 000000000..4b7151ef4 --- /dev/null +++ b/tests/fixtures/server/certs/client.cnf @@ -0,0 +1,7 @@ +[req] +distinguished_name = dn +[dn] +[ext] +basicConstraints = CA:FALSE +keyUsage = digitalSignature +extendedKeyUsage = clientAuth diff --git a/tests/fixtures/server/certs/client.pem b/tests/fixtures/server/certs/client.pem new file mode 100644 index 000000000..6458a0fe2 --- /dev/null +++ b/tests/fixtures/server/certs/client.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBqzCCAVKgAwIBAgIUBVrP4y4NCL3DYzbSrpkMIwuBg2owCgYIKoZIzj0EAwIw +GzEZMBcGA1UEAwwQdHNuYXRpdmUgdGVzdCBDQTAgFw0yNjEwMjIxNTUxMTZaGA8y +MTI2MDkyODE1NTExNlowHjEcMBoGA1UEAwwTc2NyaXB0YyB0ZXN0IGNsaWVudDBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABPYjeq7lEPCJ3iMm9vNIqhH0SgLrAyq7 +LHwiYe+9e5jSEoPFO0yrxl18YLBw//DSkI8NWeUmcnuxXzb9FnrS0b6jbzBtMAkG +A1UdEwQCMAAwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMCMB0GA1Ud +DgQWBBS/BxjyTMQkc2IMuhvzyDUhflemADAfBgNVHSMEGDAWgBT7ISW3gYKaXUJu +ZYa2OrD6IBZbBTAKBggqhkjOPQQDAgNHADBEAiBy+DnJFvCUyNmxxeMaWW93n3vJ +XPzLMFQyyoe+hKhoYQIgIDT731Nwwcw/H0Mb+nsb7HMHYihYssPzK52XFAqKA9A= +-----END CERTIFICATE----- diff --git a/tests/fixtures/server/certs/embedded-key.pem b/tests/fixtures/server/certs/embedded-key.pem new file mode 100644 index 000000000..ddf76b1d3 --- /dev/null +++ b/tests/fixtures/server/certs/embedded-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIMzj49L98KADiH/rd18MLx2kdJ8fMMd3Al4AWOE7Ma1VoAoGCCqGSM49 +AwEHoUQDQgAEIdpwV/6VqvmMJkIoZPPgnfNdn+e+NVxyLMMY5n90YMWRfy45AZwn +f/57ssBgq0FC1GDaLRe4dLL9YCSDq3pOzQ== +-----END EC PRIVATE KEY----- diff --git a/tests/fixtures/server/certs/embedded.cnf b/tests/fixtures/server/certs/embedded.cnf new file mode 100644 index 000000000..8d9556e43 --- /dev/null +++ b/tests/fixtures/server/certs/embedded.cnf @@ -0,0 +1,9 @@ +[req] +distinguished_name = dn +[dn] +[ext] +basicConstraints = CA:FALSE +keyUsage = digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = DNS:localhost, IP:127.0.0.1, IP:::1 +nsComment = "-----BEGIN CERTIFICATE-----MIIBwzCCAWigAwIBAgIUSw5Q3SUK8/VNBnfXUCOkHzR4ShUwCgYIKoZIzj0EAwIwGzEZMBcGA1UEAwwQdHNuYXRpdmUgdGVzdCBDQTAgFw0yNjA3MTcwMDU2MjBaGA8yMTI2MDYyMzAwNTYyMFowGDEWMBQGA1UEAwwNYWx0LmxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJyVZDQSTyauFgEwMTksal/hOBhIYpCAzEBCyQq6e23ETTb4NNwIL/NI0ILAr6QmhJaInY31QJq1NFHDkl4N5YujgYowgYcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGAYDVR0RBBEwD4INYWx0LmxvY2FsaG9zdDAdBgNVHQ4EFgQU/Ho4zrglTx9pFwqI+09JFLH84LQwHwYDVR0jBBgwFoAU+yElt4GCml1CbmWGtjqw+iAWWwUwCgYIKoZIzj0EAwIDSQAwRgIhAOUMUtjnIJaYCnPV4YrDMrgIziihcbkq6ZqKBSwwbCMOAiEA0vfd3QpyrdTIKwuopJLg2gA6QPnRTXrZdYr0M3NmJ3Q=-----END CERTIFICATE-----" diff --git a/tests/fixtures/server/certs/embedded.pem b/tests/fixtures/server/certs/embedded.pem new file mode 100644 index 000000000..577ea39b8 --- /dev/null +++ b/tests/fixtures/server/certs/embedded.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEbzCCBBagAwIBAgICEJIwCgYIKoZIzj0EAwIwGzEZMBcGA1UEAwwQdHNuYXRp +dmUgdGVzdCBDQTAgFw0yNjEwMjMxNjEwMzVaGA8yMTI2MDkyOTE2MTAzNVowFzEV +MBMGA1UEAwwMZW1iZWRkZWQgcGVtMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +IdpwV/6VqvmMJkIoZPPgnfNdn+e+NVxyLMMY5n90YMWRfy45AZwnf/57ssBgq0FC +1GDaLRe4dLL9YCSDq3pOzaOCA0owggNGMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWg +MBMGA1UdJQQMMAoGCCsGAQUFBwMBMCwGA1UdEQQlMCOCCWxvY2FsaG9zdIcEfwAA +AYcQAAAAAAAAAAAAAAAAAAAAATCCAqcGCWCGSAGG+EIBDQSCApgWggKULS0tLS1C +RUdJTiBDRVJUSUZJQ0FURS0tLS0tTUlJQnd6Q0NBV2lnQXdJQkFnSVVTdzVRM1NV +SzgvVk5CbmZYVUNPa0h6UjRTaFV3Q2dZSUtvWkl6ajBFQXdJd0d6RVpNQmNHQTFV +RUF3d1FkSE51WVhScGRtVWdkR1Z6ZENCRFFUQWdGdzB5TmpBM01UY3dNRFUyTWpC +YUdBOHlNVEkyTURZeU16QXdOVFl5TUZvd0dERVdNQlFHQTFVRUF3d05ZV3gwTG14 +dlkyRnNhRzl6ZERCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQkp5 +VlpEUVNUeWF1RmdFd01Ua3NhbC9oT0JoSVlwQ0F6RUJDeVFxNmUyM0VUVGI0Tk53 +SUwvTkkwSUxBcjZRbWhKYUluWTMxUUpxMU5GSERrbDRONVl1amdZb3dnWWN3Q1FZ +RFZSMFRCQUl3QURBTEJnTlZIUThFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZ +QkJRVUhBd0V3R0FZRFZSMFJCQkV3RDRJTllXeDBMbXh2WTJGc2FHOXpkREFkQmdO +VkhRNEVGZ1FVL0hvNHpyZ2xUeDlwRndxSSswOUpGTEg4NExRd0h3WURWUjBqQkJn +d0ZvQVUreUVsdDRHQ21sMUNibVdHdGpxdytpQVdXd1V3Q2dZSUtvWkl6ajBFQXdJ +RFNRQXdSZ0loQU9VTVV0am5JSmFZQ25QVjRZckRNcmdJemlpaGNia3E2WnFLQlN3 +d2JDTU9BaUVBMHZmZDNRcHlyZFRJS3d1b3BKTGcyZ0E2UVBuUlRYclpkWXIwTTNO +bUozUT0tLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tMB0GA1UdDgQWBBQ+Y3aDuoRS +0FRNT2st7Lzlo7BtDjAfBgNVHSMEGDAWgBT7ISW3gYKaXUJuZYa2OrD6IBZbBTAK +BggqhkjOPQQDAgNHADBEAiB3zl81cDKI4GvoRBDneF53eeLR6ftnaD9Pf6hCHUUA +zwIgauFJwF7rLQ0krjZXxdOMZEOl7i1sBrlOfGiIyGSn84s= +-----END CERTIFICATE----- diff --git a/tests/fixtures/tls/empty-ca.ts b/tests/fixtures/tls/empty-ca.ts new file mode 100644 index 000000000..7d1d30928 --- /dev/null +++ b/tests/fixtures/tls/empty-ca.ts @@ -0,0 +1,36 @@ +// A server's ca option as a trust store for client certificates, run with +// NODE_EXTRA_CA_CERTS naming the test CA, so the default store trusts the +// client: an empty Buffer is an empty store, an empty string and no ca use +// the default store. +import { readFileSync } from "node:fs"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f: string): Buffer => readFileSync(`${D}/${f}`); +const cert = r("localhost.pem"), key = r("localhost-key.pem"), ca = r("ca.pem"); +const cases: [string, Buffer | string | undefined][] = [ + ["empty Buffer", Buffer.alloc(0)], + ["empty string", ""], + ["no ca", undefined], + ["ca", ca], +]; +for (const [label, value] of cases) { + await new Promise((done) => { + const report = (s: tls.TLSSocket): void => { + console.log(label, s.authorized, s.authorizationError); + s.end(); + }; + const server = + value === undefined + ? tls.createServer({ cert, key, requestCert: true, rejectUnauthorized: false }, report) + : tls.createServer({ cert, key, requestCert: true, rejectUnauthorized: false, ca: value }, report); + server.listen(0, "127.0.0.1", () => { + const port = (server.address() as AddressInfo).port; + const c = tls.connect({ port, host: "127.0.0.1", ca, servername: "localhost", cert: r("client.pem"), key: r("client-key.pem") }); + c.on("error", () => {}); + c.resume(); + c.on("close", () => server.close(() => done())); + }); + }); +} diff --git a/tests/harness/__snapshots__/tls-sni-callback-fences.ts.txt b/tests/harness/__snapshots__/tls-sni-callback-fences.ts.txt index c45608bb9..80ace6df6 100644 --- a/tests/harness/__snapshots__/tls-sni-callback-fences.ts.txt +++ b/tests/harness/__snapshots__/tls-sni-callback-fences.ts.txt @@ -5,4 +5,4 @@ tls-sni-callback-fences.ts:13:54 - error SC2020: 'tls.createServer with SNICallb | ^~~~~~~~~~~~~~~~~~~~ 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 + hint: SNICallback lowers with cert and key; ALPNProtocols, minVersion, maxVersion, requestCert, ca and the validated options are not combined with it yet \ No newline at end of file diff --git a/tests/harness/__snapshots__/x509-fences.ts.txt b/tests/harness/__snapshots__/x509-fences.ts.txt new file mode 100644 index 000000000..2f5cc4e68 --- /dev/null +++ b/tests/harness/__snapshots__/x509-fences.ts.txt @@ -0,0 +1,48 @@ +x509-fences.ts:8:13 - error SC1090: Object.keys over X509Certificate values (Node keeps their fields as prototype getters, so own-key walks see none; read the fields) are not supported yet + + 7 | const cert = new X509Certificate(readFileSync("cert.pem")); + 8 | console.log(Object.keys(cert)); + | ^~~~~~~~~~~~~~~~~ + 9 | console.log(Object.entries(cert)); + +x509-fences.ts:9:13 - error SC1090: Object.entries over X509Certificate values (Node keeps their fields as prototype getters, so own-key walks see none; read the fields) are not supported yet + + 8 | console.log(Object.keys(cert)); + 9 | console.log(Object.entries(cert)); + | ^~~~~~~~~~~~~~~~~~~~ + 10 | console.log({ ...cert }); + +x509-fences.ts:10:15 - error SC1090: object spread over X509Certificate values (Node keeps their fields as prototype getters, so own-key walks see none; read the fields) are not supported yet + + 9 | console.log(Object.entries(cert)); + 10 | console.log({ ...cert }); + | ^~~~~~~ + 11 | console.log(Object.prototype.hasOwnProperty.call(cert, "subject")); + +x509-fences.ts:11:50 - error SC1090: Object.prototype.hasOwnProperty over X509Certificate values (Node keeps their fields as prototype getters, so own-key walks see none; read the fields) are not supported yet + + 10 | console.log({ ...cert }); + 11 | console.log(Object.prototype.hasOwnProperty.call(cert, "subject")); + | ^~~~ + 12 | for (const key in cert) console.log(key); + +x509-fences.ts:12:19 - error SC1090: for-in over X509Certificate values (Node keeps their fields as prototype getters, so own-key walks see none; read the fields) are not supported yet + + 11 | console.log(Object.prototype.hasOwnProperty.call(cert, "subject")); + 12 | for (const key in cert) console.log(key); + | ^~~~ + 13 | + +x509-fences.ts:16:23 - error SC1090: X509Certificate values in 'unknown' or 'any' slots (Node keeps a certificate's fields as prototype getters, and its toString() and toJSON() give the PEM; keep the X509Certificate type) are not supported yet + + 15 | // its fields, which Node's certificate is not. + 16 | const held: unknown = cert; + | ^~~~ + 17 | console.log(held); + +x509-fences.ts:21:23 - error SC1090: X509Certificate values in 'unknown' or 'any' slots (Node keeps a certificate's fields as prototype getters, and its toString() and toJSON() give the PEM; keep the X509Certificate type) are not supported yet + + 20 | } + 21 | console.log(describe([cert])); + | ^~~~ + 22 | \ No newline at end of file diff --git a/tests/harness/corpus-costs/6936-x509-certificate-fields.ts.json b/tests/harness/corpus-costs/6936-x509-certificate-fields.ts.json new file mode 100644 index 000000000..73e8107eb --- /dev/null +++ b/tests/harness/corpus-costs/6936-x509-certificate-fields.ts.json @@ -0,0 +1 @@ +{ "plain": 3042, "san": 5637 } diff --git a/tests/harness/corpus-costs/7090-x509-check-names.ts.json b/tests/harness/corpus-costs/7090-x509-check-names.ts.json new file mode 100644 index 000000000..3ee86dd28 --- /dev/null +++ b/tests/harness/corpus-costs/7090-x509-check-names.ts.json @@ -0,0 +1 @@ +{ "plain": 4003, "san": 5700 } diff --git a/tests/harness/corpus-costs/7091-x509-certificate-keys.ts.json b/tests/harness/corpus-costs/7091-x509-certificate-keys.ts.json new file mode 100644 index 000000000..a9b159564 --- /dev/null +++ b/tests/harness/corpus-costs/7091-x509-certificate-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 4335, "san": 8298 } diff --git a/tests/harness/corpus-costs/7092-x509-legacy-object.ts.json b/tests/harness/corpus-costs/7092-x509-legacy-object.ts.json new file mode 100644 index 000000000..c7e0c4ee2 --- /dev/null +++ b/tests/harness/corpus-costs/7092-x509-legacy-object.ts.json @@ -0,0 +1 @@ +{ "plain": 3910, "san": 5665 } diff --git a/tests/harness/corpus-costs/7093-x509-methods-js.mjs.json b/tests/harness/corpus-costs/7093-x509-methods-js.mjs.json new file mode 100644 index 000000000..8f60594f1 --- /dev/null +++ b/tests/harness/corpus-costs/7093-x509-methods-js.mjs.json @@ -0,0 +1 @@ +{ "plain": 4421, "san": 5823 } diff --git a/tests/harness/corpus-costs/7095-tls-peer-certificates.ts.json b/tests/harness/corpus-costs/7095-tls-peer-certificates.ts.json new file mode 100644 index 000000000..68214a26c --- /dev/null +++ b/tests/harness/corpus-costs/7095-tls-peer-certificates.ts.json @@ -0,0 +1 @@ +{ "plain": 9277, "san": 14000 } diff --git a/tests/harness/corpus-costs/7096-tls-peer-certificates.mjs.json b/tests/harness/corpus-costs/7096-tls-peer-certificates.mjs.json new file mode 100644 index 000000000..ec7c1e0f4 --- /dev/null +++ b/tests/harness/corpus-costs/7096-tls-peer-certificates.mjs.json @@ -0,0 +1 @@ +{ "plain": 4576, "san": 8354 } diff --git a/tests/harness/corpus-costs/7097-tls-client-certificates.ts.json b/tests/harness/corpus-costs/7097-tls-client-certificates.ts.json new file mode 100644 index 000000000..2776a867c --- /dev/null +++ b/tests/harness/corpus-costs/7097-tls-client-certificates.ts.json @@ -0,0 +1 @@ +{ "plain": 8917, "san": 11888 } diff --git a/tests/harness/corpus-costs/7098-tls-client-certificates-paths.ts.json b/tests/harness/corpus-costs/7098-tls-client-certificates-paths.ts.json new file mode 100644 index 000000000..f55fef2c1 --- /dev/null +++ b/tests/harness/corpus-costs/7098-tls-client-certificates-paths.ts.json @@ -0,0 +1 @@ +{ "plain": 6188, "san": 10754 } diff --git a/tests/harness/corpus-costs/7099-tls-client-certificates.mjs.json b/tests/harness/corpus-costs/7099-tls-client-certificates.mjs.json new file mode 100644 index 000000000..02414f135 --- /dev/null +++ b/tests/harness/corpus-costs/7099-tls-client-certificates.mjs.json @@ -0,0 +1 @@ +{ "plain": 4389, "san": 5772 } diff --git a/tests/harness/corpus-costs/7440-tls-server-add-context.ts.json b/tests/harness/corpus-costs/7440-tls-server-add-context.ts.json new file mode 100644 index 000000000..f61acc3da --- /dev/null +++ b/tests/harness/corpus-costs/7440-tls-server-add-context.ts.json @@ -0,0 +1 @@ +{ "plain": 2787, "san": 2973 } diff --git a/tests/harness/corpus-costs/7441-tls-client-error.ts.json b/tests/harness/corpus-costs/7441-tls-client-error.ts.json new file mode 100644 index 000000000..a928ec1f1 --- /dev/null +++ b/tests/harness/corpus-costs/7441-tls-client-error.ts.json @@ -0,0 +1 @@ +{ "plain": 6387, "san": 6135 } diff --git a/tests/harness/corpus-costs/7442-tls-server-contexts.mjs.json b/tests/harness/corpus-costs/7442-tls-server-contexts.mjs.json new file mode 100644 index 000000000..8630193d3 --- /dev/null +++ b/tests/harness/corpus-costs/7442-tls-server-contexts.mjs.json @@ -0,0 +1 @@ +{ "plain": 2866, "san": 2854 } diff --git a/tests/harness/corpus-costs/7443-tls-final-flight.ts.json b/tests/harness/corpus-costs/7443-tls-final-flight.ts.json new file mode 100644 index 000000000..2eb48c40b --- /dev/null +++ b/tests/harness/corpus-costs/7443-tls-final-flight.ts.json @@ -0,0 +1 @@ +{ "plain": 8793, "san": 8889 } diff --git a/tests/harness/corpus-costs/7444-tls-final-flight.mjs.json b/tests/harness/corpus-costs/7444-tls-final-flight.mjs.json new file mode 100644 index 000000000..ddc1e1368 --- /dev/null +++ b/tests/harness/corpus-costs/7444-tls-final-flight.mjs.json @@ -0,0 +1 @@ +{ "plain": 3913, "san": 3715 } diff --git a/tests/harness/corpus-costs/7454-tls-peer-embedded-pem.ts.json b/tests/harness/corpus-costs/7454-tls-peer-embedded-pem.ts.json new file mode 100644 index 000000000..034b69448 --- /dev/null +++ b/tests/harness/corpus-costs/7454-tls-peer-embedded-pem.ts.json @@ -0,0 +1 @@ +{ "plain": 2535, "san": 3999 } diff --git a/tests/harness/corpus-costs/7455-tls-als-contexts.mjs.json b/tests/harness/corpus-costs/7455-tls-als-contexts.mjs.json new file mode 100644 index 000000000..53219fb76 --- /dev/null +++ b/tests/harness/corpus-costs/7455-tls-als-contexts.mjs.json @@ -0,0 +1 @@ +{ "plain": 3306, "san": 4617 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index f8068ac81..20231a94e 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -954,8 +954,8 @@ 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. +// requestCert (which lowers with cert and key, whose ca the runtime reads) +// 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( @@ -990,7 +990,7 @@ listener.listen(0, () => { ); 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" + + "requestCert new tls.TLSSocket option 'requestCert' has no static lowering — requestCert lowers with cert and key options, not beside a secureContext\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", @@ -998,6 +998,93 @@ listener.listen(0, () => { }); }); +// A client certificate lowers as a cert and key pair; one without the +// other throws the fence before any connection is made. +describe("client certificates without both cert and key", () => { + test("tls.connect and https.request throw the fence", async () => { + const r = await compileAndRun( + "tls-client-certificate-fences", + `import { readFileSync } from "node:fs"; +import * as https from "node:https"; +import * as tls from "node:tls"; +const certs = "tests/fixtures/server/certs"; +const cert = readFileSync(\`\${certs}/client.pem\`); +const key = readFileSync(\`\${certs}/client-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); + } +} +report("connect cert only", () => tls.connect({ port: 1, host: "127.0.0.1", cert })); +report("connect key only", () => tls.connect({ port: 1, host: "127.0.0.1", key })); +report("request key only", () => https.request({ host: "127.0.0.1", port: 1, path: "/", key })); +`, + ); + expect(r.stderr).toBe(""); + expect(r.stdout).toBe( + "connect cert only tls.connect option 'cert' has no static lowering — a client certificate lowers with both cert and key\n" + + "connect key only tls.connect option 'key' has no static lowering — a client certificate lowers with both cert and key\n" + + "request key only https.request option 'key' has no static lowering — a client certificate lowers with both cert and key\n", + ); + }); +}); + +// server.addContext and setSecureContext answer on tls and https servers; +// a plain net server, a servername Node's RegExp would read as groups or +// alternation, and one outside ASCII throw the fence. A handshake failure +// whose OpenSSL reason the runtime does not know reaches 'tlsClientError' +// as a coded SC2020 error. +describe("tls.Server contexts and client errors without a lowering", () => { + test("addContext and setSecureContext fences, and an unknown handshake failure", async () => { + const r = await compileAndRun( + "tls-server-context-fences", + `import { readFileSync } from "node:fs"; +import * as net from "node:net"; +import type { AddressInfo } 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 plain = net.createServer() as unknown as tls.Server; +report("net addContext", () => plain.addContext("a.test", { cert, key })); +report("net setSecureContext", () => plain.setSecureContext({ cert, key })); +const server = tls.createServer({ cert, key }); +report("alternation", () => server.addContext("a.test|b.test", { cert, key })); +report("group", () => server.addContext("(a).test", { cert, key })); +report("non-ASCII", () => server.addContext("\u00e9.test", { cert, key })); +server.on("tlsClientError", (err: Error) => console.log("tlsClientError", (err as NodeJS.ErrnoException).code, err.message)); +server.listen(0, "127.0.0.1", () => { + const raw = net.connect((server.address() as AddressInfo).port, "127.0.0.1", () => { + raw.write(Buffer.from([0x16, 0x03, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00])); + }); + raw.on("data", () => {}); + raw.on("close", () => server.close()); +}); +`, + ); + expect(r.stderr).toBe(""); + expect(r.stdout).toBe( + "net addContext server.addContext on a server that is not a tls or https server is not supported yet\n" + + "net setSecureContext server.setSecureContext on a server that is not a tls or https server is not supported yet\n" + + "alternation server.addContext option 'servername' has no static lowering — a servername with regular-expression groups or alternation has no lowering\n" + + "group server.addContext option 'servername' has no static lowering — a servername with regular-expression groups or alternation has no lowering\n" + + "non-ASCII server.addContext option 'servername' has no static lowering — a servername outside ASCII has no lowering\n" + + "tlsClientError SC2020 a TLS handshake failure (mbedTLS error -0x7300) without the reason OpenSSL gives 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). @@ -1989,6 +2076,70 @@ await attempt("subtle verify", () => crypto.subtle.verify(name, raw, new Uint8Ar }); }); +// An X509Certificate's Ed25519 key needs the same unit: without it the +// certificate's public key, issuer and names still read, while verify +// rejects with the coded SC2020 Error. 7091 pins the linked path. +describe("X509Certificate Ed25519 keys without the Ed25519 unit", () => { + test("verify refuses with SC2020 in a program that never names Ed25519", async () => { + const r = await compileAndRun( + "x509-ed25519-unit-not-linked", + `import { X509Certificate } from "node:crypto"; +const cert = new X509Certificate("-----BEGIN CERTIFICATE-----\\nMIIBljCCAUigAwIBAgIUNFuRt3zKS19UAUMdLWewqHrGBigwBQYDK2VwMC8xFDAS\\nBgNVBAoMC0V4YW1wbGUgT3JnMRcwFQYDVQQDDA5lZC5leGFtcGxlLm5ldDAgFw0y\\nNjEwMjIxMDEwMTJaGA8yMTI2MDkyODEwMTAxMlowLzEUMBIGA1UECgwLRXhhbXBs\\nZSBPcmcxFzAVBgNVBAMMDmVkLmV4YW1wbGUubmV0MCowBQYDK2VwAyEAOCSLKkel\\nkOH/5aqUwI3PNJVUUNyhgu2s19LFC254fNCjdDByMB0GA1UdDgQWBBR+50zUcqL8\\n739jh6/rEYojULDmlTAfBgNVHSMEGDAWgBR+50zUcqL8739jh6/rEYojULDmlTAP\\nBgNVHRMBAf8EBTADAQH/MB8GA1UdEQQYMBaCDmVkLmV4YW1wbGUubmV0hwTAAAIH\\nMAUGAytlcANBAC/c0Bi0SVUnHCg/1pn7MaCgVivSHuoISi2SCq/0EnLefRdgsIir\\n0QxjVnxeuKN2Y1yfEO/R1W8Nwovo1l2M1gQ=\\n-----END CERTIFICATE-----\\n"); +console.log(cert.publicKey.asymmetricKeyType?.length, cert.checkIssued(cert), cert.checkHost("ed.example.net")); +try { + console.log(cert.verify(cert.publicKey)); +} catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(err.name, err.code, err.message); +} +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stderr).toBe(""); + expect(r.stdout).toBe( + "7 true ed.example.net\n" + + "Error SC2020 Ed25519 keys are supported when a string literal in the program names 'ed25519'\n", + ); + }); +}); + +// A TLS socket held in an untyped JavaScript value has no lowering for its +// certificate accessors (they link the X.509 unit, which a dynamic call +// cannot ask for), so the call throws the fence; a socket whose type is +// known runs them (corpus 7095, 7096). +describe("tlsSocket.getPeerCertificate on a dynamic value (JS lane)", () => { + test("throws the fence", async () => { + const r = await compileAndRun( + "tls-peer-certificate-dynamic", + `import * as tls from "node:tls"; +import { readFileSync } from "node:fs"; +const certs = "tests/fixtures/server/certs"; +const server = tls.createServer({ cert: readFileSync(\`\${certs}/localhost.pem\`), key: readFileSync(\`\${certs}/localhost-key.pem\`) }, (s) => s.end()); +function peer(socket) { + try { + return socket.getPeerCertificate().subject.CN; + } catch (e) { + return e.message; + } +} +server.listen(0, () => { + const c = tls.connect({ port: server.address().port, ca: readFileSync(\`\${certs}/ca.pem\`), servername: "localhost" }, () => { + console.log(peer(c)); + c.destroy(); + server.close(); + }); +}); +`, + "mjs", + ); + expect(r.exitCode).toBe(0); + expect(r.stderr).toBe(""); + expect(r.stdout).toBe( + "'TLSSocket.prototype.getPeerCertificate' on a dynamic value is not supported yet\n", + ); + }); +}); + describe("inspecting error subclasses with properties created at run time (JS lane)", () => { test("an instance whose constructor created a property is a named fence", async () => { // Node prints `[Conditional [Error]: m] { extra: 'x' }`. The property diff --git a/tests/harness/library-asyncfree.test.ts b/tests/harness/library-asyncfree.test.ts index d924f26d1..9b569c584 100644 --- a/tests/harness/library-asyncfree.test.ts +++ b/tests/harness/library-asyncfree.test.ts @@ -106,6 +106,15 @@ describe("async_free detection over the IR", () => { ).toContain("asymmetric KeyObjects"); }); + test("X509Certificate's key members refuse", async () => { + expect( + await surfaceOf( + "x509-key", + `import { X509Certificate } from "node:crypto";\nimport { readFileSync } from "node:fs";\nconst cert = new X509Certificate(readFileSync("cert.pem"));\nconsole.log(cert.verify(cert.publicKey));\n`, + ), + ).toContain("X509Certificate publicKey, verify and checkPrivateKey"); + }); + test("process signal listeners refuse", async () => { expect( await surfaceOf( diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index db4fabb3e..47b77f0fd 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -299,6 +299,21 @@ wrap empty: len 2 bytes 60 62 expect(run.stdout).toBe(BUFFERS_EXPECTED); }); + /* ── X509Certificate fields: scr_x509.c rides the archive ────────────── */ + + test("X509Certificate fields in a library", async () => { + const { archive, outDir } = await buildLibrary("x509", emission); + const probe = buildProbe("x509", archive, outDir); + const run = runProbe(probe); + expect(run.signal).toBeNull(); + expect(run.status).toBe(0); + expect(run.stdout).toBe( + "C=US\nST=California\nL=San Francisco\nO=Example Org\nOU=Dev Team\nCN=Example Root CA\nemailAddress=ca@example.com" + + "|0123456789ABCDEF|Sep 27 16:33:52 2126 GMT|true|" + + "7C:5F:CA:18:57:BF:BD:FC:67:AC:BD:21:1C:67:E8:C3:73:48:35:4E:4D:95:A8:2B:83:FF:E0:5A:E7:0A:97:A9\n", + ); + }); + /* ── cstring: NUL-terminated text in, out, and through callbacks ─────── */ test("cstring parameters, results, and callback arguments", async () => { diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index fd2077a48..ea5ddd51f 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -100,6 +100,13 @@ for (const [name, title] of [ "eventLoopUtilization and monitorEventLoopDelay follow @types/node's types", ], ["timeout-type", "NodeJS.Timeout fields, ref/unref/hasRef and clearInterval under @types/node"], + ["x509-methods", "X509Certificate's methods and the legacy object under @types/node"], + ["tls-peer-certificates", "TLS socket certificate accessors under @types/node"], + ["tls-client-certificates", "mutual TLS with client certificates under @types/node"], + [ + "tls-server-contexts", + "tls.Server addContext, setSecureContext and tlsClientError under @types/node", + ], ] as const) { test(`node-types: ${title}`, async () => { const outDir = outDirFor(name); diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c5476328e..90773c87f 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -52,6 +52,7 @@ const features: NativeLinkFeatures = { dh: false, pki: false, ed25519: false, + x509: 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 64ddd0213..859f820da 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -53,6 +53,7 @@ const base: NativeLinkFeatures = { dh: false, pki: false, ed25519: false, + x509: false, webcrypto: false, perfLoop: false, }; diff --git a/tests/harness/tls-socket-leaks.test.ts b/tests/harness/tls-socket-leaks.test.ts index 64edfac25..d657b0950 100644 --- a/tests/harness/tls-socket-leaks.test.ts +++ b/tests/harness/tls-socket-leaks.test.ts @@ -3,7 +3,9 @@ * 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); + * handshakes all fail; 7095's detailed certificates link roots to + * themselves; 7097 and 7098 hold client certificates and the chains a ca + * completes. 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"; @@ -22,6 +24,12 @@ 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", + "tests/corpus/7095-tls-peer-certificates.ts", + "tests/corpus/7097-tls-client-certificates.ts", + "tests/corpus/7098-tls-client-certificates-paths.ts", + "tests/corpus/7440-tls-server-add-context.ts", + "tests/corpus/7441-tls-client-error.ts", + "tests/corpus/7443-tls-final-flight.ts", ]; describe.skipIf(!sanitize || process.platform !== "linux")( diff --git a/tests/harness/tls-trust-store.test.ts b/tests/harness/tls-trust-store.test.ts new file mode 100644 index 000000000..714de9057 --- /dev/null +++ b/tests/harness/tls-trust-store.test.ts @@ -0,0 +1,42 @@ +// TLS trust stores that depend on the process's NODE_EXTRA_CA_CERTS: a +// server's ca option, where an empty Buffer is an empty store and an empty +// string is the default one. The corpus cannot set the environment, so the +// fixture runs here under Node and as a binary with the test CA added. +import { execFile } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +test("a server's empty ca Buffer is an empty trust store, as in Node", async () => { + const entry = join(repoRoot, "tests/fixtures/tls/empty-ca.ts"); + const outDir = join(cacheDir, `tls-trust-store-${sanitize ? "san" : "plain"}`); + mkdirSync(outDir, { recursive: true }); + const result = await compile(entry, { + outPath: join(outDir, "empty-ca"), + outDir, + sanitize, + backend: "llvm", + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const env = { + ...process.env, + NODE_EXTRA_CA_CERTS: join(repoRoot, "tests/fixtures/server/certs/ca.pem"), + }; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath, [], { cwd: repoRoot, env }), + execFileAsync(process.execPath, ["--experimental-strip-types", "--no-warnings", entry], { + cwd: repoRoot, + env, + }), + ]); + expect(native.stdout).toBe(node.stdout); + expect(node.stdout).toContain("empty Buffer false SELF_SIGNED_CERT_IN_CHAIN"); +}, 120_000); diff --git a/tests/library-mode/x509/lib.ts b/tests/library-mode/x509/lib.ts new file mode 100644 index 000000000..9b023a727 --- /dev/null +++ b/tests/library-mode/x509/lib.ts @@ -0,0 +1,41 @@ +// X509Certificate fields in a library archive: the field code lives in +// scr_x509.c, which the archive carries when the program reads a field. +import * as crypto from "node:crypto"; + +const CA = `-----BEGIN CERTIFICATE----- +MIIEITCCAwmgAwIBAgIIASNFZ4mrze8wDQYJKoZIhvcNAQELBQAwgZwxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp +c2NvMRQwEgYDVQQKDAtFeGFtcGxlIE9yZzERMA8GA1UECwwIRGV2IFRlYW0xGDAW +BgNVBAMMD0V4YW1wbGUgUm9vdCBDQTEdMBsGCSqGSIb3DQEJARYOY2FAZXhhbXBs +ZS5jb20wIBcNMjYxMDIxMTYzMzUyWhgPMjEyNjA5MjcxNjMzNTJaMIGcMQswCQYD +VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j +aXNjbzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxETAPBgNVBAsMCERldiBUZWFtMRgw +FgYDVQQDDA9FeGFtcGxlIFJvb3QgQ0ExHTAbBgkqhkiG9w0BCQEWDmNhQGV4YW1w +bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YGcCr0pxR8d +6qDdLuILcoT7vhtAjyHwfkoChnH6N3TLWzDUugnBp9NbDb7gDLqbRT4uTDoNb6zS +tEJZ4Q7/hhd0jDvoPmOy0Y3Kyj9uNsdf25GdbvwwEnlddELJCZprTZ4OhEg6w2s0 +Yy4SlviLzDXLmsggTypxeS+K8JHT9onMipi5L4GFu8n1QZbzXvSS4BkRgMXEyrtr +FOE9qSo2qkubFyd/WNKRTRIpTSc5/GGsqJ0FKDDJnN5adotcUucbJFn4noNnFUWh +M8mhBEj7ENDdJsD2EVFsI6H958jA2SbNNu4ngzAOs2AKNBYBfQK02ZQAdRNOSL3K +5FkfbII5LwIDAQABo2MwYTAdBgNVHQ4EFgQUP+yQyItpNYEls26Jfv+NnciGaK8w +HwYDVR0jBBgwFoAUP+yQyItpNYEls26Jfv+NnciGaK8wDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBABDw3Va/C+RFc/Hs +TJnHtQJtxFL0ZhnHUqg4DcDA3m+JDdIDNL8sNpLEbtn/pr0VeEWgG+MImWAQlrUW +IAZkLj9/Zg1wSO0oZ86ZyZJZmp54lUYXgG17CNl4g+F7VHt3bBwUJHTbWye/FKM6 +5OVZetCYKVj7Z+5BYuf7cumzvRqcn32fXidB/kjRX14SJ4JDPn2yFZWCr2ygIqWQ +SOuEmWbNAbYGaVZkQr2Ma8U28DWY2jb0f6nP8jLt49t9o6N/e6Fwd6I5t/mx9Mfj +0403omkxmr9vqN2kBamzVnun+2mDkRZcKPCr72Q4sfbvmDi/W5Nf1Iu2cQW8VfDO +SqpyT54= +-----END CERTIFICATE----- +`; + +export function describe(): string { + const cert = new crypto.X509Certificate(CA); + return [ + cert.subject, + cert.serialNumber, + cert.validTo, + String(cert.ca), + cert.fingerprint256, + ].join("|"); +} diff --git a/tests/library-mode/x509/probe.c b/tests/library-mode/x509/probe.c new file mode 100644 index 000000000..d8065d039 --- /dev/null +++ b/tests/library-mode/x509/probe.c @@ -0,0 +1,23 @@ +/* X509Certificate fields read through a library export. */ +#include +#include + +extern void kx_init(void); +extern void kx_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx); +extern void kx_collect(void); +extern void kx_describe(const uint8_t **out, size_t *out_len); + +static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) { + (void)ctx; (void)addr; + printf("SINK %.*s\n", (int)len, msg); +} + +int main(void) { + kx_set_panic_sink(sink, NULL); + kx_init(); + const uint8_t *s; size_t n; + kx_describe(&s, &n); + printf("%.*s\n", (int)n, s); + kx_collect(); + return 0; +} diff --git a/tests/library-mode/x509/profile.json b/tests/library-mode/x509/profile.json new file mode 100644 index 000000000..fe9b4fb59 --- /dev/null +++ b/tests/library-mode/x509/profile.json @@ -0,0 +1,14 @@ +{ + "profile_format": 1, + "name": "conformance-x509", + "entry": "lib.ts", + "emission": "llvm", + "abi": { + "prefix": "kx_", + "init_symbol": "kx_init", + "sink_register_symbol": "kx_set_panic_sink", + "collect_symbol": "kx_collect", + "result_reset_symbol": null + }, + "exports": [{ "export": "describe", "symbol": "kx_describe", "params": [], "returns": "string" }] +} From b72e164e169d18ca6e23a09b715b3dc186226217 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 18:18:27 -0500 Subject: [PATCH 028/273] Track views through more flows and standard library calls --- .../compiler/src/frontend/lowering/lowerer.ts | 79 ++++++++++++++----- packages/runtime/src/scr_array.c | 3 + .../corpus/6067-view-holes-in-typed-arrays.ts | 58 ++++++++++++++ .../6068-foreign-elements-through-views.ts | 25 ++++++ 4 files changed, 145 insertions(+), 20 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index b23ee147a..9fb65bd4c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -6338,6 +6338,20 @@ export class Lowerer { let dynamicViews: Set | null = null; // Element ABIs of the native arrays that an `any` or `unknown` value is // converted to, including arrays nested in the converted value. + // Assignments, including the logical ones (`view ??= numbers`). + const assignmentOperators = new Set([ + ts.SyntaxKind.EqualsToken, + ts.SyntaxKind.QuestionQuestionEqualsToken, + ts.SyntaxKind.BarBarEqualsToken, + ts.SyntaxKind.AmpersandAmpersandEqualsToken, + ]); + // Initializers of class properties, parameter defaults and + // destructuring defaults, which convert to the binding's type. + const initializedBinding = ( + node: ts.Node, + ): node is ts.PropertyDeclaration | ts.ParameterDeclaration | ts.BindingElement => + (ts.isPropertyDeclaration(node) || ts.isParameter(node) || ts.isBindingElement(node)) && + node.initializer !== undefined; const collectDynamicViews = (): Set => { const keys = new Set(); const dynamicValue = (node: ts.Expression): boolean => { @@ -6388,14 +6402,18 @@ export class Lowerer { dynamicValue(node.initializer) ) target(typeOrNull(node.name)); + else if (initializedBinding(node) && dynamicValue(node.initializer!)) + target(typeOrNull(node.name ?? node)); else if ( ts.isBinaryExpression(node) && - node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + assignmentOperators.has(node.operatorToken.kind) && dynamicValue(node.right) ) target(typeOrNull(node.left)); else if (ts.isReturnStatement(node) && node.expression && dynamicValue(node.expression)) target(contextualOrNull(node.expression)); + else if (ts.isYieldExpression(node) && node.expression && dynamicValue(node.expression)) + target(contextualOrNull(node.expression)); else if (ts.isCallExpression(node) || ts.isNewExpression(node)) { for (const arg of node.arguments?.nodes ?? []) if (!ts.isSpreadElement(arg) && dynamicValue(arg)) target(contextualOrNull(arg)); @@ -6427,26 +6445,45 @@ export class Lowerer { // Element ABIs of the native arrays a value converted to a dynamic type // can reach: the conversion keeps each such array as a live view, so // stores through the dynamic value write the native storage. - // A standard library function only reads an array argument it takes as - // `any` (console.log, JSON.stringify, String), except the reflective - // writes: Object.assign and defineProperty, Reflect.set and the like, - // and Array.prototype methods through call or apply. - const reflectiveWriters = new Set([ - "assign", - "defineProperty", - "defineProperties", - "setPrototypeOf", - "set", - "apply", - "call", + // Standard library functions that only read the arguments they take as + // `any` or `unknown`: the console methods, String, Number, Boolean, + // Array.isArray, Object.is, JSON.stringify without a replacer function, + // and util's inspect and format. Any other function can keep an + // argument (Promise.resolve, Map.prototype.set, push onto an + // `unknown[]`) or pass it to a callback (a JSON.stringify replacer, an + // emitted event), which can write through it. + const readerGlobals = new Map | null>([ + ["console", null], + ["JSON", new Set(["stringify"])], + ["Array", new Set(["isArray"])], + ["Object", new Set(["is"])], ]); + const readerFunctions = new Set(["String", "Number", "Boolean"]); + const utilReaders = new Set(["inspect", "format"]); const standardLibraryReader = (node: ts.CallExpression | ts.NewExpression): boolean => panicSafe(() => { + if (!ts.isCallExpression(node)) return false; const callee = peel(node.expression); - if (ts.isPropertyAccessExpression(callee) && reflectiveWriters.has(callee.name.text)) - return false; + if (ts.isIdentifier(callee)) + return [...readerFunctions].some((name) => this.isStdlibGlobal(callee, name)); + if (!ts.isPropertyAccessExpression(callee)) return false; + const member = callee.name.text; + for (const [global, members] of readerGlobals) { + if (!this.isStdlibGlobal(callee.expression, global)) continue; + if (members !== null && !members.has(member)) return false; + if (global !== "JSON") return true; + const replacer = node.arguments.nodes[1]; + return ( + replacer === undefined || + ts.isArrayLiteralExpression(peel(replacer)) || + peel(replacer).kind === ts.SyntaxKind.NullKeyword + ); + } + if (!utilReaders.has(member)) return false; const declaration = this.checker.getResolvedSignature(node)?.declaration; - return declaration !== undefined && this.isStdlibFile(ts.getSourceFileOfNode(declaration)); + if (declaration === undefined) return false; + const file = ts.getSourceFileOfNode(declaration); + return this.isStdlibFile(file) && /[\\/]util\.d\.ts$/.test(file.fileName); }, false); const collectNativeViews = (): Set => { const keys = new Set(); @@ -6504,13 +6541,15 @@ export class Lowerer { flow(node.expression, typeOrNull(node)); else if (ts.isVariableDeclaration(node) && node.type && node.initializer) flow(node.initializer, typeOrNull(node.name)); - else if ( - ts.isBinaryExpression(node) && - node.operatorToken.kind === ts.SyntaxKind.EqualsToken - ) + else if (initializedBinding(node)) flow(node.initializer!, typeOrNull(node.name ?? node)); + else if (ts.isBinaryExpression(node) && assignmentOperators.has(node.operatorToken.kind)) flow(node.right, typeOrNull(node.left)); else if (ts.isReturnStatement(node) && node.expression) flow(node.expression, contextualOrNull(node.expression)); + else if (ts.isYieldExpression(node) && node.expression) + flow(node.expression, contextualOrNull(node.expression)); + // A thrown value reaches a catch binding, which is dynamic. + else if (ts.isThrowStatement(node)) flow(node.expression, null); else if (ts.isCallExpression(node) || ts.isNewExpression(node)) { if (!standardLibraryReader(node)) for (const arg of node.arguments?.nodes ?? []) diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index 098a72c4f..a243c94b8 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -2195,6 +2195,8 @@ ScrArr *scr_arr_fill_undefined(ScrArr *a, double start, double end) { * pending when a conversion throws. +1 malloc'd buffer. */ static double *scr_arr_coerced_numbers(const ScrArr *a, size_t *count) { size_t n = a->len; + /* A sparse array's length can exceed what the buffers can count. */ + if (n > SIZE_MAX / sizeof(double)) scr_arr_oom(); double *out = malloc((n ? n : 1) * sizeof(*out)); ScrDyn **held = calloc(n ? n : 1, sizeof(*held)); if (!out || !held) scr_arr_oom(); @@ -3555,6 +3557,7 @@ static ScrArr *scr_arr_sort_foreign(ScrArr *a, bool copy) { size_t length = a->len; size_t dense = a->len < a->cap ? a->len : a->cap; size_t cap = dense + SCR_AX(a)->sparse_len; + if (cap > SIZE_MAX / sizeof(uint64_t)) scr_arr_oom(); uint64_t *slots = cap ? malloc(cap * sizeof(*slots)) : NULL; uint8_t *states = cap ? malloc(cap) : NULL; ScrStr **keys = cap ? malloc(cap * sizeof(*keys)) : NULL; diff --git a/tests/corpus/6067-view-holes-in-typed-arrays.ts b/tests/corpus/6067-view-holes-in-typed-arrays.ts index b59437646..831b3be7a 100644 --- a/tests/corpus/6067-view-holes-in-typed-arrays.ts +++ b/tests/corpus/6067-view-holes-in-typed-arrays.ts @@ -55,3 +55,61 @@ const bag = named as unknown as Record; bag["label"] = "x"; for (const value of named) console.log("named", value); console.log(named.length, bag["label"]); + +// Views that reach dynamic code through class field initializers, +// parameter defaults, logical assignments, yield and throw, and through +// standard library calls that keep their argument or pass it to a +// callback: Promise.resolve, push onto an `unknown[]`, Map.prototype.set +// and a JSON.stringify replacer. +const fieldNumbers: number[] = [1, 2]; +class FieldView { + view: unknown[] = fieldNumbers; +} +new FieldView().view[3] = 4; +for (const value of fieldNumbers) console.log("field", value); +const defaultNumbers: number[] = [1]; +function extend(view: unknown[] = defaultNumbers): void { + view.length = 3; +} +extend(); +console.log(defaultNumbers.findIndex((value) => value === undefined), defaultNumbers.length); +let assigned: unknown[] | undefined; +const assignedNumbers: number[] = [5]; +assigned ??= assignedNumbers; +assigned[2] = 6; +for (const value of assignedNumbers) console.log("assigned", value); +const yieldedNumbers: number[] = [7]; +function* yieldViews(): Generator { + yield yieldedNumbers; +} +for (const view of yieldViews()) view[2] = 8; +console.log(yieldedNumbers.findLastIndex((value) => value === undefined), yieldedNumbers.length); +const thrownNumbers: number[] = [9]; +try { + throw thrownNumbers; +} catch (error) { + (error as unknown[])[2] = 10; +} +for (const value of thrownNumbers) console.log("thrown", value); +const retainedNumbers: number[] = [12]; +const retained: unknown[] = []; +retained.push(retainedNumbers); +(retained[0] as unknown[])[2] = 13; +for (const value of retainedNumbers) console.log("kept", value); +const mappedNumbers: number[] = [14]; +const byName = new Map(); +byName.set("a", mappedNumbers); +(byName.get("a") as unknown[])[2] = 15; +console.log(mappedNumbers.findIndex((value) => value === undefined), mappedNumbers.length); +const replacedNumbers: number[] = [16]; +JSON.stringify({ replacedNumbers: replacedNumbers }, (key, value: unknown) => { + if (key === "replacedNumbers") (value as unknown[])[2] = 17; + return value; +}); +for (const value of replacedNumbers) console.log("replaced", value); +const resolvedNumbers: number[] = [11]; +void Promise.resolve(resolvedNumbers).then((view) => { + (view as unknown[])[2] = 18; + for (const value of resolvedNumbers) console.log("resolved", value); +}); + diff --git a/tests/corpus/6068-foreign-elements-through-views.ts b/tests/corpus/6068-foreign-elements-through-views.ts index 9a64ebc70..5181d7fe5 100644 --- a/tests/corpus/6068-foreign-elements-through-views.ts +++ b/tests/corpus/6068-foreign-elements-through-views.ts @@ -112,6 +112,31 @@ paddedView[1] = { }; padded.sort(); console.log(padded.length, 3 in padded, 4 in padded, padded[5]); +const spread: number[] = [4, 3]; +const spreadView: unknown[] = spread; +let spreadCalls = 0; +spreadView.push({ + toString(): string { + if (spreadCalls++ === 0) { + spreadView[1000] = 1; + for (let i = 0; i < 64; i++) spread.push(i); + } + return "2"; + }, +}); +console.log(spread.toSorted().length, spread.length); +spread.sort(); +console.log(spread.length, spread[0], spread[1], spread[3], 999 in spread, spread[1000]); +const grownMath: number[] = [1]; +const grownMathView: unknown[] = grownMath; +grownMathView[10] = { + valueOf(): number { + grownMathView[5000] = 7; + for (let i = 0; i < 256; i++) grownMath.push(i); + return 3; + }, +}; +console.log(Math.max(...grownMath), grownMath.length); // A conversion that removes the element it converts. const shrinking: number[] = [1]; From c0d6c34f78df6c6a852a703c9c1f6b505ec589e3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 20:02:25 -0500 Subject: [PATCH 029/273] Compare integer values without converting them to doubles Two kinds of number compares still went through doubles when the integers were already at hand. Integer typed-array elements and `charCodeAt` results are integers or NaN (past the end). A compare of two such values, or of one and an exact integer, all within int32 (or all within uint32), now compares their 32-bit views, which hold ToUint32 of the value, as signed (or unsigned) integers, and folds in an ordered test of each double: NaN makes `<`, `<=`, `>`, `>=` and `===` false and `!==` true, as before. A sum, difference or product of two operands that always equal their doubles, such as `2 * i + 1` of a speculated counter, has a double that is its integer rounded once, even past 2^53. Rounding is monotonic, and the neighbors of a safe integer are doubles, so such a value compares with an exact safe integer as its integer does: the compare uses the i64s. A product by a power of two never rounds, so its wide view always equals its double. --- .../compiler/src/backend/llvm/expr-context.ts | 3 + .../src/backend/llvm/expr-primitives.ts | 76 ++++++++- .../src/backend/llvm/integer-values.ts | 49 +++++- .../test/integer-view-compares.test.ts | 16 +- .../corpus/7983-integer-compares.ts.json | 6 + tests/corpus/7983-integer-compares.ts | 150 ++++++++++++++++++ .../7983-integer-compares.ts.json | 1 + 7 files changed, 287 insertions(+), 14 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7983-integer-compares.ts.json create mode 100644 tests/corpus/7983-integer-compares.ts create mode 100644 tests/harness/corpus-costs/7983-integer-compares.ts.json diff --git a/packages/compiler/src/backend/llvm/expr-context.ts b/packages/compiler/src/backend/llvm/expr-context.ts index c6dc506f4..ac5262571 100644 --- a/packages/compiler/src/backend/llvm/expr-context.ts +++ b/packages/compiler/src/backend/llvm/expr-context.ts @@ -30,6 +30,9 @@ export interface WideInteger { name: string; below: number; bound: number; + /** The double is the integer rounded once: the result of one operation on + * operands that always equal their doubles. */ + single?: true; } export type ExprOf = Extract; diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 04af0fcef..370056ec4 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -13,13 +13,14 @@ import { emitStringInputs } from "./string-lifetimes.js"; import { emitStringParts, numberPart, stringParts } from "./string-construction.js"; import { emitBorrowedInputs } from "./borrowed-inputs.js"; import { - alwaysExactInteger, + exactCompareOperands, exactInteger, int32Number, integerNumber, narrowInteger, wideArithmetic, widenInteger, + type LlInteger, } from "./integer-values.js"; import { bitwiseIdentityOperand, @@ -45,7 +46,7 @@ import { slotWidth, type ArrayAccess, } from "./dense-array-access.js"; -import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js"; +import { emitWrappingSum, integerOrNaNRange, markWrappingOperands } from "./wrapping-sums.js"; /** The signed integer predicates of the number compares. */ const INTEGER_COMPARES: Record = { @@ -57,6 +58,63 @@ const INTEGER_COMPARES: Record = { "!==": "ne", }; +const UNSIGNED_COMPARES: Record = { + "<": "ult", + "<=": "ule", + ">": "ugt", + ">=": "uge", + "===": "eq", + "!==": "ne", +}; + +/** A number compare of operands that are each an exact integer or an + * integer-or-NaN value (integerOrNaNRange: integer typed-array elements, + * `charCodeAt` results, sums of those), all within int32 or all within + * uint32: their 32-bit views compared, and NaN comparing false (unequal for + * `!==`). The 32-bit view is the value's ToUint32, which holds its bits + * whenever it is not NaN. Null when an operand has no such view. */ +function emitIntegerOrNaNCompare( + host: LlvmEmitterContext, + e: ExprOf<"bin">, + l: LlValue, + li: LlInteger | null, + r: LlValue, + ri: LlInteger | null, +): string | null { + const lr = li?.range ?? integerOrNaNRange(host, e.left), + rr = ri?.range ?? integerOrNaNRange(host, e.right); + if (!lr || !rr) return null; + const signed = withinInt32(lr) && withinInt32(rr); + if (!signed && !(withinUint32(lr) && withinUint32(rr))) return null; + const bits = (value: LlValue, integer: LlInteger | null): string | undefined => + integer ? narrowInteger(host, integer) : value.uint32; + const left = bits(l, li), + right = bits(r, ri); + if (left === undefined || right === undefined) return null; + const B = host.B; + const compare = B.tmp(); + B.line( + `${compare} = icmp ${(signed ? INTEGER_COMPARES : UNSIGNED_COMPARES)[e.op]} i32 ${left}, ${right}`, + ); + let result = compare; + for (const [value, integer] of [ + [l, li], + [r, ri], + ] as const) { + if (integer) continue; + const ordered = B.tmp(), + combined = B.tmp(); + B.line(`${ordered} = fcmp ord double ${value.name}, 0.0`); + if (e.op === "!==") { + const unordered = B.tmp(); + B.line(`${unordered} = xor i1 ${ordered}, true`); + B.line(`${combined} = or i1 ${result}, ${unordered}`); + } else B.line(`${combined} = and i1 ${result}, ${ordered}`); + result = combined; + } + return result; +} + export function emitLiteralExpr( host: LlvmEmitterContext, e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">, @@ -360,14 +418,16 @@ export function emitOperatorExpr( return value; } } else { - // An operand with only a wide view, such as `i + 1` of a speculated - // counter: compare the integers when both always equal the doubles. - const left = alwaysExactInteger(host, l, li), - right = left === null ? null : alwaysExactInteger(host, r, ri); - if (left !== null && right !== null) { - B.line(`${t} = icmp ${INTEGER_COMPARES[e.op]} i64 ${left}, ${right}`); + // An operand with only a wide view, such as `i + 1` or `2 * i + 1` + // of a speculated counter: compare the integers when that gives the + // doubles' result. + const operands = exactCompareOperands(host, l, li, r, ri); + if (operands) { + B.line(`${t} = icmp ${INTEGER_COMPARES[e.op]} i64 ${operands[0]}, ${operands[1]}`); return { name: t, type: e.type }; } + const nanCoded = emitIntegerOrNaNCompare(host, e, l, li, r, ri); + if (nanCoded) return { name: nanCoded, type: e.type }; B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`); } } else if (bit[e.op] !== undefined) { diff --git a/packages/compiler/src/backend/llvm/integer-values.ts b/packages/compiler/src/backend/llvm/integer-values.ts index 90954f4c6..497eb5b0d 100644 --- a/packages/compiler/src/backend/llvm/integer-values.ts +++ b/packages/compiler/src/backend/llvm/integer-values.ts @@ -111,13 +111,17 @@ export function wideArithmetic( rv = view(r, ri); if (!lv || !rv) return null; let below: number, bound: number; + let single = false; if (op === "*") { if (lv.below !== Infinity || rv.below !== Infinity) return null; - below = EXACT_DOUBLES; bound = productBound(lv.bound, rv.bound); + // Scaling by a power of two never rounds a double that stays finite. + below = powerOfTwo(li) || powerOfTwo(ri) ? Infinity : EXACT_DOUBLES; + single = true; } else if (lv.below === Infinity && rv.below === Infinity) { below = EXACT_DOUBLES; bound = sumBound(lv.bound, rv.bound); + single = true; } 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); @@ -132,7 +136,48 @@ export function wideArithmetic( host.B.line( `${name} = ${op === "+" ? "add" : op === "-" ? "sub" : "mul"} nsw i64 ${left}, ${right}`, ); - return { name, below, bound }; + return single ? { name, below, bound, single } : { name, below, bound }; +} + +/** Whether an exact integer is the constant 2^k or -2^k. */ +function powerOfTwo(integer: LlInteger | null): boolean { + if (integer === null || integer.range.min !== integer.range.max) return false; + const value = Math.abs(integer.range.min); + return value >= 1 && value <= 2 ** 52 && Number.isInteger(Math.log2(value)); +} + +/** The i64 operands of a number compare that gives the doubles' result: + * both equal their doubles wherever they can be, or one is an exact integer + * of magnitude at most 2^53 - 1 and the other's double is its integer + * rounded once. Rounding is monotonic, and b - 1, b and b + 1 are doubles + * for a safe integer b, so an integer rounded once compares with b as the + * integer does. Null for any other pair. */ +export function exactCompareOperands( + host: LlvmEmitterContext, + l: LlValue, + li: LlInteger | null, + r: LlValue, + ri: LlInteger | null, +): [string, string] | null { + const describe = (value: LlValue, integer: LlInteger | null) => { + if (integer) return { exact: true, single: true, bound: magnitude(integer.range) }; + const wide = value.wideInteger; + if (!wide) return null; + return { exact: wide.bound <= wide.below, single: wide.single === true, bound: wide.bound }; + }; + const a = describe(l, li), + b = describe(r, ri); + if (!a || !b) return null; + const safe = (d: { exact: boolean; bound: number }): boolean => + d.exact && d.bound <= Number.MAX_SAFE_INTEGER; + const ok = + (a.exact && b.exact) || + (safe(a) && (b.exact || b.single)) || + (safe(b) && (a.exact || a.single)); + if (!ok) return null; + const operand = (value: LlValue, integer: LlInteger | null): string => + integer ? widenInteger(host, integer) : value.wideInteger!.name; + return [operand(l, li), operand(r, ri)]; } /** An i64 index for a consumer that compares it, unsigned, with a length or diff --git a/packages/compiler/test/integer-view-compares.test.ts b/packages/compiler/test/integer-view-compares.test.ts index e988d48bd..4e814368d 100644 --- a/packages/compiler/test/integer-view-compares.test.ts +++ b/packages/compiler/test/integer-view-compares.test.ts @@ -22,7 +22,7 @@ siftDown(a, 0, Number(process.argv[2] ?? "5")); console.log(Array.from(a).join()); `; -test("integer sums of speculated locals compare and index as integers", async () => { +test("integer values compare and index as integers", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-integer-views-")); try { const entry = join(dir, "sift.ts"); @@ -43,9 +43,17 @@ test("integer sums of speculated locals compare and index as integers", async () expect(uses("icmp slt").length).toBeGreaterThan(0); // and `a[child + 1]` bounds the integer index without a double round trip. expect(uses("icmp ult").length).toBeGreaterThan(0); - // `2 * root + 1 < end` may pass 2^53, so it keeps the double compare. - expect(body).toMatch(/fmul double 0x4000000000000000, %t\d+\n\s+%t\d+ = mul nsw i64 2/); - expect(body).toContain("fcmp olt double"); + // `2 * root + 1` may pass 2^53, but its double is its integer rounded + // once (doubling is exact) and `end` is a safe integer, so the integers + // compare as the doubles do. + const doubled = /(%t\d+) = mul nsw i64 2, %t\d+\n/.exec(body)![1]!; + const plusOne = new RegExp(`(%t\\d+) = add nsw i64 \\${doubled}, 1\\n`).exec(body)![1]!; + expect(body).toMatch(new RegExp(`= icmp slt i64 \\${plusOne}, %t\\d+\\n`)); + // `a[child]! < a[child + 1]!` compares the elements' int32 bits, and is + // false when either read is out of range (NaN). + const ordered = String.raw`\s+%t\d+ = fcmp ord double %t\d+, 0\.0\n\s+%t\d+ = and i1 %t\d+, %t\d+\n`; + expect(body).toMatch(new RegExp(String.raw`icmp slt i32 %t\d+, %t\d+\n(${ordered}){2}`)); + expect(body).toMatch(new RegExp(String.raw`icmp sge i32 %t\d+, %t\d+\n(${ordered}){2}`)); } finally { await rm(dir, { recursive: true, force: true }); } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7983-integer-compares.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7983-integer-compares.ts.json new file mode 100644 index 000000000..f7c61e778 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7983-integer-compares.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7983-integer-compares.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7983-integer-compares.ts b/tests/corpus/7983-integer-compares.ts new file mode 100644 index 000000000..1114df8a8 --- /dev/null +++ b/tests/corpus/7983-integer-compares.ts @@ -0,0 +1,150 @@ +// Compares of integer values that are not doubles in the generated code: +// typed-array elements and char codes, which read as NaN past the end, and +// sums such as `2 * i + 1` whose double is the integer rounded once, near and +// past 2^53. + +function countPairs(a: Int32Array): string { + // `j` runs one past the end, where `a[j]` is NaN and every order compare + // is false. + let lt = 0; + let ge = 0; + let eq = 0; + let ne = 0; + for (let i = 0; i < a.length; i++) { + for (let j = 0; j <= a.length; j++) { + if (a[i]! < a[j]!) lt++; + if (a[i]! >= a[j]!) ge++; + if (a[i]! === a[j]!) eq++; + if (a[i]! !== a[j]!) ne++; + } + } + return `${lt} ${ge} ${eq} ${ne}`; +} + +function pastTheEnd(a: Int32Array, b: Uint8Array, c: Uint32Array): string { + const n = a.length; + const out: boolean[] = []; + out.push(a[n]! < a[0]!, a[n]! > a[0]!, a[n]! <= a[n]!, a[n]! === a[0]!, a[0]! !== a[n]!); + out.push(a[n]! < 0, a[n]! >= -1, a[-1]! < a[0]!, b[n]! <= b[0]!, b[0]! !== b[n]!); + out.push(c[n]! < c[0]!, c[n]! >= c[n]!, c[0]! === c[0]!, c[n]! !== c[0]!); + out.push(a[0]! < b[n]!, b[0]! >= a[n]!, a[0]! < c[n]!); + return out.map((v) => (v ? 1 : 0)).join(""); +} + +function unsigned(c: Uint32Array): string { + // Elements above 2^31 compare as unsigned values. + let below = 0; + let above = 0; + let same = 0; + for (let i = 0; i < c.length; i++) { + for (let j = 0; j < c.length; j++) { + if (c[i]! < c[j]!) below++; + if (c[i]! > c[j]!) above++; + if (c[i]! === c[j]!) same++; + } + } + return `${below} ${above} ${same}`; +} + +function mixed(a: Int32Array, b: Uint8Array, c: Uint32Array, d: Int16Array): string { + let count = 0; + for (let i = 0; i < a.length; i++) { + if (a[i]! < b[i]!) count += 1; + if (a[i]! < c[i]!) count += 10; + if (c[i]! > d[i]!) count += 100; + if (b[i]! >= d[i]!) count += 1000; + if (a[i]! === c[i]!) count += 10000; + } + return String(count); +} + +function charCodes(s: string): string { + // `s.charCodeAt(s.length)` is NaN. + let rising = 0; + let falls = 0; + let equal = 0; + let differ = 0; + for (let i = 0; i < s.length; i++) { + if (s.charCodeAt(i) < s.charCodeAt(i + 1)) rising++; + if (s.charCodeAt(i) >= s.charCodeAt(i + 1)) falls++; + if (s.charCodeAt(i) === s.charCodeAt(i + 1)) equal++; + if (s.charCodeAt(i) !== s.charCodeAt(i + 1)) differ++; + } + return `${rising} ${falls} ${equal} ${differ}`; +} + +function heapChildren(start: number, end: number, steps: number): string { + // `2 * i + 1` passes 2^53 for `i` at or above 2^52, where its double + // rounds; `end` stays a safe integer. + const out: string[] = []; + for (let i = start; i < start + steps; i++) { + const child = 2 * i + 1; + out.push( + [2 * i + 1 < end, 2 * i + 1 <= end, 2 * i + 1 > end, 2 * i + 1 === end, 2 * i + 1 !== end] + .map((v) => (v ? 1 : 0)) + .join("") + ` ${child}`, + ); + } + return out.join(" "); +} + +function countNear(start: number, end: number, steps: number): string { + // The same compares in a loop over integer locals only, stopping early if + // `i` reaches `end`. + let lt = 0; + let le = 0; + let gt = 0; + let eq = 0; + let ne = 0; + for (let i = start; i < start + steps && i !== end; i++) { + if (2 * i + 1 < end) lt++; + if (2 * i + 1 <= end) le++; + if (2 * i + 1 > end) gt++; + if (2 * i + 1 === end) eq++; + if (2 * i + 1 !== end) ne++; + } + return `${lt} ${le} ${gt} ${eq} ${ne}`; +} + +function descend(root: number, end: number): number { + // A sift-down's index walk: doubles `root` until `2 * root + 1` reaches `end`. + let steps = 0; + while (2 * root + 1 < end) { + root = 2 * root + 1; + steps++; + } + return steps * 1000 + (root % 1000); +} + +function scaled(i: number, end: number): string { + // Products by a power of two never round: 4 * i and 1024 * i compare exactly. + return [4 * i < end, 4 * i === end, 1024 * i - 1 < end, 3 * i + 1 < end, 2 * i - 1 > -end] + .map((v) => (v ? 1 : 0)) + .join(""); +} + +const a = new Int32Array([5, -3, 2147483647, -2147483648, 5, 0]); +const b = new Uint8Array([0, 255, 7, 128, 5, 0]); +const c = new Uint32Array([4294967295, 2147483648, 2147483647, 0, 1, 4294967295]); +const d = new Int16Array([-32768, 32767, -1, 1, 5, 0]); +console.log(countPairs(a)); +console.log(countPairs(new Int32Array(0))); +console.log(pastTheEnd(a, b, c)); +console.log(unsigned(c)); +console.log(mixed(a, b, c, d)); +console.log(charCodes("abcddcbaÿ￿\u0000")); +console.log(charCodes("")); +const p52 = 2 ** 52; +const safe = Number.MAX_SAFE_INTEGER; +console.log(heapChildren(p52 - 2, safe, 5)); +console.log(heapChildren(p52 - 2, safe - 1, 5)); +console.log(heapChildren(p52 + 100, safe, 3)); +console.log(heapChildren(-p52 - 2, -safe, 5)); +console.log(heapChildren(0, 4, 4)); +console.log(countNear(p52 - 3, safe, 6), countNear(p52 - 3, safe - 2, 6), countNear(p52, safe, 4)); +console.log(countNear(-p52 - 3, -safe, 6), countNear(-p52 + 1, -safe + 4, 4), countNear(0, 7, 6)); +console.log(descend(0, safe), descend(1, safe), descend(p52 - 1, safe), descend(p52, safe)); +console.log(descend(3, 100), descend(0, 1), descend(5, 0)); +for (const i of [0, 1, p52 / 2 - 1, p52 / 2, p52 / 1024, p52 / 2 + 1, p52, -p52]) { + console.log(scaled(i, safe), scaled(i, safe - 1), scaled(i, 2 ** 51), scaled(i, 4)); +} diff --git a/tests/harness/corpus-costs/7983-integer-compares.ts.json b/tests/harness/corpus-costs/7983-integer-compares.ts.json new file mode 100644 index 000000000..8bfb69ce2 --- /dev/null +++ b/tests/harness/corpus-costs/7983-integer-compares.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 2100 } From 7bce848e4d69c044e7d5595a2f59f19b8e33eb84 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 20:13:09 -0500 Subject: [PATCH 030/273] Freeze class instances held in untyped values Object.freeze, Object.seal and Object.preventExtensions of a class instance held in an untyped value threw SC2020 at run time. Node changes the instance itself: writes through its typed references then throw the read-only TypeError in strict mode code, adding properties through any reference fails, and Object.isFrozen, isSealed and isExtensible report the state through either reference. - In a program that freezes, seals or prevents extensions of an untyped value that is not a fresh literal, every class hierarchy that can be held there gets the integrity word that static Object.freeze already uses, and with it the existing write checks. - The runtime applies the operation to the instance's checked view and commits the view. The commit of such a hierarchy stores the view's integrity state in the word (scr_dyn_class_view_integrity) and makes the instance's property bag not extensible with it. A hierarchy without the word still throws SC2020. - These commits replace and remove the bag's field entries whatever their attributes, so a sealed or frozen view does not leave stale field values in the bag. - The analysis behind records and arrays now starts from these calls only, rather than from every untyped freeze the lowering emits, such as the frozen strings array of a tagged template. - Programs without such calls compile as before. The runtime ABI moves to v38. Corpus 7407 covers freeze, seal and preventExtensions through unknown of classes and subclasses, writes through typed and untyped references and through methods, properties added before and after the operation, and the integrity queries, and matches Node. Refs #1374 Co-authored-by: blurname <50226652+blurname@users.noreply.github.com> --- .../builtin-apis/object-freezing.mdx | 3 +- packages/cli/test/native-link-info.test.ts | 4 +- packages/compiler/src/backend/llvm/emitter.ts | 10 +- .../src/backend/llvm/expr-stream-bridges.ts | 28 +++- 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/class-attributes.ts | 24 ++- .../lowering/expressions/property-access.ts | 2 + .../src/frontend/lowering/frozen-records.ts | 20 ++- .../src/frontend/lowering/lower-calls.ts | 7 +- .../compiler/src/frontend/lowering/lowerer.ts | 7 +- packages/compiler/src/ir/ir.ts | 5 + packages/compiler/src/ir/validate.ts | 2 + .../compiler/test/class-attributes.test.ts | 30 ++++ .../test/native-codegen-integration.test.ts | 4 +- .../7407-frozen-class-instances.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 | 9 ++ packages/runtime/src/scr_json.c | 96 +++++++++--- tests/corpus/7407-frozen-class-instances.ts | 143 ++++++++++++++++++ .../7407-frozen-class-instances.ts.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- 26 files changed, 372 insertions(+), 47 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7407-frozen-class-instances.ts.json create mode 100644 tests/corpus/7407-frozen-class-instances.ts create mode 100644 tests/harness/corpus-costs/7407-frozen-class-instances.ts.json diff --git a/docs/content/limitations/builtin-apis/object-freezing.mdx b/docs/content/limitations/builtin-apis/object-freezing.mdx index 99b78ded5..030b03b4b 100644 --- a/docs/content/limitations/builtin-apis/object-freezing.mdx +++ b/docs/content/limitations/builtin-apis/object-freezing.mdx @@ -4,7 +4,8 @@ Class instances support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible` when the static type of the argument names the class. A write to a read-only field throws Node's `TypeError` in strict mode code and leaves the field unchanged in sloppy mode code. `Object.defineProperty` with a string-literal key changes the value, `writable`, `enumerable`, and `configurable` attributes of a declared field; an object-literal accessor descriptor (`get` or `set`) for a declared field is refused at compile time with `SC2020`, and another accessor descriptor for a declared field throws `SC2020` when the call runs. - Freezing, sealing, or preventing extensions of a class instance held in an untyped value throws `SC2020`. + `Object.freeze`, `Object.seal`, and `Object.preventExtensions` of a class instance held in an untyped value change the instance itself, so writes through its typed references are checked the same way. + The `TypeError` for a write to a read-only property that was added to a class instance through an untyped value names `#` where Node names the class. Functions support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible`; the operations apply to the function's own properties (`name`, `length`, `prototype`, and the ones a program assigns). Class constructors are not supported. A write to a read-only property of a checked dictionary or a function throws a `TypeError` whose message omits Node's `'x' of object '#'` (or `of function '…'`) suffix. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 8ac579449..f85b6908f 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: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 38, marker: "scr_runtime_abi_v38" }, 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_v37"); + expect(error?.stderr).toContain("scr_runtime_abi_v38"); }); }); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 8106a6478..3bf1c34d7 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1387,6 +1387,10 @@ export class LlEmitter { * commit back into their objects. Library builds keep their commits: * their values cross a native boundary. */ readonly liveViewsReadOnly: boolean; + /** The program freezes, seals or prevents extensions of untyped values + * (IrModule.untypedIntegrity): the commits of class views store the + * view's integrity state in the instance's integrity word. */ + readonly untypedIntegrity: boolean; /** Functions whose direct calls use i32 parameters or results. */ readonly int32Abis: ReadonlyMap; /** Pure number functions with speculative integer clones. */ @@ -1599,8 +1603,12 @@ export class LlEmitter { initializerBindings: this.initializerBindings, nodes: this.nodeIndex, }); + this.untypedIntegrity = mod.untypedIntegrity === true; this.liveViewsReadOnly = - mod.lib === undefined && !dynamicStores.all && dynamicStores.names.size === 0; + mod.lib === undefined && + !this.untypedIntegrity && + !dynamicStores.all && + dynamicStores.names.size === 0; this.int32Abis = this.debug === null ? findInt32Abis(mod, this.int32Slots) : new Map(); this.immortalGlobals = new Set(mod.contracts?.immortalGlobals ?? []); this.programLifetime = new Set(mod.contracts?.programLifetime ?? []); diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 1acc75cec..757e4349f 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -437,10 +437,14 @@ export function streamTypedRefCommitAdapter( host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`); host.declare(`declare void @scr_str_release(ptr)`); host.declare(`declare void @scr_dyn_key_delete(ptr, ptr, i1 zeroext)`); + // The view of a sealed or frozen instance holds its fields as + // non-configurable entries, which the bag copies. + const withAttributes = meta.def.fields.some((field) => field.name === CLASS_ATTRIBUTES); + const placeholderSet = withAttributes ? "scr_dyn_obj_set_field" : "scr_dyn_obj_set"; if (meta.def.tracksOwnFields) { host.declare(`declare ptr @scr_dyn_retain_v(ptr)`); - host.declare(`declare void @scr_dyn_obj_set(ptr, ptr, ${host.sizeType}, ptr)`); - } + host.declare(`declare void @${placeholderSet}(ptr, ptr, ${host.sizeType}, ptr)`); + } else if (withAttributes) host.declare(`declare void @scr_dyn_obj_delete_field(ptr, ptr)`); lines.push( ` %bag = call ptr @scr_dyn_new_obj()`, ` %bag_copy = call ptr @scr_dyn_copy_property_descriptors(ptr %bag, ptr %d)`, @@ -477,14 +481,16 @@ export function streamTypedRefCommitAdapter( `bag_field${index}_present:`, ` %bag_undefined${index} = call ptr @scr_dyn_undefined()`, ` %bag_placeholder${index} = call ptr @scr_dyn_retain_v(ptr %bag_undefined${index})`, - ` call void @scr_dyn_obj_set(ptr %bag, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr %bag_placeholder${index})`, + ` call void @${placeholderSet}(ptr %bag, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr %bag_placeholder${index})`, ` br label %bag_field${index}_after`, `bag_field${index}_after:`, ); else lines.push( ` %bag_key${index} = call ptr @scr_str_new(ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`, - ` call void @scr_dyn_key_delete(ptr %bag, ptr %bag_key${index}, i1 zeroext false)`, + withAttributes + ? ` call void @scr_dyn_obj_delete_field(ptr %bag, ptr %bag_key${index})` + : ` call void @scr_dyn_key_delete(ptr %bag, ptr %bag_key${index}, i1 zeroext false)`, ` call void @scr_str_release(ptr %bag_key${index})`, ); }); @@ -516,6 +522,20 @@ export function streamTypedRefCommitAdapter( host.declare(`declare void @scr_error_commit_dyn(ptr, ptr)`); lines.push(` call void @scr_error_commit_dyn(ptr %target, ptr %d)`); } + // Untyped Object.freeze, seal and preventExtensions apply to the view; + // the instance's integrity word records them. + if (host.untypedIntegrity && meta.def.fields.some((field) => field.name === CLASS_ATTRIBUTES)) { + const bag = meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES) + ? "%bag" + : "null"; + host.declare(`declare double @scr_dyn_class_view_integrity(ptr, double, ptr)`); + lines.push( + ` %word_ptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${classFieldIndex(meta, CLASS_ATTRIBUTES).index}`, + ` %word = load double, ptr %word_ptr`, + ` %word_next = call double @scr_dyn_class_view_integrity(ptr %d, double %word, ptr ${bag})`, + ` store double %word_next, ptr %word_ptr`, + ); + } lines.push(` br label %done`, `done:`, ` ret void`, `}`, ``); host.resolveThunkDefs.push(...lines); return `@${commit}`; diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 11bde6c2b..612cd2b2e 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 = 37 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v37" as const; +export const RUNTIME_ABI_VERSION = 38 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v38" 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 9846e8d89..5227f6062 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: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 38, marker: "scr_runtime_abi_v38" }, 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 e2b24f8ca..e401d5ed2 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -128,7 +128,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 38, marker: "scr_runtime_abi_v38" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/frontend/lowering/class-attributes.ts b/packages/compiler/src/frontend/lowering/class-attributes.ts index 4e7bbe801..de7cc8ca7 100644 --- a/packages/compiler/src/frontend/lowering/class-attributes.ts +++ b/packages/compiler/src/frontend/lowering/class-attributes.ts @@ -39,8 +39,14 @@ import { runtimeRooted } from "./class-descriptors.js"; type ClassObjectType = Extract; function attributeClass(lowerer: Lowerer, target: IrExpr): ClassInfo | null { - if (!isDynTypedRefType(target.type)) return null; - const info = lowerer.classes.get(target.type.className); + return isDynTypedRefType(target.type) + ? attributeClassNamed(lowerer, target.type.className) + : null; +} + +/** The compiled class `className`, when its instances can carry attributes. */ +function attributeClassNamed(lowerer: Lowerer, className: string): ClassInfo | null { + const info = lowerer.classes.get(className); if (!info || runtimeRooted(info) || info.decl == null) return null; return info; } @@ -62,6 +68,20 @@ function addHiddenField(info: ClassInfo, name: string): void { add(root); } +/** In a program that freezes, seals or prevents extensions of untyped + * values, gives every hierarchy whose instances can be held there the + * integrity word. The runtime applies the operation to the instance's + * checked view, whose commit stores the view's integrity state in the word + * (backend streamTypedRefCommitAdapter), and the write checks below read + * it. */ +export function addUntypedIntegrityWords(lowerer: Lowerer): void { + if (!lowerer.untypedIntegrity) return; + for (const [className, info] of lowerer.classes) { + if (isDynTypedRefType({ kind: "object", className }) && attributeClassNamed(lowerer, className)) + addHiddenField(info, CLASS_ATTRIBUTES); + } +} + function hiddenFieldGet(receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrExpr { return { kind: "fieldGet", obj: receiver, className, field, type: F64, loc }; } diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 1a4883e5a..4ef003f81 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -499,6 +499,8 @@ export function lowerPropertyAccessExpr( }; } if (lowerer.stdlibGlobalMember(expr, "Object") === "freeze") { + // Object.freeze as a value can freeze any untyped value it is passed. + lowerer.untypedIntegrity = true; const name = "%builtin.Object.freeze"; if (!lowerer.liftedFns.some((fn) => fn.name === name)) lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/frozen-records.ts b/packages/compiler/src/frontend/lowering/frozen-records.ts index 30eb9e455..fc770a0cb 100644 --- a/packages/compiler/src/frontend/lowering/frozen-records.ts +++ b/packages/compiler/src/frontend/lowering/frozen-records.ts @@ -24,7 +24,8 @@ import { isStrictCode } from "./strict-mode.js"; * (scr_array.h SCR_ARR_FROZEN), which the backend's array write guards test * for the array types the module lists (IrModule.freezableArrays). A type is * freezable when a static Object.freeze names it, or, in a program that - * freezes untyped values, when a value of it or of a type reaching it can be + * freezes, seals or prevents extensions of untyped values + * (noteUntypedIntegrity), when a value of it or of a type reaching it can be * held as a live untyped reference. */ type RecordType = Extract; @@ -59,6 +60,17 @@ export function lowerNativeFreeze( }; } +/** Records that the program freezes, seals or prevents extensions of the + * untyped value `node` lowers to. Unless it is a fresh object or array + * literal, the value can be any record, array or class instance the program + * holds as an untyped reference, so those types become freezable. */ +export function noteUntypedIntegrity(lowerer: Lowerer, node: ts.Expression): void { + let inner = node; + while (ts.isParenthesizedExpression(inner) || ts.isAsExpression(inner)) inner = inner.expression; + if (!ts.isObjectLiteralExpression(inner) && !ts.isArrayLiteralExpression(inner)) + lowerer.untypedIntegrity = true; +} + function isRecordWrite(s: IrStmt): s is RecordWrite { return s.kind === "recordSet" || s.kind === "recordKeySet" || s.kind === "recordKeyDelete"; } @@ -109,7 +121,6 @@ function freezableTypes( const shapes = new Set(); const arrays = new Set(); const live: IrType[] = []; - let dynFreeze = false; for (const fn of functions) everyStmtList(fn.body, { expr: (e) => { @@ -119,13 +130,12 @@ function freezableTypes( } else if (e.kind === "libCall" && e.fn === "array.freeze") { const target = e.args[0]; if (target && target.type.kind === "array") arrays.add(typeKey(target.type)); - } else if (e.kind === "libCall" && e.fn === "dyn.freeze") dynFreeze = true; - else if (e.kind === "dynFrom") live.push(e.value.type); + } else if (e.kind === "dynFrom") live.push(e.value.type); return true; }, stmt: () => true, }); - if (!dynFreeze) return { shapes, arrays }; + if (!lowerer.untypedIntegrity) return { shapes, arrays }; // An untyped reference to a class instance, or to a record or array // boxed live, reaches the values its fields and elements hold as further // live references. Class instances also cross into untyped code through diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 8ac97fd32..0db330077 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -249,7 +249,7 @@ import { import { fenceSymbolFieldCopy } from "./symbol-fields.js"; import { lowerClassDataDescriptor, lowerClassDescriptorRead } from "./class-descriptors.js"; import { lowerClassFieldDefine, lowerClassIntegrity } from "./class-attributes.js"; -import { freezableRecordType, lowerNativeFreeze } from "./frozen-records.js"; +import { freezableRecordType, lowerNativeFreeze, noteUntypedIntegrity } from "./frozen-records.js"; import { classStaticDataFor } from "./class-static-data.js"; import { lowerUrlFactory } from "./lower-url.js"; import { nullishValueUnitOf } from "./binding-analysis.js"; @@ -8544,6 +8544,7 @@ function lowerObjectStaticCall( isSealed: "dyn.isSealed", } as const; const fn = operation[member as keyof typeof operation]; + if (!member.startsWith("is")) noteUntypedIntegrity(lowerer, call.arguments.nodes[0]!); return { kind: "libCall", fn, @@ -8610,8 +8611,10 @@ function lowerObjectStaticCall( (ts.isObjectLiteralExpression(inner) || ts.isArrayLiteralExpression(inner)) ? lowerer.lowerExprExpecting(argNode, DYN) : lowerer.lowerExpr(argNode); - if (value.type.kind === "dyn") + if (value.type.kind === "dyn") { + noteUntypedIntegrity(lowerer, argNode); return { kind: "libCall", fn: "dyn.freeze", args: [value], type: DYN, loc: locOf(call) }; + } if (ts.isObjectLiteralExpression(inner) || ts.isArrayLiteralExpression(inner)) { return value; // fresh — freeze is identity here, honestly } diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3582e8e8a..f14f20265 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -151,7 +151,7 @@ import type { FsPathPlan } from "./builtins/fs-paths.js"; import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "./optional-arguments.js"; import { type ProtocolDispatch } from "./class-protocol-dispatch.js"; import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-dispatch.js"; -import { guardClassAttributeWrites } from "./class-attributes.js"; +import { addUntypedIntegrityWords, guardClassAttributeWrites } from "./class-attributes.js"; import { guardFrozenWrites, markSloppyRecordWrites } from "./frozen-records.js"; import { applyHandleProperties } from "./handle-properties.js"; import { installBufferValue } from "./containers/bytes.js"; @@ -3296,6 +3296,9 @@ export class Lowerer { readonly recordFieldDeletes = new Set(); /** typeKeys of the array types the program may freeze (IrModule.freezableArrays). */ readonly freezableArrays = new Set(); + /** Whether the program freezes, seals or prevents extensions of untyped + * values other than fresh literals (noteUntypedIntegrity). */ + untypedIntegrity = false; readonly liftedFns: IrFunction[] = []; /** The first lifted function of each name (liftedFnNamed), for the * liftedFns indexed so far. liftedFns only grows. */ @@ -7767,6 +7770,7 @@ export class Lowerer { finalizeClassMethodValues(this, functions); finalizePrivateBrands(this, functions); trackClassFieldCreation(this, functions); + addUntypedIntegrityWords(this); guardClassAttributeWrites(this, functions); guardFrozenWrites(this, functions); applyHandleProperties(this, functions); @@ -7881,6 +7885,7 @@ export class Lowerer { ...(this.freezableArrays.size > 0 ? { freezableArrays: [...this.freezableArrays].sort() } : {}), + ...(this.untypedIntegrity ? { untypedIntegrity: true as const } : {}), ...(isNodeEsmFile(this.entry, this.program) ? {} : { commonJsEntry: true }), ...(this.ffiImports.length > 0 ? { ffiImports: [...this.ffiImports] } : {}), }; diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 5916a5166..636634dab 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1073,6 +1073,11 @@ export interface IrModule { * (frontend frozen-records.ts): their writes test the array's frozen bit * (SCR_ARR_FROZEN) and throw Node's TypeError when it is set. */ freezableArrays?: string[]; + /** The program freezes, seals or prevents extensions of untyped values + * (frontend frozen-records.ts noteUntypedIntegrity). Class hierarchies + * that can be held there carry an integrity word, which the runtime sets + * through the commit of the instance's checked view. */ + untypedIntegrity?: true; /** The entry module is CommonJS under Node's classification. Node runs a * CommonJS main synchronously and drains process.nextTick callbacks * before promise jobs at the first checkpoint; an ES-module main's diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index a694e8d2c..8152775d1 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -123,6 +123,8 @@ export function validateModule(mod: IrModule): IrValidationError[] { errors.push({ message: "worker execution mode must be a boolean", loc: moduleLoc }); if (mod.declBoundary !== undefined && mod.declBoundary !== true) errors.push({ message: "declBoundary must be true when present", loc: moduleLoc }); + if (mod.untypedIntegrity !== undefined && mod.untypedIntegrity !== true) + errors.push({ message: "untypedIntegrity must be true when present", loc: moduleLoc }); if (mod.lifetimeRegions !== undefined && mod.lifetimeRegions !== true) errors.push({ message: "lifetimeRegions must be true when present", loc: moduleLoc }); if (mod.workers && mod.lib !== undefined) diff --git a/packages/compiler/test/class-attributes.test.ts b/packages/compiler/test/class-attributes.test.ts index 1a6fea654..a0e6efa62 100644 --- a/packages/compiler/test/class-attributes.test.ts +++ b/packages/compiler/test/class-attributes.test.ts @@ -82,3 +82,33 @@ b.x = 2; expect(throwing.length).toBeGreaterThan(0); expect(throwing.every((name) => name.includes("constructor"))).toBe(true); }); + +test("an untyped freeze gives every class hierarchy attribute state", async () => { + const mod = await lowerModule(` +export {}; +class Point { x = 1; } +class Labeled extends Point { label = "a"; } +function freezeUnknown(value: unknown): unknown { return Object.freeze(value); } +function move(p: Point): void { p.x = 2; } +freezeUnknown(new Labeled()); +move(new Point()); +`); + expect(mod.untypedIntegrity).toBe(true); + expect(fieldsOf(mod, "Point")).toContain("%attributes"); + expect(fieldsOf(mod, "Labeled")).toContain("%attributes"); + expect(functionsCalling(mod, "class.throwReadOnly")).toContain("move"); +}); + +test("untyped freezes of fresh literals and tagged templates leave classes unchanged", async () => { + const mod = await lowerModule( + ` +class Point { constructor() { this.x = 1; } } +const tag = (strings) => strings.length; +const frozen = Object.freeze({ a: 1 }); +console.log(tag\`a\${1}b\`, frozen.a, new Point().x); +`, + "main.js", + ); + expect(mod.untypedIntegrity).toBeUndefined(); + expect(fieldsOf(mod, "Point")).not.toContain("%attributes"); +}); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index cecebb49c..2a0f8371e 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_v37"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v38"); 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_v37"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v38"); }); 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/7407-frozen-class-instances.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7407-frozen-class-instances.ts.json new file mode 100644 index 000000000..da868130c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7407-frozen-class-instances.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7407-frozen-class-instances.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index e492c36b4..951883c07 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: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 38, marker: "scr_runtime_abi_v38" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index e22f2a37c..988c899cf 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: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 38, marker: "scr_runtime_abi_v38" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 086340014..819121c3f 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_v37(void) {} +void scr_runtime_abi_v38(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 3707b8d50..20c591cbb 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_v37(void); +void scr_runtime_abi_v38(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 5b1598a8b..bf3b8d426 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1182,6 +1182,12 @@ void scr_dyn_obj_set_field(ScrDyn *obj, const char *key, size_t key_len, ScrDyn ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor); void scr_dyn_class_field_attributes(ScrDyn *view, ScrStr *name, double bits); void scr_dyn_class_view_attributes(ScrDyn *view, double bits); +/* The integrity state of a class instance's checked view as CLASS_* bits, + * added to `bits`. The commits of hierarchies with an integrity word store + * it in the word, in programs that freeze, seal or prevent extensions of + * untyped values. `bag`, the instance's new property bag or NULL, stops + * being extensible with the view. */ +double scr_dyn_class_view_integrity(const ScrDyn *view, double bits, ScrDyn *bag); ScrDyn *scr_class_define_field(ScrDyn *current, double field_bits, double object_bits, ScrStr *name, ScrDyn *descriptor); double scr_class_defined_bits(ScrDyn *holder, ScrStr *name); @@ -1211,6 +1217,9 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value); void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn *value); void scr_dyn_key_set_computed(ScrDyn *recv, ScrDyn *key, ScrDyn *value); void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict); +/* Removes the own property `key` of an ordinary object whatever its + * attributes, as scr_dyn_obj_set_field replaces values. */ +void scr_dyn_obj_delete_field(ScrDyn *obj, const ScrStr *key); /* This thread's global object box (borrowed). Every spelling of the global * object answers it; its symbol-keyed properties are the global object's * (scr_node_builtin.c). */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 40fd73508..2f8e4b6f8 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2389,9 +2389,74 @@ void scr_record_throw_frozen(ScrStr *key, ScrStr *owner, double operation) { scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); } -/* A class instance's integrity state lives in its native layout, which only - * statically typed calls can reach (the compiler adds the state to the - * hierarchies they name). Through an untyped value the operation throws. */ +/* ── class instance attributes ─────────────────────────────────────────── + * A hierarchy passed to Object.freeze, Object.seal, Object.preventExtensions + * or Object.defineProperty keeps an integrity state per instance and an + * attribute word per redefined field (CLASS_* and FIELD_* bits in the + * compiler's ir.ts). The checked view of an instance carries them, so + * reflection over the view answers as Node does. */ +enum { + SCR_CLASS_NOT_EXTENSIBLE = 1, + SCR_CLASS_SEALED = 2, + SCR_CLASS_FROZEN = 4, + SCR_FIELD_NOT_WRITABLE = 1, + SCR_FIELD_NOT_ENUMERABLE = 2, + SCR_FIELD_NOT_CONFIGURABLE = 4, +}; + +/* Set by scr_dyn_class_view_integrity, which only the commits of + * hierarchies with an integrity word call. */ +static SCR_TL bool scr_class_integrity_committed; + +double scr_dyn_class_view_integrity(const ScrDyn *view, double bits, ScrDyn *bag) { + scr_class_integrity_committed = true; + unsigned state = (unsigned)bits; + if (view->kind != SCR_DYN_OBJ || !view->non_extensible) return state; + state |= SCR_CLASS_NOT_EXTENSIBLE; + if (bag && bag->kind == SCR_DYN_OBJ) bag->non_extensible = true; + bool sealed = true, frozen = true; + for (size_t i = 0; i < view->v.obj.len; i++) { + const ScrDynEntry *entry = &view->v.obj.entries[i]; + if (entry->configurable) sealed = false; + if (!entry->accessor && entry->writable) frozen = false; + } + if (sealed) state |= SCR_CLASS_SEALED; + if (sealed && frozen) state |= SCR_CLASS_FROZEN; + return state; +} + +/* Object.freeze, Object.seal or Object.preventExtensions (`operation`) of + * the class instance a capsule refers to. In a program that applies them to + * untyped values, every hierarchy that can be held there keeps an + * integrity word (class-attributes.ts), and its commit stores the + * integrity state of the instance's checked view in the word + * (scr_dyn_class_view_integrity). The operation applies to the view, and + * the commit records it. A first commit of the unchanged view finds out + * whether the hierarchy has the word. Answers false for other values and + * for instances whose hierarchy has no word, whose state stays as it was, + * and true when the operation applied or threw. */ +static bool scr_dyn_class_integrity(ScrDyn *object, ScrDyn *(*operation)(ScrDyn *)) { + if (object->kind != SCR_DYN_TYPED_REF || !object->v.typed_ref.commit || + object->v.typed_ref.type_key_len < 7 || memcmp(object->v.typed_ref.type_key, "object:", 7) != 0) + return false; + ScrDyn *view = scr_dyn_typed_ref_materialize(object); + bool word = false; + if (!scr_exc_pending()) { + scr_class_integrity_committed = false; + scr_dyn_typed_ref_commit(object); + word = scr_class_integrity_committed; + } + if (word && !scr_exc_pending()) { + scr_dyn_release(operation(view)); + if (!scr_exc_pending()) scr_dyn_typed_ref_commit(object); + } + scr_dyn_release(view); + return word || scr_exc_pending(); +} + +/* A class instance's integrity state lives in its native layout. Through an + * untyped value whose hierarchy has no integrity word, the operation + * throws. */ static bool scr_dyn_class_integrity_fence(const ScrDyn *object, const char *operation) { if (object->kind != SCR_DYN_TYPED_REF) return false; ScrJsonBuf buffer; @@ -2456,6 +2521,8 @@ ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) { scr_map_prevent_extensions(collection); return scr_dyn_retain(object); } + if (scr_dyn_class_integrity(object, scr_dyn_prevent_extensions)) + return scr_exc_pending() ? NULL : scr_dyn_retain(object); if (scr_dyn_class_integrity_fence(object, "Object.preventExtensions")) return NULL; ScrDyn *table = scr_dyn_fn_integrity_table(object); if (table) { @@ -2494,6 +2561,8 @@ ScrDyn *scr_dyn_seal(ScrDyn *object) { scr_map_prevent_extensions(collection); return scr_dyn_retain(object); } + if (scr_dyn_class_integrity(object, scr_dyn_seal)) + return scr_exc_pending() ? NULL : scr_dyn_retain(object); if (scr_dyn_class_integrity_fence(object, "Object.seal")) return NULL; if (object->symbol_properties) scr_dyn_release(scr_dyn_seal(object->symbol_properties)); ScrDyn *table = scr_dyn_fn_integrity_table(object); @@ -12118,6 +12187,10 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) { } } +void scr_dyn_obj_delete_field(ScrDyn *obj, const ScrStr *key) { + if (obj->kind == SCR_DYN_OBJ) scr_json_delete_member(obj, key); +} + /* Ordinary native objects carry removable own data properties. Dense * arrays, typed references and handles keep their explicit boundary; * deleting from a materialized snapshot would lose the mutation. */ @@ -16724,6 +16797,8 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value) { if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL; ScrDyn *native = scr_dyn_freeze_native(value); if (native) return native; + if (scr_dyn_class_integrity(value, scr_dyn_freeze_data)) + return scr_exc_pending() ? NULL : scr_dyn_retain(value); 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); @@ -16829,21 +16904,6 @@ ScrDyn *scr_caught_to_dyn(const ScrCaught *c) { } } -/* ── class instance attributes ─────────────────────────────────────────── - * A hierarchy passed to Object.freeze, Object.seal, Object.preventExtensions - * or Object.defineProperty keeps an integrity state per instance and an - * attribute word per redefined field (CLASS_* and FIELD_* bits in the - * compiler's ir.ts). The checked view of an instance carries them, so - * reflection over the view answers as Node does. */ -enum { - SCR_CLASS_NOT_EXTENSIBLE = 1, - SCR_CLASS_SEALED = 2, - SCR_CLASS_FROZEN = 4, - SCR_FIELD_NOT_WRITABLE = 1, - SCR_FIELD_NOT_ENUMERABLE = 2, - SCR_FIELD_NOT_CONFIGURABLE = 4, -}; - void scr_dyn_class_field_attributes(ScrDyn *view, ScrStr *name, double bits) { unsigned b = (unsigned)bits; if (b == 0 || view->kind != SCR_DYN_OBJ) return; diff --git a/tests/corpus/7407-frozen-class-instances.ts b/tests/corpus/7407-frozen-class-instances.ts new file mode 100644 index 000000000..0732b8c35 --- /dev/null +++ b/tests/corpus/7407-frozen-class-instances.ts @@ -0,0 +1,143 @@ +// Object.freeze, Object.seal and Object.preventExtensions of class instances +// held in untyped values. The operation changes the instance itself: writes +// through its typed references, through untyped references and through its +// methods then throw Node's TypeError (this module is strict mode code), and +// Object.isFrozen, isSealed and isExtensible report the state through either +// reference. +export {}; + +function report(e: unknown): void { + console.log((e as Error).name + ": " + (e as Error).message); +} + +function freezeUnknown(value: unknown): unknown { + return Object.freeze(value); +} + +function sealUnknown(value: unknown): unknown { + return Object.seal(value); +} + +function preventUnknown(value: unknown): unknown { + return Object.preventExtensions(value); +} + +function integrity(value: unknown): string { + return [Object.isFrozen(value), Object.isSealed(value), Object.isExtensible(value)].join(" "); +} + +function writeAny(value: any, key: string, next: unknown): void { + value[key] = next; +} + +function deleteAny(value: any, key: string): void { + delete value[key]; +} + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + move(dx: number): void { + this.x += dx; + } +} + +class Labeled extends Point { + label: string; + constructor(x: number, y: number, label: string) { + super(x, y); + this.label = label; + } + rename(label: string): void { + this.label = label; + } +} + +class Counter { + count = 0; + tags: string[] = []; + bump(): number { + return ++this.count; + } +} + +function frozen(): void { + const p = new Point(1, 2); + console.log(integrity(p)); + const same = freezeUnknown(p); + console.log(same === p, integrity(p), Object.isFrozen(p)); + try { p.x = 5; } catch (e) { report(e); } + try { p.move(1); } catch (e) { report(e); } + try { writeAny(p, "y", 9); } catch (e) { report(e); } + try { writeAny(p, "z", 1); } catch (e) { report(e); } + console.log(p.x, p.y, JSON.stringify(p)); + + const l = new Labeled(3, 4, "a"); + const base: Point = l; + freezeUnknown(base); + console.log(integrity(l), integrity(base)); + try { l.rename("b"); } catch (e) { report(e); } + try { base.x = 0; } catch (e) { report(e); } + try { writeAny(l, "label", "c"); } catch (e) { report(e); } + console.log(l.x, l.label, l instanceof Labeled); + freezeUnknown(l); + console.log(integrity(l)); +} + +function sealed(): void { + const c = new Counter(); + sealUnknown(c); + console.log(integrity(c)); + console.log(c.bump(), c.bump()); + c.tags.push("kept"); + try { writeAny(c, "extra", 1); } catch (e) { report(e); } + writeAny(c, "count", 10); + console.log(c.count, c.tags.join(), JSON.stringify(c)); + freezeUnknown(c); + console.log(integrity(c)); + try { c.bump(); } catch (e) { report(e); } + c.tags.push("still"); + console.log(c.count, c.tags.join()); +} + +function notExtensible(): void { + const l = new Labeled(0, 0, "n"); + preventUnknown(l); + console.log(integrity(l)); + l.move(2); + l.rename("m"); + try { writeAny(l, "extra", true); } catch (e) { report(e); } + console.log(l.x, l.label, Object.keys(l).join()); + sealUnknown(l); + console.log(integrity(l)); + writeAny(l, "y", 7); + console.log(l.y); +} + +function untouched(): void { + const p = new Point(5, 6); + const q = new Point(7, 8); + freezeUnknown(q); + p.x = 50; + writeAny(p, "y", 60); + console.log(p.x, p.y, integrity(p), integrity(q)); +} + +function added(): void { + const p = new Point(1, 1); + writeAny(p, "extra", 1); + freezeUnknown(p); + console.log(integrity(p), Object.keys(p).join()); + try { deleteAny(p, "extra"); } catch (e) { report(e); } + console.log((p as any).extra, JSON.stringify(p)); +} + +frozen(); +sealed(); +notExtensible(); +untouched(); +added(); diff --git a/tests/harness/corpus-costs/7407-frozen-class-instances.ts.json b/tests/harness/corpus-costs/7407-frozen-class-instances.ts.json new file mode 100644 index 000000000..34061c3eb --- /dev/null +++ b/tests/harness/corpus-costs/7407-frozen-class-instances.ts.json @@ -0,0 +1 @@ +{ "plain": 3923, "san": 3742 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c5476328e..030408deb 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -109,7 +109,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 38, marker: "scr_runtime_abi_v38" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From 15cc7310aca7924abc26f6d2ef91c0543ab945ef Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 15:15:34 -0700 Subject: [PATCH 031/273] Keep record values that subtype reduction folds into an inferred type TypeScript infers a function's return type as the subtype-reduced union of the values it returns: returning `{ status }` in one branch and `{ status, value }` (or a call returning either) in another infers just `{ status }`. Records have fixed layouts, so that type cannot hold the second value: the return failed its union wrap at run time ('... is not representable in the target union'), and values that reached a binding of the reduced type failed a checked narrowing. - A function with an inferred return type and no contextual signature returns the union of the record types it actually returns, including through calls of other such functions. Only value types the checker's reduction dropped from the inferred union are added, so functions whose inferred type already names every returned type keep their layout. Declared return types keep their meaning. - Reading such a union through the reduced type (a binding, a member read, JSON.stringify) uses a dedicated checked extraction that converts the wider record arms with the ordinary width coercion; other narrowings are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/builtins/json.ts | 10 +- .../src/frontend/lowering/coercions/state.ts | 9 +- .../src/frontend/lowering/coercions/unions.ts | 71 ++++++++- .../lowering/expressions/property-access.ts | 20 ++- .../src/frontend/lowering/function-returns.ts | 143 ++++++++++++++++++ .../compiler/src/frontend/lowering/lowerer.ts | 10 +- .../src/frontend/lowering/union-narrow.ts | 17 +++ ...nferred-return-overlapping-records.ts.json | 6 + ...990-inferred-return-overlapping-records.ts | 59 ++++++++ ...nferred-return-overlapping-records.ts.json | 1 + 10 files changed, 340 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7990-inferred-return-overlapping-records.ts.json create mode 100644 tests/corpus/7990-inferred-return-overlapping-records.ts create mode 100644 tests/harness/corpus-costs/7990-inferred-return-overlapping-records.ts.json diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 64f829865..5aad5077f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -1,3 +1,4 @@ +import { widensReducedRecords } from "../function-returns.js"; import { keyedHelperName } from "../stable-names.js"; import { isDateJsonSource } from "./dates.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; @@ -96,7 +97,14 @@ export function lowerJsonMethodCall( !isDateJsonSource(lowerer, argNode) ) { const narrowed = lowerer.mapTypeOf(lowerer.typeOf(argNode)); - if (narrowed && !isUnitType(narrowed) && narrowed.kind !== "void") { + // A union of record types the checker's subtype reduction spelled as + // one of them serializes each value by its own layout. + if ( + narrowed && + !isUnitType(narrowed) && + narrowed.kind !== "void" && + !widensReducedRecords(lowerer, narrowed, value.type) + ) { const helper = narrowed.kind === "union" ? lowerer.narrowedRetagHelper(argNode, value.type.unionId, narrowed.unionId, loc) diff --git a/packages/compiler/src/frontend/lowering/coercions/state.ts b/packages/compiler/src/frontend/lowering/coercions/state.ts index ad1d7c6ea..3c2ecf7bd 100644 --- a/packages/compiler/src/frontend/lowering/coercions/state.ts +++ b/packages/compiler/src/frontend/lowering/coercions/state.ts @@ -12,7 +12,14 @@ export class CoercionState { // Provenance for checked extraction; consumers must not infer it from names. readonly checkedNarrows = new Set(); // The extracted arm of each checked (non-defaulting) extraction helper. - readonly checkedNarrowArms = new Map(); + readonly checkedNarrowArms = new Map< + string, + { unionId: string; tag: number; target: IrType; widens?: true } + >(); readonly retags = new Map(); readonly narrows = new Map(); + readonly widthNarrows = new Map(); + /** Unions widenReducedReturn produced: only these read through the + * width-converting extraction (widthNarrowHelper). */ + readonly widenedReturns = new Set(); } diff --git a/packages/compiler/src/frontend/lowering/coercions/unions.ts b/packages/compiler/src/frontend/lowering/coercions/unions.ts index 434e687a5..776892487 100644 --- a/packages/compiler/src/frontend/lowering/coercions/unions.ts +++ b/packages/compiler/src/frontend/lowering/coercions/unions.ts @@ -421,6 +421,46 @@ export function narrowedArmHelper( return name; } +/** Interned `%union.narrow.width.(u)`: the checked extraction of a + * record arm from a union that also holds records whose fields strictly + * include the arm's (a function's widened inferred return, see + * widenReducedReturn). Those arms convert like any record width coercion; + * every other arm throws like narrowedArmHelper's. */ +export function widthNarrowHelper( + lowerer: Lowerer, + fromId: string, + target: IrType, + loc: SrcLoc, +): string | null { + const from = lowerer.unions.get(fromId); + if (!from || target.kind !== "record") return null; + const tag = lowerer.armTag(fromId, target); + if (tag < 0) return null; + const key = `${fromId}:${tag}`; + const existing = lowerer.coercions.widthNarrows.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, "%union.narrow.width", key); + lowerer.coercions.widthNarrows.set(key, name); + const fn = buildUnionNarrow( + name, + from, + target, + loc, + (type) => lowerer.fmt(type), + null, + (armTag, payload) => widthSubtypeConversion(lowerer, from.arms[armTag]!, target, payload, loc), + ); + if (!fn) throw new InternalCompilerError("lowerer bug: invalid checked union extraction"); + lowerer.coercions.checkedNarrowArms.set(name, { + unionId: fromId, + tag, + target: from.arms[tag]!, + widens: true, + }); + lowerer.liftedFns.push(fn); + return name; +} + /** A checked extraction at its use. A plain read tests the stored tag in * place and projects the payload directly, so member reads can borrow it; * every other arm throws the helper's TypeError with the same message. @@ -442,7 +482,8 @@ export function checkedNarrowCall( value.type.kind !== "union" || value.type.unionId !== arm.unionId || !typeEquals(type, arm.target) || - !isSafeToRepeat(value) + !isSafeToRepeat(value) || + arm.widens === true ) return call; const unionId = arm.unionId; @@ -599,3 +640,31 @@ export function deferredReadHelper( lowerer.liftedFns.push(fn); return name; } + +/** Whether `arm` is a record whose fields strictly include those of the + * record `target`: a value of the target type under TypeScript's structural + * typing, which the checker's subtype reduction may have spelled as the + * target (`{ status } | { status, value }` infers `{ status }`). */ +export function isWidthSubtypeRecord(lowerer: Lowerer, arm: IrType, target: IrType): boolean { + if (arm.kind !== "record" || target.kind !== "record") return false; + const own = lowerer.shapes.get(arm.shapeId); + const want = lowerer.shapes.get(target.shapeId); + if (!own || !want || own.tuple || want.tuple || own.indexValue || want.indexValue) return false; + if (want.fields.length >= own.fields.length) return false; + const names = new Set(own.fields.map((f) => f.name)); + return want.fields.every((f) => names.has(f.name)); +} + +/** A checked narrow that meets a width-subtype record arm converts it like + * any record width coercion instead of reporting a lying narrowing. */ +function widthSubtypeConversion( + lowerer: Lowerer, + arm: IrType, + target: IrType, + payload: IrExpr, + loc: SrcLoc, +): IrExpr | null { + if (!isWidthSubtypeRecord(lowerer, arm, target)) return null; + const lift = lowerer.widthLiftPlan(arm, target); + return lift === null ? null : lowerer.applyWidthLift(lift, payload, target, loc); +} diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 9894cfb71..bac0d0daa 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1,3 +1,5 @@ +import { widensReducedRecords } from "../function-returns.js"; +import { widthNarrowHelper } from "../coercions/unions.js"; /* 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"; @@ -1293,7 +1295,23 @@ export function lowerPropertyAccessExpr( // honest one, not a generic recitation about the outer dot. A // receiver that lowers cleanly means the MEMBER is the gap — name // the property and the receiver's type instead of "property access". - const recvLowered = lowerer.lowerExpr(expr.expression); + let recvLowered = lowerer.lowerExpr(expr.expression); + // A receiver whose value union holds record types the checker's subtype + // reduction folded into its type (`{ status } | { status, value }` typed + // `{ status }`, a call of a function with an inferred return type): read + // through the checker's type, converting the wider arms. + if (recvLowered.type.kind === "union") { + const declared = lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); + if ( + declared !== null && + declared.kind === "record" && + widensReducedRecords(lowerer, declared, recvLowered.type) + ) { + const helper = widthNarrowHelper(lowerer, recvLowered.type.unionId, declared, loc); + if (helper !== null) + recvLowered = lowerer.checkedNarrowCall(helper, recvLowered, declared, loc); + } + } // A dyn receiver — JSON.parse's `any`, or `unknown` the checker // narrowed to `object` — reads through the dyn keyed read: member // or undefined (JS's own-property answer), throwing JS's TypeError diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index dde109ea7..b1132a8b9 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -11,12 +11,14 @@ import { VOID, isUnitType, typeEquals, + typeKey, } from "../../ir/ir.js"; import { isJsSourceFile } from "../program.js"; import { PoisonError, dynFallbackType } from "./lowerer.js"; import { functionCanReturnUndefined } from "../function-completion.js"; import { jsBindingAppendsOnly, jsBindingHasOpenWrites } from "./lower-stmts.js"; import type { FnCtx } from "./function-context.js"; +import { constituentTypes } from "../ts7/checker.js"; /** Keep the conservative completion rule shared by return construction * and inference. Nested control flow may still make completion unreachable. */ @@ -175,6 +177,8 @@ export function declaredReturnType( // (`.action(() => { throw ... })`). `never` VALUES stay unmapped. if (retTsType.flags & ts.TypeFlags.Never) return VOID; let mappedReturn = lowerer.mapTypeOf(retTsType); + if (mappedReturn !== null) + mappedReturn = widenReducedReturn(lowerer, decl, retTsType, mappedReturn); // A generator declared to return Iterable returns a generator over T. if (mappedReturn && decl.asteriskToken) mappedReturn = iterableGeneratorArm(lowerer, mappedReturn); @@ -522,3 +526,142 @@ export function blockBodyOf(decl: ts.FunctionLikeDeclaration): ts.Block | null { if (ts.isBlock(body)) return body; return ts.forEachChild(decl, (c) => (ts.isBlock(c) ? c : undefined)) ?? null; } + +const widening = new Set(); + +/** An inferred return type is the checker's SUBTYPE-REDUCED union of the + * returned values' types: `return aborted` (`{ status }`) beside `return + * { status, value }` infers just `{ status }`, because the second is a + * subtype of the first. Records have fixed layouts, so that type cannot + * hold the second value: the return would drop `value` or fail the union + * wrap. A function whose return type is inferred therefore returns the + * union of the record types it actually returns. Declared return types keep + * their meaning. */ +function widenReducedReturn( + lowerer: Lowerer, + decl: ts.SignatureDeclaration, + retTsType: ts.Type, + mapped: IrType, +): IrType { + if (decl.type !== undefined || hasExplicitJsDocReturn(decl)) return mapped; + const body = (decl as { body?: ts.Node }).body; + if (body === undefined || decl.asteriskToken !== undefined) return mapped; + if (decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return mapped; + // A function expression with a contextual signature returns that + // signature's type; only free-standing inference is widened. + if ( + (ts.isFunctionExpression(decl) || ts.isArrowFunction(decl)) && + lowerer.checker.getContextualType(decl) !== undefined + ) + return mapped; + const arms = + mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped]; + const recordArms = arms.filter((a) => a.kind === "record"); + if (recordArms.length === 0 || widening.has(decl)) return mapped; + const checkerArms = new Set( + retTsType.flags & ts.TypeFlags.Union ? constituentTypes(retTsType) : [retTsType], + ); + const fieldsOf = (t: IrType): readonly string[] | null => { + if (t.kind !== "record") return null; + const shape = lowerer.shapes.get(t.shapeId); + return shape === undefined || shape.tuple || shape.indexValue !== undefined + ? null + : shape.fields.map((f) => f.name); + }; + const narrower = (part: IrType): boolean => { + const own = fieldsOf(part); + if (own === null || arms.some((a) => typeKey(a) === typeKey(part))) return false; + return recordArms.some((arm) => { + const fields = fieldsOf(arm); + return fields !== null && fields.length < own.length && fields.every((f) => own.includes(f)); + }); + }; + const extra = new Map(); + const returned = (expr: ts.Expression): void => { + // A call of another inferred function returns ITS widened type. + const callee = ts.isCallExpression(expr) + ? lowerer.checker.getResolvedSignature(expr)?.declaration + : undefined; + if ( + callee !== undefined && + ts.isFunctionLike(callee) && + (callee as { body?: ts.Node }).body !== undefined && + callee.type === undefined + ) { + let t: IrType | null = null; + widening.add(decl); + try { + t = declaredReturnType(lowerer, callee, expr); + } catch { + t = null; + } finally { + widening.delete(decl); + } + if (t === null) return; + const parts = t.kind === "union" ? (lowerer.unions.get(t.unionId)?.arms ?? []) : [t]; + for (const part of parts) if (narrower(part)) extra.set(typeKey(part), part); + return; + } + // Only a value type the checker's reduction dropped from the inferred + // union is a candidate; a constituent of the inferred type is already + // represented by it. + const valueType = lowerer.typeOf(expr); + const valueArms = + valueType.flags & ts.TypeFlags.Union ? constituentTypes(valueType) : [valueType]; + for (const part of valueArms) { + if (checkerArms.has(part) || !(part.flags & ts.TypeFlags.Object)) continue; + if (!lowerer.checker.isTypeAssignableTo(part, retTsType)) continue; + const t = lowerer.mapTypeOf(part); + if (t !== null && narrower(t)) extra.set(typeKey(t), t); + } + }; + const scan = (node: ts.Node): void => { + if (node !== decl && ts.isFunctionLike(node)) return; + if (ts.isReturnStatement(node) && node.expression !== undefined) returned(node.expression); + ts.forEachChild(node, scan); + }; + if (ts.isArrowFunction(decl) && !ts.isBlock(body)) returned(body as ts.Expression); + else scan(body); + if (extra.size === 0) return mapped; + const joined = new Map(); + for (const a of [...arms, ...extra.values()]) joined.set(typeKey(a), a); + const sorted = [...joined.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + const unionId = lowerer.unions.intern(sorted); + lowerer.coercions.widenedReturns.add(unionId); + return { kind: "union", unionId }; +} + +/** Whether `wide` is a widened inferred return that holds `narrow`'s arms plus records that are width + * subtypes of `narrow`'s record arms: the shape a subtype-reduced inferred + * type takes next to the values it was inferred from. */ +export function widensReducedRecords(lowerer: Lowerer, narrow: IrType, wide: IrType): boolean { + if (wide.kind !== "union" || typeKey(narrow) === typeKey(wide)) return false; + if (!lowerer.coercions.widenedReturns.has(wide.unionId)) return false; + const narrowArms = + narrow.kind === "union" ? (lowerer.unions.get(narrow.unionId)?.arms ?? []) : [narrow]; + const wideArms = lowerer.unions.get(wide.unionId)?.arms ?? []; + const keys = new Set(wideArms.map(typeKey)); + if (!narrowArms.every((a) => keys.has(typeKey(a)))) return false; + const fieldsOf = (t: IrType): readonly string[] | null => { + if (t.kind !== "record") return null; + const shape = lowerer.shapes.get(t.shapeId); + return shape === undefined || shape.tuple || shape.indexValue !== undefined + ? null + : shape.fields.map((f) => f.name); + }; + const narrowKeys = new Set(narrowArms.map(typeKey)); + const extras = wideArms.filter((a) => !narrowKeys.has(typeKey(a))); + return ( + extras.length > 0 && + extras.every((extra) => { + const own = fieldsOf(extra); + return ( + own !== null && + narrowArms.some((arm) => { + const f = fieldsOf(arm); + return f !== null && f.length < own.length && f.every((x) => own.includes(x)); + }) + ); + }) + ); +} diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 7aad166e9..4bf938027 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -115,6 +115,7 @@ import { checkedNarrowCall, checkedNarrowOperand, deferredReadHelper, + widthNarrowHelper, } from "./coercions/unions.js"; import { lowerUtilTypeValue, @@ -443,7 +444,7 @@ import { bindingNeverReassigned } from "./binding-analysis.js"; import type { InstantiationPath } from "./instantiation-path.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import { completeArgs, wrappedUndefined, undefinedArgFor } from "./call-arguments.js"; -import { bodyReturnType, declaredReturnType } from "./function-returns.js"; +import { bodyReturnType, declaredReturnType, widensReducedRecords } from "./function-returns.js"; import { lowerCall, lowerPromiseMethodCall, lowerFilterNarrowCall } from "./lower-calls.js"; import { lowerFfiCall, validateFfiImports } from "./native-ffi.js"; import { @@ -3464,6 +3465,7 @@ export class Lowerer { this.internTableHelpers, this.coercions.retags, this.coercions.narrows, + this.coercions.widthNarrows, ]) for (const name of table.values()) interned.add(name); for (const fn of [...this.liftedFns, ...this.implicitFns]) @@ -10013,7 +10015,11 @@ export class Lowerer { expected.kind !== "void" && this.armTag(expr.type.unionId, expected) >= 0 ) { - const helper = this.narrowedArmHelper(expr.type.unionId, expected, expr.loc); + // A function's widened inferred return (widenReducedReturn) also + // holds records with more fields; those convert instead of throwing. + const helper = widensReducedRecords(this, expected, expr.type) + ? widthNarrowHelper(this, expr.type.unionId, expected, expr.loc) + : this.narrowedArmHelper(expr.type.unionId, expected, expr.loc); if (helper) { return this.checkedNarrowCall(helper, expr, expected, expr.loc); } diff --git a/packages/compiler/src/frontend/lowering/union-narrow.ts b/packages/compiler/src/frontend/lowering/union-narrow.ts index 3bb09cb26..7ae036fad 100644 --- a/packages/compiler/src/frontend/lowering/union-narrow.ts +++ b/packages/compiler/src/frontend/lowering/union-narrow.ts @@ -28,6 +28,9 @@ export function buildUnionNarrow( loc: SrcLoc, formatType: (type: IrType) => string, undefinedDefault: IrExpr | null = null, + /** Converts another arm's payload into the target (a record arm that is + * a width subtype of a record target), or null to throw. */ + convert: ((tag: number, payload: IrExpr) => IrExpr | null) | null = null, ): IrFunction | null { if (isUnitType(target) || target.kind === "void") return null; const targetTag = from.arms.findIndex((arm) => typeEquals(arm, target)); @@ -43,6 +46,7 @@ export function buildUnionNarrow( const inputType: IrType = { kind: "union", unionId: from.id }; const input: IrExpr = { kind: "varRef", localId: "u.0", type: inputType, loc }; const body: IrStmt[] = []; + let converted: IrExpr | null = null; for (let tag = 0; tag < from.arms.length; tag++) { const arm = from.arms[tag]!; let branch: IrStmt[]; @@ -56,6 +60,19 @@ export function buildUnionNarrow( ]; } else if (arm.kind === "undefinedT" && undefinedDefault !== null) { branch = [{ kind: "return", value: undefinedDefault, loc }]; + } else if ( + convert !== null && + !isUnitType(arm) && + (converted = convert(tag, { + kind: "unionNarrow", + unionId: from.id, + tag, + value: input, + type: arm, + loc, + })) !== null + ) { + branch = [{ kind: "return", value: converted, loc }]; } else { branch = [ { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7990-inferred-return-overlapping-records.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7990-inferred-return-overlapping-records.ts.json new file mode 100644 index 000000000..6a55f1f2e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7990-inferred-return-overlapping-records.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7990-inferred-return-overlapping-records.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7990-inferred-return-overlapping-records.ts b/tests/corpus/7990-inferred-return-overlapping-records.ts new file mode 100644 index 000000000..c2d1c9ea0 --- /dev/null +++ b/tests/corpus/7990-inferred-return-overlapping-records.ts @@ -0,0 +1,59 @@ +// Functions whose inferred return type is a union of overlapping records. +// TypeScript's subtype reduction infers `{ status }` for a function that +// returns `{ status }` in one branch and `{ status, value }` in another, so +// the inferred type is narrower than the values. Returned values keep their +// fields, chained returns keep them, and bindings typed by the reduced type +// read their declared fields. +type Aborted = { status: string }; +const ABORTED: Aborted = { status: "aborted" }; + +function inner(ok: boolean, value: unknown) { + if (!ok) return ABORTED; + return { status: "valid", value }; +} + +// Returns inner's union through a call: inferred as `{ status }` alone. +function outer(ok: boolean, value: unknown) { + if (value === "stop") return ABORTED; + return inner(ok, value); +} + +function merge(entries: [string, number][]) { + const result: Record = {}; + for (const [key, n] of entries) { + if (key === "__proto__") return ABORTED; + result[key] = n; + } + return { status: "valid", value: result }; +} + +function parse(entries: [string, number][]) { + if (entries.length === 0) return ABORTED; + return merge(entries); +} + +console.log(JSON.stringify(outer(true, [1, 2]))); +console.log(JSON.stringify(outer(false, 1))); +console.log(JSON.stringify(outer(true, "stop"))); +console.log(JSON.stringify(parse([["a", 1], ["b", 2]]))); +console.log(JSON.stringify(parse([]))); + +// Into untyped positions: the value keeps every field. +const seen: unknown[] = []; +seen.push(outer(true, { nested: true })); +seen.push(parse([["c", 3]])); +console.log(JSON.stringify(seen)); +const loose: unknown = outer(true, "kept"); +console.log(JSON.stringify(loose)); + +// Bindings typed by the reduced type: declared fields read as in Node. +const r = outer(true, 42); +console.log(r.status); +let status = ""; +for (const ok of [true, false]) status += outer(ok, 0).status + " "; +console.log(status.trim()); +function statusOf(ok: boolean): string { + const local = outer(ok, null); + return local.status; +} +console.log(statusOf(true), statusOf(false)); diff --git a/tests/harness/corpus-costs/7990-inferred-return-overlapping-records.ts.json b/tests/harness/corpus-costs/7990-inferred-return-overlapping-records.ts.json new file mode 100644 index 000000000..7da9b7545 --- /dev/null +++ b/tests/harness/corpus-costs/7990-inferred-return-overlapping-records.ts.json @@ -0,0 +1 @@ +{ "plain": 3700 } From c9ce961458c83e81d798e181cbb313a1b79e6df2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 20:54:08 -0500 Subject: [PATCH 032/273] Restore the inspection context when a custom inspector throws --- .../src/frontend/lowering/lower-inspect.ts | 33 ++++++++++++------ .../compiler/src/ir/builtin-signatures.ts | 7 ++-- .../corpus/8125-inspect-custom-throws.ts.json | 6 ++++ .../8126-inspect-custom-throws.cjs.json | 6 ++++ packages/runtime/src/scr_inspect.c | 34 +++++++++++++++---- packages/runtime/src/scr_inspect.h | 8 ++--- tests/corpus/8125-inspect-custom-throws.ts | 32 +++++++++++++++++ tests/corpus/8126-inspect-custom-throws.cjs | 30 ++++++++++++++++ .../8125-inspect-custom-throws.ts.json | 1 + .../8126-inspect-custom-throws.cjs.json | 1 + 10 files changed, 134 insertions(+), 24 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8125-inspect-custom-throws.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8126-inspect-custom-throws.cjs.json create mode 100644 tests/corpus/8125-inspect-custom-throws.ts create mode 100644 tests/corpus/8126-inspect-custom-throws.cjs create mode 100644 tests/harness/corpus-costs/8125-inspect-custom-throws.ts.json create mode 100644 tests/harness/corpus-costs/8126-inspect-custom-throws.cjs.json diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 86255ca56..334c1dcac 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1277,21 +1277,34 @@ function customInspectHelper( const resultT = found.sig.ret; const call = accessorCall(lowerer, t.className, INSPECT_CUSTOM_MEMBER, v(), args, resultT, loc); // The method runs in a fresh inspect context (insp.customCallBegin); the - // walk's own context returns before the result renders. - const callStmt = (fn: "insp.customCallBegin" | "insp.customCallEnd"): IrStmt => ({ + // walk's own context returns before the result renders. The token lets + // an enclosing end restore this walk's context when the method throws. + locals.push({ id: "tok.0", name: "tok", type: F64, mutable: false }); + const end: IrStmt = { kind: "exprStmt", - expr: { kind: "libCall", fn, args: [], type: VOID, loc }, + expr: { + kind: "libCall", + fn: "insp.customCallEnd", + args: [varRef("tok.0", F64, loc)], + type: VOID, + loc, + }, loc, - }); - const render: IrStmt[] = [...argDecls, callStmt("insp.customCallBegin")]; + }; + const render: IrStmt[] = [ + ...argDecls, + { + kind: "varDecl", + localId: "tok.0", + init: { kind: "libCall", fn: "insp.customCallBegin", args: [], type: F64, loc }, + loc, + }, + ]; if (resultT.kind === "void") { - render.push({ kind: "exprStmt", expr: call, loc }, callStmt("insp.customCallEnd")); + render.push({ kind: "exprStmt", expr: call, loc }, end); } else { locals.push({ id: "res.0", name: "res", type: resultT, mutable: false }); - render.push( - { kind: "varDecl", localId: "res.0", init: call, loc }, - callStmt("insp.customCallEnd"), - ); + render.push({ kind: "varDecl", localId: "res.0", init: call, loc }, end); } const res = (): IrExpr => varRef("res.0", resultT, loc); if (resultT.kind === "string") { diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index d8f150605..4730f283d 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4492,9 +4492,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "insp.customIsSelf": { argTypes: [DYN, DYN], result: BOOL }, "insp.customResult": { argTypes: [DYN, F64, F64], result: STRING }, /** Around the method's call: the inspect and format calls it makes run - * in a fresh inspect context with the default options. */ - "insp.customCallBegin": { argTypes: [], result: VOID }, - "insp.customCallEnd": { argTypes: [], result: VOID }, + * in a fresh inspect context with the default options. begin answers the + * token end takes; end also discards what a call that threw left. */ + "insp.customCallBegin": { argTypes: [], result: F64 }, + "insp.customCallEnd": { argTypes: [F64], result: VOID }, /** Whether a runtime format string has a '%' before its last character. */ "insp.formatSpecifies": { argTypes: [STRING], result: BOOL }, "insp.formatSpecifiesDyn": { argTypes: [DYN], result: BOOL }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8125-inspect-custom-throws.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8125-inspect-custom-throws.ts.json new file mode 100644 index 000000000..1dc598fd4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8125-inspect-custom-throws.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8125-inspect-custom-throws.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8126-inspect-custom-throws.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8126-inspect-custom-throws.cjs.json new file mode 100644 index 000000000..374de8db3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8126-inspect-custom-throws.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8126-inspect-custom-throws.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 9905518ab..9cfe3a3ef 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -1706,6 +1706,8 @@ typedef struct { double break_length; double compact; const ScrDyn *user_options; + size_t nsaved; /* the nested-context stack's depth before the call */ + size_t nframes; /* the frame stack's depth before the call */ } InspCallSaved; static SCR_TL InspCallSaved *g_call_saved; @@ -1721,8 +1723,13 @@ static void insp_call_saved_cleanup(void) { /* A custom inspect function's call: Node gives each util.inspect and * util.format call its own ctx, so the calls the function makes start * with no indentation, no seen objects, no colors and the default layout - * and options. end restores the walk's context. */ -void scr_insp_custom_call_begin(void) { + * and options. begin answers a token; end(token) restores the walk's + * context as it was at that begin. A call that threw skips its end, and + * an enclosing custom function's end (or the walk's, when the function + * caught the error) discards what the throw left: the contexts of calls + * that never ended and the frames of renders that never finished. */ +double scr_insp_custom_call_begin(void) { + size_t nsaved = g_nsaved; scr_insp_nested_begin(); if (g_ncall_saved == g_call_saved_cap) { if (g_call_saved_cap == 0) scr_atexit(insp_call_saved_cleanup); @@ -1730,7 +1737,10 @@ void scr_insp_custom_call_begin(void) { g_call_saved = realloc(g_call_saved, g_call_saved_cap * sizeof(InspCallSaved)); if (!g_call_saved) insp_oom(); } + double token = (double)g_ncall_saved; InspCallSaved *sv = &g_call_saved[g_ncall_saved++]; + sv->nsaved = nsaved; + sv->nframes = g_nframes; sv->colors = g_colors; sv->custom_inspect = g_custom_inspect; sv->sorted = g_sorted; @@ -1747,10 +1757,20 @@ void scr_insp_custom_call_begin(void) { g_depth_number = 0; g_user_options = NULL; scr_insp_layout(80, 3, false); + return token; } -void scr_insp_custom_call_end(void) { - InspCallSaved *sv = &g_call_saved[--g_ncall_saved]; +void scr_insp_custom_call_end(double token) { + size_t depth = (size_t)token; + if (depth >= g_ncall_saved) return; /* an enclosing end already restored it */ + InspCallSaved *sv = &g_call_saved[depth]; + g_ncall_saved = depth; + while (g_nframes > sv->nframes) { + InspFrame *f = &g_frames[--g_nframes]; + for (size_t i = 0; i < f->len; i++) scr_str_release(f->items[i]); + free(f->items); + } + g_pending_seen = NULL; g_colors = sv->colors; g_custom_inspect = sv->custom_inspect; g_no_break = sv->no_break; @@ -1758,7 +1778,7 @@ void scr_insp_custom_call_end(void) { g_depth_number = sv->depth_number; g_user_options = sv->user_options; scr_insp_layout(sv->break_length, sv->compact, sv->sorted); - scr_insp_nested_restore(); + while (g_nsaved > sv->nsaved) scr_insp_nested_restore(); } /* The style inspect.styles maps a styleType to, as [open, close] codes: @@ -2024,7 +2044,7 @@ static ScrStr *insp_custom(ScrDyn *d, double recurse, double depth, bool *thrown } args[2] = insp_inspect_value(); ScrDyn *ret; - scr_insp_custom_call_begin(); + double token = scr_insp_custom_call_begin(); if (compiled) { ScrDyn *call[4] = {d, args[0], args[1], args[2]}; ret = scr_dyn_call(fn, call, 4, "[nodejs.util.inspect.custom]"); @@ -2033,7 +2053,7 @@ static ScrStr *insp_custom(ScrDyn *d, double recurse, double depth, bool *thrown ret = scr_dyn_call(fn, args, 3, "[nodejs.util.inspect.custom]"); scr_dyn_this_pop(); } - scr_insp_custom_call_end(); + scr_insp_custom_call_end(token); for (size_t i = 0; i < 3; i++) scr_dyn_release(args[i]); scr_dyn_release(fn); if (!ret) { diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h index d56f17bba..9619f0dee 100644 --- a/packages/runtime/src/scr_inspect.h +++ b/packages/runtime/src/scr_inspect.h @@ -127,10 +127,10 @@ void scr_insp_nested_restore(void); * value for the caller to restore. */ bool scr_insp_custom_inspect(bool enabled); /* Around a custom inspect function's call: begin gives the calls it makes - * a fresh inspect context with the default options; end restores the - * caller's. */ -void scr_insp_custom_call_begin(void); -void scr_insp_custom_call_end(void); + * a fresh inspect context with the default options and answers a token; + * end(token) restores the caller's, also after calls that threw. */ +double scr_insp_custom_call_begin(void); +void scr_insp_custom_call_end(double token); /* How the unbounded depth of the renders that follow was given, for custom * inspect functions: 0 Infinity, 1 null, 2 undefined. Answers the * previous kind for the caller to restore. */ diff --git a/tests/corpus/8125-inspect-custom-throws.ts b/tests/corpus/8125-inspect-custom-throws.ts new file mode 100644 index 000000000..33077a32b --- /dev/null +++ b/tests/corpus/8125-inspect-custom-throws.ts @@ -0,0 +1,32 @@ +// A [util.inspect.custom] method that throws inside an outer inspect: an +// enclosing custom method that catches the error, and the walk around it, +// continue with their own indentation, circular references and colors. +import * as util from "node:util"; +class Throws { + [util.inspect.custom](): string { + throw new Error("inner"); + } +} +class Catches { + [util.inspect.custom](): string { + let note = ""; + try { + note = util.inspect({ a: { b: [new Throws()] } }); + } catch (e) { + note = "caught " + (e as Error).message; + } + return note + " then " + util.inspect({ x: { y: [1, 2] } }); + } +} +const shared = { k: 1 }; +type Outer = { first: { deep: Catches }; list: unknown[]; s: object; t: object; me?: Outer }; +const outer: Outer = { first: { deep: new Catches() }, list: [new Catches(), { z: 3 }], s: shared, t: shared }; +outer.me = outer; +console.log(util.inspect(outer, { breakLength: 50 })); +console.log(JSON.stringify(util.inspect([new Catches(), "s"], { colors: true }))); +try { + console.log(util.inspect({ a: { b: new Throws() } })); +} catch (e) { + console.log("outer caught", (e as Error).message); +} +console.log({ after: { deep: { x: 1 } }, list: [1, 2, 3] }); diff --git a/tests/corpus/8126-inspect-custom-throws.cjs b/tests/corpus/8126-inspect-custom-throws.cjs new file mode 100644 index 000000000..8d4e97608 --- /dev/null +++ b/tests/corpus/8126-inspect-custom-throws.cjs @@ -0,0 +1,30 @@ +// A [util.inspect.custom] method that throws inside an outer inspect: an +// enclosing custom method that catches the error, and the walk around it, +// continue with their own indentation and colors. +const util = require("node:util"); +class Throws { + [util.inspect.custom]() { + throw new Error("inner"); + } +} +class Catches { + [util.inspect.custom]() { + let note = ""; + try { + note = util.inspect({ a: { b: [new Throws()] } }); + } catch (e) { + note = "caught " + e.message; + } + return note + " then " + util.inspect({ x: { y: [1, 2] } }); + } +} +const shared = { k: 1 }; +const outer = { first: { deep: new Catches() }, list: [new Catches(), { z: 3 }], s: shared, t: shared }; +console.log(util.inspect(outer, { breakLength: 50 })); +console.log(JSON.stringify(util.inspect([new Catches(), "s"], { colors: true }))); +try { + console.log(util.inspect({ a: { b: new Throws() } })); +} catch (e) { + console.log("outer caught", e.message); +} +console.log({ after: { deep: { x: 1 } }, list: [1, 2, 3] }); diff --git a/tests/harness/corpus-costs/8125-inspect-custom-throws.ts.json b/tests/harness/corpus-costs/8125-inspect-custom-throws.ts.json new file mode 100644 index 000000000..7d8f08f56 --- /dev/null +++ b/tests/harness/corpus-costs/8125-inspect-custom-throws.ts.json @@ -0,0 +1 @@ +{ "plain": 3090, "san": 5295 } diff --git a/tests/harness/corpus-costs/8126-inspect-custom-throws.cjs.json b/tests/harness/corpus-costs/8126-inspect-custom-throws.cjs.json new file mode 100644 index 000000000..49ea4c6d4 --- /dev/null +++ b/tests/harness/corpus-costs/8126-inspect-custom-throws.cjs.json @@ -0,0 +1 @@ +{ "plain": 2826, "san": 4308 } From 01e88d0b4862fb9d3b5c86e1e0b7a5f372e774b8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 21:30:21 -0500 Subject: [PATCH 033/273] Support named properties and Symbol.species on typed arrays held as untyped values --- packages/cli/test/native-link-info.test.ts | 4 +- 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 | 5 +- .../compiler/test/llvm-runtime-abi.test.ts | 7 +- .../test/native-codegen-integration.test.ts | 4 +- .../7753-typed-array-named-properties.js.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_bytes.c | 13 +- packages/runtime/src/scr_bytes.h | 8 + packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_dyn.h | 6 + packages/runtime/src/scr_dyn_invoke.c | 14 + packages/runtime/src/scr_inspect.c | 79 ++- packages/runtime/src/scr_json.c | 497 ++++++++++++++++-- packages/runtime/src/scr_symbol.c | 10 + .../7752-typed-array-dynamic-methods.js | 12 + .../7753-typed-array-named-properties.js | 143 +++++ .../7753-typed-array-named-properties.js.json | 1 + .../typed-array-property-refusals.test.ts | 64 +++ 23 files changed, 836 insertions(+), 53 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7753-typed-array-named-properties.js.json create mode 100644 tests/corpus/7753-typed-array-named-properties.js create mode 100644 tests/harness/corpus-costs/7753-typed-array-named-properties.js.json create mode 100644 tests/harness/typed-array-property-refusals.test.ts diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 8ac579449..187dbc0d5 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: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 40, marker: "scr_runtime_abi_v40" }, 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_v37"); + expect(error?.stderr).toContain("scr_runtime_abi_v40"); }); }); diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 11bde6c2b..229d8ec33 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 = 37 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v37" as const; +export const RUNTIME_ABI_VERSION = 40 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v40" 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 9846e8d89..6e7a09fb3 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: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 40, marker: "scr_runtime_abi_v40" }, 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 e2b24f8ca..b2f5995fb 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -128,7 +128,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 40, marker: "scr_runtime_abi_v40" }, 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 073ebed91..9eaf9535b 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1087,7 +1087,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (!argNode || (ts.isIdentifier(argNode) && argNode.text === "undefined")) { return { kind: "libCall", fn: "sym.newAnon", args: [], type: SYMBOL_T, loc }; } - const desc = lowerer.lowerExpr(argNode); + let desc = lowerer.lowerExpr(argNode); + // A number or boolean description is its ToString. + if (desc.type.kind === "f64" || desc.type.kind === "bool") + desc = lowerer.ensureString(desc, argNode); if (desc.type.kind !== "string") { lowerer.noLowering( `Symbol with a '${lowerer.fmt(desc.type)}' description`, diff --git a/packages/compiler/test/llvm-runtime-abi.test.ts b/packages/compiler/test/llvm-runtime-abi.test.ts index 6cfd862b7..a457feb72 100644 --- a/packages/compiler/test/llvm-runtime-abi.test.ts +++ b/packages/compiler/test/llvm-runtime-abi.test.ts @@ -355,12 +355,13 @@ describe("LLVM backend declares match scr_runtime.h prototypes", () => { #include "scr_runtime.h" /* LLVM emits: %ScrBytes = type { i64, i64, i32, ptr, ptr, i8, i8, i8, i8, ptr }. - * Keep every ABI fact used by its field-index GEPs explicit here. */ + * Keep every ABI fact used by its field-index GEPs explicit here. The C + * struct continues with fields only the runtime reads. */ _Static_assert(sizeof(size_t) == 8, "LLVM ScrBytes expects 64-bit size_t"); _Static_assert(sizeof(void *) == 8, "LLVM ScrBytes expects 64-bit pointers"); _Static_assert(sizeof(ScrBytesElem) == 4, "LLVM ScrBytes elem field is i32"); _Static_assert(_Alignof(ScrBytes) == 8, "LLVM ScrBytes alignment changed"); -_Static_assert(sizeof(ScrBytes) == 56, "LLVM ScrBytes size changed"); +_Static_assert(sizeof(ScrBytes) == 64, "ScrBytes size changed"); _Static_assert(offsetof(ScrBytes, rc) == 0, "LLVM ScrBytes.rc offset changed"); _Static_assert(offsetof(ScrBytes, len) == 8, "LLVM ScrBytes.len offset changed"); _Static_assert(offsetof(ScrBytes, elem) == 16, "LLVM ScrBytes.elem offset changed"); @@ -370,7 +371,9 @@ _Static_assert(offsetof(ScrBytes, is_buffer) == 40, "LLVM ScrBytes.is_buffer off _Static_assert(offsetof(ScrBytes, is_data_view) == 41, "LLVM ScrBytes.is_data_view offset changed"); _Static_assert(offsetof(ScrBytes, external) == 42, "LLVM ScrBytes.external offset changed"); _Static_assert(offsetof(ScrBytes, untransferable) == 43, "LLVM ScrBytes.untransferable offset changed"); +_Static_assert(offsetof(ScrBytes, properties_traced) == 44, "ScrBytes.properties_traced must stay in padding"); _Static_assert(offsetof(ScrBytes, shared) == 48, "LLVM ScrBytes.shared offset changed"); +_Static_assert(offsetof(ScrBytes, properties) == 56, "ScrBytes.properties must follow the LLVM prefix"); `, ); const driver = resolveCc(); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index cecebb49c..d35cbfefd 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_v37"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v40"); 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_v37"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v40"); }); 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/7753-typed-array-named-properties.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7753-typed-array-named-properties.js.json new file mode 100644 index 000000000..74a4316d9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7753-typed-array-named-properties.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7753-typed-array-named-properties.js" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index e492c36b4..27cafe022 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: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 40, marker: "scr_runtime_abi_v40" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index e22f2a37c..318880a54 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: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 40, marker: "scr_runtime_abi_v40" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 3926bf83e..e761641c2 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -190,6 +190,8 @@ ScrBytes *scr_bytes_take_data(uint8_t *data, size_t len) { b->external = false; b->untransferable = false; b->shared = NULL; + b->properties = NULL; + b->properties_traced = false; #ifdef SCR_RC_AUDIT scr_live_bytes++; #endif @@ -304,6 +306,7 @@ void scr_bytes_release(ScrBytes *b) { if (!b || b->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */ if (--b->rc == 0) { scr_weak_dispose_inline(b); + if (b->properties) scr_dyn_release(b->properties); if (b->backing) { scr_bytes_release(b->backing); /* a view: data points into the owner */ } else if (b->shared) { @@ -740,6 +743,8 @@ ScrBytes *scr_bytes_subarray(ScrBytes *b, double start, double end) { v->external = false; v->untransferable = false; v->shared = owner->shared; + v->properties = NULL; + v->properties_traced = false; #ifdef SCR_RC_AUDIT scr_live_bytes++; #endif @@ -866,7 +871,9 @@ ScrBytes *scr_bytes_from_dyn(ScrBytesElem elem, const ScrDyn *value, bool from) if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(elem, value->v.bytes); if (!from && scr_buffer_storage_is(value)) return scr_array_buffer_view(elem, value, scr_dyn_undefined(), scr_dyn_undefined()); if (!from && (value->kind == SCR_DYN_NUM || value->kind == SCR_DYN_BOOL || - value->kind == SCR_DYN_STR || value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF)) { + value->kind == SCR_DYN_STR || value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF || + value->kind == SCR_DYN_SYMBOL || value->kind == SCR_DYN_BIGINT)) { + /* ToIndex: a Symbol or BigInt length throws ToNumber's TypeError. */ double number; if (!scr_dyn_number_coerce_js(value, &number)) return NULL; return scr_bytes_new(elem, number); @@ -992,6 +999,8 @@ ScrBytes *scr_dataview_new(ScrBytes *src, double byte_off, bool has_len, double v->external = false; v->untransferable = false; v->shared = owner->shared; + v->properties = NULL; + v->properties_traced = false; #ifdef SCR_RC_AUDIT scr_live_bytes++; #endif @@ -2883,6 +2892,8 @@ static ScrBytes *scr_pool_take(size_t len) { v->external = false; v->untransferable = false; v->shared = NULL; + v->properties = NULL; + v->properties_traced = false; #ifdef SCR_RC_AUDIT scr_live_bytes++; #endif diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 7c0a935cc..64e3e299a 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -65,7 +65,15 @@ typedef struct ScrBytes { bool is_data_view; /* DataView is distinct from a numeric typed array. */ bool external; /* Root aliases foreign memory; releasing it never frees data. */ bool untransferable; /* A Buffer pool slab: its ArrayBuffer cannot be transferred. */ + /* The cycle collector's last trace of `properties` from the one checked + * value that held this view (see scr_dyn_trace_v). In padding, so the + * offsets compiled code uses hold. */ + bool properties_traced; ScrSharedBytes *shared; /* Neutral backing, borrowed by views from their local owner. */ + /* Named properties a program adds to the view (`ta.foo = 1`, + * Object.defineProperty): a checked object, NULL until the first one. + * Runtime-only and last, so compiled code's field indices hold. */ + struct ScrDyn *properties; } ScrBytes; /* Copies out of and into shared storage, made of relaxed atomic accesses diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 086340014..06cc7ccd7 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_v37(void) {} +void scr_runtime_abi_v40(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 3707b8d50..bdb9942fe 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_v37(void); +void scr_runtime_abi_v40(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 bd76c12e2..b4955220a 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -917,6 +917,9 @@ void scr_dyn_install_iterator_symbol(ScrDyn *key); void scr_dyn_install_has_instance_symbol(ScrDyn *key); void scr_dyn_install_async_iterator_symbol(ScrDyn *key); void scr_dyn_install_async_dispose_symbol(ScrDyn *key); +void scr_dyn_install_species_symbol(ScrDyn *key); +/* Records each well-known symbol the symbol unit creates. */ +void scr_dyn_install_well_known_symbol(ScrDyn *key); /* True when key is Symbol.asyncDispose (registered by the symbol unit when * the program names it); handle symbol_get ops outside that unit use it. */ bool scr_dyn_is_async_dispose_symbol(const ScrDyn *key); @@ -1003,6 +1006,9 @@ ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrS 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); +/* A typed array's, Buffer's or DataView's own named property (+1), or NULL + * without an exception when it has none. */ +ScrDyn *scr_bytes_named_get(const ScrDyn *view, const char *key, size_t key_len); /* The Buffer-flavored twin (stream chunks): string coercion/toString * decode utf8 instead of joining elements. */ ScrDyn *scr_dyn_new_buffer(const ScrBytes *b); diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index b76fe38d2..96035cccc 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -1602,6 +1602,20 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_BYTES) { ScrBytes *bytes = recv->v.bytes; + /* An own property of the view shadows the prototype's method. */ + ScrDyn *member = bytes->properties ? scr_bytes_named_get(recv, method, strlen(method)) : NULL; + if (!member && scr_exc_pending()) return NULL; + if (member) { + ScrDyn *result = NULL; + if (!scr_dyn_is_callable(member)) dyn_throw_not_fn(what); + else { + scr_dyn_this_push_dyn(recv); + result = scr_dyn_call(member, args, argc, what); + scr_dyn_this_pop(); + } + scr_dyn_release(member); + return result; + } if (bytes->is_data_view) { static const char *const kinds[] = { "Uint8", "Int8", "Uint16", "Int16", "Uint32", "Int32", "Float32", "Float64", "BigUint64", "BigInt64", "Float16" }; const bool get = !strncmp(method, "get", 3); diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 011a1f582..ce85c00ff 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -743,6 +743,52 @@ ScrStr *scr_insp_regex(ScrRegex *re) { return ib_take(&out); } +/* Named properties of a typed array, Buffer or DataView (its + * ScrBytes.properties table): slot k is the k-th string-keyed entry, then + * the k-th symbol key. */ +static size_t insp_view_property_slots(const ScrDyn *props) { + if (!props) return 0; + return props->v.obj.len + (props->symbol_keys ? props->symbol_keys->v.arr.len : 0); +} + +static ScrStr *insp_accessor_tag(const ScrDynEntry *ent); +static void insp_key_into(InspBuf *b, const char *key, size_t len); + +/* Slot k's `key: value` entry when it is enumerable, else NULL. */ +static ScrStr *insp_view_property(ScrDyn *props, size_t k, double recurse, double depth) { + const ScrDynEntry *ent; + ScrStr *key = NULL; + if (k < props->v.obj.len) { + ent = &props->v.obj.entries[k]; + } else { + ScrDyn *symbol = props->symbol_keys->v.arr.items[k - props->v.obj.len]; + ent = scr_dyn_own_symbol_entry(props, symbol); + if (ent && ent->enumerable) key = scr_insp_dyn(symbol, recurse, depth); + } + if (!ent || !ent->enumerable) return NULL; + ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse, depth); + InspBuf eb = {0}; + if (key) ib_bytes(&eb, key->data, key->len); + else insp_key_into(&eb, ent->key, ent->key_len); + ib_cstr(&eb, ": "); + ib_bytes(&eb, val->data, val->len); + scr_str_release(key); + scr_str_release(val); + return ib_take(&eb); +} + +static size_t insp_view_property_count(ScrDyn *props) { + size_t count = 0; + for (size_t k = 0; k < insp_view_property_slots(props); k++) { + if (k < props->v.obj.len) count += props->v.obj.entries[k].enumerable; + else { + const ScrDynEntry *ent = scr_dyn_own_symbol_entry(props, props->symbol_keys->v.arr.items[k - props->v.obj.len]); + count += ent && ent->enumerable; + } + } + return count; +} + /* Buffer: the custom-inspect hex form ``, capped at * INSPECT_MAX_BYTES (50); renders fully at any depth (custom inspection * runs before the depth check in Node). u8 element kind only (the @@ -765,6 +811,17 @@ ScrStr *scr_insp_buffer(ScrBytes *b) { remaining > 1 ? "s" : ""); ib_bytes(&out, more, (size_t)n); } + /* Named properties follow as `, key: value`, rendered as the members of + * an object at depth 0 (Buffer's custom inspect). */ + bool first = true; + for (size_t k = 0; k < insp_view_property_slots(b->properties); k++) { + ScrStr *entry = insp_view_property(b->properties, k, 1, 2); + if (!entry) continue; + if (!first || b->len) ib_cstr(&out, ", "); + first = false; + ib_bytes(&out, entry->data, entry->len); + scr_str_release(entry); + } ib_char(&out, '>'); return ib_take(&out); } @@ -848,6 +905,12 @@ static ScrStr *scr_insp_data_view(const ScrBytes *b, double recurse, double dept entry = ib_take(&eb); scr_insp_entry(entry, false); scr_str_release(entry); + for (size_t k = 0; k < insp_view_property_slots(b->properties); k++) { + entry = insp_view_property(b->properties, k, recurse + 1, depth); + if (!entry) continue; + scr_insp_entry(entry, false); + scr_str_release(entry); + } ScrStr *base = scr_str_new("", 0); ScrStr *b0 = scr_str_new("DataView {", 10); ScrStr *b1 = scr_str_new("}", 1); @@ -1455,7 +1518,10 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth) { char prefix[48]; int pn = snprintf(prefix, sizeof prefix, "%s(%zu) [", scr_bytes_elem_name(b->elem), b->len); - if (b->len == 0) { + /* Named properties follow the elements as `key: value` entries. */ + ScrDyn *props = b->properties; + size_t nprops = insp_view_property_count(props); + if (b->len == 0 && nprops == 0) { InspBuf out = {0}; ib_bytes(&out, prefix, (size_t)pn); ib_char(&out, ']'); @@ -1479,10 +1545,19 @@ ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth) { scr_insp_entry(m, true); scr_str_release(m); } + size_t entries = shown + (more ? 1 : 0); + for (size_t k = 0; nprops && k < insp_view_property_slots(props); k++) { + ScrStr *entry = insp_view_property(props, k, recurse + 1, depth); + if (!entry) continue; + /* Node's grouping pads by the element at the entry's output position. */ + scr_insp_entry(entry, entries < b->len); + scr_str_release(entry); + entries++; + } ScrStr *base = scr_str_new("", 0); ScrStr *b0 = scr_str_new(prefix, (size_t)pn); 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, nprops ? entries > 100 : more); 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 4af8c63b5..928d43490 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -202,6 +202,33 @@ void scr_dyn_install_has_instance_symbol(ScrDyn *key) { scr_atexit(scr_has_instance_symbol_cleanup); } +/* Symbol.species, installed when the program first evaluates it: no + * constructor names a species before then. */ +static SCR_TL ScrDyn *scr_species_symbol; +static void scr_species_symbol_cleanup(void) { + scr_dyn_release(scr_species_symbol); + scr_species_symbol = NULL; +} +void scr_dyn_install_species_symbol(ScrDyn *key) { + if (scr_species_symbol) return; + scr_species_symbol = scr_dyn_retain(key); + scr_atexit(scr_species_symbol_cleanup); +} + +/* The well-known symbols the program has created. They live until exit, + * so their identities are compared without a reference. */ +static SCR_TL const ScrSym *scr_well_known_symbols[24]; +static SCR_TL size_t scr_well_known_count; +void scr_dyn_install_well_known_symbol(ScrDyn *key) { + if (scr_well_known_count < sizeof scr_well_known_symbols / sizeof scr_well_known_symbols[0]) + scr_well_known_symbols[scr_well_known_count++] = key->v.symbol.value; +} +static bool scr_dyn_well_known_symbol(const ScrDyn *key) { + for (size_t i = 0; i < scr_well_known_count; i++) + if (scr_well_known_symbols[i] == key->v.symbol.value) return true; + return false; +} + void scr_dyn_install_iterator_symbol(ScrDyn *key) { if (scr_iterator_symbol) return; scr_iterator_symbol = scr_dyn_retain(key); @@ -880,6 +907,17 @@ void scr_dyn_trace_v(void *ptr, ScrTraceVisit visit, void *ctx) { } break; case SCR_DYN_FUNC: visit(d->v.fn.clo, ctx); break; + case SCR_DYN_BYTES: { + /* A view's named properties belong to its ScrBytes. Only a view this + * value alone holds owns that edge; a shared view's table stays out of + * trial deletion. Teardown reads the snapshot, not the count. */ + ScrBytes *bytes = d->v.bytes; + if (bytes->properties) { + bytes->properties_traced = bytes->rc == 1; + if (bytes->properties_traced) visit(bytes->properties, ctx); + } + break; + } case SCR_DYN_PROXY: visit(d->v.proxy.target, ctx); visit(d->v.proxy.handler, ctx); @@ -1144,6 +1182,8 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { scr_str_release(d->v.str); break; case SCR_DYN_BYTES: + /* The collector frees a traced table itself. */ + if (collected && d->v.bytes->properties && d->v.bytes->properties_traced) d->v.bytes->properties = NULL; scr_bytes_release(d->v.bytes); break; case SCR_DYN_ARR: @@ -2417,10 +2457,27 @@ static bool scr_dyn_typed_ref_view_answer(ScrDyn *object, bool (*query)(const Sc return true; } +static ScrDyn *scr_bytes_properties(const ScrDyn *v); static bool scr_dyn_is_extensible_view(const ScrDyn *view) { + if (view->kind == SCR_DYN_BYTES) return !view->v.bytes->properties || !view->v.bytes->properties->non_extensible; return (view->kind == SCR_DYN_OBJ || view->kind == SCR_DYN_ARR) && !view->non_extensible; } +/* A view's integrity state lives on its named-property table. A typed array + * with elements cannot be frozen, and cannot be sealed while extensible: as + * in V8, the named properties are sealed or frozen and the view made + * non-extensible before the TypeError. */ +static bool scr_bytes_integrity_fence(const ScrDyn *view, const char *verb) { + if (view->v.bytes->is_data_view || view->v.bytes->len == 0) return false; + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "Cannot "); + scr_jb_puts(&message, verb); + scr_jb_puts(&message, " array buffer views with elements"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + return true; +} + /* A plain function's own properties (name, length, prototype, and the ones * a program adds) live in its property table, which carries the function's * integrity state too: freezing or sealing the function freezes or seals @@ -2467,6 +2524,7 @@ ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) { if (object->kind == SCR_DYN_ARR && object->v.arr.properties) object->v.arr.properties->non_extensible = true; } + if (object->kind == SCR_DYN_BYTES) scr_bytes_properties(object)->non_extensible = true; return scr_dyn_retain(object); } @@ -2495,6 +2553,12 @@ ScrDyn *scr_dyn_seal(ScrDyn *object) { return scr_dyn_retain(object); } if (scr_dyn_class_integrity_fence(object, "Object.seal")) return NULL; + if (object->kind == SCR_DYN_BYTES) { + bool extensible = scr_dyn_is_extensible_view(object); + scr_dyn_release(scr_dyn_seal(scr_bytes_properties(object))); + if (extensible && scr_bytes_integrity_fence(object, "seal")) return NULL; + return scr_dyn_retain(object); + } if (object->symbol_properties) scr_dyn_release(scr_dyn_seal(object->symbol_properties)); ScrDyn *table = scr_dyn_fn_integrity_table(object); if (table) { @@ -2530,6 +2594,8 @@ bool scr_dyn_is_sealed(const ScrDyn *object) { if (object->kind == SCR_DYN_ARR) return object->non_extensible && (object->v.arr.len == 0 || object->v.arr.sealed) && (!object->v.arr.properties || scr_dyn_is_sealed(object->v.arr.properties)); + /* V8 answers from a typed array's named properties alone. */ + if (object->kind == SCR_DYN_BYTES) return object->v.bytes->properties && scr_dyn_is_sealed(object->v.bytes->properties); if (object->kind != SCR_DYN_OBJ) return true; if (!object->non_extensible) return false; for (size_t i = 0; i < object->v.obj.len; i++) if (object->v.obj.entries[i].configurable) return false; @@ -2736,6 +2802,65 @@ static bool scr_dyn_data_view_proto_name(const ScrStr *key) { return scr_dyn_name_in(key, names, sizeof names / sizeof names[0]); } +/* A canonical numeric string ("1", "-0", "1.5", "Infinity"): typed arrays + * answer those keys as element indices, never as named properties. */ +static bool scr_bytes_numeric_key(const ScrStr *key, double *index) { + if (key->len == 2 && memcmp(key->data, "-0", 2) == 0) { *index = -0.0; return true; } + *index = scr_string_to_number(key); + char spelling[32]; + size_t length = scr_f64_to_str(*index, spelling); + return length == key->len && memcmp(spelling, key->data, length) == 0; +} + +/* Whether the view has an own named property `key`. */ +static bool scr_bytes_has_named(const ScrDyn *v, const ScrStr *key) { + const ScrDyn *properties = v->v.bytes->properties; + return properties && scr_dyn_obj_get(properties, key->data, key->len); +} + +/* The view's own named property `key` read with the view as a getter's + * `this` (+1), or NULL without an exception when the view has none. Only + * reads the table, so callers do not link the typed array methods. */ +ScrDyn *scr_bytes_named_get(const ScrDyn *view, const char *key, size_t key_len) { + const ScrDyn *properties = view->v.bytes->properties; + if (!properties || !scr_dyn_obj_get(properties, key, key_len)) return NULL; + return scr_dyn_obj_read_receiver(properties, key, key_len, view); +} + +/* The view's named-property table, created on first use. Borrowed. The + * table belongs to the ScrBytes, which several checked values can wrap. */ +static ScrDyn *scr_bytes_properties(const ScrDyn *v) { + ScrBytes *bytes = v->v.bytes; + if (!bytes->properties) bytes->properties = scr_dyn_new_obj_null_proto(); + return bytes->properties; +} + +/* Accessors without a setter on the view's prototype chain: writing one + * that the view does not own throws V8's strict-mode TypeError. */ +static bool scr_bytes_getter_name(const ScrDyn *v, const ScrStr *key) { + static const char *const names[] = { "byteLength", "byteOffset", "buffer", "length", "parent", "offset" }; + size_t count = v->v.bytes->is_data_view ? 3 : v->buffer ? 6 : 4; + return scr_dyn_name_in(key, names, count); +} + +/* Keys that the runtime's conversions, iteration and printing of a view + * read from its prototype without consulting its own properties. */ +static bool scr_bytes_protocol_name(const ScrStr *key) { + static const char *const names[] = { "toString", "valueOf", "toLocaleString", "toJSON", "join" }; + return scr_dyn_name_in(key, names, sizeof names / sizeof names[0]); +} + +static void scr_bytes_protocol_fence(const char *key, size_t key_len) { + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "defining "); + scr_jb_write(&message, key, key_len); + scr_jb_puts(&message, " on a typed array, Buffer or DataView is not supported yet"); + ScrStr *text = scr_jb_finish(&message); + scr_throw_error_msg_code(SCR_ERR_ERROR, text->data, text->len, "SC2020"); + scr_str_release(text); +} + /* `in` on a typed array, Buffer or DataView. On a typed array, a canonical * numeric key (one that ToString(ToNumber(key)) reproduces, or "-0") * answers by bounds alone: an integral index, not -0, below the length. @@ -2743,6 +2868,7 @@ static bool scr_dyn_data_view_proto_name(const ScrStr *key) { * Buffer, or Object.prototype. A DataView has no indexed elements: its keys * name members of DataView.prototype or Object.prototype. */ static bool scr_dyn_bytes_in(const ScrDyn *v, const ScrStr *key) { + if (scr_bytes_has_named(v, key)) return true; if (v->v.bytes->is_data_view) return scr_dyn_data_view_proto_name(key) || scr_dyn_object_proto_name(key); if (key->len == 2 && memcmp(key->data, "-0", 2) == 0) return false; double index = scr_string_to_number(key); @@ -2858,6 +2984,19 @@ ScrDyn *scr_dyn_own_descriptor(const ScrDyn *value, const ScrStr *key) { scr_dyn_obj_set(out, "configurable", 12, scr_dyn_new_bool(false)); return out; } + if (value->kind == SCR_DYN_BYTES && scr_bytes_has_named(value, key)) + return scr_dyn_own_descriptor(value->v.bytes->properties, key); + if (value->kind == SCR_DYN_BYTES && !value->v.bytes->is_data_view) { + if (!scr_dyn_canonical_own_index(key, value->v.bytes->len)) return scr_dyn_retain(scr_dyn_undefined()); + size_t index = 0; + for (size_t i = 0; i < key->len; i++) index = index * 10 + (size_t)(key->data[i] - '0'); + ScrDyn *out = scr_dyn_new_obj(); + scr_dyn_obj_set(out, "value", 5, scr_dyn_new_num(scr_bytes_get(value->v.bytes, (double)index))); + scr_dyn_obj_set(out, "writable", 8, scr_dyn_new_bool(true)); + scr_dyn_obj_set(out, "enumerable", 10, scr_dyn_new_bool(true)); + scr_dyn_obj_set(out, "configurable", 12, scr_dyn_new_bool(true)); + return out; + } if (value->kind == SCR_DYN_ARR) { if (value->v.arr.properties && scr_dyn_obj_get(value->v.arr.properties, key->data, key->len)) @@ -4847,6 +4986,55 @@ static double *dyn_bytes_values(const ScrBytes *b, size_t n) { return values; } +/* SpeciesConstructor of a typed array: NULL (and no exception) for the + * default constructor of its kind, else the species constructor (+1). + * Only a `constructor` the program set on the view can differ. */ +static ScrDyn *scr_bytes_species(ScrDyn *recv, bool *failed) { + *failed = false; + ScrStr *name = scr_str_new("constructor", 11); + bool own = scr_bytes_has_named(recv, name); + ScrDyn *constructor = own ? scr_dyn_obj_read_receiver(recv->v.bytes->properties, name->data, name->len, recv) : NULL; + scr_str_release(name); + if (!own) return NULL; + if (!constructor) { *failed = true; return NULL; } + if (constructor->kind == SCR_DYN_UNDEF) { scr_dyn_release(constructor); return NULL; } + if (constructor->kind != SCR_DYN_OBJ && constructor->kind != SCR_DYN_FUNC) { + scr_dyn_release(constructor); + static const char message[] = "object.constructor is not an object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + *failed = true; + return NULL; + } + ScrDyn *species = scr_species_symbol ? scr_dyn_symbol_key_get(constructor, scr_species_symbol, true) : NULL; + scr_dyn_release(constructor); + if (scr_exc_pending()) { scr_dyn_release(species); *failed = true; return NULL; } + if (!species || species->kind == SCR_DYN_UNDEF || species->kind == SCR_DYN_NULL || + scr_bytes_ctor_elem(species) == (int)recv->v.bytes->elem) { + scr_dyn_release(species); + return NULL; + } + if (!scr_dyn_is_callable(species)) { + scr_dyn_release(species); + static const char message[] = "object.constructor[Symbol.species] is not a constructor"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + *failed = true; + return NULL; + } + return species; +} + +/* TypedArraySpeciesCreate(recv, [n]): a new array of the receiver's kind + * (and Buffer flavor), or the species constructor's checked result. */ +static ScrDyn *scr_bytes_species_create(ScrDyn *recv, size_t n) { + bool failed; + ScrDyn *species = scr_bytes_species(recv, &failed); + if (failed) return NULL; + if (!species) return dyn_bytes_wrap(recv, scr_bytes_new(recv->v.bytes->elem, (double)n)); + ScrDyn *result = scr_bytes_static_create(species, n); + scr_dyn_release(species); + return result; +} + ScrDyn *scr_dyn_bytes_method(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, bool *handled) { ScrBytes *bytes = recv->v.bytes; size_t len = bytes->len; @@ -4866,11 +5054,41 @@ ScrDyn *scr_dyn_bytes_method(ScrDyn *recv, const char *method, ScrDyn *const *ar if (scr_exc_pending()) return NULL; double endD = scr_dyn_bytes_index_arg(args, argc, 1, (double)blen); if (scr_exc_pending()) return NULL; - ScrBytes *out = recv->buffer || scr_dyn_bytes_named(method, "subarray") - ? scr_bytes_subarray(bytes, startD, endD) : scr_bytes_slice(bytes, startD, endD); - ScrDyn *d = recv->buffer ? scr_dyn_new_buffer(out) : scr_dyn_new_bytes(out); + const bool subarray = recv->buffer || scr_dyn_bytes_named(method, "subarray"); + bool failed; + ScrDyn *species = recv->buffer ? NULL : scr_bytes_species(recv, &failed); + if (!recv->buffer && failed) return NULL; + ScrBytes *out = subarray ? scr_bytes_subarray(bytes, startD, endD) : scr_bytes_slice(bytes, startD, endD); + if (!species) { + ScrDyn *d = recv->buffer ? scr_dyn_new_buffer(out) : scr_dyn_new_bytes(out); + scr_bytes_release(out); + return d; + } + ScrDyn *result; + if (subarray) { + /* The species constructor views the same buffer. */ + ScrDyn *ctor_args = scr_dyn_new_arr(); + scr_dyn_arr_push(ctor_args, scr_array_buffer_from_bytes(bytes)); + scr_dyn_arr_push(ctor_args, scr_dyn_new_num(scr_bytes_byte_offset(out))); + scr_dyn_arr_push(ctor_args, scr_dyn_new_num((double)out->len)); + ScrStr *what = scr_str_new("TypedArray species", 18); + result = scr_dyn_construct(species, ctor_args, what); + scr_str_release(what); + scr_dyn_release(ctor_args); + if (result && (result->kind != SCR_DYN_BYTES || result->v.bytes->is_data_view)) { + scr_dyn_release(result); + result = NULL; + static const char message[] = "this is not a typed array."; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } + } else { + result = scr_bytes_static_create(species, out->len); + for (size_t i = 0; result && i < out->len; i++) + scr_bytes_set(result->v.bytes, (double)i, scr_bytes_get(out, (double)i)); + } + scr_dyn_release(species); scr_bytes_release(out); - return d; + return result; } if (scr_dyn_bytes_named(method, "set")) { double offset = scr_dyn_bytes_index_arg(args, argc, 1, 0); @@ -4908,6 +5126,22 @@ ScrDyn *scr_dyn_bytes_method(ScrDyn *recv, const char *method, ScrDyn *const *ar scr_str_release(joined); return out; } + if (scr_dyn_bytes_named(method, "toLocaleString")) { + /* Each element's Number.prototype.toLocaleString, joined by ",". */ + ScrJsonBuf b; + scr_jb_init(&b); + for (size_t i = 0; i < len; i++) { + if (i > 0) scr_jb_putc(&b, ','); + if (i >= bytes->len) continue; + ScrStr *text = scr_intl_num_format_en_us(scr_bytes_get(bytes, (double)i)); + scr_jb_write(&b, text->data, text->len); + scr_str_release(text); + } + ScrStr *joined = scr_jb_finish(&b); + ScrDyn *out = scr_dyn_new_str(joined); + scr_str_release(joined); + return out; + } if (scr_dyn_bytes_named(method, "keys") || scr_dyn_bytes_named(method, "values") || scr_dyn_bytes_named(method, "entries")) return scr_dyn_native_handle_iterator(recv, scr_dyn_bytes_named(method, "keys") ? 1 : scr_dyn_bytes_named(method, "entries") ? 2 : 0); if (scr_dyn_bytes_named(method, "indexOf") || scr_dyn_bytes_named(method, "lastIndexOf") || scr_dyn_bytes_named(method, "includes")) { @@ -4946,7 +5180,8 @@ ScrDyn *scr_dyn_bytes_method(ScrDyn *recv, const char *method, ScrDyn *const *ar if (!scr_dyn_bytes_cb_check(args, argc)) return NULL; ScrDyn *cb = args[0], *this_arg = argc > 1 ? args[1] : scr_dyn_undefined(); const bool backward = scr_dyn_bytes_named(method, "findLast") || scr_dyn_bytes_named(method, "findLastIndex"); - ScrBytes *mapped = scr_dyn_bytes_named(method, "map") ? scr_bytes_new(bytes->elem, (double)len) : NULL; + ScrDyn *mapped = NULL; + if (scr_dyn_bytes_named(method, "map") && !(mapped = scr_bytes_species_create(recv, len))) return NULL; double *kept = scr_dyn_bytes_named(method, "filter") ? malloc((len ? len : 1) * sizeof *kept) : NULL; size_t nkept = 0; for (size_t step = 0; step < len; step++) { @@ -4957,7 +5192,7 @@ ScrDyn *scr_dyn_bytes_method(ScrDyn *recv, const char *method, ScrDyn *const *ar scr_dyn_release(index); if (!r) { scr_dyn_release(item); - if (mapped) scr_bytes_release(mapped); + scr_dyn_release(mapped); free(kept); return NULL; } @@ -4966,8 +5201,8 @@ ScrDyn *scr_dyn_bytes_method(ScrDyn *recv, const char *method, ScrDyn *const *ar bool converted = scr_dyn_number_coerce_js(r, &v); scr_dyn_release(r); scr_dyn_release(item); - if (!converted) { scr_bytes_release(mapped); return NULL; } - if (k < mapped->len) scr_bytes_set(mapped, (double)k, v); + if (!converted) { scr_dyn_release(mapped); return NULL; } + if (k < mapped->v.bytes->len) scr_bytes_set(mapped->v.bytes, (double)k, v); continue; } bool truthy = scr_dyn_truthy(r); @@ -4988,12 +5223,12 @@ ScrDyn *scr_dyn_bytes_method(ScrDyn *recv, const char *method, ScrDyn *const *ar } scr_dyn_release(item); } - if (mapped) return dyn_bytes_wrap(recv, mapped); + if (mapped) return mapped; if (kept) { - ScrBytes *out = scr_bytes_new(bytes->elem, (double)nkept); - for (size_t i = 0; i < nkept; i++) scr_bytes_set(out, (double)i, kept[i]); + ScrDyn *out = scr_bytes_species_create(recv, nkept); + for (size_t i = 0; out && i < nkept; i++) scr_bytes_set(out->v.bytes, (double)i, kept[i]); free(kept); - return dyn_bytes_wrap(recv, out); + return out; } if (scr_dyn_bytes_named(method, "some")) return scr_dyn_new_bool(false); if (scr_dyn_bytes_named(method, "every")) return scr_dyn_new_bool(true); @@ -5120,6 +5355,8 @@ static ScrDyn *scr_dyn_bytes_method_value(const ScrStr *name, size_t arity) { /* Shared view metadata and indexed reads for both compiler backends. */ ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key) { ScrBytes *bytes = value->v.bytes; + /* Own named properties first; a getter sees the view as `this`. */ + if (scr_bytes_has_named(value, key)) return scr_bytes_named_get(value, key->data, key->len); if (key->len == 10 && memcmp(key->data, "byteLength", 10) == 0) return scr_dyn_new_num(scr_bytes_byte_len(bytes)); if (key->len == 10 && memcmp(key->data, "byteOffset", 10) == 0) return scr_dyn_new_num(scr_bytes_byte_offset(bytes)); if (key->len == 6 && memcmp(key->data, "buffer", 6) == 0) return scr_array_buffer_from_bytes(bytes); @@ -6385,6 +6622,10 @@ static ScrDyn *scr_dyn_symbol_receiver(const ScrDyn *value) { if (value->kind == SCR_DYN_FUNC) return scr_dyn_fn_properties(value); if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_HANDLE) return scr_dyn_retain((ScrDyn *)value); + /* A view keeps its symbol properties on its named-property table, which + * writes create first (scr_bytes_symbol_write). */ + if (value->kind == SCR_DYN_BYTES) + return value->v.bytes->properties ? scr_dyn_retain(value->v.bytes->properties) : NULL; if (value->kind == SCR_DYN_PROXY) { static const char message[] = "Symbol properties on this native value have no lowering"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); @@ -7798,8 +8039,46 @@ void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key) { entry->value = scr_dyn_retain(scr_dyn_undefined()); } +/* Symbol keys that the runtime reads from a view's prototype without + * consulting its own properties: the well-known symbols and + * util.inspect.custom. */ +static bool scr_bytes_protocol_symbol(const ScrDyn *key) { + if (scr_dyn_well_known_symbol(key)) return true; + ScrStr *description = key->v.symbol.description(key->v.symbol.value); + bool custom = description && description->len == 26 && !memcmp(description->data, "nodejs.util.inspect.custom", 26); + scr_str_release(description); + return custom; +} + +/* Before a symbol-keyed write or definition on a view: false with an + * exception pending for a key the view cannot own, else true with the + * view's table created. Symbol.toStringTag is an accessor without a setter + * on %TypedArray%.prototype, so assigning it throws V8's TypeError. */ +static bool scr_bytes_symbol_write(ScrDyn *value, const ScrDyn *key, bool assign) { + ScrDyn *receiver = scr_dyn_symbol_receiver(value); + bool own = receiver && scr_dyn_symbol_entry(receiver, key, true); + scr_dyn_release(receiver); + if (own) return true; + ScrStr *description = key->v.symbol.description(key->v.symbol.value); + bool tag = assign && !value->v.bytes->is_data_view && scr_dyn_well_known_symbol(key) && description && + description->len == 18 && !memcmp(description->data, "Symbol.toStringTag", 18); + if (tag) { + static const char message[] = "Cannot set property Symbol(Symbol.toStringTag) of [object Object] which has only a getter"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } else if (scr_bytes_protocol_symbol(key)) { + ScrStr *name = key->v.symbol.render(key->v.symbol.value); + scr_bytes_protocol_fence(name->data, name->len); + scr_str_release(name); + } + scr_str_release(description); + if (scr_exc_pending()) return false; + scr_bytes_properties(value); + return true; +} + void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored) { if (scr_dyn_error_prototype_fence(value)) return; + if (value->kind == SCR_DYN_BYTES && !scr_bytes_symbol_write(value, key, true)) return; ScrDyn *receiver = scr_dyn_symbol_receiver(value); if (!receiver) { if (!scr_exc_pending()) { @@ -7822,6 +8101,18 @@ void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored) { scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); } else { bool present = scr_dyn_symbol_entry(receiver, key, true) != NULL; + if (!present && receiver->non_extensible) { + ScrStr *rendered = key->v.symbol.render(key->v.symbol.value); + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "Cannot add property "); + scr_jb_write(&message, rendered->data, rendered->len); + scr_jb_puts(&message, ", object is not extensible"); + scr_str_release(rendered); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + scr_dyn_release(receiver); + return; + } scr_dyn_symbol_table(receiver); ScrStr *name = scr_dyn_symbol_storage_key(key); scr_dyn_key_set(receiver->symbol_properties, name, stored); @@ -7832,6 +8123,7 @@ void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored) { } static ScrDyn *scr_dyn_define_symbol(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) { + if (target->kind == SCR_DYN_BYTES && !scr_bytes_symbol_write(target, key, false)) return NULL; ScrDyn *receiver = scr_dyn_symbol_receiver(target); if (!receiver) { if (!scr_exc_pending()) { @@ -8026,6 +8318,53 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) scr_dyn_release(defined); return scr_dyn_retain(target); } + if (target->kind == SCR_DYN_BYTES) { + ScrStr *name = scr_dyn_property_key(key); + if (!name) return NULL; + double index; + bool numeric = !target->v.bytes->is_data_view && scr_bytes_numeric_key(name, &index); + if (numeric) { + /* [[DefineOwnProperty]] of an element: a valid index and a data + * descriptor that keeps it writable, enumerable and configurable. */ + bool valid = !signbit(index) && trunc(index) == index && index < (double)target->v.bytes->len; + const ScrDyn *configurable = scr_dyn_obj_get(descriptor, "configurable", 12); + const ScrDyn *enumerable = scr_dyn_obj_get(descriptor, "enumerable", 10); + const ScrDyn *writable = scr_dyn_obj_get(descriptor, "writable", 8); + bool accessor = scr_dyn_obj_get(descriptor, "get", 3) || scr_dyn_obj_get(descriptor, "set", 3); + if (!valid) { + scr_str_release(name); + static const char message[] = "Invalid typed array index"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + if (accessor || (configurable && !scr_dyn_truthy(configurable)) || + (enumerable && !scr_dyn_truthy(enumerable)) || (writable && !scr_dyn_truthy(writable))) { + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "Cannot redefine property: "); + scr_jb_write(&message, name->data, name->len); + scr_str_release(name); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + return NULL; + } + scr_str_release(name); + const ScrDyn *value = scr_dyn_obj_get(descriptor, "value", 5); + double number; + if (value) { + if (!scr_dyn_number_coerce_js(value, &number)) return NULL; + scr_bytes_set(target->v.bytes, index, number); + } + return scr_dyn_retain(target); + } + bool fenced = scr_bytes_protocol_name(name) && !scr_bytes_has_named(target, name); + if (fenced) scr_bytes_protocol_fence(name->data, name->len); + scr_str_release(name); + if (fenced) return NULL; + ScrDyn *defined = scr_dyn_define_property(scr_bytes_properties(target), key, descriptor); + if (!defined) return NULL; + scr_dyn_release(defined); + return scr_dyn_retain(target); + } if (target->kind != SCR_DYN_OBJ) { static const char msg[] = "Object.defineProperty called on non-object"; scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); @@ -8173,7 +8512,7 @@ ScrDyn *scr_dyn_get_own_property_descriptor(ScrDyn *target, ScrDyn *key) { scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); return NULL; } - if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_PROXY && target->kind != SCR_DYN_FUNC && target->kind != SCR_DYN_ARR && target->kind != SCR_DYN_STR && target->kind != SCR_DYN_BOOL && target->kind != SCR_DYN_NUM && target->kind != SCR_DYN_TYPED_REF) { + if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_PROXY && target->kind != SCR_DYN_FUNC && target->kind != SCR_DYN_ARR && target->kind != SCR_DYN_STR && target->kind != SCR_DYN_BOOL && target->kind != SCR_DYN_NUM && target->kind != SCR_DYN_TYPED_REF && target->kind != SCR_DYN_BYTES) { static const char msg[] = "Object.getOwnPropertyDescriptor on this value is not supported yet"; scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); return NULL; @@ -8477,7 +8816,8 @@ static bool scr_dyn_json_write(ScrJsonBuf *b, const ScrDyn *d) { const ScrBytes *bytes = d->v.bytes; if (d->buffer) scr_jb_puts(b, "{\"type\":\"Buffer\",\"data\":["); else scr_jb_putc(b, '{'); - for (size_t i = 0; i < bytes->len; i++) { + size_t count = bytes->is_data_view ? 0 : bytes->len; + for (size_t i = 0; i < count; i++) { if (i > 0) scr_jb_putc(b, ','); if (!d->buffer) { char idx[32]; @@ -8486,6 +8826,32 @@ static bool scr_dyn_json_write(ScrJsonBuf *b, const ScrDyn *d) { } scr_jb_put_f64(b, scr_bytes_get(bytes, (double)i)); } + /* Named properties follow the elements; a Buffer's toJSON leaves them + * out. */ + ScrDyn *named = d->buffer || !bytes->properties ? NULL : scr_dyn_obj_keys(bytes->properties); + bool first = count == 0; + for (size_t i = 0; named && i < named->v.arr.len; i++) { + const ScrStr *key = named->v.arr.items[i]->v.str; + ScrDyn *value = scr_dyn_obj_read_receiver(bytes->properties, key->data, key->len, d); + if (!value) { scr_dyn_release(named); return false; } + ScrJsonBuf probe; + scr_jb_init(&probe); + bool present = scr_dyn_json_write(&probe, value); + scr_dyn_release(value); + if (!present) { + scr_jb_dispose(&probe); + if (scr_exc_pending()) { scr_dyn_release(named); return false; } + continue; + } + if (!first) scr_jb_putc(b, ','); + first = false; + scr_jb_put_json_str(b, key); + scr_jb_putc(b, ':'); + ScrStr *body = scr_jb_finish(&probe); + scr_jb_write(b, body->data, body->len); + scr_str_release(body); + } + scr_dyn_release(named); scr_jb_puts(b, d->buffer ? "]}" : "}"); return true; } @@ -10221,6 +10587,7 @@ bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) { } if (v->kind == SCR_DYN_STR) return scr_dyn_canonical_own_index(key, (size_t)scr_str_utf16_len(v->v.str)); + if (v->kind == SCR_DYN_BYTES) return scr_dyn_bytes_in(v, key); return false; } @@ -10283,7 +10650,13 @@ static void scr_dyn_throw_property(const ScrDyn *object, const char *key, size_t scr_jb_puts(&buffer, deleting ? "Cannot delete property '" : "Cannot assign to read only property '"); scr_jb_write(&buffer, key, key_len); scr_jb_puts(&buffer, deleting ? "' of " : "' of object '"); - if (object->null_proto) scr_jb_puts(&buffer, "[object Object]"); + if (object->kind == SCR_DYN_BYTES) { + /* V8 names a typed array by its tag and a DataView by its class. */ + const char *kind = object->v.bytes->is_data_view ? "DataView" : scr_bytes_elem_name(object->v.bytes->elem); + scr_jb_puts(&buffer, object->v.bytes->is_data_view ? "#<" : "[object "); + scr_jb_puts(&buffer, kind); + scr_jb_puts(&buffer, object->v.bytes->is_data_view ? ">" : "]"); + } else if (object->null_proto) scr_jb_puts(&buffer, "[object Object]"); else { scr_jb_puts(&buffer, "#<"); scr_jb_puts(&buffer, name ? name : "Object"); @@ -10497,19 +10870,36 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { } if (recv->kind == SCR_DYN_BYTES) { /* Integer-indexed exotic writes coerce even for an invalid canonical - * numeric index, then ignore an out-of-range index without an error. */ - double index = scr_string_to_number(key); - char spelling[32]; - size_t length = scr_f64_to_str(index, spelling); - bool negative_zero = key->len == 2 && memcmp(key->data, "-0", 2) == 0; - if (negative_zero || (length == key->len && memcmp(spelling, key->data, length) == 0)) { + * numeric index, then ignore an out-of-range index without an error. + * A DataView has no elements: its numeric keys are named properties. */ + double index; + if (!recv->v.bytes->is_data_view && scr_bytes_numeric_key(key, &index)) { double number; if (!scr_dyn_number_coerce_js(value, &number)) return; - if (!negative_zero && index >= 0 && index < (double)recv->v.bytes->len && trunc(index) == index) { + if (!signbit(index) && index < (double)recv->v.bytes->len && trunc(index) == index) { scr_bytes_set(recv->v.bytes, index, number); } return; } + /* Any other key is a named property of the view. */ + if (!scr_bytes_has_named(recv, key)) { + if (scr_bytes_getter_name(recv, key)) { + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "Cannot set property "); + scr_jb_write(&message, key->data, key->len); + scr_jb_puts(&message, recv->v.bytes->is_data_view ? " of # which has only a getter" + : " of [object Object] which has only a getter"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + return; + } + if (scr_bytes_protocol_name(key)) { + scr_bytes_protocol_fence(key->data, key->len); + return; + } + } + scr_dyn_object_key_set(scr_bytes_properties(recv), key, value, recv); + return; } ScrJsonBuf b; @@ -12181,6 +12571,11 @@ static ScrDyn *scr_json_member(const ScrDyn *holder, const ScrStr *key) { scr_dyn_release(view); return member; } + /* A view's named properties, getters included, read through the view. */ + if (holder->kind == SCR_DYN_BYTES && (holder->v.bytes->is_data_view || scr_bytes_has_named(holder, key))) { + ScrDyn *named = scr_bytes_named_get(holder, key->data, key->len); + return named || scr_exc_pending() ? named : scr_dyn_retain(scr_dyn_undefined()); + } if (holder->kind == SCR_DYN_ARR || holder->kind == SCR_DYN_BYTES) { size_t index = 0; for (size_t i = 0; i < key->len; i++) index = index * 10 + (size_t)(key->data[i] - '0'); @@ -12324,6 +12719,23 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { scr_dyn_key_delete(recv->v.arr.properties, key, strict); return; } + if (recv->kind == SCR_DYN_BYTES) { + /* A typed array's elements cannot be deleted; other numeric keys name + * no property. Named properties follow their configurability. */ + double index; + if (!recv->v.bytes->is_data_view && scr_bytes_numeric_key(key, &index)) { + if (strict && !signbit(index) && trunc(index) == index && index < (double)recv->v.bytes->len) + scr_dyn_throw_property(recv, key->data, key->len, true); + return; + } + ScrDynEntry *entry = recv->v.bytes->properties ? scr_dyn_entry(recv->v.bytes->properties, key) : NULL; + if (entry && !entry->configurable) { + if (strict) scr_dyn_throw_property(recv, key->data, key->len, true); + return; + } + if (entry) scr_json_delete_member(recv->v.bytes->properties, key); + return; + } if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL) { const char *message = "Cannot convert undefined or null to object"; scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); @@ -13982,7 +14394,7 @@ ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value) { if (value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BYTES) { ScrDyn *keys = scr_dyn_new_arr(); size_t length = value->kind == SCR_DYN_ARR ? value->v.arr.len : - value->kind == SCR_DYN_BYTES ? value->v.bytes->len : + value->kind == SCR_DYN_BYTES ? (value->v.bytes->is_data_view ? 0 : value->v.bytes->len) : (size_t)scr_str_utf16_len(value->v.str); for (size_t i = 0; i < length; i++) { if (value->kind == SCR_DYN_ARR && !scr_dyn_arr_has_index(value, i)) continue; @@ -13992,8 +14404,10 @@ ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value) { } if (value->kind != SCR_DYN_BYTES) scr_dyn_arr_push(keys, scr_dyn_objwalk_key("length", 6)); - if (value->kind == SCR_DYN_ARR && value->v.arr.properties) { - ScrDyn *properties = scr_dyn_obj_own_keys(value->v.arr.properties); + ScrDyn *named = value->kind == SCR_DYN_ARR ? value->v.arr.properties + : value->kind == SCR_DYN_BYTES ? value->v.bytes->properties : NULL; + if (named) { + ScrDyn *properties = scr_dyn_obj_own_keys(named); for (size_t i = 0; i < properties->v.arr.len; i++) scr_dyn_arr_push(keys, scr_dyn_retain(properties->v.arr.items[i])); scr_dyn_release(properties); @@ -14258,7 +14672,7 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) { return out; } if (v->kind == SCR_DYN_ARR || v->kind == SCR_DYN_BYTES) { - size_t n = v->kind == SCR_DYN_ARR ? v->v.arr.len : v->v.bytes->len; + size_t n = v->kind == SCR_DYN_ARR ? v->v.arr.len : v->v.bytes->is_data_view ? 0 : v->v.bytes->len; for (size_t i = 0; i < n; i++) { if (v->kind == SCR_DYN_ARR && !scr_dyn_arr_has_index(v, i)) continue; char key[24]; @@ -14279,10 +14693,11 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) { scr_dyn_arr_push(out, pair); } } - if (v->kind == SCR_DYN_ARR && v->v.arr.properties) { - ScrDyn *names = scr_dyn_obj_own_keys(v->v.arr.properties); + ScrDyn *named = v->kind == SCR_DYN_ARR ? v->v.arr.properties : v->v.bytes->properties; + if (named) { + ScrDyn *names = scr_dyn_obj_own_keys(named); for (size_t i = 0; i < names->v.arr.len; i++) { - if (!scr_dyn_objwalk_entry(out, v->v.arr.properties, mode, names->v.arr.items[i]->v.str, v)) { + if (!scr_dyn_objwalk_entry(out, named, mode, names->v.arr.items[i]->v.str, v)) { scr_dyn_release(names); scr_dyn_release(out); return NULL; @@ -14386,6 +14801,7 @@ ScrDyn *scr_dyn_for_in_keys(const ScrDyn *v) { return keys; } case SCR_DYN_ARR: + case SCR_DYN_BYTES: return scr_dyn_obj_keys(v); case SCR_DYN_STR: { ScrDyn *keys = scr_dyn_new_arr(); @@ -14639,7 +15055,7 @@ static void scr_dyn_assign_strings_from(ScrDyn *target, const ScrDyn *src, bool scr_dyn_proxy_unsupported("Object.assign"); return; } - if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_FUNC) return; + if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_FUNC && target->kind != SCR_DYN_BYTES) return; if (src->kind == SCR_DYN_TYPED_REF) { ScrDyn *materialized = scr_dyn_typed_ref_materialize(src); if (materialized) scr_dyn_assign_strings_from(target, materialized, define); @@ -14702,13 +15118,15 @@ static void scr_dyn_assign_from(ScrDyn *target, const ScrDyn *src, bool define) return; } ScrDyn *view = src->kind == SCR_DYN_FUNC ? scr_dyn_fn_properties(src) : scr_dyn_retain((ScrDyn *)src); + /* A view's symbol properties live on its named-property table. */ + const ScrDyn *holder = src->kind == SCR_DYN_BYTES ? src->v.bytes->properties : view; ScrDyn *symbols = scr_dyn_new_arr(); - if (view->symbol_keys) for (size_t i = 0; i < view->symbol_keys->v.arr.len; i++) - scr_dyn_arr_push(symbols, scr_dyn_retain(view->symbol_keys->v.arr.items[i])); + if (holder && holder->symbol_keys) for (size_t i = 0; i < holder->symbol_keys->v.arr.len; i++) + scr_dyn_arr_push(symbols, scr_dyn_retain(holder->symbol_keys->v.arr.items[i])); scr_dyn_assign_strings_from(target, view, define); for (size_t i = 0; i < symbols->v.arr.len && !scr_exc_pending(); i++) { ScrDyn *key = symbols->v.arr.items[i]; - ScrDynEntry *entry = scr_dyn_symbol_entry(view, key, true); + ScrDynEntry *entry = scr_dyn_symbol_entry(holder, key, true); if (!entry || !entry->enumerable) continue; ScrDyn *value = scr_dyn_symbol_key_get(src, key, false); if (!value) break; @@ -15093,7 +15511,7 @@ bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key) { return scr_dyn_canonical_own_index(key, (size_t)scr_str_utf16_len(v->v.str)); } if (v->kind == SCR_DYN_BYTES) { - return scr_dyn_canonical_own_index(key, v->v.bytes->len); + return (!v->v.bytes->is_data_view && scr_dyn_canonical_own_index(key, v->v.bytes->len)) || scr_bytes_has_named(v, key); } if (v->kind == SCR_DYN_FUNC) { ScrDyn *desc = scr_dyn_own_descriptor(v, key); @@ -15130,7 +15548,8 @@ bool scr_dyn_property_is_enumerable(const ScrDyn *value, const ScrStr *key) { return (scr_dyn_canonical_own_index(key, value->v.arr.len) && scr_dyn_has_own(value, key)) || (value->v.arr.properties && scr_dyn_obj_enumerable(value->v.arr.properties, key->data, key->len)); if (value->kind == SCR_DYN_BYTES) - return scr_dyn_canonical_own_index(key, value->v.bytes->len); + return (!value->v.bytes->is_data_view && scr_dyn_canonical_own_index(key, value->v.bytes->len)) || + (value->v.bytes->properties && scr_dyn_obj_enumerable(value->v.bytes->properties, key->data, key->len)); return false; } ScrDyn *scr_dyn_obj_values(const ScrDyn *v) { return scr_dyn_objwalk(v, SCR_OBJWALK_VALUES); } @@ -16834,6 +17253,11 @@ ScrDyn *scr_dyn_freeze_data(ScrDyn *value) { ScrDyn *native = scr_dyn_freeze_native(value); if (native) return native; if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; + if (value->kind == SCR_DYN_BYTES) { + scr_dyn_release(scr_dyn_freeze_data(scr_bytes_properties(value))); + if (scr_bytes_integrity_fence(value, "freeze")) return NULL; + return scr_dyn_retain(value); + } if (value->symbol_properties) scr_dyn_release(scr_dyn_freeze_data(value->symbol_properties)); if (value->kind == SCR_DYN_ARR) { value->non_extensible = true; @@ -16875,6 +17299,9 @@ bool scr_dyn_is_frozen(const ScrDyn *value) { if (value->kind == SCR_DYN_ARR) return scr_dyn_is_sealed(value) && (value->v.arr.len == 0 || value->v.arr.frozen) && (!value->v.arr.properties || scr_dyn_is_frozen(value->v.arr.properties)); + if (value->kind == SCR_DYN_BYTES) + return (value->v.bytes->is_data_view || value->v.bytes->len == 0) && value->v.bytes->properties && + scr_dyn_is_frozen(value->v.bytes->properties); if (value->kind == SCR_DYN_OBJ && !value->copied_from_native) { if (!value->non_extensible) return false; for (size_t i = 0; i < value->v.obj.len; i++) { diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 647c31dc3..fb0b6cb0b 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -80,6 +80,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) { scr_str_release(description); symbol->reg_next = g_sym_well_known; g_sym_well_known = scr_sym_retain(symbol); + { + ScrDyn *key = scr_dyn_new_symbol(symbol); + scr_dyn_install_well_known_symbol(key); + scr_dyn_release(key); + } if (name->len == 8 && !memcmp(name->data, "iterator", 8)) { ScrDyn *key = scr_dyn_new_symbol(symbol); scr_dyn_install_iterator_symbol(key); @@ -90,6 +95,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) { scr_dyn_install_has_instance_symbol(key); scr_dyn_release(key); } + if (name->len == 7 && !memcmp(name->data, "species", 7)) { + ScrDyn *key = scr_dyn_new_symbol(symbol); + scr_dyn_install_species_symbol(key); + scr_dyn_release(key); + } if (name->len == 13 && !memcmp(name->data, "asyncIterator", 13)) { ScrDyn *key = scr_dyn_new_symbol(symbol); scr_dyn_install_async_iterator_symbol(key); diff --git a/tests/corpus/7752-typed-array-dynamic-methods.js b/tests/corpus/7752-typed-array-dynamic-methods.js index a728e3ce9..d4f077b49 100644 --- a/tests/corpus/7752-typed-array-dynamic-methods.js +++ b/tests/corpus/7752-typed-array-dynamic-methods.js @@ -123,3 +123,15 @@ function plainIterator(TA) { console.log(new TA(source), TA.from(source, (v) => v * 2), gets, nexts); } plainIterator(Int16Array); +// ToIndex of a Symbol or BigInt length, toLocaleString, and Symbol descriptions +// that are not strings. +function small(TA) { + for (const bad of [Symbol("s"), 1n]) { + try { new TA(bad); console.log("no throw"); } catch (e) { console.log(e.constructor.name); } + } + console.log(new TA([1, 1234.5, -0, 0]).toLocaleString(), new TA(0).toLocaleString(), typeof new TA(1).toLocaleString); +} +small(Float64Array); +small(Uint16Array); +const s = Symbol(1), t = Symbol(true), u = Symbol(2.5); +console.log(s.description, t.description, u.toString(), typeof s.description); diff --git a/tests/corpus/7753-typed-array-named-properties.js b/tests/corpus/7753-typed-array-named-properties.js new file mode 100644 index 000000000..b49f100b5 --- /dev/null +++ b/tests/corpus/7753-typed-array-named-properties.js @@ -0,0 +1,143 @@ +// Named properties on typed arrays, Buffers and DataViews (assignment, +// Object.defineProperty, reflection, deletion, integrity levels, JSON and +// inspection), and the Symbol.species constructor of map, filter, slice and +// subarray. +function box(value) { + const holder = {}; + holder.value = value; + return holder.value; +} + +function expandos(TA) { + const a = new TA([1, 2]); + a.foo = 1; + a.bar = "x"; + a[5] = 9; + a["1.5"] = 7; + console.log(a, a.foo, "foo" in a, a.hasOwnProperty("bar"), Object.keys(a), Object.entries(a).length); + console.log(Object.getOwnPropertyNames(a), JSON.stringify(Object.getOwnPropertyDescriptor(a, "foo")), JSON.stringify(Object.getOwnPropertyDescriptor(a, "0"))); + Object.defineProperty(a, "hidden", { value: 3, enumerable: false }); + Object.defineProperty(a, "0", { value: 8 }); + console.log(a, a.hidden, Object.keys(a)); + try { Object.defineProperty(a, "1", { value: 1, configurable: false }); } catch (e) { console.log(e.constructor.name, e.message); } + try { Object.defineProperty(a, "7", { value: 1 }); } catch (e) { console.log(e.constructor.name, e.message); } + try { Object.defineProperty(a, "-0", { value: 1 }); } catch (e) { console.log(e.constructor.name, e.message); } + try { Object.defineProperty(a, "0", { get() { return 1; } }); } catch (e) { console.log(e.constructor.name, e.message); } + let reads = 0; + Object.defineProperty(a, "g", { get() { reads++; return this.length; }, enumerable: true }); + console.log(a.g, reads, new TA(0)); + const e = new TA(0); + e.k = { deep: [1] }; + console.log(e); +} + +function species(TA, Other) { + const s = new TA([1, 2, 3, 4]); + const calls = []; + s.constructor = {}; + s.constructor[Symbol.species] = function (n) { calls.push(n); return new Other(n); }; + const m = s.map((v) => v * 1.5), f = s.filter((v) => v > 1), sl = s.slice(1, 3); + console.log(m, f, sl, calls); + s.constructor[Symbol.species] = function (buffer, offset, length) { calls.push([offset, length]); return new TA(buffer, offset, length); }; + const sub = s.subarray(1, 3); + sub[0] = 50; + console.log(sub, s[1], calls.length); + s.constructor[Symbol.species] = function () { return {}; }; + try { s.map((v) => v); } catch (err) { console.log(err.constructor.name); } + s.constructor[Symbol.species] = function () { return new TA(1); }; + try { s.slice(); } catch (err) { console.log(err.constructor.name); } + s.constructor = 5; + try { s.filter(() => true); } catch (err) { console.log(err.constructor.name); } + s.constructor = undefined; + console.log(s.map((v) => v + 1)); + const t = new TA([3, 1]); + Object.defineProperty(t, "constructor", { get() { calls.push("get"); return undefined; } }); + console.log(t.slice(), calls[calls.length - 1]); + const u = new TA([1, 2, 3]); + u.constructor = { [Symbol.species]: Uint16Array }; + console.log(u.filter(() => true), u.subarray(1), u.subarray(1).constructor === Uint16Array); +} + +function reflection(TA) { + const a = new TA([1, 2]); + a.foo = 1; + a[Symbol.for("s")] = 2; + const keys = []; + for (const k in a) keys.push(k); + console.log(keys, { ...a }, JSON.stringify(a), Object.values(a)); + console.log(a.propertyIsEnumerable("foo"), Object.hasOwn(a, "foo"), Object.getOwnPropertySymbols(a), a[Symbol.for("s")]); + console.log(Object.assign(new TA(2), { x: 1, 0: 9 }), Object.assign({}, a)); + delete a.foo; + delete a[5]; + delete a.length; + console.log(a, "foo" in a, a.length); + try { delete a[0]; } catch (err) { console.log(err.message); } + Object.defineProperty(a, "fixed", { value: 1 }); + try { delete a.fixed; } catch (err) { console.log(err.message); } + try { a.fixed = 2; } catch (err) { console.log(err.message); } + console.log(a.subarray(0, 1).fixed, a.slice().fixed); + let seen; + Object.defineProperty(a, "s", { set(v) { seen = [v, this === a]; } }); + a.s = 7; + console.log(seen); + a.map = 5; + try { a.map((v) => v); } catch (err) { console.log(err.message); } + for (const key of ["length", "byteLength", "byteOffset", "buffer"]) { + try { a[key] = 1; } catch (err) { console.log(err.message); } + } + try { a[Symbol.toStringTag] = "x"; } catch (err) { console.log(err.message); } + // Cycles through a view's own properties are collected. + const loop = new TA(1); + loop.self = loop; + loop.size = () => loop.length; + console.log(loop.self === loop, loop.size()); +} + +function integrity(TA) { + const results = []; + const attempt = (f) => { try { f(); results.push("ok"); } catch (err) { results.push(err.message); } }; + const sealed = new TA(1); + sealed.x = 1; + attempt(() => Object.seal(sealed)); + console.log(results, Object.isExtensible(sealed), Object.isSealed(sealed), JSON.stringify(Object.getOwnPropertyDescriptor(sealed, "x"))); + attempt(() => Object.seal(sealed)); + const frozen = new TA(1); + frozen.x = 1; + attempt(() => Object.freeze(frozen)); + attempt(() => Object.freeze(frozen)); + console.log(results, Object.isFrozen(frozen), JSON.stringify(Object.getOwnPropertyDescriptor(frozen, "x"))); + const closed = new TA(1); + Object.defineProperty(closed, "q", { value: 1 }); + Object.preventExtensions(closed); + try { closed.z = 1; } catch (err) { console.log(err.message); } + try { closed[Symbol("n")] = 1; } catch (err) { console.log(err.message); } + console.log(Object.isExtensible(closed), Object.isSealed(closed), Object.isFrozen(closed)); + const empty = new TA(0); + empty.k = 1; + Object.freeze(empty); + try { empty.k = 2; } catch (err) { console.log(err.message); } + console.log(Object.isFrozen(empty), Object.isSealed(empty), empty.k); +} + +expandos(Uint8Array); +expandos(Float64Array); +species(Int16Array, Float32Array); +reflection(Uint8Array); +reflection(Int32Array); +integrity(Uint16Array); + +const view = box(new DataView(new ArrayBuffer(2))); +view[0] = 5; +view.x = 1; +console.log(view, Object.keys(view), view[0], JSON.stringify(view), "0" in view); +try { view.byteLength = 1; } catch (err) { console.log(err.message); } +const buffer = box(Buffer.from([1, 2])); +buffer.foo = 1; +buffer[Symbol.for("k")] = 2; +console.log(buffer, Object.keys(buffer), JSON.stringify(buffer)); +for (const key of ["parent", "offset", "length"]) { + try { buffer[key] = 1; } catch (err) { console.log(err.message); } +} +const empty = box(Buffer.alloc(0)); +empty.foo = { a: [1, { b: 2 }] }; +console.log(empty); diff --git a/tests/harness/corpus-costs/7753-typed-array-named-properties.js.json b/tests/harness/corpus-costs/7753-typed-array-named-properties.js.json new file mode 100644 index 000000000..9ed866538 --- /dev/null +++ b/tests/harness/corpus-costs/7753-typed-array-named-properties.js.json @@ -0,0 +1 @@ +{ "plain": 3900, "san": 4400 } diff --git a/tests/harness/typed-array-property-refusals.test.ts b/tests/harness/typed-array-property-refusals.test.ts new file mode 100644 index 000000000..b2d6d498a --- /dev/null +++ b/tests/harness/typed-array-property-refusals.test.ts @@ -0,0 +1,64 @@ +import { execFile } from "node:child_process"; +import { mkdtempSync, writeFileSync } from "node:fs"; +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 execFileAsync = promisify(execFile); + +// The runtime's conversions, iteration and printing of a typed array, Buffer +// or DataView read these members from the prototype, so defining one on the +// view refuses with a catchable error instead of being ignored. +test("typed array members the runtime reads from the prototype refuse as own properties", async () => { + const outDir = mkdtempSync(join(tmpdir(), "scriptc-typed-array-refusals-")); + const entry = join(outDir, "main.mjs"); + writeFileSync( + entry, + ` +function attempt(define) { + try { define(); console.log("defined"); } catch (error) { console.log(error.name, error.message); } +} +function run(TA, view) { + const a = new TA(2); + attempt(() => { a.toString = () => "x"; }); + attempt(() => { a.valueOf = () => 1; }); + attempt(() => Object.defineProperty(a, "toJSON", { value: () => 1 })); + attempt(() => { a.join = () => ""; }); + attempt(() => { a[Symbol.iterator] = function* () {}; }); + attempt(() => { a[Symbol.for("nodejs.util.inspect.custom")] = () => "x"; }); + attempt(() => { view.toLocaleString = () => ""; }); + attempt(() => { a.label = "kept"; }); + console.log(a.label, String(a)); +} +run(Uint8Array, new DataView(new ArrayBuffer(1))); +`, + ); + const result = await compile(entry, { + backend: "llvm", + sanitize: process.env["SCRIPTC_SAN"] === "1", + outDir, + outPath: join(outDir, "program"), + }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + if (!result.ok) return; + const { stdout, stderr } = await execFileAsync(result.binaryPath, [], { encoding: "utf8" }); + const refused = (key: string): string => + `Error defining ${key} on a typed array, Buffer or DataView is not supported yet`; + expect(stdout).toBe( + [ + refused("toString"), + refused("valueOf"), + refused("toJSON"), + refused("join"), + refused("Symbol(Symbol.iterator)"), + refused("Symbol(nodejs.util.inspect.custom)"), + refused("toLocaleString"), + "defined", + "kept 0,0", + "", + ].join("\n"), + ); + expect(stderr).toBe(""); +}); From 2a98dbfa329c8b9e176179257429edb8d2f6a5e6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 21:57:25 -0500 Subject: [PATCH 034/273] Answer constructor reads on instances without a property bag The constructor read asked dyn.hasOwn on the instance's property bag, which is undefined until the instance has one, and on the bag's prototype, so it threw "Cannot convert undefined or null to object" for an instance without own properties (for example `new ParseFailure('p').constructor` for a class that extends SyntaxError). It now goes through a lifted helper that answers false before the instance has a bag and when the bag has no prototype, as the dyn.hasKey read did. --- .../lowering/class-dynamic-dispatch.ts | 96 ++++++++++++++++++- .../7621-class-methods-foreign-receivers.mjs | 39 ++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index a1bc41782..5132e487f 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -3966,6 +3966,94 @@ export class ClassDynamicDispatch { return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc }; } + /** `%dyn.class.bagHasOwn[OrPrototype]`: whether an instance's property bag + * holds `key` as its own property, or, with `prototype`, whether the bag's + * prototype object does. False before the instance has a bag. */ + private readBagHasOwn( + lowerer: Lowerer, + bag: IrExpr, + key: IrExpr, + prototype: boolean, + loc: SrcLoc, + ): IrExpr { + const name = prototype ? "%dyn.class.bagHasOwnOrPrototype" : "%dyn.class.bagHasOwn"; + let helper = lowerer.liftedFnNamed(name); + if (!helper) { + const value = varRef("p.0", DYN, loc); + const keyRef = varRef("p.1", STRING, loc); + const proto = varRef("proto", DYN, loc); + const hasOwn = (target: IrExpr): IrExpr => ({ + kind: "libCall", + fn: "dyn.hasOwn", + args: [target, keyRef], + type: BOOL, + loc, + }); + const returnFalse: IrStmt = { + kind: "return", + value: { kind: "boolLit", value: false, type: BOOL, loc }, + loc, + }; + helper = { + name, + params: [ + { localId: "p.0", name: "bag", type: DYN }, + { localId: "p.1", name: "key", type: STRING }, + ], + returnType: BOOL, + locals: [ + { id: "p.0", name: "bag", type: DYN, mutable: false }, + { id: "p.1", name: "key", type: STRING, mutable: false }, + ...(prototype ? [{ id: "proto", name: "proto", type: DYN, mutable: false }] : []), + ], + body: [ + { + kind: "if", + cond: { kind: "dynTest", test: "undefined", value, type: BOOL, loc }, + then: [returnFalse], + else_: null, + loc, + }, + ...(prototype + ? ([ + { + kind: "if", + cond: hasOwn(value), + then: [ + { + kind: "return", + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }, + ], + else_: null, + loc, + }, + { + kind: "varDecl", + localId: "proto", + init: { kind: "libCall", fn: "dyn.getPrototype", args: [value], type: DYN, loc }, + loc, + }, + { + kind: "if", + cond: { kind: "dynTest", test: "nullish", value: proto, type: BOOL, loc }, + then: [returnFalse], + else_: null, + loc, + }, + { kind: "return", value: hasOwn(proto), loc }, + ] satisfies IrStmt[]) + : [{ kind: "return", value: hasOwn(value), loc } satisfies IrStmt]), + ], + loc, + }; + lowerer.liftedFns.push(helper); + } + this.generated.add(helper); + return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc }; + } + /** The hints for which some boxed class declares a conversion method. A * string conversion calls toString first, so without a declared toString * or Symbol.toPrimitive it answers Object.prototype.toString's @@ -5633,12 +5721,18 @@ export class ClassDynamicDispatch { const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl), DYN); const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc); const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; + // `constructor` is an own property of every class prototype, so an + // ancestor's prototype never answers for an instance of this class: + // the instance's own property answers, then one this class's + // prototype object defines (a reflected prototype, or a write to + // `C.prototype.constructor`), and otherwise the class itself. + const answered = this.readBagHasOwn(lowerer, bag, key, hasClassPrototypeData(info), loc); return [ { kind: "return", value: { kind: "ternary", - cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc), + cond: answered, then: { kind: "libCall", fn: "dyn.bagGet", diff --git a/tests/corpus/7621-class-methods-foreign-receivers.mjs b/tests/corpus/7621-class-methods-foreign-receivers.mjs index beb046ea4..8e4feedbd 100644 --- a/tests/corpus/7621-class-methods-foreign-receivers.mjs +++ b/tests/corpus/7621-class-methods-foreign-receivers.mjs @@ -128,3 +128,42 @@ report("square both", () => Square.prototype.both.call(foreign)); report("tiny describe", () => Tiny.prototype.describe.call(foreign)); report("tiny tag", () => Tiny.prototype.tag.call(foreign)); report("square describe on tiny", () => Square.prototype.describe.call(tiny)); + +// A base method that builds `new this.constructor()` on a subclass instance +// builds the subclass, also once the base prototype is complete (a super call +// above reads it) and `copy` is read through its prototype slot. +class Node3 { + constructor() { + this.a = 0; + } + copy(source) { + this.a = source.a; + return this; + } + clone() { + return new this.constructor().copy(this); + } +} +class Mesh3 extends Node3 { + constructor(geometry) { + super(); + this.geometry = geometry; + } + copy(source) { + super.copy(source); + this.geometry = source.geometry; + return this; + } +} +const copyValue = Node3.prototype.copy; +const mesh = new Mesh3({ id: 1 }); +mesh.a = 5; +const meshClone = mesh.clone(); +console.log( + meshClone instanceof Mesh3, + meshClone.constructor === Mesh3, + mesh.constructor === Mesh3, + meshClone.a, + JSON.stringify(meshClone.geometry), + typeof copyValue, +); From 27593e366eeb4c4b270b1c809ea5d7512bfed15e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 22:08:02 -0500 Subject: [PATCH 035/273] Make Dates mutable objects with Node's setters and conversions --- benchmarks/runtime/README.md | 1 + benchmarks/runtime/workloads/date-math.json | 6 + benchmarks/runtime/workloads/date-math.ts | 52 +++++ .../content/limitations/builtin-apis/date.mdx | 9 +- .../compiler/src/backend/llvm/lib-dispatch.ts | 9 + .../compiler/src/backend/llvm/lib-shared.ts | 2 + .../compiler/src/diagnostics/diagnostic.ts | 2 +- .../frontend/lowering/abstract-equality.ts | 38 ++++ .../lowering/builtins/date-classes.ts | 22 +- .../src/frontend/lowering/builtins/dates.ts | 192 +++++++++++++++++- .../src/frontend/lowering/date-values.ts | 60 ++++-- .../frontend/lowering/expressions/binary.ts | 13 ++ .../lowering/expressions/property-access.ts | 6 +- .../lowering/expressions/relational.ts | 10 + .../frontend/lowering/expressions/typeof.ts | 1 + .../frontend/lowering/expressions/updates.ts | 31 ++- .../src/frontend/lowering/lower-calls.ts | 6 + .../src/frontend/lowering/lower-classes.ts | 23 +++ .../src/frontend/lowering/lower-exprs.ts | 1 + .../src/frontend/lowering/lower-generators.ts | 10 +- .../src/frontend/lowering/lower-stmts.ts | 27 ++- .../src/frontend/lowering/lowerer.test.ts | 2 +- .../compiler/src/frontend/lowering/lowerer.ts | 3 + .../src/frontend/lowering/surfaces.ts | 7 + .../compiler/src/ir/builtin-signatures.ts | 9 + packages/compiler/src/ir/ir.ts | 13 +- packages/compiler/src/ir/validate.ts | 19 +- packages/compiler/surface-manifest.json | 7 + packages/compiler/test/ir.test.ts | 10 +- .../tests/corpus/8020-date-setters.ts.json | 6 + .../tests/corpus/8021-date-coercions.ts.json | 6 + .../tests/corpus/8022-date-unions.ts.json | 6 + .../corpus/8023-date-clone-throw.ts.json | 6 + .../corpus/8024-date-setters-dynamic.mjs.json | 6 + .../corpus/8025-date-subclass-setters.ts.json | 6 + ...026-date-subclass-setters-dynamic.mjs.json | 6 + packages/runtime/src/scr_lib.c | 126 +++++++++++- packages/runtime/src/scr_number.h | 17 ++ tests/corpus/8020-date-setters.ts | 115 +++++++++++ tests/corpus/8021-date-coercions.ts | 47 +++++ tests/corpus/8022-date-unions.ts | 68 +++++++ tests/corpus/8023-date-clone-throw.ts | 32 +++ tests/corpus/8024-date-setters-dynamic.mjs | 24 +++ tests/corpus/8025-date-subclass-setters.ts | 30 +++ .../8026-date-subclass-setters-dynamic.mjs | 17 ++ tests/diagnostics/stdlib-fence.ts | 10 - .../__snapshots__/assert-class-fences.ts.txt | 2 +- .../class-interface-refinements.ts.txt | 4 +- .../__snapshots__/date-formatters.ts.txt | 4 +- .../dynamic-representable-types.ts.txt | 2 +- tests/harness/__snapshots__/generics.ts.txt | 2 +- .../harness/__snapshots__/maps-surface.ts.txt | 2 +- .../regex-match-destructuring-fences.ts.txt | 2 +- ...engine-2103-optional-tuple-elements.ts.txt | 10 +- .../harness/__snapshots__/stdlib-fence.ts.txt | 81 +++----- tests/harness/__snapshots__/tuples.ts.txt | 4 +- .../union-clone-override-width.ts.txt | 2 +- .../corpus-costs/8020-date-setters.ts.json | 1 + .../corpus-costs/8021-date-coercions.ts.json | 1 + .../corpus-costs/8022-date-unions.ts.json | 1 + .../8023-date-clone-throw.ts.json | 1 + .../8024-date-setters-dynamic.mjs.json | 1 + .../8025-date-subclass-setters.ts.json | 1 + ...026-date-subclass-setters-dynamic.mjs.json | 1 + 64 files changed, 1061 insertions(+), 180 deletions(-) create mode 100644 benchmarks/runtime/workloads/date-math.json create mode 100644 benchmarks/runtime/workloads/date-math.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8020-date-setters.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8021-date-coercions.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8022-date-unions.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8023-date-clone-throw.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8024-date-setters-dynamic.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8025-date-subclass-setters.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8026-date-subclass-setters-dynamic.mjs.json create mode 100644 tests/corpus/8020-date-setters.ts create mode 100644 tests/corpus/8021-date-coercions.ts create mode 100644 tests/corpus/8022-date-unions.ts create mode 100644 tests/corpus/8023-date-clone-throw.ts create mode 100644 tests/corpus/8024-date-setters-dynamic.mjs create mode 100644 tests/corpus/8025-date-subclass-setters.ts create mode 100644 tests/corpus/8026-date-subclass-setters-dynamic.mjs create mode 100644 tests/harness/corpus-costs/8020-date-setters.ts.json create mode 100644 tests/harness/corpus-costs/8021-date-coercions.ts.json create mode 100644 tests/harness/corpus-costs/8022-date-unions.ts.json create mode 100644 tests/harness/corpus-costs/8023-date-clone-throw.ts.json create mode 100644 tests/harness/corpus-costs/8024-date-setters-dynamic.mjs.json create mode 100644 tests/harness/corpus-costs/8025-date-subclass-setters.ts.json create mode 100644 tests/harness/corpus-costs/8026-date-subclass-setters-dynamic.mjs.json diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 4f152e4bb..11c049140 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -10,6 +10,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs | | `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` | +| `date-math` | cpu | schedules built with Date setters (local and UTC), weekday and month-end rules through the getters, Dates sorted by time value | | `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner | | `dictionary-walks` | cpu | `Record` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted | | `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods | diff --git a/benchmarks/runtime/workloads/date-math.json b/benchmarks/runtime/workloads/date-math.json new file mode 100644 index 000000000..b43078582 --- /dev/null +++ b/benchmarks/runtime/workloads/date-math.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/date-math.ts", + "args": [ + "40" + ] +} diff --git a/benchmarks/runtime/workloads/date-math.ts b/benchmarks/runtime/workloads/date-math.ts new file mode 100644 index 000000000..56ec1066e --- /dev/null +++ b/benchmarks/runtime/workloads/date-math.ts @@ -0,0 +1,52 @@ +// Calendar arithmetic on Dates: schedules built by adjusting Dates with the +// UTC and local setters, weekday and month-end rules read back through the +// getters, Dates compared and sorted by time value, and ISO text for a +// sample. Argument: the number of rounds. +process.env.TZ = "America/New_York"; + +const rounds = Number(process.argv[2] ?? "40"); +let seed = 2026; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} + +/** The next business day at 09:00 local time on or after `from`. */ +function nextBusinessDay(from: Date): Date { + const day = new Date(from.getTime()); + day.setHours(9, 0, 0, 0); + if (day < from) day.setDate(day.getDate() + 1); + while (day.getDay() === 0 || day.getDay() === 6) day.setDate(day.getDate() + 1); + return day; +} + +/** The last day of `date`'s month, in UTC. */ +function monthEnd(date: Date): number { + const end = new Date(date.getTime()); + end.setUTCDate(1); + end.setUTCMonth(end.getUTCMonth() + 1); + end.setUTCDate(0); + return end.getUTCDate(); +} + +let checksum = 0; +let sample = ""; +for (let round = 0; round < rounds; round++) { + const due: Date[] = []; + const cursor = new Date(Date.UTC(2020 + (round % 7), 0, 1, 12)); + for (let i = 0; i < 4000; i++) { + cursor.setUTCMinutes(cursor.getUTCMinutes() + Math.floor(random() * 4000)); + const next = nextBusinessDay(cursor); + checksum += next.getDate() + monthEnd(next) + next.getHours(); + if (i % 3 === 0) next.setMonth(next.getMonth() + 1, 15); + due.push(next); + } + due.sort((a, b) => +a - +b); + let previous = due[0]!; + for (const date of due) { + checksum += (+date - +previous) / 3600000; + previous = date; + } + sample = due[due.length >> 1]!.toISOString(); +} +console.log(Math.round(checksum), sample); diff --git a/docs/content/limitations/builtin-apis/date.mdx b/docs/content/limitations/builtin-apis/date.mdx index 89266b745..d783984fa 100644 --- a/docs/content/limitations/builtin-apis/date.mdx +++ b/docs/content/limitations/builtin-apis/date.mdx @@ -1,6 +1,7 @@ -- **Date:** Spread constructor arguments, setters, thrown Date values, and locale formatters are unsupported. - Dates are objects: `===` compares identity, `new Date(date)` makes a new Date, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in `Date | undefined` and `Date | null` slots, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators. - Unions of a Date with other non-nullish types are unsupported. - Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, relational comparison, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. +- **Date:** Spread constructor arguments and locale formatters are unsupported. + Dates are objects with identity: `===` and `==` compare identity, `new Date(date)` makes a new Date, setters change the one Date every reference holds, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in unions with other types, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators. + Thrown and rejected Dates keep their identity; `structuredClone` and `postMessage` copy them. + Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, the local and UTC calendar getters and setters, `setTime`, `getTimezoneOffset`, and single-string `Date.parse` compile. + Unary `+`, `-` and `Number(date)` read the time value; `String(date)`, template literals and `+` with a string read the `toString()` text; `date[Symbol.toPrimitive](hint)` compiles with a literal hint. A Date converted to `unknown` is the same Date object. See [Dates and timezones](#dates-and-timezones) for timezone data. diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 9cd11feb1..d8ca5add8 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -649,6 +649,15 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll if (MAY_THROW_LIB_FNS.has(e.fn) && LIB_FN_SYMS[e.fn] === undefined) { throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc); } + // A setter's write: the time value into the Date object (ScrDate.ms). + if (e.fn === "date.store") { + const date = emitBorrowedInput(host, e.args[0]!).name; + const ms = host.emitExpr(e.args[1]!).name; + const field = B.tmp(); + B.line(`${field} = getelementptr inbounds i8, ptr ${date}, i64 8 ; ->ms`); + B.line(`store double ${ms}, ptr ${field}`); + return { name: ms, type: e.type }; + } // queueMicrotask's checked-dynamic form carries the AsyncLocalStorage // context like the static one (lib-process.ts). const sym = diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 21f32431c..69d028539 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -334,6 +334,8 @@ export const LIB_FN_SYMS: Record = { "date.getTime": "scr_date_get_time", "date.valueOf": "scr_date_get_time", "date.fromTime": "scr_date_get_time", + "date.setFields": "scr_date_set_fields", + "date.timeClip": "scr_date_new_ms", "date.parseGetTime": "scr_date_parse", "date.utc": "scr_date_utc", "date.newLocal": "scr_date_new_local", diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 2fd8b6fb0..55c3e9047 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -622,7 +622,7 @@ export function unsupportedTypeDiag(typeText: string, loc: SrcLoc): ScrDiagnosti code: "SC2001", message: `values of type '${typeText}' cannot be compiled yet ` + - `(supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')`, + `(supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')`, loc, milestone: "M2", }; diff --git a/packages/compiler/src/frontend/lowering/abstract-equality.ts b/packages/compiler/src/frontend/lowering/abstract-equality.ts index 6c74b3546..634ff6f22 100644 --- a/packages/compiler/src/frontend/lowering/abstract-equality.ts +++ b/packages/compiler/src/frontend/lowering/abstract-equality.ts @@ -5,6 +5,7 @@ import { DYN, DYN_HANDLE_KINDS, F64, + STRING, type IrExpr, type IrStmt, type IrType, @@ -285,6 +286,43 @@ export function lowerAbstractEquality( }; return negated ? { kind: "unary", op: "!", operand: result, type: BOOL, loc } : result; } + // Two Dates are equal when they are the same object, as with ===. + if (left.type.kind === "date" && right.type.kind === "date") + return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc }; + // A Date and a string or number compare through the Date's default + // primitive, its toString() text. + if (left.type.kind === "date" || right.type.kind === "date") { + const otherType = left.type.kind === "date" ? right.type : left.type; + if (otherType.kind === "string" || otherType.kind === "f64") { + // Both operands evaluate before the Date converts. + const l = lowerer.declareHiddenLocal("%looseA", left.type); + const r = lowerer.declareHiddenLocal("%looseB", right.type); + const stmts: IrStmt[] = [ + { kind: "varDecl", localId: l.id, init: left, loc }, + { kind: "varDecl", localId: r.id, init: right, loc }, + ]; + const primitive = (value: IrExpr): IrExpr => { + if (value.type.kind !== "date") return value; + const text: IrExpr = { + kind: "libCall", + fn: "date.toString", + args: [value], + type: STRING, + loc, + }; + return otherType.kind === "string" + ? text + : { kind: "libCall", fn: "num.fromString", args: [text], type: F64, loc }; + }; + const a = primitive(varRef(l.id, left.type, loc)); + const b = primitive(varRef(r.id, right.type, loc)); + const result: IrExpr = + otherType.kind === "string" + ? { kind: "strEq", negated, left: a, right: b, type: BOOL, loc } + : { kind: "bin", op: negated ? "!==" : "===", left: a, right: b, type: BOOL, loc }; + return { kind: "seqExpr", stmts, result, type: BOOL, loc }; + } + } if (!supportsPair(lowerer, left.type, right.type)) return null; if (left.kind === "strLit" && right.kind === "strLit") { return { kind: "boolLit", value: (left.value === right.value) !== negated, type: BOOL, loc }; diff --git a/packages/compiler/src/frontend/lowering/builtins/date-classes.ts b/packages/compiler/src/frontend/lowering/builtins/date-classes.ts index a2b0fc20a..c8aab8687 100644 --- a/packages/compiler/src/frontend/lowering/builtins/date-classes.ts +++ b/packages/compiler/src/frontend/lowering/builtins/date-classes.ts @@ -2,7 +2,13 @@ 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 { + DATE_METHOD_HINT, + dateJsonType, + dateSetterTime, + dateValueMethod, + isDateSetter, +} from "./dates.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import { BOOL, @@ -248,6 +254,20 @@ export function lowerInheritedDateMethodCall( type: result.type, loc, }); + if (isDateSetter(name) && name !== "setTime") { + // The field setters change the Date the class's time slot holds. + const date = lowerer.declareHiddenLocal("%dateTime", DATE_T); + stmts.push({ + kind: "varDecl", + localId: date.id, + init: timeOf(lowerer, value, root, loc), + loc, + }); + const current = varRef(date.id, DATE_T, loc); + const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [current], type: F64, loc }; + const next = dateSetterTime(lowerer, call, name, stmts, time); + return done({ kind: "libCall", fn: "date.store", args: [current, next], type: F64, loc }); + } if (name === "setTime") { // Every argument is evaluated before setTime converts the first one. let ms: IrExpr = { kind: "numLit", value: Number.NaN, type: F64, loc }; diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index 0204386ab..79696d1ba 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -2,12 +2,15 @@ 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 { lowerToNumberArguments } from "../lower-exprs.js"; import { programDeclaresDateClass } from "./date-classes.js"; import { BOOL, + DATE_T, F64, type IrExpr, type IrLibFn, + type IrLocal, type IrStmt, type IrType, NULL_T, @@ -36,6 +39,32 @@ const DATE_GETTER_FNS: Readonly> = { getTimezoneOffset: "date.getTimezoneOffset", }; +/** The setters that change calendar fields: the field code + * scr_date_set_fields takes (scr_number.h SCR_DATE_SET_*; 8 marks the UTC + * forms) and the most arguments each takes. */ +const DATE_FIELD_SETTERS: ReadonlyMap = new Map([ + ["setMilliseconds", { code: 0, max: 1 }], + ["setSeconds", { code: 1, max: 2 }], + ["setMinutes", { code: 2, max: 3 }], + ["setHours", { code: 3, max: 4 }], + ["setDate", { code: 4, max: 1 }], + ["setMonth", { code: 5, max: 2 }], + ["setFullYear", { code: 6, max: 3 }], + ["setUTCMilliseconds", { code: 8, max: 1 }], + ["setUTCSeconds", { code: 9, max: 2 }], + ["setUTCMinutes", { code: 10, max: 3 }], + ["setUTCHours", { code: 11, max: 4 }], + ["setUTCDate", { code: 12, max: 1 }], + ["setUTCMonth", { code: 13, max: 2 }], + ["setUTCFullYear", { code: 14, max: 3 }], +]); + +/** Whether `name` is a Date setter scriptc lowers: setTime or a field + * setter. */ +export function isDateSetter(name: string): boolean { + return name === "setTime" || DATE_FIELD_SETTERS.has(name); +} + const DATE_TEXT_FNS: ReadonlyMap = new Map([ ["toString", "date.toString"], ["toDateString", "date.toDateString"], @@ -43,11 +72,11 @@ const DATE_TEXT_FNS: ReadonlyMap = new Map([ ]); 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"; + "getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters and setters, setTime(), and getTimezoneOffset() are supported; locale formatters have no lowering"; -/** The Date slice: statics plus read-only Date values backed by one - * TimeClip'd millisecond scalar. Construction lives in lowerNew; - * identity and mutating methods remain fenced. */ +/** Date statics and methods on Date objects (a TimeClip'd millisecond + * time value with identity): getters, text forms and setters. + * Construction lives in lowerNew. */ export function lowerDateCall( lowerer: Lowerer, call: ts.CallExpression, @@ -67,16 +96,18 @@ export function lowerDateCall( 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 + // A Date a JavaScript file constructs is a checked-dynamic value; a + // setter on it takes the dynamic method call, which changes the Date the + // handle holds. 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)) && + (isDateSetter(name) || programDeclaresDateClass(lowerer)) && tryLowerExpression(lowerer, access.expression)?.type.kind === "dyn" ) return null; if (name === "toJSON") return lowerDateToJson(lowerer, call, access); + if (isDateSetter(name)) return lowerDateSetter(lowerer, call, access, name); if (call.arguments.nodes.length !== 0) { lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT); } @@ -253,3 +284,150 @@ export function isDateJsonSource(lowerer: Lowerer, node: ts.Node): boolean { lowerer.isStdlibMember(access) ); } + +/** `d.setTime(t)` and the field setters (`d.setHours(h, m?, s?, ms?)` ...) + * on a Date object: the receiver, then the arguments in order, then the + * new time value (scr_date_set_fields over the time value the Date holds + * once the arguments have run) stored into the Date and answered. Missing + * optional arguments keep their fields; arguments past a setter's last + * one are evaluated and ignored. A Date local that holds its time value + * (date-values.ts) takes the new time value itself. */ +function lowerDateSetter( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, + name: string, +): IrExpr { + const loc = locOf(call); + const nodes = call.arguments.nodes; + if (nodes.some((node) => ts.isSpreadElement(node))) + lowerer.noLowering(`Date.prototype.${name} with spread arguments`, call, DATE_METHOD_HINT); + const timeLocal = timeValueLocalOf(lowerer, access.expression); + const stmts: IrStmt[] = []; + let receiver: IrExpr | null = null; + if (timeLocal === null) { + // A checked-dynamic receiver takes the dynamic method call + // (lowerDateCall), so the value here is a Date object. + const value = lowerer.lowerExpr(access.expression); + if (value.type.kind !== "date") + lowerer.badType(access.expression, lowerer.typeOf(access.expression)); + const date = lowerer.declareHiddenLocal("%date", DATE_T); + stmts.push({ kind: "varDecl", localId: date.id, init: value, loc }); + receiver = { kind: "varRef", localId: date.id, type: DATE_T, loc }; + } + const time: IrExpr = + receiver === null + ? { kind: "varRef", localId: timeLocal!.id, type: F64, loc } + : { kind: "libCall", fn: "date.getTime", args: [receiver], type: F64, loc }; + const next = dateSetterTime(lowerer, call, name, stmts, time); + if (receiver === null) { + stmts.push({ kind: "assign", localId: timeLocal!.id, value: next, loc }); + return { + kind: "seqExpr", + stmts, + result: { kind: "varRef", localId: timeLocal!.id, type: F64, loc }, + type: F64, + loc, + }; + } + return { + kind: "seqExpr", + stmts, + result: { kind: "libCall", fn: "date.store", args: [receiver, next], type: F64, loc }, + type: F64, + loc, + }; +} + +/** The new time value of setter `name` called with `call`'s arguments on a + * Date whose time value `time` reads: the arguments evaluate into + * temporaries appended to `stmts` (ToNumber of each, an undefined one is + * NaN), and the result reads `time` after them. Missing optional arguments + * keep their fields; arguments past a setter's last one are evaluated and + * ignored. */ +export function dateSetterTime( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + stmts: IrStmt[], + time: IrExpr, +): IrExpr { + const loc = locOf(call); + const converted = lowerToNumberArguments( + lowerer, + call.arguments.nodes as readonly ts.Expression[], + ); + stmts.push(...converted.stmts); + const args: IrExpr[] = converted.values.map((value) => { + if (value.kind === "numLit") return value; + const arg = lowerer.declareHiddenLocal("%dateArg", F64); + stmts.push({ kind: "varDecl", localId: arg.id, init: value, loc }); + return { kind: "varRef", localId: arg.id, type: F64, loc }; + }); + const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); + if (name === "setTime") + return { + kind: "libCall", + fn: "date.timeClip", + args: [args[0] ?? number(Number.NaN)], + type: F64, + loc, + }; + const setter = DATE_FIELD_SETTERS.get(name)!; + const used = args.slice(0, setter.max); + const operands = [...used]; + while (operands.length < 4) operands.push(number(0)); + return { + kind: "libCall", + fn: "date.setFields", + args: [time, number(setter.code), number(used.length), ...operands], + type: F64, + loc, + }; +} + +/** The local a Date binding that holds its time value lowers to, for an + * identifier `node` that names one (date-values.ts); null otherwise. */ +function timeValueLocalOf(lowerer: Lowerer, node: ts.Expression): IrLocal | null { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (!ts.isIdentifier(node)) return null; + const local = lowerer.resolveLocal(node); + return local !== undefined && local !== null && lowerer.timeValueDateLocals.has(local) + ? local + : null; +} + +/** `d[Symbol.toPrimitive](hint)` on a Date with a literal hint: "number" + * answers the time value, "string" and "default" the toString() text (a + * Date's default hint is a string). Null for other calls. */ +export function lowerDateToPrimitiveCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.ElementAccessExpression, +): IrExpr | null { + const key = access.argumentExpression; + if ( + !ts.isPropertyAccessExpression(key) || + key.name.text !== "toPrimitive" || + !ts.isIdentifier(key.expression) || + !lowerer.isStdlibGlobal(key.expression, "Symbol") || + lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date" + ) + return null; + const loc = locOf(call); + const hint = call.arguments.nodes.length === 1 ? call.arguments.nodes[0]! : null; + const text = hint !== null && ts.isStringLiteralLike(hint) ? hint.text : null; + if (text !== "number" && text !== "string" && text !== "default") + return lowerer.noLowering( + 'Date.prototype[Symbol.toPrimitive] without a "number", "string" or "default" literal hint', + call, + ); + const raw = lowerer.lowerExpr(access.expression); + const date: IrExpr = + raw.type.kind === "dyn" + ? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: DATE_T, loc } + : raw; + return text === "number" + ? { kind: "libCall", fn: "date.valueOf", args: [date], type: F64, loc } + : { kind: "libCall", fn: "date.toString", args: [date], type: STRING, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/date-values.ts b/packages/compiler/src/frontend/lowering/date-values.ts index ef5eafebe..669bc1dbc 100644 --- a/packages/compiler/src/frontend/lowering/date-values.ts +++ b/packages/compiler/src/frontend/lowering/date-values.ts @@ -1,5 +1,6 @@ import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; +import { isDateSetter } from "./builtins/dates.js"; /** Date methods whose receiver is only read for its time value: a Date * local used as their receiver can store the time value instead of a Date @@ -41,12 +42,14 @@ const RELATIONAL = new Set([ /** True when a local `const` (or a `let` that is never assigned again) * initialized with `new Date(...)` can keep the Date's time value instead - * of a Date object: no use observes the Date's identity. Every use reads the time value as - * a Date method receiver, a String() or template operand, a relational or - * string `+` operand, or `new Date(d)`'s argument, inside the declaring - * function, or inside the block or loop that scopes a module-level - * binding (no closure captures it). Top-level module bindings keep the - * object. Other uses box the time value into a new Date at the use. */ + * of a Date object: no use observes the Date's identity. Every use reads + * the time value as a Date method receiver (a setter replaces it), a + * String(), Number() or template operand, a unary + or - operand, a + * relational or string `+` operand, or `new Date(d)`'s argument, inside + * the declaring function, or inside the block or loop that scopes a + * module-level binding (no closure captures it). Top-level module bindings + * keep the object. Other uses box the time value into a new Date at the + * use. */ export function isTimeValueDateDeclaration( lowerer: Lowerer, decl: ts.VariableDeclaration, @@ -94,17 +97,21 @@ function decideTimeValueDate(lowerer: Lowerer, decl: ts.VariableDeclaration): bo const symbol = lowerer.checker.getSymbolAtLocation(decl.name); if (!symbol) return false; let ok = true; + let sets = false; const visit = (node: ts.Node): void => { if (!ok) return; if (ts.isIdentifier(node) && node !== decl.name && node.text === decl.name.text) { if (lowerer.checker.getSymbolAtLocation(node) === symbol) { - if (enclosingFunction(node) !== fn || !readsTimeValue(lowerer, node)) ok = false; + const use = enclosingFunction(node) === fn ? timeValueUse(lowerer, node) : null; + if (use === null) ok = false; + else if (use === "set") sets = true; } return; } ts.forEachChild(node, visit); }; visit(scope); + if (ok && sets) lowerer.timeValueDateWrites.add(decl); return ok; } @@ -164,29 +171,46 @@ function enclosingFunction(node: ts.Node): ts.FunctionLikeDeclaration | null { return null; } -/** A use of the binding that reads only its time value. */ -function readsTimeValue(lowerer: Lowerer, id: ts.Identifier): boolean { +/** How a use of the binding uses its time value: "read" for a use that + * reads only the time value, "set" for a setter call that replaces it, or + * null for any other use. */ +function timeValueUse(lowerer: Lowerer, id: ts.Identifier): "read" | "set" | null { let node: ts.Node = id; while (ts.isParenthesizedExpression(node.parent)) node = node.parent; const parent = node.parent; + if (parent === undefined) return null; if (ts.isPropertyAccessExpression(parent) && parent.expression === node) { const call = parent.parent; - return ( - !parent.questionDotToken && - ts.isCallExpression(call) && - call.expression === parent && - !call.questionDotToken && - TIME_VALUE_METHODS.has(parent.name.text) && - lowerer.isStdlibMember(parent) - ); + if ( + parent.questionDotToken || + !ts.isCallExpression(call) || + call.expression !== parent || + call.questionDotToken || + !lowerer.isStdlibMember(parent) + ) + return null; + if (isDateSetter(parent.name.text)) return "set"; + return TIME_VALUE_METHODS.has(parent.name.text) ? "read" : null; } + return readsTimeValue(lowerer, node, parent) ? "read" : null; +} + +/** A use of the binding, other than a method call, that reads only its + * time value. */ +function readsTimeValue(lowerer: Lowerer, node: ts.Node, parent: ts.Node): boolean { if (ts.isTemplateSpan(parent)) return parent.expression === node; + // Unary + and -, the number hint. + if (ts.isPrefixUnaryExpression(parent)) + return ( + parent.operator === ts.SyntaxKind.PlusToken || parent.operator === ts.SyntaxKind.MinusToken + ); if (ts.isCallExpression(parent)) { return ( parent.arguments.nodes.length === 1 && parent.arguments.nodes[0] === node && ts.isIdentifier(parent.expression) && - lowerer.isStdlibGlobal(parent.expression, "String") + (lowerer.isStdlibGlobal(parent.expression, "String") || + lowerer.isStdlibGlobal(parent.expression, "Number")) ); } if (ts.isNewExpression(parent)) { diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 00a23caf5..de6648836 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -1600,6 +1600,19 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr loc, }; } + // A Date against an untyped value: the same Date object, whose + // checked-dynamic handle the untyped value would hold. + if (dynSide.type.kind === "dyn" && scalarSide.type.kind === "date") { + const boxed = lowerer.coerceToExpected(scalarSide, DYN); + return { + kind: "dynScalarEq", + left: scalarSide === left ? boxed : left, + right: scalarSide === right ? boxed : right, + ...(negated ? { negated: true as const } : {}), + type: BOOL, + loc, + }; + } if ( dynSide.type.kind === "dyn" && (scalarSide.type.kind === "f64" || diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 1a4883e5a..874c0c6cc 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -112,6 +112,7 @@ import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../. import { typedArrayLacksMember } from "../builtins/typed-array-members.js"; import { TEMPORAL_GLOBAL } from "../../builtin-modules.js"; import { tryLowerExpression } from "./try-lower-expression.js"; +import { DATE_METHOD_HINT } from "../builtins/dates.js"; import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, @@ -2055,6 +2056,9 @@ export function lowerIntrinsicProperty( ); } if (kind === "date") { + // A Date held as a checked-dynamic value (a Date a JavaScript file + // constructs) answers its method values at run time. + if (tryLowerExpression(lowerer, expr.expression)?.type.kind === "dyn") return null; const methods = new Set([ "getTime", "valueOf", @@ -2083,7 +2087,7 @@ export function lowerIntrinsicProperty( lowerer.noLowering( `Date.prototype.${name}`, expr, - "getTime(), valueOf(), toISOString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering", + DATE_METHOD_HINT, lowerer.checker.getSymbolAtLocation(expr.name), ); } diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 1651bf0d9..2903e4e14 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -288,6 +288,16 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc if (!target) { const date = lowerer.isStdlibGlobal(expr.right, "Date"); const url = lowerer.isStdlibGlobal(expr.right, "URL"); + // A catch binding holds a thrown Date as its checked-dynamic handle. + const caughtDate = date ? lowerer.caughtLocalOf(expr.left) : null; + if (caughtDate) + return { + kind: "libCall", + fn: "dyn.nativeDateIs", + args: [{ kind: "caughtToDyn", value: varRef(caughtDate.id, CAUGHT, loc), type: DYN, loc }], + type: BOOL, + loc, + }; if ((date || url) && !lowerer.caughtLocalOf(expr.left)) { const value = lowerer.lowerExpr(expr.left); if (value.type.kind === "dyn") diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts index fbbc068b9..0676f3cf5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts +++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts @@ -281,6 +281,7 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc promise: "object", bytes: "object", regex: "object", + date: "object", generator: "object", classval: "function", moduleNs: "object", diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index bdcd94a44..35d5d1a2e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -120,6 +120,11 @@ function coercedUnion(lowerer: Lowerer, type: IrType): boolean { return arms.some((arm) => arm.kind !== "f64" && !isUnitType(arm)); } +/** ToNumber of a Date: its time value (valueOf, the number hint). */ +function dateTimeValue(date: IrExpr, loc: SrcLoc): IrExpr { + return { kind: "libCall", fn: "date.valueOf", args: [date], type: F64, loc }; +} + /** ToNumber of a boolean: 1 for true, 0 for false. */ function booleanToNumber(value: IrExpr, loc: SrcLoc): IrExpr { return { @@ -145,23 +150,25 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio const 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)) + : raw.type.kind === "date" + ? dateTimeValue(raw, loc) + : coercedUnion(lowerer, raw.type) ? ({ kind: "libCall", fn: "dyn.toNumberCoerce", - args: [raw], + args: [{ kind: "dynFrom", value: raw, type: DYN, loc }], type: F64, loc, } satisfies IrExpr) - : lowerOptionalNumber(lowerer, raw, loc, expr.operand); + : 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 }; @@ -185,6 +192,8 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc }; } if (raw.type.kind === "bool") return booleanToNumber(raw, loc); + // A Date's number hint answers its time value. + if (raw.type.kind === "date") return dateTimeValue(raw, loc); const classNumber = classToNumberConversion(lowerer, raw, expr.operand, "unary plus"); if (classNumber) return classNumber; const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand); diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 073ebed91..6ca6b7016 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,5 +1,6 @@ import { keyedHelperName } from "./stable-names.js"; import { perKeyOrder } from "./key-order-witness.js"; +import { lowerDateToPrimitiveCall } from "./builtins/dates.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; import { lowerDynStreamMemberCall } from "./dyn-streams.js"; @@ -1816,6 +1817,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // The element spelling of an unsupported native Web method must fence // before lowering the callee into a checked-dynamic keyed read. if (ts.isElementAccessExpression(expr.expression)) { + const datePrimitive = lowerDateToPrimitiveCall(lowerer, expr, expr.expression); + if (datePrimitive) return datePrimitive; const timersDispose = lowerMockTimersElementCall(lowerer, expr, expr.expression); if (timersDispose) return timersDispose; const symbolMethod = lowerClassSymbolMethodCall(lowerer, expr); @@ -2456,6 +2459,9 @@ function lowerNumberConstructorValue( }; const optionalNumber = lowerOptionalStringNumber(lowerer, arg, loc); if (optionalNumber) return optionalNumber; + // A Date's number hint answers its time value. + if (arg.type.kind === "date") + return { kind: "libCall", fn: "date.valueOf", args: [arg], type: F64, loc }; // A program class instance converts through its own methods. const classNumber = classToNumberConversion(lowerer, arg, argNode); if (classNumber) return classNumber; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 3e49304d2..7655fcea7 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -10426,6 +10426,29 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [arg], type: F64, loc }; return { kind: "libCall", fn: "date.newMs", args: [time], type: DATE_T, loc }; } + // A union (`Date | string | number`) or checked-dynamic value: the + // runtime's constructor reads a Date's time value, parses a string + // and converts anything else to a primitive and a number. + if ( + (arg.type.kind === "union" || arg.type.kind === "dyn") && + lowerer.dynConvertible(arg.type) + ) { + const value: IrExpr = { + kind: "libCall", + fn: "date.nativeNewValue", + args: [ + { + kind: "dynArrLit", + elems: [lowerer.coerceToExpected(arg, DYN)], + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }; + return { kind: "libCall", fn: "date.checkedValue", args: [value], type: DATE_T, loc }; + } lowerer.noLowering( `new Date of '${lowerer.fmt(arg.type)}' values`, args[0]!, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index f2b601619..88e9ddb12 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -2320,6 +2320,7 @@ export function typeofAnswer(arm: IrType): string | null { case "set": case "generator": case "regex": + case "date": case "bytes": case "url": case "searchParams": diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index f837cc0af..ccfa4ad39 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -183,13 +183,9 @@ export function lowerGenMethodCall( ); } arg = lowerer.lowerExpr(argNode); - if (arg.type.kind === "date") { - lowerer.unsupported( - "SC1090", - argNode, - ".throw() with a Date value (the exception channel cannot preserve Date's object kind; throw an Error or primitive instead)", - ); - } + // A Date throws as its checked-dynamic handle, the same Date object + // (the throw statement's rule). + if (arg.type.kind === "date") arg = lowerer.coerceToExpected(arg, DYN); if (arg.type.kind === "void" || arg.type.kind === "caught" || isUnitType(arg.type)) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index c50676863..9e9e75794 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1624,6 +1624,7 @@ const THROWN_AS_DYN: ReadonlySet = new Set([ "bytes", "func", "regex", + "date", ]); export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null { @@ -1828,9 +1829,7 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt return { kind: "rethrow", localId: caughtLocal.id, loc: locOf(stmt) }; } // Exception-representable values move into the runtime's exception - // cell. `throw f()` of a void call has no value; Date has numeric - // storage internally, but cannot preserve its object kind through the - // untyped catch boundary. Reject both rather than changing JS identity. + // cell. `throw f()` of a void call has no value. let value = lowerer.lowerExpr(stmt.expression); if (value.type.kind === "void") lowerer.badType(stmt.expression, lowerer.typeOf(stmt.expression)); @@ -1842,19 +1841,12 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt } else if (value.type.kind === "record" && lowerer.dynConvertible(value.type)) { value = { kind: "dynFrom", value, liveRef: true, type: DYN, loc: locOf(stmt.expression) }; } else if (THROWN_AS_DYN.has(value.type.kind) && lowerer.dynConvertible(value.type)) { - // Arrays, Maps, Sets, typed arrays, Buffers, functions, regexes and - // unions over them keep their identity and contents the same way: - // the catch binding, `typeof`, `instanceof`, `Array.isArray`, - // inspection and typed views read the thrown object. + // Arrays, Maps, Sets, typed arrays, Buffers, functions, regexes, + // Dates and unions over them keep their identity and contents the + // same way: the catch binding, `typeof`, `instanceof`, + // `Array.isArray`, inspection and typed views read the thrown object. value = lowerer.coerceInto(stmt.expression, value, DYN); } - if (value.type.kind === "date") { - lowerer.unsupported( - "SC1090", - stmt.expression, - "throwing Date values (catch bindings cannot preserve Date's object kind; throw an Error or primitive instead)", - ); - } // A dyn throw rides the REF cell arm (scr_throw_ref over the checked-dynamic tree // node — identity preserved through catch bindings, caughtToDyn // passes it back by reference). The JS-lane traced-call shape: @@ -5450,7 +5442,12 @@ export function lowerVarDecl( if (isTimeValueDateDeclaration(lowerer, decl)) { const loc = locOf(decl); const date = lowerer.lowerExprExpecting(decl.initializer!, DATE_T); - const local = lowerer.declareLocal(decl.name, decl.name.text, F64, isLet); + const local = lowerer.declareLocal( + decl.name, + decl.name.text, + F64, + isLet || lowerer.timeValueDateWrites.has(decl), + ); lowerer.timeValueDateLocals.add(local); const init: IrExpr = { kind: "libCall", fn: "date.getTime", args: [date], type: F64, loc }; return { kind: "varDecl", localId: local.id, init, loc }; diff --git a/packages/compiler/src/frontend/lowering/lowerer.test.ts b/packages/compiler/src/frontend/lowering/lowerer.test.ts index 7fbb9661f..f36695234 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.test.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.test.ts @@ -239,7 +239,7 @@ test("optional union widening keeps refused and missing contracts current", () = unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), }; expect(lowerer.withUndefinedArmOf(pending)).not.toBeNull(); - lowerer.unions.finalizeRecursive(recursive, [{ kind: "date" }, STRING]); + lowerer.unions.finalizeRecursive(recursive, [{ kind: "promise", inner: F64 }, STRING]); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 2fcfc063b..bdb7ce6c1 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2362,6 +2362,9 @@ export class Lowerer { * Date's time value instead of a Date object (date-values.ts). */ readonly timeValueDateDecisions = new Map(); readonly timeValueDateLocals = new Set(); + /** Declarations of those locals that a setter call writes + * (date-values.ts). */ + readonly timeValueDateWrites = new Set(); /** Record bindings whose flow class reaches storage outside the class: * they keep the declared layout, so an initializer from unchecked reads * converts once instead of the binding adopting the widened variant. */ diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index d133eeb43..a2ca2cd1c 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2329,6 +2329,13 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ ], note: "zero arguments, one milliseconds/date-string argument, or local-time year/month fields; values are the read-only TimeClip scalar slice", }, + { + id: "stdlib.date.setters", + kind: "stdlib", + name: "Date.prototype setters", + fns: ["date.setFields", "date.timeClip", "date.store"], + note: "setTime and the local and UTC field setters, with their optional arguments", + }, { id: "stdlib.date.UTC", kind: "stdlib", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 37d9769e0..aaa58c088 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4858,6 +4858,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * lib-call argument passes the time value itself, and any other use * creates the Date. */ "date.fromTime": { argTypes: [F64], result: DATE_T }, + /** Date.prototype's field setters over a time value + * (scr_date_set_fields): time value, setter code, argument count and four + * arguments; answers the new time value. */ + "date.setFields": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: F64 }, + /** TimeClip of a number (setTime's argument). */ + "date.timeClip": { argTypes: [F64], result: F64 }, + /** Stores a time value into a Date object and answers it (a setter's + * write). */ + "date.store": { argTypes: [DATE_T, F64], result: F64 }, /** Date.prototype.toString / toDateString / toTimeString and String(date): * V8's local-time formats with the host zone's long name, "Invalid Date" * for an invalid date. Owned (+1) string; never throws. */ diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index d910374fb..b2511f8a8 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -855,9 +855,8 @@ export function funcOf(params: IrType[], ret: IrType): IrType { * promises nullable-only; declared maybe-promise unions are admitted by * maybePromiseUnionArmsOk. */ export function unionContainerArmsOk(arms: IrType[]): boolean { - return ( - arms.every((a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b))) && - dateUnionArmsOk(arms) + return arms.every( + (a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b)), ); } @@ -868,13 +867,7 @@ export function unionContainerArmsOk(arms: IrType[]): boolean { * non-thenable path). Data arms must not be thenable; the type mapper * checks that against the checker types before minting such a union. */ export function maybePromiseUnionArmsOk(arms: IrType[]): boolean { - return arms.filter((a) => a.kind === "promise").length <= 1 && dateUnionArmsOk(arms); -} - -/** A Date arm has a union home beside undefined and null arms only: no - * narrowing contract tells it from other data arms yet. */ -function dateUnionArmsOk(arms: IrType[]): boolean { - return arms.every((a, i) => a.kind !== "date" || arms.every((b, j) => j === i || isUnitType(b))); + return arms.filter((a) => a.kind === "promise").length <= 1; } /** Canonical, injective text form of an IrType — the building block of diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index a694e8d2c..b1c571db2 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -55,7 +55,6 @@ import { typeEquals, typeKey, maybePromiseUnionArmsOk, - unionContainerArmsOk, VOID, } from "./ir.js"; import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from "./ir.js"; @@ -814,9 +813,6 @@ export function validateModule(mod: IrModule): IrValidationError[] { loc: noLoc, }); } - if (arm.kind === "date" && !unionContainerArmsOk(u.arms)) { - errors.push({ message: `union ${u.id}: date arm ${i} beside non-unit arms`, loc: noLoc }); - } if (arm.kind === "record" && !recordsById.has(arm.shapeId)) { errors.push({ message: `union ${u.id}: arm ${i} references undeclared shape "${arm.shapeId}"`, @@ -6334,15 +6330,10 @@ function validateFunction( } } else { // throw: any exception-representable payload (the throw - // statement's rule). Date cannot retain its object kind in the - // untyped exception cell. + // statement's rule). if (e.arg === null) { err("genResume throw with no payload", e.loc); - } else if ( - e.arg.type.kind === "void" || - e.arg.type.kind === "caught" || - e.arg.type.kind === "date" - ) { + } else if (e.arg.type.kind === "void" || e.arg.type.kind === "caught") { err(`genResume throw of a ${e.arg.type.kind} value`, e.loc); } } @@ -6783,11 +6774,7 @@ function validateFunction( checkExpr(s.value); // dyn throws are allowed: the dyn node rides the REF cell arm by // reference (the JS-lane `throw err` of a dyn argument). - if ( - s.value.type.kind === "void" || - s.value.type.kind === "caught" || - s.value.type.kind === "date" - ) { + if (s.value.type.kind === "void" || s.value.type.kind === "caught") { err(`throw of a ${s.value.type.kind} value`, s.loc); } break; diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 75541b8e5..2a98957b2 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -4251,6 +4251,13 @@ "status": "static", "note": "one date string, using V8's date parser shared with new Date(dateString)" }, + { + "id": "stdlib.date.setters", + "kind": "stdlib", + "name": "Date.prototype setters", + "status": "static", + "note": "setTime and the local and UTC field setters, with their optional arguments" + }, { "id": "stdlib.date.toDateString", "kind": "stdlib", diff --git a/packages/compiler/test/ir.test.ts b/packages/compiler/test/ir.test.ts index 7f5e9f3c9..5d835ef8a 100644 --- a/packages/compiler/test/ir.test.ts +++ b/packages/compiler/test/ir.test.ts @@ -8,12 +8,10 @@ test("hand-built fib module validates", () => { expect(validateModule(fibModule)).toEqual([]); }); -test("validator rejects Date union arms beside other data arms", () => { - const bad = structuredClone(fibModule); - bad.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "string" }] }]; - expect(validateModule(bad).map((e) => e.message)).toContain( - "union u-date: date arm 0 beside non-unit arms", - ); +test("validator accepts Date union arms beside other arms", () => { + const mixed = structuredClone(fibModule); + mixed.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "string" }] }]; + expect(validateModule(mixed)).toEqual([]); const nullable = structuredClone(fibModule); nullable.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "undefinedT" }] }]; expect(validateModule(nullable)).toEqual([]); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8020-date-setters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8020-date-setters.ts.json new file mode 100644 index 000000000..5b23d8ecd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8020-date-setters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8020-date-setters.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8021-date-coercions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8021-date-coercions.ts.json new file mode 100644 index 000000000..176788025 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8021-date-coercions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8021-date-coercions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8022-date-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8022-date-unions.ts.json new file mode 100644 index 000000000..ab8c55b7b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8022-date-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8022-date-unions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8023-date-clone-throw.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8023-date-clone-throw.ts.json new file mode 100644 index 000000000..934d260d8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8023-date-clone-throw.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8023-date-clone-throw.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8024-date-setters-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8024-date-setters-dynamic.mjs.json new file mode 100644 index 000000000..505f6305d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8024-date-setters-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8024-date-setters-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8025-date-subclass-setters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8025-date-subclass-setters.ts.json new file mode 100644 index 000000000..f1c4d20e0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8025-date-subclass-setters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8025-date-subclass-setters.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8026-date-subclass-setters-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8026-date-subclass-setters-dynamic.mjs.json new file mode 100644 index 000000000..8a0fe9b5a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8026-date-subclass-setters-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8026-date-subclass-setters-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index fb3524c81..69c19e1b7 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -11660,6 +11660,56 @@ double scr_date_new_local(double y, double mo, double d, return scr_date_from_local(scr_date_v8_make_day(y, mo, d), scr_date_v8_make_time(h, mi, s, ms)); } +/* Date.prototype's setters over a time value, as V8's builtins-date.cc + * computes them: the fields of `t` (local fields for the local setters) + * with the first `argc` of them replaced, in the order the setter names + * them, by `a`..`d`, through MakeDay/MakeTime/MakeDate, then UTC(t) for + * the local setters and TimeClip. An invalid `t` stays invalid, except + * that setFullYear and setUTCFullYear start from +0: January 1 of the + * epoch year at midnight (local midnight for setFullYear). Returns the new + * time value; the caller stores it into the Date. Never throws. */ +double scr_date_set_fields(double t, double setter_code, double argc_count, + double a, double b, double c, double d) { + int32_t setter = (int32_t)setter_code, argc = (int32_t)argc_count; + bool utc = (setter & SCR_DATE_SET_UTC) != 0; + int32_t field = setter & ~SCR_DATE_SET_UTC; + ScrDateParts p; + if (isnan(t)) { + if (field != SCR_DATE_SET_FULL_YEAR) return NAN; + p = (ScrDateParts){1970, 0, 1, 4, 0, 0, 0, 0, 0}; + } else if (utc) { + if (!scr_date_utc_parts(t, &p)) return NAN; + } else if (!scr_date_local_parts(t, &p)) { + return NAN; + } + double y = (double)p.year, mo = p.month, dt = p.date; + double h = p.hours, mi = p.minutes, s = p.seconds, ms = p.milliseconds; + double args[4] = {a, b, c, d}; + double *slots[7] = {&y, &mo, &dt, &h, &mi, &s, &ms}; + /* The first field each setter names, in slots' order. */ + int first; + switch (field) { + case SCR_DATE_SET_MILLISECONDS: first = 6; break; + case SCR_DATE_SET_SECONDS: first = 5; break; + case SCR_DATE_SET_MINUTES: first = 4; break; + case SCR_DATE_SET_HOURS: first = 3; break; + case SCR_DATE_SET_DATE: first = 2; break; + case SCR_DATE_SET_MONTH: first = 1; break; + case SCR_DATE_SET_FULL_YEAR: first = 0; break; + default: return NAN; + } + /* Each setter takes at most the fields from its own down to the next + * larger unit's end: setHours up to milliseconds, setMonth up to the + * date, setFullYear up to the date. */ + int last = first <= 2 ? 2 : 6; + for (int i = 0; i < argc && first + i <= last && i < 4; i++) *slots[first + i] = args[i]; + double day = scr_date_v8_make_day(y, mo, dt); + double time = scr_date_v8_make_time(h, mi, s, ms); + if (!utc) return scr_date_from_local(day, time); + if (!isfinite(day) || !isfinite(time)) return NAN; + return scr_date_new_ms(day * 86400000.0 + time); +} + /* ── V8's date-string parser, the fallback ───────────────────────────── * scr_date_parse answers the bounded grammars directly and asks * this parser only when they produce NaN. It follows V8's DateParser @@ -13203,6 +13253,27 @@ ScrDate *scr_dyn_native_date_check(const ScrDyn *value, const ScrDynPath *path) static const char scr_date_to_date_string_name[] = "toDateString"; static const char scr_date_to_time_string_name[] = "toTimeString"; +/* The field setters a checked-dynamic Date answers (scr_date_set_fields). */ +static const struct { + const char *name; + int code, max; +} scr_native_date_setters[] = { + {"setMilliseconds", SCR_DATE_SET_MILLISECONDS, 1}, + {"setSeconds", SCR_DATE_SET_SECONDS, 2}, + {"setMinutes", SCR_DATE_SET_MINUTES, 3}, + {"setHours", SCR_DATE_SET_HOURS, 4}, + {"setDate", SCR_DATE_SET_DATE, 1}, + {"setMonth", SCR_DATE_SET_MONTH, 2}, + {"setFullYear", SCR_DATE_SET_FULL_YEAR, 3}, + {"setUTCMilliseconds", SCR_DATE_SET_UTC | SCR_DATE_SET_MILLISECONDS, 1}, + {"setUTCSeconds", SCR_DATE_SET_UTC | SCR_DATE_SET_SECONDS, 2}, + {"setUTCMinutes", SCR_DATE_SET_UTC | SCR_DATE_SET_MINUTES, 3}, + {"setUTCHours", SCR_DATE_SET_UTC | SCR_DATE_SET_HOURS, 4}, + {"setUTCDate", SCR_DATE_SET_UTC | SCR_DATE_SET_DATE, 1}, + {"setUTCMonth", SCR_DATE_SET_UTC | SCR_DATE_SET_MONTH, 2}, + {"setUTCFullYear", SCR_DATE_SET_UTC | SCR_DATE_SET_FULL_YEAR, 3}, +}; + static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)self; (void)what; @@ -13232,6 +13303,25 @@ static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *metho ((ScrDate *)ptr)->ms = scr_date_new_ms(time); return scr_dyn_new_num(((ScrDate *)ptr)->ms); } + for (size_t i = 0; i < sizeof scr_native_date_setters / sizeof *scr_native_date_setters; i++) { + if (strcmp(method, scr_native_date_setters[i].name)) continue; + /* V8's order: ToNumber of the first argument, then the time value, + * then (unless the date is invalid) ToNumber of the optional ones the + * setter takes. */ + int code = scr_native_date_setters[i].code, max = scr_native_date_setters[i].max; + double values[4] = {NAN, 0, 0, 0}; + if (argc > 0 && !scr_dyn_number_coerce_js(args[0], &values[0])) return NULL; + double t = ((ScrDate *)ptr)->ms; + int used = argc < (size_t)max ? (int)argc : max; + if (used < 1) used = 1; + if (!isnan(t) || (code & ~SCR_DATE_SET_UTC) == SCR_DATE_SET_FULL_YEAR) { + for (int k = 1; k < used; k++) + if (!scr_dyn_number_coerce_js(args[k], &values[k])) return NULL; + } + double next = scr_date_set_fields(t, code, used, values[0], values[1], values[2], values[3]); + ((ScrDate *)ptr)->ms = next; + return scr_dyn_new_num(next); + } #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); @@ -13311,7 +13401,9 @@ static const char *const scr_native_date_methods[] = { "getFullYear", "getUTCFullYear", "getMonth", "getUTCMonth", "getDate", "getUTCDate", "getDay", "getUTCDay", "getHours", "getUTCHours", "getMinutes", "getUTCMinutes", "getSeconds", "getUTCSeconds", "getMilliseconds", - "getUTCMilliseconds", "getTimezoneOffset", "setTime", + "getUTCMilliseconds", "getTimezoneOffset", "setTime", "setMilliseconds", "setSeconds", + "setMinutes", "setHours", "setDate", "setMonth", "setFullYear", "setUTCMilliseconds", + "setUTCSeconds", "setUTCMinutes", "setUTCHours", "setUTCDate", "setUTCMonth", "setUTCFullYear", }; #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]; @@ -13397,12 +13489,40 @@ static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len) { return NULL; } +/* structuredClone of a Date: a new Date with the same time value. */ +static ScrDyn *scr_native_date_clone(void *h) { + return scr_dyn_native_date_from_ms(((ScrDate *)h)->ms); +} + +/* A Date crossing threads (postMessage, workerData) carries its time value + * in a plain allocation any thread may read and free. */ +static void *scr_native_date_share(void *h) { + double *ms = malloc(sizeof *ms); + if (!ms) scr_trap("scriptc: out of memory\n"); + *ms = ((ScrDate *)h)->ms; + return ms; +} + +static ScrDyn *scr_native_date_adopt(void *payload) { + return scr_dyn_native_date_from_ms(*(double *)payload); +} + +static void scr_native_date_drop(void *payload) { free(payload); } + +static const ScrDynHandleTransfer scr_native_date_transfer = { + .share = scr_native_date_share, .adopt = scr_native_date_adopt, .drop = scr_native_date_drop}; + /* A checked-dynamic handle for the Date object `date` (borrowed; the * handle retains it), so the value stays the same object. */ ScrDyn *scr_dyn_native_date(ScrDate *date) { static const ScrDynHandleOps ops = { - "Date", &scr_date_retain_v, &scr_date_release_v, &scr_native_date_invoke, - &scr_native_date_get, NULL, NULL, NULL, + .cls = "Date", + .retain = &scr_date_retain_v, + .release = &scr_date_release_v, + .invoke = &scr_native_date_invoke, + .get = &scr_native_date_get, + .clone = &scr_native_date_clone, + .transfer = &scr_native_date_transfer, }; scr_dyn_handle_install(SCR_DYNH_DATE, &ops); return scr_dyn_new_handle(date, SCR_DYNH_DATE); diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index a71b0ccfd..39564c061 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -111,6 +111,23 @@ double scr_date_now(void); /* integer ms since epoch, like Node */ extern SCR_TL const double *scr_date_mock_now; double scr_date_new_ms(double ms); /* TimeClip (NaN when invalid) */ double scr_date_get_time(double ms); +/* Date.prototype's setters (setMilliseconds ... setFullYear and their UTC + * forms) over a time value: the new time value for `argc` arguments + * `a`..`d` (scr_lib.c). `setter` is an SCR_DATE_SET_* code; it and `argc` + * are whole numbers passed as doubles, as the compiler's lib calls pass + * numbers. */ +enum { + SCR_DATE_SET_MILLISECONDS = 0, + SCR_DATE_SET_SECONDS = 1, + SCR_DATE_SET_MINUTES = 2, + SCR_DATE_SET_HOURS = 3, + SCR_DATE_SET_DATE = 4, + SCR_DATE_SET_MONTH = 5, + SCR_DATE_SET_FULL_YEAR = 6, + SCR_DATE_SET_UTC = 8, +}; +double scr_date_set_fields(double t, double setter, double argc, + double a, double b, double c, double d); /* A Date object: the TimeClip'd time value in milliseconds (NaN for an * invalid date). Dates are immutable; their identity is the pointer. * Retain/release follow the runtime's RC conventions (SIZE_MAX immortal). */ diff --git a/tests/corpus/8020-date-setters.ts b/tests/corpus/8020-date-setters.ts new file mode 100644 index 000000000..6bfeff5c8 --- /dev/null +++ b/tests/corpus/8020-date-setters.ts @@ -0,0 +1,115 @@ +// Date setters: every local and UTC setter with each argument count, the +// time values they return, normalization of out-of-range fields, local +// setters across a daylight saving transition, setters on invalid dates, +// explicit undefined arguments, fractional arguments, and the TimeClip +// range. A setter changes the one Date object every alias holds. +export {}; +process.env.TZ = "America/Chicago"; + +const show = (label: string, d: Date): void => { + console.log(label, d.getTime(), Number.isNaN(d.getTime()) ? "invalid" : d.toISOString()); +}; + +const d = new Date(Date.UTC(2026, 0, 31, 10, 20, 30, 400)); +console.log(d.setMilliseconds(999), d.getMilliseconds()); +console.log(d.setSeconds(5), d.setSeconds(6, 7)); +console.log(d.setMinutes(1), d.setMinutes(2, 3), d.setMinutes(4, 5, 6)); +console.log(d.setHours(7), d.setHours(8, 9), d.setHours(10, 11, 12), d.setHours(13, 14, 15, 16)); +console.log(d.setDate(15), d.setMonth(1), d.setMonth(2, 3)); +console.log(d.setFullYear(2025), d.setFullYear(2024, 1), d.setFullYear(2023, 4, 6)); +show("local", d); + +const u = new Date(Date.UTC(2026, 0, 31)); +console.log(u.setUTCMilliseconds(1), u.setUTCSeconds(2, 3), u.setUTCMinutes(4, 5, 6)); +console.log(u.setUTCHours(7, 8, 9, 10), u.setUTCDate(1), u.setUTCMonth(5, 30)); +console.log(u.setUTCFullYear(1999), u.setUTCFullYear(2000, 1, 29)); +show("utc", u); + +// Fields out of range carry into the next larger field, both ways. +const n = new Date(Date.UTC(2026, 0, 31, 12)); +n.setUTCMonth(1); +show("Jan 31 + month", n); +n.setUTCDate(0); +show("day 0", n); +n.setUTCMonth(13); +show("month 13", n); +n.setUTCHours(-1); +show("hour -1", n); +n.setUTCMinutes(60 * 24 + 1); +show("minutes past a day", n); +n.setUTCSeconds(-0.5); +show("fractional seconds", n); +n.setUTCMilliseconds(1.9); +show("fractional ms", n); + +// Local setters in a zone with daylight saving time: 2026-03-08 02:30 +// does not exist in Chicago and 2026-11-01 01:30 happens twice. +const spring = new Date(2026, 2, 8, 0, 0); +spring.setHours(2, 30); +show("spring gap", spring); +console.log(spring.getHours(), spring.getMinutes(), spring.getTimezoneOffset()); +const fall = new Date(2026, 10, 1, 0, 0); +fall.setHours(1, 30); +show("fall overlap", fall); +fall.setMinutes(fall.getMinutes() + 60); +show("fall overlap + 60 min", fall); +const walk = new Date(2026, 2, 7, 12); +for (let i = 0; i < 3; i++) { + walk.setDate(walk.getDate() + 1); + console.log("walk", walk.getDate(), walk.getHours(), walk.getTimezoneOffset()); +} + +// Invalid dates: setFullYear starts from +0 local time, the others keep NaN. +const bad = new Date(NaN); +console.log(bad.setHours(1), bad.setDate(2), bad.setUTCMonth(3), bad.setMilliseconds(4)); +console.log(bad.setFullYear(2020)); +show("after setFullYear", bad); +const badUtc = new Date("not a date"); +console.log(badUtc.setUTCFullYear(2021, 6)); +show("after setUTCFullYear", badUtc); + +// Explicit undefined and NaN arguments make the date invalid; a missing +// optional argument keeps the current field. +const opt = new Date(Date.UTC(2026, 5, 15, 9)); +console.log(opt.setUTCHours(10, undefined), Number.isNaN(opt.getTime())); +const opt2 = new Date(Date.UTC(2026, 5, 15, 9)); +console.log(opt2.setUTCMonth(Number.NaN), opt2.setUTCMonth(2)); + +// setTime and the TimeClip range. +const t = new Date(0); +console.log(t.setTime(8.64e15), t.setTime(8.64e15 + 1), t.setTime(-8.64e15), t.setTime(1.5)); +console.log(t.setTime(-0), Object.is(t.getTime(), 0), t.setTime(Number.NaN)); +const far = new Date(Date.UTC(275760, 8, 13)); +console.log(far.getTime(), far.setUTCMilliseconds(1)); + +// Every alias sees the change: a second binding, an array slot, a Map +// value, a record field, a closure and a parameter. +const shared = new Date(Date.UTC(2026, 6, 4)); +const alias = shared; +const list = [shared, new Date(Date.UTC(2000, 0, 1))]; +const byName = new Map([["shared", shared]]); +const holder = { when: shared }; +const bump = (): number => shared.setUTCDate(shared.getUTCDate() + 1); +const nextYear = (date: Date): void => { + date.setUTCFullYear(date.getUTCFullYear() + 1); +}; +alias.setUTCHours(6); +list[0]!.setUTCMinutes(30); +byName.get("shared")!.setUTCSeconds(15); +holder.when.setUTCMilliseconds(250); +bump(); +nextYear(shared); +show("shared", shared); +console.log(alias === shared, list[0] === shared, byName.get("shared") === holder.when); +console.log(list.map((x) => x.toISOString()).join(" ")); + +// A Date a loop adjusts in place. +const cursor = new Date(Date.UTC(2026, 0, 1)); +let weekdays = 0; +while (cursor.getUTCFullYear() === 2026) { + const day = cursor.getUTCDay(); + if (day !== 0 && day !== 6) weekdays++; + cursor.setUTCDate(cursor.getUTCDate() + 1); +} +console.log("weekdays in 2026", weekdays); +show("cursor", cursor); diff --git a/tests/corpus/8021-date-coercions.ts b/tests/corpus/8021-date-coercions.ts new file mode 100644 index 000000000..2410853a4 --- /dev/null +++ b/tests/corpus/8021-date-coercions.ts @@ -0,0 +1,47 @@ +// Dates converted to primitives as Node converts them: unary +, Number() +// and relational operators read the time value (the number hint), while + +// with a string, template literals and String() read the text (a Date's +// default hint is a string). Symbol.toPrimitive, valueOf and toString answer +// the same values; == compares two Dates by identity and a Date with a +// primitive through its default primitive. typeof, truthiness and +// instanceof treat a Date as an object, invalid ones included. +export {}; +process.env.TZ = "UTC"; + +const a = new Date(Date.UTC(2026, 9, 23, 8, 30)); +const b = new Date(Date.UTC(2026, 9, 24, 8, 30)); +const bad = new Date(Number.NaN); + +console.log(+a, -a, +b - +a, (+b - +a) / 3600000, Number(a), Math.max(+a, +b) === b.getTime()); +console.log(a + "!"); +console.log(`at ${a}`); +console.log(String(b)); +console.log("" + a === a.toString(), a + "1" === a.toString() + "1"); +console.log(a[Symbol.toPrimitive]("number"), a[Symbol.toPrimitive]("string") === a.toString()); +console.log(a[Symbol.toPrimitive]("default") === a.toString(), a.valueOf() === a.getTime()); +console.log(a < b, a <= b, a > b, a >= b, +b > a.getTime() + 1); + +const same = a; +const twin = new Date(a.getTime()); +console.log(a == same, a == twin, a === twin, a != twin, a.getTime() === twin.getTime()); +console.log(a == (a.toString() as unknown), a == (a.getTime() as unknown)); + +console.log(typeof a, typeof bad, a instanceof Date, bad instanceof Date); +console.log(Object.prototype.toString.call(a), !a, !!bad, bad ? "truthy" : "falsy"); + +// Invalid dates. +console.log(+bad, +bad - +a, `${bad}`, String(bad), bad + "", Number.isNaN(bad.getTime())); +console.log(bad.toJSON(), JSON.stringify({ when: bad, at: a })); +try { + bad.toISOString(); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} + +// Sorting and summing through the number hint. +const days = [b, a, new Date(Date.UTC(2026, 0, 1))]; +days.sort((x, y) => +x - +y); +console.log(days.map((x) => x.getUTCDate()).join(",")); +let span = 0; +for (let i = 1; i < days.length; i++) span += +days[i]! - +days[i - 1]!; +console.log(span / 86400000); diff --git a/tests/corpus/8022-date-unions.ts b/tests/corpus/8022-date-unions.ts new file mode 100644 index 000000000..dedcd8bf1 --- /dev/null +++ b/tests/corpus/8022-date-unions.ts @@ -0,0 +1,68 @@ +// Dates in unions with other arms: Date | string, Date | number and +// Date | a record, narrowed with instanceof and typeof, in arrays, Map +// values, optional fields and function results. A Date arm keeps its +// identity, so a setter through a union-typed alias changes the original. +export {}; +process.env.TZ = "UTC"; + +type When = Date | string | number; + +function describe(when: When): string { + if (when instanceof Date) return `date ${when.toISOString()}`; + if (typeof when === "string") return `text ${when}`; + return `ms ${when}`; +} + +function normalize(when: When): Date { + return when instanceof Date ? when : new Date(when); +} + +const fixed = new Date(Date.UTC(2026, 9, 23)); +const values: When[] = [fixed, "2026-01-02T03:04:05Z", 0, new Date(Number.NaN)]; +for (const value of values) { + const text = value instanceof Date && Number.isNaN(value.getTime()) ? "invalid" : describe(value); + console.log(text); +} +console.log(normalize(fixed) === fixed, normalize("2000-01-01").getUTCFullYear(), normalize(1e12).getUTCFullYear()); + +const held: When = fixed; +if (held instanceof Date) held.setUTCHours(12); +console.log(fixed.getUTCHours(), held === fixed); + +type Stamp = Date | { label: string; at: number }; +const stamps: Stamp[] = [fixed, { label: "start", at: 5 }]; +for (const stamp of stamps) { + console.log(stamp instanceof Date ? stamp.getUTCDate() : `${stamp.label}@${stamp.at}`); +} + +const byKey = new Map(); +byKey.set("a", fixed); +byKey.set("b", "later"); +for (const [key, value] of byKey) console.log(key, value instanceof Date ? value.getTime() : value); + +interface Task { + name: string; + due?: Date; + done: Date | null; +} +const tasks: Task[] = [ + { name: "write", due: fixed, done: null }, + { name: "ship", done: new Date(Date.UTC(2026, 11, 1)) }, +]; +for (const task of tasks) { + const due = task.due ? task.due.toISOString().slice(0, 10) : "none"; + const done = task.done === null ? "open" : task.done.toISOString().slice(0, 10); + console.log(task.name, due, done); +} +tasks[0]!.due!.setUTCDate(30); +console.log(fixed.getUTCDate(), tasks[0]!.due === fixed); + +function latest(dates: (Date | undefined)[]): Date | string { + let best: Date | undefined; + for (const date of dates) if (date && (best === undefined || date > best)) best = date; + return best ?? "none"; +} +const found = latest([undefined, fixed, new Date(0)]); +console.log(found === fixed, latest([])); + +console.log(JSON.stringify({ values, stamps })); diff --git a/tests/corpus/8023-date-clone-throw.ts b/tests/corpus/8023-date-clone-throw.ts new file mode 100644 index 000000000..b9d569d00 --- /dev/null +++ b/tests/corpus/8023-date-clone-throw.ts @@ -0,0 +1,32 @@ +// Dates through structuredClone and MessageChannel, which copy them as +// new Dates with the same time value, and Dates thrown and rejected as +// values, which keep their identity. +import { MessageChannel } from "node:worker_threads"; +process.env.TZ = "UTC"; + +const at = new Date(Date.UTC(2026, 9, 23, 7)); +const copy = structuredClone(at); +console.log(copy instanceof Date, copy === at, copy.getTime() === at.getTime()); +copy.setUTCFullYear(2000); +console.log(at.getUTCFullYear(), copy.getUTCFullYear()); + +const nested = structuredClone({ at, list: [at, at], invalid: new Date(Number.NaN) }); +console.log(nested.list[0] instanceof Date, nested.at === at, nested.list.length); +console.log(nested.at.toISOString(), Number.isNaN(nested.invalid.getTime())); + +try { + throw at; +} catch (error) { + console.log(error instanceof Date, error === at); +} + +const { port1, port2 } = new MessageChannel(); +port2.on("message", (data: { when: Date; again: Date }) => { + const { when, again } = data; + console.log("message", when instanceof Date, when === at, again instanceof Date, when.toISOString()); + port2.close(); + Promise.reject(at).catch((reason: unknown) => { + console.log("rejected", reason instanceof Date, reason === at); + }); +}); +port1.postMessage({ when: at, again: at }); diff --git a/tests/corpus/8024-date-setters-dynamic.mjs b/tests/corpus/8024-date-setters-dynamic.mjs new file mode 100644 index 000000000..9b6392b60 --- /dev/null +++ b/tests/corpus/8024-date-setters-dynamic.mjs @@ -0,0 +1,24 @@ +// Date setters in JavaScript, on Dates held as untyped values: each setter +// converts its arguments with ToNumber (valueOf runs, undefined is NaN) and +// changes the one Date object every alias holds. +process.env.TZ = "Europe/Berlin"; + +const d = new Date(Date.UTC(2026, 2, 28, 12)); +const alias = d; +const box = { d }; +console.log(d.setHours(1, 30), d.getHours(), d.getTimezoneOffset()); +console.log(alias.setDate(29), box.d.getHours(), d.getTimezoneOffset()); +console.log(d.setMinutes("15"), d.setSeconds(true), d.setMilliseconds(null)); +console.log(d.setUTCFullYear(2030, 11, 31), d.toISOString()); +console.log(d.setMonth(undefined), Number.isNaN(d.getTime())); +console.log(d.setFullYear(2001), d.toISOString()); + +let calls = 0; +const later = { valueOf() { calls++; return 45; } }; +console.log(d.setUTCMinutes(later, later), calls, d.toISOString()); + +const other = new Date(0); +console.log(other.setHours(5), other.getHours(), typeof d.setMonth); +const list = [new Date(0), new Date(86400000)]; +list.forEach((x, i) => x.setUTCHours(i + 1)); +console.log(list.map((x) => x.toISOString()).join(" ")); diff --git a/tests/corpus/8025-date-subclass-setters.ts b/tests/corpus/8025-date-subclass-setters.ts new file mode 100644 index 000000000..a6414a047 --- /dev/null +++ b/tests/corpus/8025-date-subclass-setters.ts @@ -0,0 +1,30 @@ +// Setters on instances of a class declared `extends Date`: each changes +// the instance's time value, which the inherited getters, the subclass's +// own methods and toISOString then read; invalid instances follow the same +// rules as Dates. +export {}; +process.env.TZ = "Asia/Kolkata"; + +class Deadline extends Date { + label: string; + constructor(label: string, ms: number) { + super(ms); + this.label = label; + } + extend(days: number): this { + this.setDate(this.getDate() + days); + return this; + } + describe(): string { + return `${this.label} ${this.toISOString()}`; + } +} + +const due = new Deadline("report", Date.UTC(2026, 9, 23, 18, 30)); +console.log(due.describe()); +console.log(due.setHours(9, 15), due.getHours(), due.getMinutes()); +console.log(due.extend(10).describe()); +console.log(due.setUTCMonth(0, 31), due.setMonth(1), due.getDate(), due.getMonth()); +console.log(due.setFullYear(2024, 1, 29), due.describe()); +console.log(due.setTime(Number.NaN), Number.isNaN(due.getTime()), due.setUTCFullYear(2030)); +console.log(due.describe(), due instanceof Date, due instanceof Deadline); diff --git a/tests/corpus/8026-date-subclass-setters-dynamic.mjs b/tests/corpus/8026-date-subclass-setters-dynamic.mjs new file mode 100644 index 000000000..8b89d6ec8 --- /dev/null +++ b/tests/corpus/8026-date-subclass-setters-dynamic.mjs @@ -0,0 +1,17 @@ +// Setters on instances of a JavaScript class declared `extends Date`, held +// as untyped values: the inherited setters change the instance's time value. +process.env.TZ = "America/Sao_Paulo"; + +class Shift extends Date { + start() { + this.setHours(8, 0, 0, 0); + return this; + } +} + +const shift = new Shift(Date.UTC(2026, 4, 5, 15)); +console.log(shift.start().toISOString(), shift.getHours()); +console.log(shift.setDate(31), shift.getMonth(), shift.setUTCHours(0), shift.toISOString()); +const values = [shift, new Shift(0)]; +for (const value of values) value.setUTCMinutes(45); +console.log(values.map((value) => value.toISOString()).join(" "), values[0] === shift); diff --git a/tests/diagnostics/stdlib-fence.ts b/tests/diagnostics/stdlib-fence.ts index d28333178..0952436e1 100644 --- a/tests/diagnostics/stdlib-fence.ts +++ b/tests/diagnostics/stdlib-fence.ts @@ -21,8 +21,6 @@ const fmt = new Intl.NumberFormat(); // Constructors. const d = new Date(...([2024, 0] as const)); // spread field arguments stay fenced -const mutableDate = new Date(0); -mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice const wr = new WeakRef({}); const px = new Proxy({ a: 1 }, {}); const buf = new ArrayBuffer(8); @@ -73,14 +71,6 @@ function pick(a: string | number, b: number | boolean): boolean { } const picked = pick(1, 2); -function throwDate(): void { - throw new Date(0); // the exception cell cannot preserve Date's object kind -} -throwDate(); -function* dateGenerator(): Generator { - yield 1; -} -dateGenerator().throw(new Date(0)); // Related classes may store one object under different union tags. A // structural arm beside them leaves no identity-preserving comparison. class FenceBase { diff --git a/tests/harness/__snapshots__/assert-class-fences.ts.txt b/tests/harness/__snapshots__/assert-class-fences.ts.txt index 29d2e4668..f70683e22 100644 --- a/tests/harness/__snapshots__/assert-class-fences.ts.txt +++ b/tests/harness/__snapshots__/assert-class-fences.ts.txt @@ -16,7 +16,7 @@ assert-class-fences.ts:12:16 - error SC2020: 'assert.Assert instances used as va hint: bind `new Assert(...)` with literal options to a const and only call methods on it (`const a = new Assert({ diff: 'full' }); a.strictEqual(x, y)`) -assert-class-fences.ts:13:22 - error SC2001: values of type 'object | Assert' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +assert-class-fences.ts:13:22 - error SC2001: values of type 'object | Assert' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 12 | const passed = new Assert(); 13 | function useInstance(a: Assert | object): void { diff --git a/tests/harness/__snapshots__/class-interface-refinements.ts.txt b/tests/harness/__snapshots__/class-interface-refinements.ts.txt index 9d7d7b87f..89f3ead6d 100644 --- a/tests/harness/__snapshots__/class-interface-refinements.ts.txt +++ b/tests/harness/__snapshots__/class-interface-refinements.ts.txt @@ -7,7 +7,7 @@ class-interface-refinements.ts:8:26 - error SC2002: record shapes must match exa hint: width subtyping compiles as a copy (each expected field copied off the source; extra fields drop) — this pair is outside the copy's rules; build a literal with exactly the expected fields instead -class-interface-refinements.ts:12:16 - error SC2001: values of type 'AddedStorage' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +class-interface-refinements.ts:12:16 - error SC2001: values of type 'AddedStorage' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 11 | interface AddedStorage extends Box { extra: string; } 12 | function added(view: AddedStorage): string { return view.extra; } @@ -23,7 +23,7 @@ class-interface-refinements.ts:13:13 - error SC2004: uses of 'added' inherit the hint: the declaration of 'added' did not compile — fix the diagnostic reported there and these sites clear with it -class-interface-refinements.ts:16:18 - error SC2001: values of type 'ChangedMethod' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +class-interface-refinements.ts:16:18 - error SC2001: values of type 'ChangedMethod' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 15 | interface ChangedMethod extends Box { read(): number; } 16 | function changed(view: ChangedMethod): number { return view.read(); } diff --git a/tests/harness/__snapshots__/date-formatters.ts.txt b/tests/harness/__snapshots__/date-formatters.ts.txt index 75e15760b..d6dd27c5a 100644 --- a/tests/harness/__snapshots__/date-formatters.ts.txt +++ b/tests/harness/__snapshots__/date-formatters.ts.txt @@ -5,7 +5,7 @@ date-formatters.ts:4:1 - error SC2020: 'Date.prototype.toLocaleTimeString' is pa | ^~~~~~~~~~~~~~~~~~~~~~~~~ 5 | date.toLocaleString(); - 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 + hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters and setters, setTime(), and getTimezoneOffset() are supported; locale formatters have no lowering date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part of the standard library types but has no scriptc lowering yet @@ -14,4 +14,4 @@ date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part o | ^~~~~~~~~~~~~~~~~~~~~ 6 | - 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 \ No newline at end of file + hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters and setters, setTime(), and getTimezoneOffset() are supported; locale formatters have no lowering \ No newline at end of file diff --git a/tests/harness/__snapshots__/dynamic-representable-types.ts.txt b/tests/harness/__snapshots__/dynamic-representable-types.ts.txt index 21402b597..6241894f1 100644 --- a/tests/harness/__snapshots__/dynamic-representable-types.ts.txt +++ b/tests/harness/__snapshots__/dynamic-representable-types.ts.txt @@ -1,4 +1,4 @@ -dynamic-representable-types.ts:7:7 - error SC2001: values of type '[number, (string | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +dynamic-representable-types.ts:7:7 - error SC2001: values of type '[number, (string | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 6 | const bare: any = 41; 7 | const optTuple: [number, string?] = [1]; diff --git a/tests/harness/__snapshots__/generics.ts.txt b/tests/harness/__snapshots__/generics.ts.txt index 4dfc8019f..86dd94bb9 100644 --- a/tests/harness/__snapshots__/generics.ts.txt +++ b/tests/harness/__snapshots__/generics.ts.txt @@ -12,7 +12,7 @@ generics.ts:30:10 - error SC1090: '.join()' on arrays of this element type (numb | ^~~~~~~~~~~ 31 | } -generics.ts:44:7 - error SC2001: values of type 'typeof Box' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +generics.ts:44:7 - error SC2001: values of type 'typeof Box' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 43 | // The uninstantiated family as a value: no thunk, no single ctor ABI. 44 | const BoxAlias = Box; diff --git a/tests/harness/__snapshots__/maps-surface.ts.txt b/tests/harness/__snapshots__/maps-surface.ts.txt index c1db0576d..ed4ebb79f 100644 --- a/tests/harness/__snapshots__/maps-surface.ts.txt +++ b/tests/harness/__snapshots__/maps-surface.ts.txt @@ -1,4 +1,4 @@ -maps-surface.ts:21:11 - error SC2001: values of type '(v: number, k: string, theMap: Map) => void' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +maps-surface.ts:21:11 - error SC2001: values of type '(v: number, k: string, theMap: Map) => void' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 20 | // The forEach callback receives (value, key) — no third map parameter. 21 | m.forEach((v, k, theMap) => console.log(k, v)); diff --git a/tests/harness/__snapshots__/regex-match-destructuring-fences.ts.txt b/tests/harness/__snapshots__/regex-match-destructuring-fences.ts.txt index d4eb59168..43d4eff77 100644 --- a/tests/harness/__snapshots__/regex-match-destructuring-fences.ts.txt +++ b/tests/harness/__snapshots__/regex-match-destructuring-fences.ts.txt @@ -5,7 +5,7 @@ regex-match-destructuring-fences.ts:6:16 - error SC1031: rest bindings over rege | ^~~~~~~ 7 | const { input = "none" } = m; -regex-match-destructuring-fences.ts:6:19 - error SC2001: values of type '{ [n: number]: string; input: string; 0: string; length: number; toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: (NumberFormatOptions & DateTimeFormatOptions) | undefined): string; ... 39 more ...; with(index: number, value: string): string[]; }' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +regex-match-destructuring-fences.ts:6:19 - error SC2001: values of type '{ [n: number]: string; input: string; 0: string; length: number; toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: (NumberFormatOptions & DateTimeFormatOptions) | undefined): string; ... 39 more ...; with(index: number, value: string): string[]; }' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 5 | const m = /(a)(b)?/.exec("ab")!; 6 | const { index, ...rest } = m; diff --git a/tests/harness/__snapshots__/retired-engine-2103-optional-tuple-elements.ts.txt b/tests/harness/__snapshots__/retired-engine-2103-optional-tuple-elements.ts.txt index 4d3dd52c3..dfdb00720 100644 --- a/tests/harness/__snapshots__/retired-engine-2103-optional-tuple-elements.ts.txt +++ b/tests/harness/__snapshots__/retired-engine-2103-optional-tuple-elements.ts.txt @@ -43,7 +43,7 @@ retired-engine-2103-optional-tuple-elements.ts:19:1 - error SC2004: uses of 'f2' hint: the declaration of 'f2' did not compile — fix the diagnostic reported there and these sites clear with it -retired-engine-2103-optional-tuple-elements.ts:20:7 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +retired-engine-2103-optional-tuple-elements.ts:20:7 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 19 | f2([]); 20 | const t: [string?, number?] = ["x"]; @@ -59,7 +59,7 @@ retired-engine-2103-optional-tuple-elements.ts:21:13 - error SC2004: uses of 't' hint: the declaration of 't' did not compile — fix the diagnostic reported there and these sites clear with it -retired-engine-2103-optional-tuple-elements.ts:22:7 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +retired-engine-2103-optional-tuple-elements.ts:22:7 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 21 | console.log(t.length, `${t[0]} ${t[1]}`); 22 | const full: [string?, number?] = ["y", 7]; @@ -75,14 +75,14 @@ retired-engine-2103-optional-tuple-elements.ts:23:13 - error SC2004: uses of 'fu hint: the declaration of 'full' did not compile — fix the diagnostic reported there and these sites clear with it -retired-engine-2103-optional-tuple-elements.ts:24:7 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +retired-engine-2103-optional-tuple-elements.ts:24:7 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 23 | console.log(full.length, `${full[0]} ${full[1]}`); 24 | const none: [string?, number?] = []; | ^~~~ 25 | console.log(none.length); -retired-engine-2103-optional-tuple-elements.ts:24:34 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +retired-engine-2103-optional-tuple-elements.ts:24:34 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 23 | console.log(full.length, `${full[0]} ${full[1]}`); 24 | const none: [string?, number?] = []; @@ -98,7 +98,7 @@ retired-engine-2103-optional-tuple-elements.ts:25:13 - error SC2004: uses of 'no hint: the declaration of 'none' did not compile — fix the diagnostic reported there and these sites clear with it -retired-engine-2103-optional-tuple-elements.ts:27:7 - error SC2001: values of type '[string, ...number[]]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +retired-engine-2103-optional-tuple-elements.ts:27:7 - error SC2001: values of type '[string, ...number[]]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 26 | // A rest-element tuple rides the same island representation. 27 | const r: [string, ...number[]] = ["head", 1, 2, 3]; diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index c183f7cdc..313a9391b 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -55,88 +55,65 @@ stdlib-fence.ts:23:11 - error SC2020: 'new Date with spread arguments' is part o 22 | // Constructors. 23 | const d = new Date(...([2024, 0] as const)); // spread field arguments stay fenced | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 24 | const mutableDate = new Date(0); + 24 | const wr = new WeakRef({}); hint: new Date(), new Date(milliseconds), new Date(dateString), and new Date(year, monthIndex, ...) with listed arguments are supported -stdlib-fence.ts:25:1 - error SC2020: 'Date.prototype.setUTCFullYear with arguments' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:24:7 - error SC2020: 'WeakRef<{}>' is part of the standard library types but has no scriptc lowering yet - 24 | const mutableDate = new Date(0); - 25 | mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 26 | const wr = new WeakRef({}); - - hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering - -stdlib-fence.ts:26:7 - error SC2020: 'WeakRef<{}>' is part of the standard library types but has no scriptc lowering yet - - 25 | mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice - 26 | const wr = new WeakRef({}); + 23 | const d = new Date(...([2024, 0] as const)); // spread field arguments stay fenced + 24 | const wr = new WeakRef({}); | ^~ - 27 | const px = new Proxy({ a: 1 }, {}); + 25 | const px = new Proxy({ a: 1 }, {}); hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -stdlib-fence.ts:26:12 - error SC2020: 'new WeakRef' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:24:12 - error SC2020: 'new WeakRef' is part of the standard library types but has no scriptc lowering yet - 25 | mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice - 26 | const wr = new WeakRef({}); + 23 | const d = new Date(...([2024, 0] as const)); // spread field arguments stay fenced + 24 | const wr = new WeakRef({}); | ^~~~~~~~~~~~~~~ - 27 | const px = new Proxy({ a: 1 }, {}); + 25 | const px = new Proxy({ a: 1 }, {}); hint: deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead -stdlib-fence.ts:41:31 - error SC1090: '.split()' on a 'RegExp | string' separator (pass a string, or a regex literal) is not supported yet +stdlib-fence.ts:39:31 - error SC1090: '.split()' on a 'RegExp | string' separator (pass a string, or a regex literal) is not supported yet - 40 | const norm = "abc".normalize(); - 41 | const limited = "a,b,c".split(Math.random() ? "," : /,/, 2); // union separators stay fenced; limits lower + 38 | const norm = "abc".normalize(); + 39 | const limited = "a,b,c".split(Math.random() ? "," : /,/, 2); // union separators stay fenced; limits lower | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 42 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer) + 40 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer) -stdlib-fence.ts:43:19 - error SC2020: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:41:19 - error SC2020: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet - 42 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer) - 43 | const localized = (1234.5).toLocaleString(); + 40 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer) + 41 | const localized = (1234.5).toLocaleString(); | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 44 | // Unlowered call FORMS of lowered members. (push is fully variadic now — + 42 | // Unlowered call FORMS of lowered members. (push is fully variadic now — hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US") -stdlib-fence.ts:49:1 - error SC2020: '.forEach with 2 arguments' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:47:1 - error SC2020: '.forEach with 2 arguments' is part of the standard library types but has no scriptc lowering yet - 48 | const joined = nums.join(); // the omitted separator lowers - 49 | nums.forEach((x) => console.log(x), { unused: true }); // thisArg stays fenced + 46 | const joined = nums.join(); // the omitted separator lowers + 47 | nums.forEach((x) => console.log(x), { unused: true }); // thisArg stays fenced | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 50 | const pos = "abc".includes("b", 1); // the position form lowers now (indexOf's clamp) + 48 | const pos = "abc".includes("b", 1); // the position form lowers now (indexOf's clamp) hint: the thisArg parameter has no lowering — use an arrow function -stdlib-fence.ts:77:9 - error SC1090: throwing Date values (catch bindings cannot preserve Date's object kind; throw an Error or primitive instead) are not supported yet +stdlib-fence.ts:84:10 - error SC1090: comparisons of related class values beside structural union members (narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) are not supported yet - 76 | function throwDate(): void { - 77 | throw new Date(0); // the exception cell cannot preserve Date's object kind - | ^~~~~~~~~~~ - 78 | } - -stdlib-fence.ts:83:23 - error SC1090: .throw() with a Date value (the exception channel cannot preserve Date's object kind; throw an Error or primitive instead) is not supported yet - - 82 | } - 83 | dateGenerator().throw(new Date(0)); - | ^~~~~~~~~~~ - 84 | // Related classes may store one object under different union tags. A - -stdlib-fence.ts:94:10 - error SC1090: comparisons of related class values beside structural union members (narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) are not supported yet - - 93 | function relatedStrict(a: FenceValue, b: FenceValue): boolean { - 94 | return a === b; + 83 | function relatedStrict(a: FenceValue, b: FenceValue): boolean { + 84 | return a === b; | ^~~~~~~ - 95 | } + 85 | } -stdlib-fence.ts:97:10 - error SC2020: 'Object.is over related class values beside structural union members' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:87:10 - error SC2020: 'Object.is over related class values beside structural union members' is part of the standard library types but has no scriptc lowering yet - 96 | function relatedSameValue(a: FenceValue, b: FenceSub): boolean { - 97 | return Object.is(a, b); + 86 | function relatedSameValue(a: FenceValue, b: FenceSub): boolean { + 87 | return Object.is(a, b); | ^~~~~~~~~~~~~~~ - 98 | } + 88 | } hint: narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms \ No newline at end of file diff --git a/tests/harness/__snapshots__/tuples.ts.txt b/tests/harness/__snapshots__/tuples.ts.txt index a6f58710e..440904095 100644 --- a/tests/harness/__snapshots__/tuples.ts.txt +++ b/tests/harness/__snapshots__/tuples.ts.txt @@ -12,14 +12,14 @@ tuples.ts:12:6 - error SC1090: tuple indexing with a non-literal index (tuples a | ^ 13 | -tuples.ts:15:7 - error SC2001: values of type '[string, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +tuples.ts:15:7 - error SC2001: values of type '[string, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 14 | // Optional and rest elements have no fixed arity/shape. 15 | const opt: [string, number?] = ["x"]; | ^~~ 16 | const rest: [string, ...number[]] = ["y", 1, 2]; -tuples.ts:16:7 - error SC2001: values of type '[string, ...number[]]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +tuples.ts:16:7 - error SC2001: values of type '[string, ...number[]]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 15 | const opt: [string, number?] = ["x"]; 16 | const rest: [string, ...number[]] = ["y", 1, 2]; diff --git a/tests/harness/__snapshots__/union-clone-override-width.ts.txt b/tests/harness/__snapshots__/union-clone-override-width.ts.txt index 6b5cc3566..8915968ef 100644 --- a/tests/harness/__snapshots__/union-clone-override-width.ts.txt +++ b/tests/harness/__snapshots__/union-clone-override-width.ts.txt @@ -1,4 +1,4 @@ -union-clone-override-width.ts:8:18 - error SC2001: values of type '{ kind: "left"; left: number; payload: WiderPayload; } | { kind: "right"; right: number; payload: WiderPayload; }' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +union-clone-override-width.ts:8:18 - error SC2001: values of type '{ kind: "left"; left: number; payload: WiderPayload; } | { kind: "right"; right: number; payload: WiderPayload; }' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown') 7 | // copy its record. The inferred spread result keeps the wider payload. 8 | const result = { ...source, payload }; diff --git a/tests/harness/corpus-costs/8020-date-setters.ts.json b/tests/harness/corpus-costs/8020-date-setters.ts.json new file mode 100644 index 000000000..ad5d9a546 --- /dev/null +++ b/tests/harness/corpus-costs/8020-date-setters.ts.json @@ -0,0 +1 @@ +{ "plain": 2891, "san": 4454 } diff --git a/tests/harness/corpus-costs/8021-date-coercions.ts.json b/tests/harness/corpus-costs/8021-date-coercions.ts.json new file mode 100644 index 000000000..cd0090be6 --- /dev/null +++ b/tests/harness/corpus-costs/8021-date-coercions.ts.json @@ -0,0 +1 @@ +{ "plain": 2589, "san": 3385 } diff --git a/tests/harness/corpus-costs/8022-date-unions.ts.json b/tests/harness/corpus-costs/8022-date-unions.ts.json new file mode 100644 index 000000000..ce89111f9 --- /dev/null +++ b/tests/harness/corpus-costs/8022-date-unions.ts.json @@ -0,0 +1 @@ +{ "plain": 2786, "san": 3724 } diff --git a/tests/harness/corpus-costs/8023-date-clone-throw.ts.json b/tests/harness/corpus-costs/8023-date-clone-throw.ts.json new file mode 100644 index 000000000..50a23d33a --- /dev/null +++ b/tests/harness/corpus-costs/8023-date-clone-throw.ts.json @@ -0,0 +1 @@ +{ "plain": 2847, "san": 33492 } diff --git a/tests/harness/corpus-costs/8024-date-setters-dynamic.mjs.json b/tests/harness/corpus-costs/8024-date-setters-dynamic.mjs.json new file mode 100644 index 000000000..0007f1971 --- /dev/null +++ b/tests/harness/corpus-costs/8024-date-setters-dynamic.mjs.json @@ -0,0 +1 @@ +{ "plain": 2645, "san": 3529 } diff --git a/tests/harness/corpus-costs/8025-date-subclass-setters.ts.json b/tests/harness/corpus-costs/8025-date-subclass-setters.ts.json new file mode 100644 index 000000000..cb128790d --- /dev/null +++ b/tests/harness/corpus-costs/8025-date-subclass-setters.ts.json @@ -0,0 +1 @@ +{ "plain": 2383, "san": 3249 } diff --git a/tests/harness/corpus-costs/8026-date-subclass-setters-dynamic.mjs.json b/tests/harness/corpus-costs/8026-date-subclass-setters-dynamic.mjs.json new file mode 100644 index 000000000..0615e7224 --- /dev/null +++ b/tests/harness/corpus-costs/8026-date-subclass-setters-dynamic.mjs.json @@ -0,0 +1 @@ +{ "plain": 2483, "san": 3294 } From 5d8804db382fb30944dc90fc910ccb76b0325fcd Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 22:40:04 -0500 Subject: [PATCH 036/273] Version string scans over the UTF-16 units of strings that are not ASCII `for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) n++` over a text with any non-ASCII character failed the loop version's ASCII guard and ran the checked loop. Each read tested the ASCII bit, compared the string with the runtime's published scan receiver, tested the bounds, loaded the units pointer, and kept the opaque fallback. The guard now also accepts a version's one string receiver when it is not ASCII. One runtime read publishes the string as the scan receiver (ScrStrScan), whose UTF-16 units the runtime holds once a scan has read it enough. The guard then tests the accesses against the published unit count, and a third copy of the loop reads each unit with one 16-bit load from the units pointer taken in the guard. The guard takes that path only while the runtime publishes the receiver itself, so the length it took is the receiver's UTF-16 length on both fast paths. Both fast copies read it for the receiver's `length`, which a while loop's condition otherwise asks the runtime for on every iteration. The runtime drops a string's units when the string is released or appended to in place, or when its index entry is evicted because another non-ASCII string is indexed. The receiver is a binding the loop does not write, so the first two cannot happen while the loop runs. A units copy is emitted only for a loop that uses unit-safe operations (string-unit-versions.ts): numbers, locals, records and class fields, typed arrays, number arrays, Math, the receiver's charCodeAt and length, and calls of functions made of the same operations that read no string. Tests of `s.charAt(i)` against one-unit literals now become charCodeAt compares in the IR pass (char-unit-locals.ts), so a version sees one kind of read. --- .../backend/llvm/bytes-loop-versions.test.ts | 4 +- .../src/backend/llvm/bytes-loop-versions.ts | 143 +++++++++++- .../compiler/src/backend/llvm/emit-module.ts | 6 +- .../src/backend/llvm/emit-statements.ts | 36 ++- packages/compiler/src/backend/llvm/emitter.ts | 18 ++ .../src/backend/llvm/expr-containers.ts | 19 +- .../compiler/src/backend/llvm/string-reads.ts | 22 ++ .../backend/llvm/string-unit-versions.test.ts | 86 +++++++ .../src/backend/llvm/string-unit-versions.ts | 144 ++++++++++++ .../compiler/src/ir/char-unit-locals.test.ts | 23 ++ packages/compiler/src/ir/char-unit-locals.ts | 100 +++++--- .../test/string-char-loop-emission.test.ts | 8 +- .../test/string-unit-loop-emission.test.ts | 94 ++++++++ .../7305-string-unit-loop-versions.ts.json | 6 + .../corpus/7305-string-unit-loop-versions.ts | 219 ++++++++++++++++++ .../7305-string-unit-loop-versions.ts.json | 1 + 16 files changed, 874 insertions(+), 55 deletions(-) create mode 100644 packages/compiler/src/backend/llvm/string-unit-versions.test.ts create mode 100644 packages/compiler/src/backend/llvm/string-unit-versions.ts create mode 100644 packages/compiler/test/string-unit-loop-emission.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7305-string-unit-loop-versions.ts.json create mode 100644 tests/corpus/7305-string-unit-loop-versions.ts create mode 100644 tests/harness/corpus-costs/7305-string-unit-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 aef39ef75..0c0f1aaa0 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -580,7 +580,9 @@ test("the guard tests a string's ASCII bit and the fast copy reads bytes", () => 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")); + // The byte copy, up to the units copy that follows it. + const units = body.search(/^bytes\.version\.fast\.units\d+:/m); + const fast = body.slice(body.search(/^bytes\.version\.fast\d+:/m), units < 0 ? undefined : units); 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. diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index 1b51deee1..301f65de2 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -12,7 +12,13 @@ import { } from "./counted-loops.js"; import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js"; import { f64Lit } from "./common.js"; -import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js"; +import { + emitStringAsciiFlag, + emitStringByteLength, + STRING_CHAR_CODE_AT_DECL, + stringScanType, +} from "./string-reads.js"; +import { unitSafeLoop } from "./string-unit-versions.js"; type Node = IrExpr | IrStmt; type VarRef = IrExpr & { kind: "varRef" }; @@ -73,6 +79,14 @@ export interface BytesLoopVersion { receivers: { ref: VarRef; global: boolean; string: boolean }[]; /** Receivers the guard requires to have no shared storage. */ ownedReceivers: number[]; + /** The string reads among `bounds`. */ + stringReads: ReadonlySet; + /** The version's one string receiver, when the loop may also run over + * its UTF-16 units if it is not ASCII (string-unit-versions.ts). */ + unitsReceiver: number | null; + /** The units receiver's `length` reads in the loop. Both fast copies + * read the length the guard took (its UTF-16 length) in their place. */ + unitLengths: ReadonlySet; fixed: FixedCheck[]; affine: AffineCheck[]; /** While loops: accesses at a counter below a loop-invariant bound. */ @@ -114,6 +128,9 @@ export interface BytesLoopFacts { /** String bindings held as a slice of another string (string-slices.ts): * they have no string header for the guard to read. */ scalarStrings: ReadonlySet; + /** The functions a loop over a string's UTF-16 units may call; absent + * or null where emitted code does not read the runtime's scan receiver. */ + unitSafeFunctions?: ReadonlySet | null; } const MAX_COST = 400; @@ -670,6 +687,19 @@ export function matchBytesLoopVersion( // 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])); + const stringReads = new Set(); + for (const group of [...fixedKept, ...affineKept]) + if (receivers[group.check.receiver]!.string) + for (const node of group.nodes) stringReads.add(node); + for (const group of boundedKept) + if (receivers[group.check.receiver]!.string) + for (const { node } of group.nodes) stringReads.add(node); + const strings = order.filter((index) => receivers[index]!.string); + const unitLengths = + facts.unitSafeFunctions != null && strings.length === 1 + ? unitSafeLoop(loop, receivers[strings[0]!]!.ref.localId, facts.unitSafeFunctions) + : null; + const unitsReceiver = unitLengths !== null ? renumber.get(strings[0]!)! : null; const needsBound = affineKept.length > 0 && counted !== null && @@ -679,6 +709,9 @@ export function matchBytesLoopVersion( owned: ownedNodes, receivers: order.map((index) => receivers[index]!), ownedReceivers: ownedKept.map(([receiver]) => renumber.get(receiver)!), + stringReads, + unitsReceiver, + unitLengths: unitLengths ?? new Set(), fixed: fixedKept.map((g) => ({ receiver: renumber.get(g.check.receiver)!, end: g.check.end, @@ -1018,13 +1051,22 @@ function findWhileCounterAccesses( /** 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 - * integer form is not established, or a module receiver not yet set). */ + * integer form is not established, or a module receiver not yet set). + * + * With `unitsFast`, the version's units receiver may instead be a string + * that is not ASCII: the guard has the runtime publish it as the scan + * receiver (one read), tests the same accesses against its UTF-16 length, + * and branches to `unitsFast`. It returns the units pointer that copy + * reads, which stays valid while the loop runs (string-unit-versions.ts), + * and for a version with a units receiver, the receiver's UTF-16 length + * on both fast paths (an i64). */ export function emitBytesLoopVersionGuard( host: LlvmEmitterContext, version: BytesLoopVersion, fast: string, slow: string, -): void { + unitsFast?: string, +): { units: string | null; length: string | null } | null { const B = host.B; const next = (cond: string): void => { if (cond === "true") return; @@ -1043,19 +1085,36 @@ export function emitBytesLoopVersionGuard( } return value; }); - const lengths = receivers.map((receiver, index) => { - const len = version.receivers[index]!.string - ? emitStringByteLength(host, receiver) - : emitBytesHeaderLength(host, receiver); + const unitsIndex = unitsFast !== undefined ? version.unitsReceiver : null; + const units: { read: { pointer: string; taken: string; published: string } | null } = { + read: null, + }; + const widen = (len: string): string => { if (host.sizeType === "i64") return len; const wide = B.tmp(); B.line(`${wide} = zext ${host.sizeType} ${len} to i64`); return wide; + }; + const lengths = receivers.map((receiver, index) => { + if (index === unitsIndex) { + const read = emitUnitsLength(host, receiver); + units.read = read; + return read.length; + } + return widen( + version.receivers[index]!.string + ? emitStringByteLength(host, receiver) + : emitBytesHeaderLength(host, receiver), + ); }); const conditions: string[] = []; + // The published count is the receiver's UTF-16 length only while the + // runtime publishes the receiver itself. + if (units.read) conditions.push(units.read.published); // 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]!)); + if (string && index !== unitsIndex) + conditions.push(emitStringAsciiFlag(host, receivers[index]!)); }); for (const index of version.ownedReceivers) { const owned = B.tmp(); @@ -1158,7 +1217,7 @@ export function emitBytesLoopVersionGuard( // Unreachable: every term has a proven range. Stay checked. B.br(slow); B.startBlock(B.newLabel("bytes.version.dead")); - return; + return null; } const wide = widenInteger(host, integer); const sum = B.tmp(); @@ -1215,8 +1274,70 @@ export function emitBytesLoopVersionGuard( valid = all; } } - if (valid === "true") B.br(fast); - else B.condBr(valid, fast, slow); + const length = version.unitsReceiver !== null ? lengths[version.unitsReceiver]! : null; + const taken = units.read; + const target = taken ? B.newLabel("bytes.version.units") : fast; + if (valid === "true") B.br(target); + else B.condBr(valid, target, slow); + if (!taken) return { units: null, length }; + B.startBlock(target); + B.condBr(taken.taken, unitsFast!, fast); + return { units: taken.pointer, length }; +} + +/** The length of a version's units receiver `s` in UTF-16 units: its byte + * length when it is ASCII; otherwise, after one runtime read publishes it + * as the scan receiver, the published unit count (0 when the runtime does + * not hold its units). Also the units pointer, whether the loop is to + * read it, and whether the published receiver is `s`. */ +function emitUnitsLength( + host: LlvmEmitterContext, + s: string, +): { length: string; pointer: string; taken: string; published: string } { + const B = host.B; + const sz = host.sizeType; + const type = stringScanType(sz); + const ascii = emitStringAsciiFlag(host, s); + const bytes = emitStringByteLength(host, s); + const from = B.currentLabel(); + const publish = B.newLabel("bytes.version.publish"), + join = B.newLabel("bytes.version.length"); + B.condBr(ascii, join, publish); + B.startBlock(publish); + host.declare(STRING_CHAR_CODE_AT_DECL); + const read = B.tmp(), + receiver = B.tmp(), + same = B.tmp(), + pointerSlot = B.tmp(), + pointer = B.tmp(), + countSlot = B.tmp(), + count = B.tmp(), + published = B.tmp(); + B.line(`${read} = call double @scr_str_char_code_at(ptr ${s}, double 0.0)`); + B.line(`${receiver} = load ptr, ptr @scr_str_scan`); + B.line(`${same} = icmp eq ptr ${receiver}, ${s}`); + B.line(`${pointerSlot} = getelementptr inbounds ${type}, ptr @scr_str_scan, i32 0, i32 1`); + B.line(`${pointer} = load ptr, ptr ${pointerSlot}`); + B.line(`${countSlot} = getelementptr inbounds ${type}, ptr @scr_str_scan, i32 0, i32 2`); + B.line(`${count} = load ${sz}, ptr ${countSlot}`); + B.line(`${published} = select i1 ${same}, ${sz} ${count}, ${sz} 0`); + const end = B.currentLabel(); + B.br(join); + B.startBlock(join); + const length = B.tmp(), + units = B.tmp(), + taken = B.tmp(); + B.line(`${length} = phi ${sz} [ ${bytes}, %${from} ], [ ${published}, %${end} ]`); + B.line(`${units} = phi ptr [ null, %${from} ], [ ${pointer}, %${end} ]`); + B.line(`${taken} = phi i1 [ false, %${from} ], [ true, %${end} ]`); + const exact = B.tmp(); + B.line(`${exact} = phi i1 [ true, %${from} ], [ ${same}, %${end} ]`); + let wide = length; + if (sz !== "i64") { + wide = B.tmp(); + B.line(`${wide} = zext ${sz} ${length} to i64`); + } + return { length: wide, pointer: units, taken, published: exact }; } function scaleCounter(host: LlvmEmitterContext, value: string, scale: number): string { diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index e5258e583..069f3b615 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -61,7 +61,7 @@ import { hasLoneSurrogate, wtf8Bytes } from "../literals.js"; import { undefinedArmTag } from "../../ir/analysis.js"; import { NULLABLE_ABSENT, NULLABLE_NULL } from "./nullable-unions.js"; import { literalEqualityDefinition } from "./string-literal-equality.js"; -import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js"; +import { STRING_READ_HELPERS, stringScanDecl, stringScanInline } from "./string-reads.js"; import { emitLibraryIdentityLines } from "../library-identity-markers.js"; import { InternalCompilerError } from "../../errors.js"; import { targetCpuAttribute } from "../target-cpu.js"; @@ -1586,9 +1586,7 @@ export function helperDefs(emitter: LlEmitter): string[] { // A scan's receiver reads inline too (ScrStrScan): from ordinary global // storage, or, in worker executables, from the thread's static TLS block, // one thread-pointer-relative load (see executableTls). - const scanInline = - emitter.executableTls || - (emitter.mod.workers !== true && emitter.mod.lib?.threadInstances !== true); + const scanInline = stringScanInline(emitter); let scanDeclared = false; for (const helper of STRING_READ_HELPERS) { if (!emitter.decls.has(helper.decl)) continue; diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 55e31219c..6de298ec4 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -2082,28 +2082,56 @@ function emitVersionedLoop( 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 unitsFast = + version.unitsReceiver !== null ? B.newLabel("bytes.version.fast.units") : undefined; + const guard = emitBytesLoopVersionGuard(emitter, version, fast, slow, unitsFast); + const units = guard?.units ?? null; const originalBounds = emitter.bytesBounds, originalOwned = emitter.bytesOwned, - originalRanges = emitter.integerRanges; + originalRanges = emitter.integerRanges, + originalUnitReads = emitter.stringUnitReads, + originalLengthReads = emitter.stringLengthReads; LOOPS.of(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 || version.paired?.loop.guarded) LOOPS.of(emitter).provenCountedGuards.add(s); + B.startBlock(fast); + emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); + // Both fast copies read the string's length the guard took. + const length = guard?.length; + if (length != null && version.unitLengths.size > 0) + emitter.stringLengthReads = new Map([ + ...originalLengthReads, + ...[...version.unitLengths].map((node) => [node, length] as const), + ]); emitter.emitStmt(s); B.br(done); + if (unitsFast !== undefined && units !== null) { + // The string reads take the receiver's UTF-16 units. + B.startBlock(unitsFast); + emitter.bytesBounds = new Set([ + ...originalBounds, + ...[...version.bounds].filter((node) => !version.stringReads.has(node)), + ]); + emitter.stringUnitReads = new Map([ + ...originalUnitReads, + ...[...version.stringReads].map((node) => [node, units] as const), + ]); + emitter.emitStmt(s); + B.br(done); + } } finally { emitter.bytesBounds = originalBounds; emitter.bytesOwned = originalOwned; emitter.integerRanges = originalRanges; + emitter.stringUnitReads = originalUnitReads; + emitter.stringLengthReads = originalLengthReads; LOOPS.of(emitter).provenCountedGuards.delete(s); LOOPS.of(emitter).bytesLoopVersioning.delete(s); } diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 8106a6478..0a2756388 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -104,6 +104,8 @@ import { dropUnwrittenPropertyBags } from "../../ir/property-bags.js"; import { freezeInitOnlyGlobals } from "../../ir/init-only-globals.js"; import { hoistFinalFieldReads } from "../../ir/field-values.js"; import { holdCharLocalsAsUnits } from "../../ir/char-unit-locals.js"; +import { unitSafeFunctions } from "./string-unit-versions.js"; +import { stringScanInline } from "./string-reads.js"; import { findInt32Abis, INT32_ABI_SUFFIX, @@ -1477,6 +1479,12 @@ export class LlEmitter { * (one-unit tests and switches, charAtUnitRead), with the charAt each * stands for: a loop version's proof for the charAt covers its read. */ readonly charAtUnitReads = new Map(); + /** String reads a loop version proved in bounds of a string's UTF-16 + * units, with the units pointer to read (string-unit-versions.ts). */ + stringUnitReads: ReadonlyMap = new Map(); + /** `length` reads of a loop version's string, with the i64 UTF-16 + * length its guard took (bytes-loop-versions.ts). */ + stringLengthReads: ReadonlyMap = new Map(); /** Worker programs: typed-array accesses whose views an enclosing version * guard proved thread-owned. */ bytesOwned: ReadonlySet = new Set(); @@ -4305,11 +4313,21 @@ export class LlEmitter { workers: this.mod.workers === true, owned: this.bytesOwned, scalarStrings: new Set(this.stringSlices.keys()), + unitSafeFunctions: this.unitSafeFunctions(), }, paired, ); } + private unitSafe: ReadonlySet | null | undefined; + /** The functions a loop over a string's UTF-16 units may call, or null + * where emitted reads do not use the runtime's scan receiver. */ + private unitSafeFunctions(): ReadonlySet | null { + if (this.unitSafe === undefined) + this.unitSafe = stringScanInline(this) ? unitSafeFunctions(this.mod) : null; + return this.unitSafe; + } + /** The alias tag of a whole-slot load of a binding (module globals only). */ slotAlias(id: string): string { const binding = this.binding(id); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 279b8c044..bba7629b9 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -53,6 +53,7 @@ import { STRING_CHAR_CODE_AT_INT_MARKER, emitAsciiChar, emitAsciiCharCode, + emitUnitCharCode, STRING_UTF16_LEN_DECL, } from "./string-reads.js"; import { exactInteger, widenInteger } from "./integer-values.js"; @@ -322,6 +323,15 @@ export function charAtUnitRead( return read; } +/** The UTF-16 units an enclosing loop version proved the index of `e` + * below the count of (string-unit-versions.ts). */ +function provenUnitsRead(host: LlvmEmitterContext, e: IrExpr): string | null { + const units = host.stringUnitReads.get(e); + if (units !== undefined) return units; + const origin = host.charAtUnitReads.get(e); + return origin !== undefined ? (host.stringUnitReads.get(origin) ?? null) : null; +} + /** An enclosing loop version proved the string of `e` ASCII and its index * below the length (bytes-loop-versions.ts). */ function provenStringRead(host: LlvmEmitterContext, e: IrExpr): boolean { @@ -364,9 +374,14 @@ export function emitStrIntrinsic( const method = e.method; switch (method) { case "length": { + const t = B.tmp(); + const length = host.stringLengthReads.get(e); + if (length !== undefined) { + B.line(`${t} = sitofp i64 ${length} to double`); + return { name: t, type: e.type }; + } // sc_str_utf16_len (string-reads.ts) answers proven-ASCII strings inline. host.declare(STRING_UTF16_LEN_DECL); - const t = B.tmp(); B.line(`${t} = call double @sc_str_utf16_len(ptr ${r.name})`); return { name: t, type: e.type }; } @@ -376,6 +391,8 @@ export function emitStrIntrinsic( const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null; if (index) { const wide = widenInteger(host, index); + const units = provenUnitsRead(host, e); + if (units !== null) return { name: emitUnitCharCode(host, units, wide), type: e.type }; if (provenStringRead(host, e)) return { name: emitAsciiCharCode(host, r.name, wide), type: e.type }; host.declare(STRING_CHAR_CODE_AT_INT_MARKER); diff --git a/packages/compiler/src/backend/llvm/string-reads.ts b/packages/compiler/src/backend/llvm/string-reads.ts index 441c925f3..08df79cf2 100644 --- a/packages/compiler/src/backend/llvm/string-reads.ts +++ b/packages/compiler/src/backend/llvm/string-reads.ts @@ -75,6 +75,19 @@ export function emitAsciiCharCode(host: LlvmEmitterContext, s: string, index: st return code; } +/** Unit `index` (an i64, below their count) of a string's UTF-16 `units` + * (ScrStrScan): one 16-bit load, as a double. */ +export function emitUnitCharCode(host: LlvmEmitterContext, units: string, index: string): string { + const B = host.B; + const pointer = B.tmp(), + unit = B.tmp(), + code = B.tmp(); + B.line(`${pointer} = getelementptr inbounds i16, ptr ${units}, i64 ${index}`); + B.line(`${unit} = load i16, ptr ${pointer}`); + B.line(`${code} = uitofp i16 ${unit} to double`); + return code; +} + /** The one-character string of unit `index` (an i64) of a proven-ASCII * string `s` whose length covers it: the immortal `@sc_ascii_chars` entry * for its byte, as the inline charAt helpers answer. */ @@ -114,6 +127,15 @@ function loadLength(sz: string): string[] { ]; } +/** Whether emitted reads use the runtime's scan receiver: from ordinary + * global storage, or, in worker executables, from the thread's static TLS + * block (executableTls). */ +export function stringScanInline(host: LlvmEmitterContext): boolean { + return ( + host.executableTls || (host.mod.workers !== true && host.mod.lib?.threadInstances !== true) + ); +} + /** The runtime's ScrStrScan record and the declaration naming it. */ export function stringScanType(sz: string): string { return `{ ptr, ptr, ${sz} }`; diff --git a/packages/compiler/src/backend/llvm/string-unit-versions.test.ts b/packages/compiler/src/backend/llvm/string-unit-versions.test.ts new file mode 100644 index 000000000..5b0634b1a --- /dev/null +++ b/packages/compiler/src/backend/llvm/string-unit-versions.test.ts @@ -0,0 +1,86 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../../index.js"; +import type { IrModule, IrStmt } from "../../ir/ir.js"; +import { unitSafeFunctions, unitSafeLoop } from "./string-unit-versions.js"; + +async function lower(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-unit-versions-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const loopOf = (mod: IrModule, name: string): IrStmt & { kind: "for" } => + mod.functions.find((f) => f.name === name)!.body.find((s) => s.kind === "for") as IrStmt & { + kind: "for"; + }; + +test("loops over a string's units call only functions that read no string", async () => { + const mod = await lower(` +function lower(c: number): boolean { + return c >= 97 && c <= 122; +} +function word(c: number): boolean { + return lower(c) || (c >= 48 && c <= 57) || Math.abs(c - 200) < 50; +} +function codeOf(t: string, i: number): number { + return t.charCodeAt(i); +} +function viaCodeOf(t: string): number { + return codeOf(t, 0); +} +function words(s: string): number { + let n = 0; + const starts = [0]; + for (let i = 0; i < s.length; i++) { + if (word(s.charCodeAt(i))) n++; + else starts.push(i); + } + return n + starts.length; +} +function pairs(s: string, t: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) n += s.charCodeAt(i) + codeOf(t, i); + return n; +} +function both(s: string, t: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) n += s.charCodeAt(i) + t.charCodeAt(i); + return n; +} +function concatenates(s: string): string { + let out = ""; + for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) out += "-"; + return out; +} +console.log(words("ab c"), pairs("ab", "cd"), both("ab", "cd"), concatenates("a b"), viaCodeOf("x")); +`); + const safe = unitSafeFunctions(mod); + expect(safe.has("lower")).toBe(true); + expect(safe.has("word")).toBe(true); + // A string read, directly or through a call, makes a function unsafe. + expect(safe.has("codeOf")).toBe(false); + expect(safe.has("viaCodeOf")).toBe(false); + + const param = (fn: string, index = 0): string => + mod.functions.find((f) => f.name === fn)!.params[index]!.localId; + // A unit-safe loop answers its receiver's length reads: here the + // condition's `s.length`. + const lengths = unitSafeLoop(loopOf(mod, "words"), param("words"), safe); + expect(lengths?.size).toBe(1); + expect([...lengths!].every((e) => e.kind === "strIntrinsic" && e.method === "length")).toBe(true); + expect(unitSafeLoop(loopOf(mod, "pairs"), param("pairs"), safe)).toBeNull(); + expect(unitSafeLoop(loopOf(mod, "both"), param("both"), safe)).toBeNull(); + expect(unitSafeLoop(loopOf(mod, "concatenates"), param("concatenates"), safe)).toBeNull(); +}); diff --git a/packages/compiler/src/backend/llvm/string-unit-versions.ts b/packages/compiler/src/backend/llvm/string-unit-versions.ts new file mode 100644 index 000000000..022c3a513 --- /dev/null +++ b/packages/compiler/src/backend/llvm/string-unit-versions.ts @@ -0,0 +1,144 @@ +import type { IrExpr, IrModule, IrStmt } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; + +/* Loop versions over the UTF-16 units of a string that is not ASCII. + * + * The runtime publishes the receiver of an indexed scan with its UTF-16 + * units (ScrStrScan, string-reads.ts), and drops them when the string is + * released or appended to in place, or when its index entry is evicted: + * the runtime indexes another non-ASCII string. A loop version's string + * receiver is a binding the loop does not write, which keeps it alive and + * unappended for the whole loop. Its fast copy over units therefore only + * needs a body that indexes no other string: the operations below, and + * calls of functions made of the same operations without string reads + * (`unitSafeFunctions`). */ + +const ARRAY_METHODS = new Set(["push", "pop", "length"]); + +/** Whether `e` (or statement `s`) uses only operations that index no + * string: numbers, booleans, locals, records and class fields, typed + * arrays, number arrays and Math. `reads` admits the string reads a + * version covers; `calls` the callees. */ +function unitSafe( + node: { e: IrExpr } | { s: IrStmt }, + reads: (e: IrExpr & { kind: "strIntrinsic" }) => boolean, + calls: (callee: string) => boolean, +): boolean { + const expr = (e: IrExpr): boolean => { + switch (e.kind) { + case "varRef": + case "numLit": + case "boolLit": + case "bin": + case "unary": + case "logical": + case "ternary": + case "toBool": + case "seqExpr": + case "assignExpr": + case "incDec": + case "recordGet": + case "fieldGet": + case "bytesIntrinsic": + break; + case "arrayGet": + if (e.type.kind !== "f64" && e.type.kind !== "bool") return false; + break; + case "arrIntrinsic": { + // Growing or shrinking an array of numbers allocates; it reads no + // string. + const array = e.receiver.type; + if ( + !ARRAY_METHODS.has(e.method) || + array.kind !== "array" || + (array.elem.kind !== "f64" && array.elem.kind !== "bool") + ) + return false; + break; + } + case "libCall": + if (!e.fn.startsWith("math.")) return false; + break; + case "call": + if (!calls(e.callee)) return false; + break; + case "strIntrinsic": + if (!reads(e)) return false; + break; + default: + return false; + } + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + switch (s.kind) { + case "varDecl": + case "assign": + case "exprStmt": + case "if": + case "block": + case "while": + case "doWhile": + case "for": + case "break": + case "continue": + case "return": + case "recordSet": + case "fieldSet": + case "bytesSet": + break; + default: + return false; + } + return everyStmtChild(s, expr, stmt); + }; + return "e" in node ? expr(node.e) : stmt(node.s); +} + +/** The module's functions a units version may call: synchronous + * functions without captures whose bodies use only unit-safe operations, + * read no string, and call only such functions. */ +export function unitSafeFunctions(mod: IrModule): ReadonlySet { + const candidates = new Map( + mod.functions + .filter((fn) => !fn.async && !fn.generator && !(fn.captures?.length ?? 0)) + .map((fn) => [fn.name, fn]), + ); + const safe = new Set(candidates.keys()); + for (let settled = false; !settled;) { + settled = true; + for (const name of safe) { + const fn = candidates.get(name)!; + const ok = fn.body.every((s) => + unitSafe( + { s }, + () => false, + (callee) => safe.has(callee), + ), + ); + if (!ok) { + safe.delete(name); + settled = false; + } + } + } + return safe; +} + +/** The `length` reads of the binding `receiver` in a loop whose every + * part is unit-safe, reading only charCodeAt and length of `receiver` and + * calling only `safe` functions; null for any other loop. */ +export function unitSafeLoop( + loop: IrStmt & { kind: "for" | "while" }, + receiver: string, + safe: ReadonlySet, +): ReadonlySet | null { + const lengths = new Set(); + const reads = (e: IrExpr & { kind: "strIntrinsic" }): boolean => { + if (e.receiver.kind !== "varRef" || e.receiver.localId !== receiver) return false; + if (e.method === "length") lengths.add(e); + return e.method === "charCodeAt" || e.method === "length"; + }; + const calls = (callee: string): boolean => safe.has(callee); + return unitSafe({ s: loop }, reads, calls) ? lengths : null; +} diff --git a/packages/compiler/src/ir/char-unit-locals.test.ts b/packages/compiler/src/ir/char-unit-locals.test.ts index ad16ecebd..9ec25b75a 100644 --- a/packages/compiler/src/ir/char-unit-locals.test.ts +++ b/packages/compiler/src/ir/char-unit-locals.test.ts @@ -96,3 +96,26 @@ console.log(escapes("a b", 1), rebound(" x", 0), wide("ab", 0), against("a", "b expect(fn(after, name)).toBe(fn(before, name)); } }); + +test("charAt tests in place compare charCodeAt", async () => { + const mod = holdCharLocalsAsUnits( + await lower(` +function tests(s: string, i: number): number { + let n = 0; + if (s.charAt(i) === " ") n += 1; + if ("a" <= s.charAt(i) && s.charAt(i) < "n") n += 2; + if (s.charAt(i) === "ab") n += 4; + return n; +} +console.log(tests("a b", 1), tests("abc", 0), tests("", 2)); +`), + ); + // The two-unit literal keeps its string comparison. + expect(reads(fn(mod, "tests"))).toEqual([ + "charCodeAt", + "charCodeAt", + "charCodeAt", + "strEq", + "charAt", + ]); +}); diff --git a/packages/compiler/src/ir/char-unit-locals.ts b/packages/compiler/src/ir/char-unit-locals.ts index beeeffaf3..840b7478f 100644 --- a/packages/compiler/src/ir/char-unit-locals.ts +++ b/packages/compiler/src/ir/char-unit-locals.ts @@ -12,6 +12,8 @@ import { mapExprChildren, mapStmtChildren } from "./traverse.js"; type CharAt = IrExpr & { kind: "strIntrinsic" }; type UnitUse = IrExpr & { kind: "strEq" | "strCmp" }; +const FLIPPED = { "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const; + /** Character locals held as their code unit. * * `const c = s[i]` (or `s.charAt(i)`) followed only by tests such as @@ -28,7 +30,8 @@ type UnitUse = IrExpr & { kind: "strEq" | "strCmp" }; * * Only immutable unboxed string locals qualify, with one declaration whose * initializer is a one-argument charAt, and with no other mention of the - * local. */ + * local. The same tests of `s.charAt(i)` in place compare + * `s.charCodeAt(i)`, so a loop version sees one kind of read. */ export function holdCharLocalsAsUnits(mod: IrModule): IrModule { let changed = false; const functions = mod.functions.map((fn) => { @@ -43,7 +46,6 @@ function rewriteFunction(fn: IrFunction): IrFunction { const candidates = new Set( fn.locals.filter((l) => !l.mutable && !l.boxed && l.type.kind === "string").map((l) => l.id), ); - if (candidates.size === 0) return fn; // Every mention of a candidate, in any field, against the mentions the // rewrite understands. const mentions = new Map(); @@ -67,7 +69,7 @@ function rewriteFunction(fn: IrFunction): IrFunction { for (const key in node) if (key !== "loc") count(node[key]); } }; - count(fn.body); + if (candidates.size > 0) count(fn.body); const held = new Set(); for (const id of candidates) if ( @@ -76,7 +78,7 @@ function rewriteFunction(fn: IrFunction): IrFunction { mentions.get(id) === understood.get(id) ) held.add(id); - if (held.size === 0) return fn; + let changed = false; const unitRef = (id: string, loc: IrExpr["loc"]): IrExpr => ({ kind: "varRef", @@ -88,38 +90,28 @@ function rewriteFunction(fn: IrFunction): IrFunction { if (e.kind === "strEq" || e.kind === "strCmp") { const use = unitUse(e); if (use && held.has(use.id)) { - const code: IrExpr = { kind: "numLit", value: use.code, type: F64, loc: e.loc }; - const c = unitRef(use.id, e.loc); - if (e.kind === "strEq") - return { - kind: "bin", - op: e.negated ? "!==" : "===", - left: c, - right: code, - type: BOOL, - loc: e.loc, - }; - // The comparison with the local on the left. - const op = use.left - ? e.op - : ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const)[e.op]; - if (op === ">" || op === ">=") - return { kind: "bin", op, left: c, right: code, type: BOOL, loc: e.loc }; - const negation: IrExpr = { - kind: "bin", - op: op === "<" ? ">=" : ">", - left: c, - right: code, - type: BOOL, - loc: e.loc, + changed = true; + return charUnitTest(e, unitRef(use.id, e.loc), use.code, use.left); + } + // `s.charAt(i)` in place: its unit read, `s.charCodeAt(i)`. + const direct = charAtTest(e); + if (direct) { + changed = true; + const read: IrExpr = { + ...direct.charAt, + method: "charCodeAt", + receiver: rewriteExpr(direct.charAt.receiver), + args: direct.charAt.args.map(rewriteExpr), + type: F64, }; - return { kind: "unary", op: "!", operand: negation, type: BOOL, loc: e.loc }; + return charUnitTest(e, read, direct.code, direct.left); } } return mapExprChildren(e, rewriteExpr, rewriteStmt); }; const rewriteStmt = (s: IrStmt): IrStmt => { - if (s.kind === "varDecl" && held.has(s.localId) && s.init?.kind === "strIntrinsic") + if (s.kind === "varDecl" && held.has(s.localId) && s.init?.kind === "strIntrinsic") { + changed = true; return { ...s, init: { @@ -130,7 +122,9 @@ function rewriteFunction(fn: IrFunction): IrFunction { type: F64, }, }; - if (s.kind === "switch" && s.disc.kind === "varRef" && held.has(s.disc.localId)) + } + if (s.kind === "switch" && s.disc.kind === "varRef" && held.has(s.disc.localId)) { + changed = true; return { ...s, disc: unitRef(s.disc.localId, s.disc.loc), @@ -143,11 +137,14 @@ function rewriteFunction(fn: IrFunction): IrFunction { body: c.body.map(rewriteStmt), })), }; + } return mapStmtChildren(s, rewriteExpr, rewriteStmt); }; + const body = fn.body.map(rewriteStmt); + if (!changed) return fn; return { ...fn, - body: fn.body.map(rewriteStmt), + body, locals: fn.locals.map((l) => (held.has(l.id) ? unitLocal(l) : l)), }; } @@ -179,6 +176,45 @@ function understoodMention(node: Record): string | null { return null; } +/** A one-unit test of a string that reads one character or "" (charAt), + * on its unit `unit`, which reads NaN for "": an equality, or an ordered + * comparison with the unit on the left when `unitLeft`. "" is below every + * one-unit literal, and NaN fails every ordered comparison, so `c < "x"` + * becomes `!(c >= 120)`. */ +function charUnitTest(e: UnitUse, unit: IrExpr, code: number, unitLeft: boolean): IrExpr { + const literal: IrExpr = { kind: "numLit", value: code, type: F64, loc: e.loc }; + if (e.kind === "strEq") + return { + kind: "bin", + op: e.negated ? "!==" : "===", + left: unit, + right: literal, + type: BOOL, + loc: e.loc, + }; + const op = unitLeft ? e.op : FLIPPED[e.op]; + if (op === ">" || op === ">=") + return { kind: "bin", op, left: unit, right: literal, type: BOOL, loc: e.loc }; + const negation: IrExpr = { + kind: "bin", + op: op === "<" ? ">=" : ">", + left: unit, + right: literal, + type: BOOL, + loc: e.loc, + }; + return { kind: "unary", op: "!", operand: negation, type: BOOL, loc: e.loc }; +} + +/** A test between `s.charAt(i)` and a literal of one code unit. */ +function charAtTest(e: UnitUse): { charAt: CharAt; code: number; left: boolean } | null { + const literalRight = e.right.kind === "strLit"; + const literal = literalRight ? e.right : e.left; + const other = literalRight ? e.left : e.right; + if (literal.kind !== "strLit" || literal.value.length !== 1 || !isCharAt(other)) return null; + return { charAt: other, code: literal.value.charCodeAt(0), left: literalRight }; +} + function isCharAt(e: IrExpr): e is CharAt { return e.kind === "strIntrinsic" && e.method === "charAt" && e.args.length === 1; } diff --git a/packages/compiler/test/string-char-loop-emission.test.ts b/packages/compiler/test/string-char-loop-emission.test.ts index 6f5756ce8..9f2553d85 100644 --- a/packages/compiler/test/string-char-loop-emission.test.ts +++ b/packages/compiler/test/string-char-loop-emission.test.ts @@ -43,8 +43,12 @@ console.log(spaces("a b c"), letters("abC1z"), chars("xyz").join("-"), chars("é ll.match( new RegExp(`define internal [^\\n]+ @sc_bf_${name}\\([^\\n]*\\) #0 \\{[\\s\\S]*?\\n\\}`), )![0]; - // The fast copy follows the checked one. - const fast = (fn: string): string => fn.slice(fn.lastIndexOf("bytes.version.fast")); + // The byte copy follows the checked one, and the units copy (for + // strings that are not ASCII) follows it. + const fast = (fn: string): string => { + const units = fn.search(/^bytes\.version\.fast\.units\d+:/m); + return fn.slice(fn.search(/^bytes\.version\.fast\d+:/m), units < 0 ? undefined : units); + }; // `s[i] === " "` compares the byte; the checked copy keeps the helper. const spaces = body("spaces"); diff --git a/packages/compiler/test/string-unit-loop-emission.test.ts b/packages/compiler/test/string-unit-loop-emission.test.ts new file mode 100644 index 000000000..32ee8411b --- /dev/null +++ b/packages/compiler/test/string-unit-loop-emission.test.ts @@ -0,0 +1,94 @@ +import { execFileSync } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +test("scans of strings that are not ASCII read their UTF-16 units", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-unit-loops-")); + try { + const entry = join(dir, "main.ts"); + await writeFile( + entry, + ` +function spaces(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) n++; + return n; +} +function skip(s: string): number { + let pos = 0; + while (pos < s.length && s.charCodeAt(pos) !== 32) pos++; + return pos; +} +function pairs(s: string, t: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === t.charCodeAt(i % t.length)) n++; + return n; +} +let text = ""; +for (let k = 0; k < 400; k++) text += k % 3 === 0 ? "café " : "naïve façade "; +const other = text.replace(/a/g, "ä"); +let total = 0; +for (let pass = 0; pass < 4; pass++) + total += spaces(text) + spaces(other) + pairs(text, other) + skip(text); +console.log(total, spaces("plain ascii text"), spaces("")); +`, + ); + const outPath = join(dir, "main.ir.json"); + const lowered = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!lowered.ok) + throw new Error(lowered.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const body = (name: string): string => + ll.match( + new RegExp( + `define internal [^\\n]+ @sc_bf_${name}(?:\\.i32)?\\([^\\n]*\\) #0 \\{[\\s\\S]*?\\n\\}`, + ), + )![0]; + + const spaces = body("spaces"); + // A string that is not ASCII is published as the scan receiver once, + // before the loop, and its copy loads units. + expect(spaces).toMatch(/call double @scr_str_char_code_at\(ptr %\S+, double 0\.0\)/); + // The copies follow the checked one: bytes, then units. + const bytesAt = spaces.search(/^bytes\.version\.fast\d+:/m); + const unitsAt = spaces.search(/^bytes\.version\.fast\.units\d+:/m); + expect(bytesAt).toBeGreaterThan(0); + expect(unitsAt).toBeGreaterThan(bytesAt); + const units = spaces.slice(unitsAt); + expect(units).toMatch(/load i16, ptr %\S+\n\s+%\S+ = uitofp i16/); + expect(units).not.toContain("@sc_str_char_code_at_int"); + // An ASCII string still reads bytes. + const bytes = spaces.slice(bytesAt, unitsAt); + expect(bytes).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/); + expect(bytes).not.toContain("@sc_str_char_code_at_int"); + // A while loop over a counter below the length reads units too. + const skip = body("skip"); + expect(skip).toMatch(/^bytes\.version\.fast\.units\d+:/m); + // Its fast copies read the length the guard took; the checked copy + // still asks for it. + const skipFastAt = skip.search(/^bytes\.version\.fast\d+:/m); + expect(skip.slice(0, skipFastAt)).toContain("@sc_str_utf16_len"); + expect(skip.slice(skipFastAt)).not.toContain("@sc_str_utf16_len"); + // Two strings cannot both be the scan receiver. + expect(body("pairs")).not.toContain("bytes.version.fast.units"); + + 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)); + let text = ""; + for (let k = 0; k < 400; k++) text += k % 3 === 0 ? "café " : "naïve façade "; + const other = text.replace(/a/g, "ä"); + const count = (s: string): number => s.split(" ").length - 1; + let pairs = 0; + for (let i = 0; i < text.length; i++) + if (text.charCodeAt(i) === other.charCodeAt(i % other.length)) pairs++; + const total = 4 * (count(text) + count(other) + pairs + text.indexOf(" ")); + expect(execFileSync(exe, { encoding: "utf8" })).toBe(`${total} 2 0\n`); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7305-string-unit-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7305-string-unit-loop-versions.ts.json new file mode 100644 index 000000000..21b8255e4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7305-string-unit-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7305-string-unit-loop-versions.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7305-string-unit-loop-versions.ts b/tests/corpus/7305-string-unit-loop-versions.ts new file mode 100644 index 000000000..c7600167d --- /dev/null +++ b/tests/corpus/7305-string-unit-loop-versions.ts @@ -0,0 +1,219 @@ +// charCodeAt scans of strings that are not ASCII, repeated so the runtime +// holds their UTF-16 units: counts, hashes, neighbor reads, descending +// loops, reads past the end, line starts pushed to a number array, helper +// functions that read no string, a helper that reads another string, a +// second non-ASCII string read in the same loop, two texts scanned in +// turn, a short text, a text appended to between scans, surrogate pairs +// and lone surrogates, ASCII texts beside them, and while loops over a +// counter below the length: word and identifier scans, peeks past the +// counter, and a lexer class whose position is a field. Loops also read +// the length in their bodies, and loops over a fresh text that no scan +// has published run from its end and over its last units. + +function isWordUnit(c: number): boolean { + return (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || c > 127; +} + +function isWordOrDigit(c: number): boolean { + return isWordUnit(c) || (c >= 48 && c <= 57); +} + +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 = 7; + for (let i = 0; i < s.length; i++) h = (Math.imul(h, 31) + s.charCodeAt(i)) | 0; + return h; +} + +function rises(s: string): number { + let n = 0; + for (let i = 1; i < s.length - 1; i++) + if (s.charCodeAt(i) > s.charCodeAt(i - 1) && s.charCodeAt(i) >= s.charCodeAt(i + 1)) n++; + return n; +} + +function backwards(s: string): number { + let h = 0; + for (let i = s.length - 1; i >= 0; i -= 3) h = (h * 7 + s.charCodeAt(i)) % 1000003; + return h; +} + +function pastEnd(s: string): string { + let sum = 0; + for (let i = 0; i <= s.length; i++) sum += s.charCodeAt(i); + return String(sum); +} + +function lineStarts(s: string): number[] { + const starts = [0]; + for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) starts.push(i + 1); + return starts; +} + +function wordUnits(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (isWordOrDigit(s.charCodeAt(i))) n++; + return n; +} + +function codeOf(t: string, i: number): number { + return t.charCodeAt(i % t.length); +} + +function mixed(s: string, t: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) n = (n + s.charCodeAt(i) * codeOf(t, i)) % 1000003; + return n; +} + +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 surrogates(s: string): string { + let high = 0, + low = 0; + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i); + if (c >= 0xd800 && c < 0xdc00) high++; + else if (c >= 0xdc00 && c < 0xe000) low++; + } + return `${high}/${low}`; +} + +function wordCount(s: string): number { + let pos = 0, + n = 0; + const end = s.length; + while (pos < end) { + while (pos < end && s.charCodeAt(pos) === 32) pos++; + if (pos >= end) break; + n++; + while (pos < end && s.charCodeAt(pos) !== 32) pos++; + } + return n; +} + +function wordUnitRuns(s: string): number { + let pos = 0, + total = 0; + while (pos < s.length) { + if (isWordUnit(s.charCodeAt(pos))) { + const start = pos; + while (pos < s.length && isWordOrDigit(s.charCodeAt(pos))) pos++; + total += pos - start; + } else pos = (pos + 1) | 0; + } + return total; +} + +function doubled(s: string): number { + let pos = 0, + n = 0; + while (pos < s.length) { + if (s.charCodeAt(pos) === s.charCodeAt(pos + 1)) n++; + pos += 1; + } + return n; +} + +class Scanner { + pos = 0; + src: string; + constructor(src: string) { + this.src = src; + } + line(): number { + const src = this.src; + const start = this.pos; + let pos = start; + while (pos < src.length && src.charCodeAt(pos) !== 10) pos++; + this.pos = pos + 1; + return pos - start; + } +} + +function lineLengths(s: string): number { + const scanner = new Scanner(s); + let total = 0, + lines = 0; + while (scanner.pos < s.length) { + total = (total * 31 + scanner.line()) % 1000003; + lines++; + } + return total * 1000 + lines; +} + +function remaining(s: string): number { + let pos = 0; + let sum = 0; + while (pos < s.length) { + if (s.charCodeAt(pos) === 32) sum += s.length - pos; + pos++; + } + return sum; +} + +function lastLineLength(s: string): number { + let last = s.length; + for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) last = s.length - i - 1; + return last; +} + +function from(s: string, start: number): number { + let pos = start; + let sum = 0; + while (pos < s.length) { + sum = sum * 3 + s.charCodeAt(pos); + pos++; + } + return sum; +} + +const words = ["Größe", "naïve", "façade", "東京", "ok", "✓", "mañana", "plain", "😀", "end\n"]; +function text(count: number, seed: number): string { + let out = ""; + for (let k = 0; k < count; k++) out += words[(k * 7 + seed) % words.length] + (k % 5 === 0 ? " " : " "); + return out; +} + +const big = text(3000, 1); +const other = text(2000, 4); +const ascii = big.replace(/[^\x00-\x7f]/g, "x"); +const short = "café ✓ 東京"; +const lone = "a\uD800b\uDFFFc " + big.slice(0, 500) + "\uD83D"; +for (let pass = 0; pass < 3; pass++) { + console.log( + pass, + spaces(big), + hash(big), + rises(big), + backwards(big), + pastEnd(big), + lineStarts(big).length, + wordUnits(big), + ); + console.log(" ", spaces(other), hash(other), spaces(big), hash(other), wordUnits(other)); + console.log(" ", mixed(big, other), sameAt(big, other), surrogates(big), surrogates(lone)); + console.log(" ", spaces(ascii), hash(ascii), spaces(short), hash(short), hash(lone), rises(lone)); + console.log(" ", wordCount(big), wordUnitRuns(big), doubled(big), lineLengths(big)); + console.log(" ", wordCount(ascii), wordUnitRuns(other), doubled(lone), lineLengths(lone), wordCount("")); + const fresh = text(40 + pass, pass) + "ü"; + console.log(" ", remaining(big), remaining(ascii), lastLineLength(big), lastLineLength(other)); + console.log(" ", from(fresh, fresh.length), from(fresh, fresh.length - 3), from(big, big.length - 4)); +} + +let grown = text(1500, 2); +for (let round = 0; round < 3; round++) { + console.log("grown", round, grown.length, spaces(grown), hash(grown), hash(grown)); + grown += text(200, round) + "Ünïcödé"; +} +console.log("empty", spaces(""), hash(""), pastEnd(""), lineStarts("").length); diff --git a/tests/harness/corpus-costs/7305-string-unit-loop-versions.ts.json b/tests/harness/corpus-costs/7305-string-unit-loop-versions.ts.json new file mode 100644 index 000000000..f40375eee --- /dev/null +++ b/tests/harness/corpus-costs/7305-string-unit-loop-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 10500, "san": 14500 } From 8c94f6f10c4cd613e65eb7cab70e333c219bda35 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 23:10:56 -0500 Subject: [PATCH 037/273] Fix Date setter edge cases and the library profile fences --- .../lowering/builtins/date-classes.ts | 10 ++-- .../src/frontend/lowering/builtins/dates.ts | 9 ++-- .../src/frontend/lowering/date-values.ts | 37 +++++++++++-- .../frontend/lowering/expressions/binary.ts | 22 ++++++++ .../corpus/8027-date-setter-order.ts.json | 6 +++ .../8028-date-setter-order-dynamic.mjs.json | 6 +++ tests/corpus/8027-date-setter-order.ts | 53 +++++++++++++++++++ .../corpus/8028-date-setter-order-dynamic.mjs | 17 ++++++ .../8027-date-setter-order.ts.json | 1 + .../8028-date-setter-order-dynamic.mjs.json | 1 + tests/harness/library-profile.test.ts | 1 + 11 files changed, 151 insertions(+), 12 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8027-date-setter-order.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8028-date-setter-order-dynamic.mjs.json create mode 100644 tests/corpus/8027-date-setter-order.ts create mode 100644 tests/corpus/8028-date-setter-order-dynamic.mjs create mode 100644 tests/harness/corpus-costs/8027-date-setter-order.ts.json create mode 100644 tests/harness/corpus-costs/8028-date-setter-order-dynamic.mjs.json diff --git a/packages/compiler/src/frontend/lowering/builtins/date-classes.ts b/packages/compiler/src/frontend/lowering/builtins/date-classes.ts index c8aab8687..9d152de56 100644 --- a/packages/compiler/src/frontend/lowering/builtins/date-classes.ts +++ b/packages/compiler/src/frontend/lowering/builtins/date-classes.ts @@ -255,17 +255,19 @@ export function lowerInheritedDateMethodCall( loc, }); if (isDateSetter(name) && name !== "setTime") { - // The field setters change the Date the class's time slot holds. + // The field setters change the Date the class's time slot holds once + // the arguments are evaluated: an argument that calls setTime on the + // instance gives the slot a new Date. const date = lowerer.declareHiddenLocal("%dateTime", DATE_T); + const current = varRef(date.id, DATE_T, loc); + const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [current], type: F64, loc }; + const next = dateSetterTime(lowerer, call, name, stmts, time); stmts.push({ kind: "varDecl", localId: date.id, init: timeOf(lowerer, value, root, loc), loc, }); - const current = varRef(date.id, DATE_T, loc); - const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [current], type: F64, loc }; - const next = dateSetterTime(lowerer, call, name, stmts, time); return done({ kind: "libCall", fn: "date.store", args: [current, next], type: F64, loc }); } if (name === "setTime") { diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index 79696d1ba..075047606 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -342,9 +342,9 @@ function lowerDateSetter( /** The new time value of setter `name` called with `call`'s arguments on a * Date whose time value `time` reads: the arguments evaluate into * temporaries appended to `stmts` (ToNumber of each, an undefined one is - * NaN), and the result reads `time` after them. Missing optional arguments - * keep their fields; arguments past a setter's last one are evaluated and - * ignored. */ + * NaN), and the result reads `time` after them. A missing first argument + * is NaN; missing optional arguments keep their fields; arguments past a + * setter's last one are evaluated and ignored. */ export function dateSetterTime( lowerer: Lowerer, call: ts.CallExpression, @@ -374,7 +374,8 @@ export function dateSetterTime( loc, }; const setter = DATE_FIELD_SETTERS.get(name)!; - const used = args.slice(0, setter.max); + // The first argument is required: a missing one is undefined, so NaN. + const used = args.length === 0 ? [number(Number.NaN)] : args.slice(0, setter.max); const operands = [...used]; while (operands.length < 4) operands.push(number(0)); return { diff --git a/packages/compiler/src/frontend/lowering/date-values.ts b/packages/compiler/src/frontend/lowering/date-values.ts index 669bc1dbc..f769e0866 100644 --- a/packages/compiler/src/frontend/lowering/date-values.ts +++ b/packages/compiler/src/frontend/lowering/date-values.ts @@ -102,7 +102,7 @@ function decideTimeValueDate(lowerer: Lowerer, decl: ts.VariableDeclaration): bo if (!ok) return; if (ts.isIdentifier(node) && node !== decl.name && node.text === decl.name.text) { if (lowerer.checker.getSymbolAtLocation(node) === symbol) { - const use = enclosingFunction(node) === fn ? timeValueUse(lowerer, node) : null; + const use = enclosingFunction(node) === fn ? timeValueUse(lowerer, node, symbol) : null; if (use === null) ok = false; else if (use === "set") sets = true; } @@ -174,7 +174,11 @@ function enclosingFunction(node: ts.Node): ts.FunctionLikeDeclaration | null { /** How a use of the binding uses its time value: "read" for a use that * reads only the time value, "set" for a setter call that replaces it, or * null for any other use. */ -function timeValueUse(lowerer: Lowerer, id: ts.Identifier): "read" | "set" | null { +function timeValueUse( + lowerer: Lowerer, + id: ts.Identifier, + symbol: ts.Symbol, +): "read" | "set" | null { let node: ts.Node = id; while (ts.isParenthesizedExpression(node.parent)) node = node.parent; const parent = node.parent; @@ -192,12 +196,34 @@ function timeValueUse(lowerer: Lowerer, id: ts.Identifier): "read" | "set" | nul if (isDateSetter(parent.name.text)) return "set"; return TIME_VALUE_METHODS.has(parent.name.text) ? "read" : null; } - return readsTimeValue(lowerer, node, parent) ? "read" : null; + return readsTimeValue(lowerer, node, parent, symbol) ? "read" : null; +} + +/** True when `node` calls a setter on the binding `symbol` names. */ +function callsSetterOf(lowerer: Lowerer, node: ts.Node, symbol: ts.Symbol): boolean { + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + isDateSetter(node.expression.name.text) + ) { + let receiver = node.expression.expression; + while (ts.isParenthesizedExpression(receiver)) receiver = receiver.expression; + if (ts.isIdentifier(receiver) && lowerer.checker.getSymbolAtLocation(receiver) === symbol) + return true; + } + return ( + ts.forEachChild(node, (child) => callsSetterOf(lowerer, child, symbol) || undefined) ?? false + ); } /** A use of the binding, other than a method call, that reads only its * time value. */ -function readsTimeValue(lowerer: Lowerer, node: ts.Node, parent: ts.Node): boolean { +function readsTimeValue( + lowerer: Lowerer, + node: ts.Node, + parent: ts.Node, + symbol: ts.Symbol, +): boolean { if (ts.isTemplateSpan(parent)) return parent.expression === node; // Unary + and -, the number hint. if (ts.isPrefixUnaryExpression(parent)) @@ -222,6 +248,9 @@ function readsTimeValue(lowerer: Lowerer, node: ts.Node, parent: ts.Node): boole ); } if (ts.isBinaryExpression(parent)) { + // A left operand converts after the right one is evaluated: a setter + // there changes the time value the comparison or concatenation reads. + if (parent.left === node && callsSetterOf(lowerer, parent.right, symbol)) return false; const other = parent.left === node ? parent.right : parent.left; const otherKind = lowerer.mapTypeOf(lowerer.typeOf(other))?.kind; if (RELATIONAL.has(parent.operatorToken.kind)) return otherKind === "date"; diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index de6648836..590fa745b 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -2037,6 +2037,28 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr loc, }; if (left.type.kind === "string" || right.type.kind === "string") { + // A Date on the left converts after the right operand is + // evaluated, which can run its setters. + if (left.type.kind === "date" && !isSafeToRepeat(right)) { + const date = lowerer.declareHiddenLocal("%plus", left.type); + const other = lowerer.declareHiddenLocal("%plus", right.type); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: date.id, init: left, loc }, + { kind: "varDecl", localId: other.id, init: right, loc }, + ], + result: { + kind: "strConcat", + left: concatOperand(lowerer, varRef(date.id, left.type, loc), expr.left), + right: concatOperand(lowerer, varRef(other.id, right.type, loc), expr.right), + type: STRING, + loc, + }, + type: STRING, + loc, + }; + } return { kind: "strConcat", left: concatOperand(lowerer, left, expr.left), diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8027-date-setter-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8027-date-setter-order.ts.json new file mode 100644 index 000000000..cfdfddfc6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8027-date-setter-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8027-date-setter-order.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8028-date-setter-order-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8028-date-setter-order-dynamic.mjs.json new file mode 100644 index 000000000..0a1b69c5f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8028-date-setter-order-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8028-date-setter-order-dynamic.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/8027-date-setter-order.ts b/tests/corpus/8027-date-setter-order.ts new file mode 100644 index 000000000..d0a175c2f --- /dev/null +++ b/tests/corpus/8027-date-setter-order.ts @@ -0,0 +1,53 @@ +// Date setters and evaluation order: a setter called without arguments +// makes the Date invalid, an argument that calls setTime on the same +// Date subclass instance is seen by the setter, and a comparison or +// concatenation reads its left Date after the right side runs a setter. +export {}; +process.env.TZ = "UTC"; + +const show = (label: string, d: Date): void => { + console.log(label, d.getTime(), Number.isNaN(d.getTime()) ? "invalid" : d.toISOString()); +}; + +// No arguments: the first argument is undefined, so NaN. +const local = new Date(Date.UTC(2026, 5, 15, 9, 30)); +// @ts-expect-error: the first argument is required +console.log(local.setHours(), Number.isNaN(local.getTime())); +const utc = new Date(Date.UTC(2026, 5, 15, 9, 30)); +// @ts-expect-error: the first argument is required +console.log(utc.setUTCMonth(), utc.setUTCFullYear(2020)); +show("utc after", utc); +const kept = new Date(Date.UTC(2026, 5, 15)); +const keep = kept; +// @ts-expect-error: the first argument is required +console.log(keep.setMinutes(), kept.getTime()); + +class Stamp extends Date { + label(): string { + return `stamp ${this.toISOString()}`; + } +} +const stamp = new Stamp(Date.UTC(2026, 0, 1)); +// @ts-expect-error: the first argument is required +console.log(stamp.setSeconds(), Number.isNaN(stamp.getTime())); +stamp.setTime(Date.UTC(2026, 0, 2)); +// @ts-expect-error: the first argument is required +console.log(stamp.setUTCDate(), stamp.setUTCFullYear(2024, 1, 29), stamp.label()); + +// An argument that calls setTime on the same instance. +const moved = new Stamp(Date.UTC(2026, 3, 10, 12)); +console.log(moved.setUTCHours((moved.setTime(Date.UTC(2000, 0, 1)), 5)), moved.label()); +const plain = new Date(Date.UTC(2026, 3, 10, 12)); +console.log(plain.setUTCMinutes((plain.setTime(Date.UTC(2001, 0, 1)), 7))); +show("plain", plain); + +// The left operand converts after the right operand runs a setter. +const a = new Date(Date.UTC(2026, 0, 1)); +const b = new Date(Date.UTC(2026, 0, 2)); +console.log(a < (a.setUTCDate(3), b), a > b); +const c = new Date(Date.UTC(2026, 0, 1)); +console.log(c >= (c.setTime(0), new Date(1)), c <= new Date(1)); +const s = new Date(Date.UTC(2026, 0, 1)); +console.log(s + (s.setUTCFullYear(1999), "!")); +const n = new Date(Date.UTC(2026, 0, 1)); +console.log(+n === (n.setTime(5), 5), +n); diff --git a/tests/corpus/8028-date-setter-order-dynamic.mjs b/tests/corpus/8028-date-setter-order-dynamic.mjs new file mode 100644 index 000000000..f52e410c1 --- /dev/null +++ b/tests/corpus/8028-date-setter-order-dynamic.mjs @@ -0,0 +1,17 @@ +// Date setters reached through untyped values: without arguments each +// one makes the Date invalid, and an argument that calls setTime on the +// same Date is seen by the setter. +process.env.TZ = "UTC"; + +const names = [ + "setMilliseconds", "setSeconds", "setMinutes", "setHours", "setDate", "setMonth", "setFullYear", + "setUTCMilliseconds", "setUTCSeconds", "setUTCMinutes", "setUTCHours", "setUTCDate", "setUTCMonth", "setUTCFullYear", +]; +for (const name of names) { + const d = new Date(Date.UTC(2026, 5, 15)); + console.log(name, d[name](), d.getTime()); +} +const d = new Date(Date.UTC(2026, 3, 10, 12)); +console.log(d.setUTCHours((d.setTime(Date.UTC(2000, 0, 1)), 5)), d.toISOString()); +const a = new Date(Date.UTC(2026, 0, 1)); +console.log(a < (a.setUTCDate(3), new Date(Date.UTC(2026, 0, 2)))); diff --git a/tests/harness/corpus-costs/8027-date-setter-order.ts.json b/tests/harness/corpus-costs/8027-date-setter-order.ts.json new file mode 100644 index 000000000..aff4bec01 --- /dev/null +++ b/tests/harness/corpus-costs/8027-date-setter-order.ts.json @@ -0,0 +1 @@ +{ "plain": 2489, "san": 3288 } diff --git a/tests/harness/corpus-costs/8028-date-setter-order-dynamic.mjs.json b/tests/harness/corpus-costs/8028-date-setter-order-dynamic.mjs.json new file mode 100644 index 000000000..a7aeb13a5 --- /dev/null +++ b/tests/harness/corpus-costs/8028-date-setter-order-dynamic.mjs.json @@ -0,0 +1 @@ +{ "plain": 2430, "san": 2975 } diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index b39daefa1..9ced69b5c 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -563,6 +563,7 @@ describe("library profile fences", () => { "stdlib.date.getUTCSeconds", "stdlib.date.now", "stdlib.date.parse", + "stdlib.date.setters", "stdlib.date.toDateString", "stdlib.date.toISOString", "stdlib.date.toString", From 0309a211061953c20cdfd6c7c7b2438591b67d63 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 23:29:39 -0500 Subject: [PATCH 038/273] Fix the tsgo and tsc-ts IR identity failure --- tests/corpus/7455-tls-als-contexts.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/corpus/7455-tls-als-contexts.mjs b/tests/corpus/7455-tls-als-contexts.mjs index 38783fdf9..bc59a78d4 100644 --- a/tests/corpus/7455-tls-als-contexts.mjs +++ b/tests/corpus/7455-tls-als-contexts.mjs @@ -21,7 +21,7 @@ const server = tls.createServer({ cert, key }, (socket) => { server.on("tlsClientError", (err) => serverEvents.push(`tlsClientError ${err.code} ${als.getStore()}`)); await als.run("listen", () => new Promise((resolve) => server.listen(0, "127.0.0.1", () => { serverEvents.push(`listening ${als.getStore()}`); - resolve(); + resolve(undefined); }))); const { port } = server.address(); await als.run("client", () => new Promise((resolve) => { @@ -32,7 +32,7 @@ await als.run("client", () => new Promise((resolve) => { client.on("data", () => clientEvents.push(`data ${als.getStore()}`)); client.on("close", () => { clientEvents.push(`close ${als.getStore()}`); - resolve(); + resolve(undefined); }); })); await als.run("untrusted", () => new Promise((resolve) => { From 3b951ef4e22df32a4d5a703eb86c4e3233c155d0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 23:43:30 -0500 Subject: [PATCH 039/273] Let objects inherit from JavaScript class prototypes --- .../limitations/builtin-apis/prototypes.mdx | 7 +- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../lowering/class-dynamic-dispatch.ts | 96 ++++++++++++- .../src/frontend/lowering/class-prototypes.ts | 71 +++++++++- .../src/frontend/lowering/lower-inspect.ts | 13 +- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 1 + packages/compiler/src/ir/runtime-features.ts | 2 + .../8260-class-prototype-views.mjs.json | 6 + packages/runtime/src/scr_json.c | 43 +++++- packages/runtime/src/scr_qs.h | 2 + packages/runtime/src/scr_util_callable.c | 54 ++++--- tests/corpus/8260-class-prototype-views.mjs | 134 ++++++++++++++++++ .../8260-class-prototype-views.mjs.json | 1 + tests/harness/errors.test.ts | 30 ++++ 15 files changed, 439 insertions(+), 23 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8260-class-prototype-views.mjs.json create mode 100644 tests/corpus/8260-class-prototype-views.mjs create mode 100644 tests/harness/corpus-costs/8260-class-prototype-views.mjs.json diff --git a/docs/content/limitations/builtin-apis/prototypes.mdx b/docs/content/limitations/builtin-apis/prototypes.mdx index 170b11793..7a7e01635 100644 --- a/docs/content/limitations/builtin-apis/prototypes.mdx +++ b/docs/content/limitations/builtin-apis/prototypes.mdx @@ -1,5 +1,10 @@ - **Prototypes:** Named properties and methods on program class prototypes support replacement, inheritance, and deletion. - Bare prototype reflection, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported. + A module-level JavaScript class's prototype can become the prototype of other objects: `Object.create(C.prototype)`, `Object.setPrototypeOf(F.prototype, C.prototype)`, `Object.setPrototypeOf(object, C.prototype)`, and `util.inherits(F, C)` with an ordinary constructor `F`. + The inherited methods and accessors run with the inheriting object as `this`. + Methods that use `#private` members or `super` throw a TypeError for a receiver that is not an instance of the class. + `C.prototype` also reads as a value for `Object.getPrototypeOf` and for `===` and `!==` comparisons. + Changing the prototype of a class prototype (`Object.setPrototypeOf(C.prototype, X)`, `util.inherits(C, F)`, or `Object.setPrototypeOf` on a class prototype reached through an instance) is unsupported. + Other bare prototype reflection, TypeScript class prototypes as values, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported. 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. diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 014191314..20ad7fa02 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -82,6 +82,7 @@ export const LIB_FN_SYMS: Record = { "util.isDeepStrictEqual": "scr_util_is_deep_strict_equal", "util.styleText": "scr_util_style_text", "util.inherits": "scr_util_inherits", + "util.inheritsPrototype": "scr_util_inherits_prototype", "util.deprecate": "scr_util_deprecate", "util.callbackify": "scr_util_callbackify", "util.promisifyDyn": "scr_util_promisify", diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index f219fd5e5..d658fb20a 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -4105,6 +4105,94 @@ export class ClassDynamicDispatch { return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc }; } + /** `%dyn.class.bagHasOwn[OrPrototype]`: whether an instance's property bag + * holds `key` as its own property, or, with `prototype`, whether the bag's + * prototype object does. False before the instance has a bag. */ + private readBagHasOwn( + lowerer: Lowerer, + bag: IrExpr, + key: IrExpr, + prototype: boolean, + loc: SrcLoc, + ): IrExpr { + const name = prototype ? "%dyn.class.bagHasOwnOrPrototype" : "%dyn.class.bagHasOwn"; + let helper = lowerer.liftedFnNamed(name); + if (!helper) { + const value = varRef("p.0", DYN, loc); + const keyRef = varRef("p.1", STRING, loc); + const proto = varRef("proto", DYN, loc); + const hasOwn = (target: IrExpr): IrExpr => ({ + kind: "libCall", + fn: "dyn.hasOwn", + args: [target, keyRef], + type: BOOL, + loc, + }); + const returnFalse: IrStmt = { + kind: "return", + value: { kind: "boolLit", value: false, type: BOOL, loc }, + loc, + }; + helper = { + name, + params: [ + { localId: "p.0", name: "bag", type: DYN }, + { localId: "p.1", name: "key", type: STRING }, + ], + returnType: BOOL, + locals: [ + { id: "p.0", name: "bag", type: DYN, mutable: false }, + { id: "p.1", name: "key", type: STRING, mutable: false }, + ...(prototype ? [{ id: "proto", name: "proto", type: DYN, mutable: false }] : []), + ], + body: [ + { + kind: "if", + cond: { kind: "dynTest", test: "undefined", value, type: BOOL, loc }, + then: [returnFalse], + else_: null, + loc, + }, + ...(prototype + ? ([ + { + kind: "if", + cond: hasOwn(value), + then: [ + { + kind: "return", + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }, + ], + else_: null, + loc, + }, + { + kind: "varDecl", + localId: "proto", + init: { kind: "libCall", fn: "dyn.getPrototype", args: [value], type: DYN, loc }, + loc, + }, + { + kind: "if", + cond: { kind: "dynTest", test: "nullish", value: proto, type: BOOL, loc }, + then: [returnFalse], + else_: null, + loc, + }, + { kind: "return", value: hasOwn(proto), loc }, + ] satisfies IrStmt[]) + : [{ kind: "return", value: hasOwn(value), loc } satisfies IrStmt]), + ], + loc, + }; + lowerer.liftedFns.push(helper); + } + this.generated.add(helper); + return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc }; + } + /** The hints for which some boxed class declares a conversion method. A * string conversion calls toString first, so without a declared toString * or Symbol.toPrimitive it answers Object.prototype.toString's @@ -5866,12 +5954,18 @@ export class ClassDynamicDispatch { const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl), DYN); const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc); const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; + // `constructor` is an own property of every class prototype, so an + // ancestor's prototype never answers for an instance of this class: + // the instance's own property answers, then one this class's + // prototype object defines (a reflected prototype, or a write to + // `C.prototype.constructor`), and otherwise the class itself. + const answered = this.readBagHasOwn(lowerer, bag, key, hasClassPrototypeData(info), loc); return [ { kind: "return", value: { kind: "ternary", - cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc), + cond: answered, then: { kind: "libCall", fn: "dyn.bagGet", diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index cb2ab2280..595b64e05 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -13,7 +13,7 @@ import { type IrStmt, type SrcLoc, } from "../../ir/ir.js"; -import { locOf } from "../program.js"; +import { isJsSourceFile, locOf } from "../program.js"; import { classMemberNameOf, classValueRef, @@ -503,6 +503,69 @@ export function isErrorPrototypeValue( ); } +/** Whether a bare `C.prototype` read becomes the prototype of another object + * or is only compared or inspected: the argument of `Object.create` or + * `Object.getPrototypeOf` / `Reflect.getPrototypeOf`, the prototype argument + * of `Object.setPrototypeOf` / `Reflect.setPrototypeOf`, or an operand of + * `===` / `!==`. In these positions the complete view of the + * prototype (reflectClassPrototype) answers. Objects that inherit from it + * run the class's methods with themselves as the receiver, which the + * methods' receiver-independent lowering (reflectedClassMethodValue) + * supports. Other positions, such as `Object.setPrototypeOf(C.prototype, X)`, + * which would relink the class, keep the diagnostic. */ +function prototypeViewPosition(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean { + let node: ts.Node = expr; + let parent = node.parent; + while (parent && ts.isParenthesizedExpression(parent)) { + node = parent; + parent = parent.parent; + } + if (!parent) return false; + if (ts.isCallExpression(parent) && ts.isPropertyAccessExpression(parent.expression)) { + const args = parent.arguments.nodes; + const objectMethod = lowerer.stdlibGlobalMember(parent.expression, "Object"); + const reflectMethod = lowerer.stdlibGlobalMember(parent.expression, "Reflect"); + if ( + objectMethod === "create" || + objectMethod === "getPrototypeOf" || + reflectMethod === "getPrototypeOf" + ) + return args[0] === node; + if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf") + return args[1] === node; + return false; + } + return ( + ts.isBinaryExpression(parent) && + (parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken || + parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken) + ); +} + +/** A module-level JavaScript class named directly (not a class value that + * may hold a subclass at run time): its prototype has a complete view whose + * methods accept any receiver. */ +function viewableClass(lowerer: Lowerer, info: ClassInfo, receiver: ts.Expression): boolean { + return ( + info.decl !== null && + info.localClass === undefined && + isJsSourceFile(ts.getSourceFileOfNode(info.decl)) && + exactClassOfReceiver(lowerer, receiver) === info + ); +} + +/** The complete prototype view of the class `node` names, when it is a + * viewable class (util.inherits's superCtor); null otherwise. */ +export function classPrototypeView( + lowerer: Lowerer, + node: ts.Expression, + loc: SrcLoc, +): IrExpr | null { + const info = exactClassOfReceiver(lowerer, node); + if (!info || !viewableClass(lowerer, info, node)) return null; + return reflectClassPrototype(lowerer, info, loc, classValueRef(lowerer, info, node)); +} + /** Named data and method slots have native descriptors. Bare reflection and * accessor replacement still require a complete view of the prototype. */ export function lowerClassPrototypeData( @@ -566,6 +629,12 @@ export function lowerClassPrototypeData( }); return reflectClassPrototype(lowerer, info, locOf(expr), receiver); } + if ( + name === null && + prototypeViewPosition(lowerer, expr) && + viewableClass(lowerer, info, expr.expression) + ) + return reflectClassPrototype(lowerer, info, locOf(expr), classValueRef(lowerer, info, expr)); if (name === null || (isCompiledPrototypeMember(lowerer, info, name) && !method)) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index e7a43bde1..cb7eba971 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,5 +1,6 @@ import { keyedHelperName } from "./stable-names.js"; import { perKeyOrder } from "./key-order-witness.js"; +import { classPrototypeView } from "./class-prototypes.js"; import { dynUndefinedExpr, boolLit, @@ -3433,8 +3434,14 @@ function lowerUtilCallableCall( member === "inherits" && expr.arguments.nodes[1] !== undefined ? builtinClassKindOf(lowerer, expr.arguments.nodes[1]) : null; + // A superCtor class whose prototype has a complete view (a module-level + // JavaScript class named directly) passes that view to the runtime. + const superView = + member === "inherits" && builtinBase === null && expr.arguments.nodes[1] !== undefined + ? classPrototypeView(lowerer, expr.arguments.nodes[1], loc) + : null; for (const [index, arg] of expr.arguments.nodes.slice(0, classArgs).entries()) { - if (index === 1 && builtinBase !== null) continue; + if (index === 1 && (builtinBase !== null || superView !== null)) continue; if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval") lowerer.noLowering( `util.${member} over a class constructor`, @@ -3466,7 +3473,9 @@ function lowerUtilCallableCall( for (const arg of expr.arguments.nodes.slice(arity)) stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc }); const type = fn === "util.inherits" ? VOID : DYN; - const result: IrExpr = { kind: "libCall", fn, args, type, loc }; + const result: IrExpr = superView + ? { kind: "libCall", fn: "util.inheritsPrototype", args: [...args, superView], type, loc } + : { kind: "libCall", fn, args, type, loc }; return { kind: "seqExpr", stmts, result, type: result.type, loc }; } diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 390c7fbc3..38512fdef 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -591,6 +591,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "util.styleText", "util.toUSVString", "util.inherits", + "util.inheritsPrototype", "util.deprecate", "util.callbackify", "util.promisifyDyn", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index e6ad0614a..b87ff24c6 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1146,6 +1146,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * arguments (scr_util_callable.c). The function results are native dyn * wrappers; call sites with a static function type check them back. */ "util.inherits": { argTypes: [DYN, DYN], result: VOID }, + "util.inheritsPrototype": { argTypes: [DYN, DYN, DYN], result: VOID }, "util.deprecate": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, "util.callbackify": { argTypes: [DYN], result: DYN }, /** util.promisify over an untyped function value: a native dyn function diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 2ae19375f..14402b6b5 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -144,6 +144,7 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "util.debuglog", "util.deprecate", "util.inherits", + "util.inheritsPrototype", "util.styleText", ]); @@ -173,6 +174,7 @@ const UTIL_CALLABLE_LIB_FNS: ReadonlySet = new Set([ "util.debuglog", "util.deprecate", "util.inherits", + "util.inheritsPrototype", ]); /** Entry points that link the regex unit: the regex and glob matchers (fs.glob's walk compiles its patterns with the glob matcher), and readline's terminal unit (scr_readline_tty.c), which measures with the regex unit's stripVTControlCharacters. */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8260-class-prototype-views.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8260-class-prototype-views.mjs.json new file mode 100644 index 000000000..6cf42e773 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8260-class-prototype-views.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8260-class-prototype-views.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 580b816c4..b8f3bf8f9 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2169,6 +2169,40 @@ static bool scr_dyn_prototype_valid(const ScrDyn *prototype) { } /* A valid receiver or prototype whose kind this operation does not handle. */ +/* Compiled class prototypes. Compiled code binds a class's methods, its + * base chain and its instanceof answers when it compiles, so the + * prototype's own [[Prototype]] is fixed: scr_dyn_set_prototype refuses to + * change it. Each module-level class prototype registers when it is + * created (scr_dyn_name_class_prototype) and stays alive until its thread + * exits, as the class does. */ +static SCR_TL ScrDyn **scr_dyn_fixed_prototypes; +static SCR_TL size_t scr_dyn_fixed_count, scr_dyn_fixed_cap; + +static void scr_dyn_fixed_prototypes_cleanup(void) { + for (size_t i = 0; i < scr_dyn_fixed_count; i++) scr_dyn_release(scr_dyn_fixed_prototypes[i]); + free(scr_dyn_fixed_prototypes); + scr_dyn_fixed_prototypes = NULL; + scr_dyn_fixed_count = scr_dyn_fixed_cap = 0; +} + +static void scr_dyn_fix_prototype(ScrDyn *prototype) { + if (scr_dyn_fixed_count == scr_dyn_fixed_cap) { + if (scr_dyn_fixed_cap == 0) scr_atexit(scr_dyn_fixed_prototypes_cleanup); + size_t cap = scr_dyn_fixed_cap ? scr_dyn_fixed_cap * 2 : 8; + ScrDyn **grown = realloc(scr_dyn_fixed_prototypes, cap * sizeof *grown); + if (!grown) scr_json_oom(); + scr_dyn_fixed_prototypes = grown; + scr_dyn_fixed_cap = cap; + } + scr_dyn_fixed_prototypes[scr_dyn_fixed_count++] = scr_dyn_retain(prototype); +} + +static bool scr_dyn_prototype_fixed(const ScrDyn *object) { + for (size_t i = 0; i < scr_dyn_fixed_count; i++) + if (scr_dyn_fixed_prototypes[i] == object) return true; + return false; +} + static void scr_dyn_prototype_unsupported(const char *operation) { ScrJsonBuf b; scr_jb_init(&b); @@ -2285,6 +2319,11 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { return NULL; } if (!scr_dyn_prototype_valid(prototype)) return NULL; + if (object->kind == SCR_DYN_OBJ && scr_dyn_fixed_count && scr_dyn_prototype_fixed(object) && + !(prototype->kind == SCR_DYN_NULL ? object->null_proto : prototype == object->prototype)) { + scr_dyn_prototype_unsupported("Object.setPrototypeOf"); + return NULL; + } bool typed = scr_dyn_typed_ref_prototype(prototype); if (scr_exc_pending()) return NULL; if ((object->kind != SCR_DYN_OBJ && object->kind != SCR_DYN_ARR) || @@ -6354,7 +6393,9 @@ static void scr_dyn_name_prototype_chain(const ScrDyn *object) { } 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; + if (prototype->kind != SCR_DYN_OBJ) return; + if (!scr_dyn_prototype_fixed(prototype)) scr_dyn_fix_prototype(prototype); + if (prototype->v.obj.class_name || name->len == 0) return; scr_dyn_stamp_prototype_name(prototype, name->data, name->len); } diff --git a/packages/runtime/src/scr_qs.h b/packages/runtime/src/scr_qs.h index fc5ca5da5..2232d2d48 100644 --- a/packages/runtime/src/scr_qs.h +++ b/packages/runtime/src/scr_qs.h @@ -62,6 +62,8 @@ bool scr_util_terminal_colors(void); * (scr_util_promisify.c). Arguments are borrowed; function results are +1 * native dyn functions, or NULL with Node's TypeError pending. */ void scr_util_inherits(struct ScrDyn *ctor, struct ScrDyn *super_ctor); +void scr_util_inherits_prototype(struct ScrDyn *ctor, struct ScrDyn *super_ctor, + struct ScrDyn *super_prototype); struct ScrDyn *scr_util_deprecate(struct ScrDyn *fn, struct ScrDyn *msg, struct ScrDyn *code, struct ScrDyn *options); struct ScrDyn *scr_util_callbackify(struct ScrDyn *original); diff --git a/packages/runtime/src/scr_util_callable.c b/packages/runtime/src/scr_util_callable.c index c905ea8ae..60de92121 100644 --- a/packages/runtime/src/scr_util_callable.c +++ b/packages/runtime/src/scr_util_callable.c @@ -149,23 +149,9 @@ static bool uc_define(ScrDyn *target, const char *key, ScrDyn *descriptor) { * Node 24's lib/util.js: nullish arguments and an undefined * superCtor.prototype throw ERR_INVALID_ARG_TYPE; then super_ becomes a * writable, configurable, non-enumerable own property and - * ctor.prototype's prototype becomes superCtor.prototype. */ -void scr_util_inherits(ScrDyn *ctor, ScrDyn *super_ctor) { - if (uc_nullish(ctor)) { - scr_dyn_arg_type_fail("ctor", "of type function", ctor); - return; - } - if (uc_nullish(super_ctor)) { - scr_dyn_arg_type_fail("superCtor", "of type function", super_ctor); - return; - } - if ((ctor->kind == SCR_DYN_FUNC && ctor->v.fn.class_obj) || - (super_ctor->kind == SCR_DYN_FUNC && super_ctor->v.fn.class_obj)) { - uc_fence("util.inherits over class constructors has no lowering; declare the subclass with extends"); - return; - } - ScrDyn *super_prototype = uc_get(super_ctor, "prototype"); - if (!super_prototype) return; + * ctor.prototype's prototype becomes superCtor.prototype. super_prototype + * is superCtor.prototype (owned), or NULL when superCtor is no object. */ +static void uc_inherits(ScrDyn *ctor, ScrDyn *super_ctor, ScrDyn *super_prototype) { if (super_prototype->kind == SCR_DYN_UNDEF) { scr_dyn_prop_type_fail("superCtor.prototype", "of type object", super_prototype); scr_dyn_release(super_prototype); @@ -198,6 +184,40 @@ void scr_util_inherits(ScrDyn *ctor, ScrDyn *super_ctor) { scr_dyn_release(super_prototype); } +void scr_util_inherits(ScrDyn *ctor, ScrDyn *super_ctor) { + if (uc_nullish(ctor)) { + scr_dyn_arg_type_fail("ctor", "of type function", ctor); + return; + } + if (uc_nullish(super_ctor)) { + scr_dyn_arg_type_fail("superCtor", "of type function", super_ctor); + return; + } + if ((ctor->kind == SCR_DYN_FUNC && ctor->v.fn.class_obj) || + (super_ctor->kind == SCR_DYN_FUNC && super_ctor->v.fn.class_obj)) { + uc_fence("util.inherits over class constructors has no lowering; declare the subclass with extends"); + return; + } + ScrDyn *super_prototype = uc_get(super_ctor, "prototype"); + if (!super_prototype) return; + uc_inherits(ctor, super_ctor, super_prototype); +} + +/* util.inherits(ctor, C) where C is a compiled class whose prototype the + * compiler exposes as super_prototype (borrowed): the class value's own + * properties do not hold it. ctor itself may not be a class. */ +void scr_util_inherits_prototype(ScrDyn *ctor, ScrDyn *super_ctor, ScrDyn *super_prototype) { + if (uc_nullish(ctor)) { + scr_dyn_arg_type_fail("ctor", "of type function", ctor); + return; + } + if (ctor->kind == SCR_DYN_FUNC && ctor->v.fn.class_obj) { + uc_fence("util.inherits over class constructors has no lowering; declare the subclass with extends"); + return; + } + uc_inherits(ctor, super_ctor, scr_dyn_retain(super_prototype)); +} + /* ── util.deprecate ──────────────────────────────────────────────────── * The wrapper warns once (and once per code across wrappers) before each * call reaches fn with the call's receiver and arguments; construction diff --git a/tests/corpus/8260-class-prototype-views.mjs b/tests/corpus/8260-class-prototype-views.mjs new file mode 100644 index 000000000..e08b8f206 --- /dev/null +++ b/tests/corpus/8260-class-prototype-views.mjs @@ -0,0 +1,134 @@ +// A class's prototype as the prototype of other objects: ES5 constructors +// that inherit from a class through Object.create, Object.setPrototypeOf and +// util.inherits, and plain objects that adopt a class prototype. Inherited +// methods, accessors and calls between methods run with the object as the +// receiver. +import util from "node:util"; + +class Shape { + constructor(name) { + this.name = name; + } + get label() { + return `${this.name} (${this.sides()} sides)`; + } + sides() { + return 0; + } + describe() { + return `${this.label}: area ${this.area().toFixed(2)}`; + } + area() { + return 0; + } +} + +class Polygon extends Shape { + sides() { + return this.points.length; + } + perimeter() { + let total = 0; + for (let i = 0; i < this.points.length; i++) { + const [x1, y1] = this.points[i]; + const [x2, y2] = this.points[(i + 1) % this.points.length]; + total += Math.hypot(x2 - x1, y2 - y1); + } + return total; + } +} + +// Object.create(Class.prototype) for an ES5 subclass. +function Square(size) { + this.name = "square"; + this.size = size; + this.points = [ + [0, 0], + [size, 0], + [size, size], + [0, size], + ]; +} +Square.prototype = Object.create(Polygon.prototype); +Square.prototype.constructor = Square; +Square.prototype.area = function () { + return this.size * this.size; +}; + +const square = new Square(3); +console.log(square.describe()); +console.log(square.perimeter(), square.sides(), square.label); +console.log(square instanceof Square, square instanceof Polygon, square instanceof Shape); +console.log(Object.getPrototypeOf(Square.prototype) === Polygon.prototype); +console.log(Object.getPrototypeOf(Polygon.prototype) === Shape.prototype); +console.log(Object.keys(square).join(",")); +const inherited = []; +for (const key in square) inherited.push(key); +console.log(inherited.join(",")); + +// Object.setPrototypeOf on an ES5 constructor's prototype. +function Circle(radius) { + this.name = "circle"; + this.radius = radius; +} +Circle.prototype.area = function () { + return Math.PI * this.radius * this.radius; +}; +Object.setPrototypeOf(Circle.prototype, Shape.prototype); +const circle = new Circle(2); +console.log(circle.describe(), circle instanceof Shape, circle instanceof Polygon); + +// util.inherits with a class as the superCtor. +function Triangle(a, b, c) { + this.name = "triangle"; + this.points = [a, b, c]; +} +util.inherits(Triangle, Polygon); +Triangle.prototype.area = function () { + const [[x1, y1], [x2, y2], [x3, y3]] = this.points; + return Math.abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2); +}; +const triangle = new Triangle([0, 0], [4, 0], [0, 3]); +console.log(Triangle.super_ === Polygon, triangle instanceof Polygon); +console.log(triangle.describe(), triangle.perimeter()); + +// A plain object that adopts a class prototype (rehydrating parsed data). +const parsed = JSON.parse('{"name":"lot","points":[[0,0],[2,0],[2,1]]}'); +Object.setPrototypeOf(parsed, Polygon.prototype); +console.log(parsed.sides(), parsed.perimeter().toFixed(3), parsed.label, parsed instanceof Polygon); +const fresh = Object.create(Shape.prototype); +fresh.name = "blank"; +console.log(fresh.describe(), fresh instanceof Shape, Object.getPrototypeOf(fresh) === Shape.prototype); + +// Native instances keep their own methods and answers. +const native = new Polygon("native"); +native.points = [ + [0, 0], + [1, 0], + [0, 1], +]; +console.log(native.describe(), native.perimeter().toFixed(3), native instanceof Polygon); + +// util.inspect names each object by its prototype's constructor. +function Plain() { + this.v = 1; +} +Object.setPrototypeOf(Plain.prototype, Shape.prototype); +console.log(new Square(1).size, fresh, new Plain(), Object.prototype.toString.call(fresh)); +console.log(util.inspect(new Plain()), util.inspect(parsed, { depth: 0 })); + +// A subclass keeps its own constructor below a base whose prototype other +// objects inherit: `new this.constructor()` in a base method builds the +// subclass, also while the subclass's prototype holds a replaceable method. +class Disc extends Shape { + area() { + return 3; + } + rebuild() { + return new this.constructor("copy of " + this.name); + } +} +const areaValue = Shape.prototype.area; +const disc = new Disc("d"); +const rebuilt = disc.rebuild(); +console.log(disc.constructor === Disc, rebuilt instanceof Disc, rebuilt.area(), rebuilt.name, typeof areaValue); diff --git a/tests/harness/corpus-costs/8260-class-prototype-views.mjs.json b/tests/harness/corpus-costs/8260-class-prototype-views.mjs.json new file mode 100644 index 000000000..f06add31f --- /dev/null +++ b/tests/harness/corpus-costs/8260-class-prototype-views.mjs.json @@ -0,0 +1 @@ +{ "plain": 8037, "san": 6172 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index c8849a10f..add31681d 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -2190,3 +2190,33 @@ for (const run of runs) { ); }); }); + +describe("class prototype relinks (JS lane)", () => { + test("relinking a class prototype reached through an instance throws", async () => { + // Compiled code binds a class's inherited methods, so its prototype's + // [[Prototype]] cannot change at run time. Node would print "other"; + // the native program refuses instead of answering A's method. + const r = await compileAndRun( + "class-prototype-relink", + `'use strict'; +class A { + a() { + return 'A.a'; + } +} +class B extends A {} +const prototype = Object.getPrototypeOf(new B()); +Object.setPrototypeOf(prototype, Object.getPrototypeOf(prototype)); +console.log('same prototype', new B().a()); +Object.setPrototypeOf(prototype, { a: () => 'other' }); +console.log(new B().a()); +`, + "cjs", + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe("same prototype A.a\n"); + expect(r.stderr).toBe( + "Uncaught Error: Object.setPrototypeOf with this value is not supported yet\n", + ); + }); +}); From 970a53c1ba17516b203fe445de2769392321ab2f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 00:05:48 -0500 Subject: [PATCH 040/273] Store (T | undefined)[] elements as T so widened arrays stay one array A number[], string[] or boolean[] seen as (T | undefined)[] through a cast, a binding or a parameter is the same array in Node: undefined written through either name is visible through both. The type mapper now stores (T | undefined)[] of a number, string or boolean T as T elements, which hold a present undefined by their slot state, so the wider name shares the storage instead of converting to a separate union array. Writers of these arrays store undefined as a present undefined: element writes, push, unshift and splice, literals, Array.from (with a length or a mapper), Array.of, generator drains, flatMap, rest packs, copies from Map and Set values and Object.values, and checked conversions from dynamic values. Promise.all over Promise[] fills the result inside the combinator's settlement, so it settles on Node's turn. The element-state analysis counts callbacks and iterables whose checker type admits undefined. Readers keep their optional reads: index reads through a union of arrays, searches with a direct needle, and asserted reads, which throw a TypeError for undefined. Comparators and callbacks that receive present elements take the element type, without an adapter. --- packages/compiler/src/backend/llvm/dyn.ts | 25 ++- .../src/backend/llvm/expr-stream-bridges.ts | 19 +- .../src/frontend/lowering/array-values.ts | 140 +++++++++++++++ .../frontend/lowering/builtins/promises.ts | 162 +++++++++++++++++- .../src/frontend/lowering/call-arguments.ts | 10 ++ .../frontend/lowering/coercions/structural.ts | 110 ++++++++++-- .../lowering/containers/array-callbacks.ts | 66 +++++-- .../lowering/containers/array-construction.ts | 67 +++++++- .../lowering/containers/array-methods.ts | 106 +++++++++++- .../lowering/containers/callback-arguments.ts | 13 +- .../lowering/containers/collection-methods.ts | 12 ++ .../lowering/containers/indexed-objects.ts | 106 +++++++++--- .../lowering/expressions/array-literals.ts | 23 ++- .../lowering/expressions/element-access.ts | 45 +++++ .../frontend/lowering/iterator-adapters.ts | 58 +++---- .../compiler/src/frontend/lowering/lowerer.ts | 58 ++++++- .../lowering/runtime-optional-elements.ts | 9 +- .../frontend/lowering/sanitize-class-types.ts | 2 + packages/compiler/src/frontend/type-mapper.ts | 19 +- packages/compiler/src/ir/ir.ts | 11 +- packages/compiler/src/ir/validate.ts | 10 +- ...ional-element-arrays-share-storage.ts.json | 6 + .../8001-optional-element-writers.ts.json | 6 + ...0-optional-element-arrays-share-storage.ts | 61 +++++++ tests/corpus/8001-optional-element-writers.ts | 103 +++++++++++ ...ional-element-arrays-share-storage.ts.json | 1 + .../8001-optional-element-writers.ts.json | 1 + 27 files changed, 1145 insertions(+), 104 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8000-optional-element-arrays-share-storage.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8001-optional-element-writers.ts.json create mode 100644 tests/corpus/8000-optional-element-arrays-share-storage.ts create mode 100644 tests/corpus/8001-optional-element-writers.ts create mode 100644 tests/harness/corpus-costs/8000-optional-element-arrays-share-storage.ts.json create mode 100644 tests/harness/corpus-costs/8001-optional-element-writers.ts.json diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 6d02daa8b..f3c8231e1 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -547,6 +547,13 @@ export class LlDyn { /* ── dyn node plumbing ─────────────────────────────────────────────── */ /** Loads a dyn node's kind tag (after the size_t rc word). */ + /** i1: the checked-dynamic value `d` is undefined. */ + isUndefined(B: BlockBuilder, d: string): string { + const is = B.tmp(); + B.line(`${is} = icmp eq i32 ${this.kindOf(B, d)}, ${DYN_KIND.UNDEF}`); + return is; + } + private kindOf(B: BlockBuilder, d: string): string { const p = B.tmp(); const k = B.tmp(); @@ -1655,7 +1662,8 @@ export class LlDyn { * BUILD the typed value (+1), or throw the catchable path-annotated * TypeError and return a dummy with the pending flag set. */ dynCheckHelper(t: IrType, preserveRefs = false): string { - const key = typeKey(t); + // A `(T | undefined)[]` stored as T elements also takes undefined ones. + const key = t.kind === "array" && t.optionalElements ? `${typeKey(t)}?` : typeKey(t); const builderKey = preserveRefs ? `identity:${key}` : key; const existing = this.dynBuilders.get(builderKey); if (existing) return existing; @@ -2435,6 +2443,21 @@ export class LlDyn { B.line(`${ip} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 2`); B.line(`store ${host.sizeType} ${i}, ptr ${ip}`); const e = this.itemAt(B, items, i); + if (t.optionalElements) { + // An undefined element is a present undefined element. + const undef = B.tmp(); + const storeUndefined = B.newLabel("dca.undef"); + const checkLabel = B.newLabel("dca.check"); + B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`); + B.condBr(undef, storeUndefined, checkLabel); + B.startBlock(storeUndefined); + host.declare(`declare void @scr_arr_set_undefined(ptr, double)`); + const at = B.tmp(); + B.line(`${at} = ${sizeToDouble(this.S)} ${this.S} ${i} to double`); + B.line(`call void @scr_arr_set_undefined(ptr ${a}, double ${at})`); + B.br(doneLabel); + B.startBlock(checkLabel); + } const v = B.tmp(); B.line(`${v} = call ${this.valTy(elem)} @${c}(ptr ${e}, ptr ${pathSlot})`); this.pendingBail( diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 46c2d884f..814ef76b5 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -744,13 +744,16 @@ export function streamTypedRefMaterializeAdapter( t: IrType, ): LlStreamTypedRefAdapter { const key = typeKey(t); - const existing = host.liveDynRefAdapters.get(key); + // A `(T | undefined)[]` stored as T elements shares T[]'s capsule type, + // but its adapters also store undefined elements. + const adapterKey = t.kind === "array" && t.optionalElements ? `${key}?` : key; + const existing = host.liveDynRefAdapters.get(adapterKey); if (existing) return existing; // The converter depends only on the static type. Intern the complete // graph across roots and union arms so recursive types emit once. const snapshot = `sc_ldr_${host.liveDynRefAdapters.size}_materialize`; const adapter: LlStreamTypedRefAdapter = { snapshot, commit: "null" }; - host.liveDynRefAdapters.set(key, adapter); + host.liveDynRefAdapters.set(adapterKey, adapter); host.journal?.allocated( snapshot.replace(/_materialize$/, ""), "streamTypedRefMaterializeAdapter", @@ -1257,6 +1260,18 @@ function liveArrayOps( G.terminate(`ret ptr ${boxed}`); const S = new BlockBuilder(); + if (t.optionalElements) { + // A `(T | undefined)[]` stores undefined as a present undefined. + const undef = host.dyn.isUndefined(S, "%v"); + const storeUndefined = S.newLabel("live.array.undefined"); + const other = S.newLabel("live.array.value"); + S.condBr(undef, storeUndefined, other); + S.startBlock(storeUndefined); + host.declare(`declare void @scr_arr_set_undefined(ptr, double)`); + S.line(`call void @scr_arr_set_undefined(ptr %p, double %i)`); + S.terminate(`ret i1 true`); + S.startBlock(other); + } const fits = S.tmp(); S.line(`${fits} = call zeroext i1 @${host.dyn.dynMatchHelper(elem)}(ptr %v)`); const store = S.newLabel("live.array.store"); diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts index f191ad942..6e4a45f86 100644 --- a/packages/compiler/src/frontend/lowering/array-values.ts +++ b/packages/compiler/src/frontend/lowering/array-values.ts @@ -269,6 +269,140 @@ function readsMutableLocal(lowerer: Lowerer, e: IrExpr): boolean { }); } +/** A number, string or boolean element type. Arrays of it hold a present + * undefined in the element's state, so `(T | undefined)[]` stores T + * elements (the type mapper) and a T[] seen as one stays the same array. */ +export function primitiveElement(elem: IrType): boolean { + return elem.kind === "f64" || elem.kind === "string" || elem.kind === "bool"; +} + +/** `value` is the element type with undefined, stored into an array of a + * primitive element type: a present undefined or the element. */ +export function optionalElementValue(lowerer: Lowerer, value: IrExpr, elem: IrType): boolean { + if (!primitiveElement(elem)) return false; + if (value.type.kind === "undefinedT") return true; + return ( + value.type.kind === "union" && + lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0 && + typeEquals(lowerer.stripUndefinedArm(value.type), elem) + ); +} + +/** A value of a primitive element type that holds a slot until the value + * stored there replaces it (insertedValues), or stands for a slot that is + * not read. */ +export function elementPlaceholder(elem: IrType, loc: SrcLoc): IrExpr { + if (elem.kind === "f64") return { kind: "numLit", value: 0, type: F64, loc }; + if (elem.kind === "bool") return { kind: "boolLit", value: false, type: BOOL, loc }; + return { kind: "strLit", value: "", type: STRING, loc }; +} + +/** Values inserted into an array (push, unshift, splice, a literal), in + * source order. A value that can be undefined (optionalElementValue) is + * kept in a temporary: the insertion takes a placeholder for it, and + * `fill` then stores the value at the placeholder's index. */ +export function insertedValues( + lowerer: Lowerer, + values: readonly IrExpr[], + elem: IrType, + coerce: (value: IrExpr, index: number) => IrExpr, + loc: SrcLoc, +): { + stmts: IrStmt[]; + args: IrExpr[]; + optional: boolean; + fill: (arr: IrExpr, first: IrExpr) => IrStmt[]; +} { + if (!values.some((value) => optionalElementValue(lowerer, value, elem))) + return { stmts: [], args: values.map(coerce), optional: false, fill: () => [] }; + const stmts: IrStmt[] = []; + const args: IrExpr[] = []; + const held: { offset: number; value: IrExpr }[] = []; + values.forEach((value, offset) => { + // Every value is evaluated in order before the insertion. + if (value.type.kind === "undefinedT") { + if (value.kind !== "unitLit") stmts.push({ kind: "exprStmt", expr: value, loc }); + args.push(elementPlaceholder(elem, loc)); + held.push({ offset, value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } }); + return; + } + const temp = lowerer.declareHiddenLocal("%inserted", value.type); + stmts.push({ kind: "varDecl", localId: temp.id, init: value, loc }); + const stable = varRef(temp.id, temp.type, loc); + if (optionalElementValue(lowerer, value, elem)) { + args.push(elementPlaceholder(elem, loc)); + held.push({ offset, value: stable }); + } else { + args.push(coerce(stable, offset)); + } + }); + const fill = (arr: IrExpr, first: IrExpr): IrStmt[] => + held.map(({ offset, value }) => + arrayValueStore( + lowerer, + arr, + offset === 0 + ? first + : { + kind: "bin", + op: "+", + left: first, + right: { kind: "numLit", value: offset, type: F64, loc }, + type: F64, + loc, + }, + value, + elem, + loc, + ), + ); + return { stmts, args, optional: true, fill }; +} + +/** Statements appending `value` to `arr` inside a lifted helper. A value + * that can be undefined in a primitive array (optionalElementValue) is held + * in the helper's local `slot` (which the caller declares with the value's + * type) and stored as a present undefined or the element; any other value + * converts to the element type. */ +export function appendElement( + lowerer: Lowerer, + arr: IrExpr, + value: IrExpr, + elem: IrType, + slot: string, + loc: SrcLoc, +): IrStmt[] { + if (optionalElementValue(lowerer, value, elem) && value.type.kind !== "undefinedT") { + const held = varRef(slot, value.type, loc); + const length: IrExpr = { + kind: "arrIntrinsic", + method: "length", + receiver: arr, + args: [], + type: F64, + loc, + }; + return [ + { kind: "varDecl", localId: slot, init: value, loc }, + arrayValueStore(lowerer, arr, length, held, elem, loc), + ]; + } + return [ + { + kind: "exprStmt", + expr: { + kind: "arrIntrinsic", + method: "push", + receiver: arr, + args: [lowerer.coerceToExpected(value, elem)], + type: F64, + loc, + }, + loc, + }, + ]; +} + /** Store a stabilized value without changing the array's element ABI. */ export function arrayValueStore( lowerer: Lowerer, @@ -279,6 +413,12 @@ export function arrayValueStore( loc: SrcLoc, ): IrStmt { if (typeEquals(value.type, elem)) return { kind: "arraySet", arr, index, value, loc }; + // An undefined value of a `(T | undefined)[]` (primitiveElement). + if (value.type.kind === "undefinedT" && primitiveElement(elem)) { + const write: IrStmt = { kind: "arraySetUndefined", arr, index, loc }; + if (value.kind === "unitLit") return write; + return { kind: "block", body: [{ kind: "exprStmt", expr: value, loc }, write], loc }; + } if (value.type.kind === "union" && lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0) { // The tag test and the VALUE arm extraction both consume the value. Keep // an arbitrary expression (especially an array read or call) single diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index e9d4322ad..c3b2cd479 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -3,7 +3,8 @@ import { checkedPromiseAll, thenReader } from "../checked-promise-all.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; -import { varRef } from "../../../ir/build.js"; +import { countedFor, varRef } from "../../../ir/build.js"; +import { arrayValueStore, optionalElementValue, primitiveElement } from "../array-values.js"; import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js"; import { isJsSourceFile, locOf } from "../../program.js"; import { @@ -14,6 +15,7 @@ import { type IrFunction, type IrStmt, type IrType, + type SrcLoc, UNDEFINED_T, VOID, arrayOf, @@ -435,6 +437,28 @@ export function lowerPromiseStaticCall( inner.kind === "void" ? { kind: "promise", inner: VOID } : lowerer.mapTypeOf(lowerer.typeOf(call)); + // Promise[] combines into a `(T | undefined)[]`, which + // stores T elements (primitiveElement): collect the values, then copy + // them in with their present undefined values. + if ( + resultT?.kind === "promise" && + resultT.inner.kind === "array" && + primitiveElement(resultT.inner.elem) && + optionalElementValue( + lowerer, + { kind: "unitLit", unit: "undefined", type: inner, loc }, + resultT.inner.elem, + ) + ) { + const joined = optionalPromiseAll( + lowerer, + entries, + inner, + resultT as IrType & { kind: "promise" }, + loc, + ); + if (joined) return joined; + } if ( resultT?.kind !== "promise" || (inner.kind !== "void" && @@ -954,3 +978,139 @@ export function lowerAwaitedUniformPromiseAll( loc, }; } + +/** `Promise.all(ps)` over `Promise[]` for a number, string or + * boolean T, whose result is a `(T | undefined)[]` (stored as T elements). + * The combinator joins the entries (promise.allTuple), and its builder + * reads each fulfilled entry into the result with its present undefined, + * inside the combinator's settlement, so the result settles on Node's + * turn. Entries of another type keep the general combinator. */ +function optionalPromiseAll( + lowerer: Lowerer, + entries: IrExpr, + inner: IrType, + resultT: IrType & { kind: "promise" }, + loc: SrcLoc, +): IrExpr | null { + const entriesT = entries.type; + if (entriesT.kind !== "array" || entriesT.elem.kind !== "promise") return null; + if (!typeEquals(entriesT.elem.inner, inner)) return null; + const outT = resultT.inner as IrType & { kind: "array" }; + const voidPromise: IrType & { kind: "promise" } = { kind: "promise", inner: VOID }; + const joinedT = arrayOf(voidPromise); + const key = `promise.allOptional:${typeKey(entriesT)}:${typeKey(outT)}`; + let helper = lowerer.arrHofHelpers.get(key); + if (!helper) { + helper = keyedHelperName(lowerer, "%promise.allOptional", key); + lowerer.arrHofHelpers.set(key, helper); + const builder = keyedHelperName(lowerer, "%promise.allOptionalValues", key); + const length = (arr: IrExpr): IrExpr => ({ + kind: "arrIntrinsic", + method: "length", + receiver: arr, + args: [], + type: F64, + loc, + }); + const joined = varRef("joined.0", joinedT, loc); + const out = varRef("out.0", outT, loc); + lowerer.liftedFns.push({ + name: builder, + params: [{ localId: "joined.0", name: "joined", type: joinedT }], + returnType: outT, + locals: [ + { id: "joined.0", name: "joined", type: joinedT, mutable: false }, + { id: "out.0", name: "out", type: outT, mutable: false }, + { id: "i.0", name: "i", type: F64, mutable: true }, + { id: "v.0", name: "v", type: inner, mutable: false }, + ], + body: [ + { + kind: "varDecl", + localId: "out.0", + init: { kind: "arrayLit", elems: [], type: outT, loc }, + loc, + }, + countedFor(loc, length(joined), (index) => [ + { + kind: "varDecl", + localId: "v.0", + init: { + kind: "libCall", + fn: "promise.fulfilledValue", + args: [{ kind: "arrayGet", arr: joined, index, type: voidPromise, loc }], + type: inner, + loc, + }, + loc, + }, + arrayValueStore(lowerer, out, index, varRef("v.0", inner, loc), outT.elem, loc), + ]), + { kind: "return", value: out, loc }, + ], + loc, + }); + const ps = varRef("ps.0", entriesT, loc); + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "ps.0", name: "ps", type: entriesT }], + returnType: resultT, + locals: [ + { id: "ps.0", name: "ps", type: entriesT, mutable: false }, + { id: "joined.0", name: "joined", type: joinedT, mutable: false }, + { id: "i.0", name: "i", type: F64, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "joined.0", + init: { kind: "arrayLit", elems: [], type: joinedT, loc }, + loc, + }, + countedFor(loc, length(ps), (index) => [ + { + kind: "exprStmt", + expr: { + kind: "arrIntrinsic", + method: "push", + receiver: joined, + args: [ + { + kind: "promiseVoidWiden", + value: { kind: "arrayGet", arr: ps, index, type: entriesT.elem, loc }, + type: voidPromise, + loc, + }, + ], + type: F64, + loc, + }, + loc, + }, + ]), + { + kind: "return", + value: { + kind: "libCall", + fn: "promise.allTuple", + args: [ + joined, + { + kind: "closure", + fnName: builder, + captures: [], + type: funcOf([joinedT], outT), + loc, + }, + ], + type: resultT, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: helper, args: [entries], type: resultT, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index 060ffe9d2..6da0cb337 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -28,6 +28,7 @@ import { checkedIterableSpread } from "./checked-iterable-spread.js"; import { type ParamShape } from "./call-signatures.js"; import { arrayValueRead, arrayValueType } from "./array-values.js"; import { buildArrayConversion } from "./coercions/builders.js"; +import { lowerArraySpreadItems } from "./containers/array-construction.js"; import { restTupleElementTypes, typeKey } from "../type-mapper.js"; function fixedTupleSpreadInfo( @@ -403,6 +404,15 @@ export function completeArgs( } } if (restType.kind !== "array") lowerer.unsupported("SC1090", blame, "this call form"); + // A `(T | undefined)[]` rest (stored as T elements) packs undefined + // arguments as present undefined elements. + const restNodes = sources.slice(restAt); + if (restType.optionalElements === true && !restNodes.some(isIr)) { + out.push( + lowerArraySpreadItems(lowerer, restNodes as ts.Expression[], restType.elem, restType, loc), + ); + return out; + } // The rest pack is a fresh array per call; surplus SPREADS copy // their elements in (JS-exact — `f(a, ...xs, b, ...ys)` packs in // order, sources untouched). diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 4321a5ead..93ae1c231 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -1,12 +1,13 @@ import { keyedHelperName } from "../stable-names.js"; import { constructedValue } from "../constructor-returns.js"; -import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; +import { countedFor, dynUndefinedExpr, varRef } from "../../../ir/build.js"; import type { FieldLift } from "./structural-plans.js"; import { buildArrayConversion } from "./builders.js"; +import { arrayValueRead, arrayValueStore, primitiveElement } from "../array-values.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, UNDEFINED_T } from "../../../ir/ir.js"; +import { BOOL, DYN, F64, STRING, UNDEFINED_T, typeEquals } from "../../../ir/ir.js"; import { typeKey } from "../../type-mapper.js"; import { findStaticOn, findGenericStaticOn } from "../lower-classes.js"; import { lowerRecordOvfCaptureHelper } from "../containers/indexed-objects.js"; @@ -54,6 +55,9 @@ export function widthCoerce(lowerer: Lowerer, expr: IrExpr, expected: IrType): I return lowerer.classStaticsProjection(expr.type.className, expected.shapeId, expr.loc); } if (expected.kind === "array" && expr.type.kind === "array") { + const optional = optionalArrayHelper(lowerer, expr.type, expected, expr.loc); + if (optional) + return { kind: "call", callee: optional, args: [expr], type: expected, loc: expr.loc }; const helper = lowerer.arrayWidthHelper(expr.type, expected, expr.loc); if (helper) return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc }; @@ -518,6 +522,69 @@ export function emptyArrayLiftHelper( return name; } +/** Copy an array of `T | undefined` unions (a collection's values, an + * iterator drain) into a `(T | undefined)[]` of a number, string or + * boolean T, which stores T elements: undefined values become present + * undefined elements. Null for other element types. */ +function optionalArrayHelper( + lowerer: Lowerer, + fromT: IrType & { kind: "array" }, + toT: IrType & { kind: "array" }, + loc: SrcLoc, +): string | null { + const from = fromT.elem; + const to = toT.elem; + if ( + !primitiveElement(to) || + from.kind !== "union" || + lowerer.armTag(from.unionId, UNDEFINED_T) < 0 || + !typeEquals(lowerer.stripUndefinedArm(from), to) + ) + return null; + const key = `arrOptional:${typeKey(from)}:${typeKey(to)}`; + const existing = lowerer.valueHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, "%arr.width", key); + lowerer.valueHelpers.set(key, name); + const input = varRef("a.0", fromT, loc); + const out = varRef("out.0", toT, loc); + lowerer.liftedFns.push({ + name, + params: [{ localId: "a.0", name: "a", type: fromT }], + returnType: toT, + locals: [ + { id: "a.0", name: "a", type: fromT, mutable: true }, + { id: "out.0", name: "out", type: toT, mutable: false }, + { id: "i.0", name: "i", type: F64, mutable: true }, + { id: "v.0", name: "v", type: from, mutable: false }, + ], + body: [ + { + kind: "varDecl", + localId: "out.0", + init: { kind: "arrayLit", elems: [], type: toT, loc }, + loc, + }, + countedFor( + loc, + { kind: "arrIntrinsic", method: "length", receiver: input, args: [], type: F64, loc }, + (index) => [ + { + kind: "varDecl", + localId: "v.0", + init: arrayValueRead(lowerer, input, index, from, loc), + loc, + }, + arrayValueStore(lowerer, out, index, varRef("v.0", from, loc), to, loc), + ], + ), + { kind: "return", value: out, loc }, + ], + loc, + }); + return name; +} + /** Copy an array through its validated element conversion. Intern before * building the body so recursive element conversions can reuse this helper. */ export function arrayWidthHelper( @@ -542,14 +609,37 @@ export function arrayWidthHelper( name, arrT, { kind: "arrayLit", elems: [], type: outT, loc }, - (element, _index, result) => ({ - kind: "arrIntrinsic", - method: "push", - receiver: result, - args: [lowerer.applyWidthLift(elemLift, element, toElem, loc)], - type: F64, - loc, - }), + (element, index, result) => { + let value = lowerer.applyWidthLift(elemLift, element, toElem, loc); + // A number, string or boolean array can hold present undefined + // elements (`(T | undefined)[]` stores T): they, and holes, copy + // as the target's undefined. + const missing = primitiveElement(fromElem) ? lowerer.wrappedUndefined(toElem, loc) : null; + if (missing !== null) + value = { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: { kind: "arrayState", arr: varRef("a.0", arrT, loc), index, type: F64, loc }, + right: { kind: "numLit", value: 1, type: F64, loc }, + type: BOOL, + loc, + }, + then: value, + else_: missing, + type: toElem, + loc, + }; + return { + kind: "arrIntrinsic", + method: "push", + receiver: result, + args: [value], + type: F64, + loc, + }; + }, loc, ), ); diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index edf65ab2b..1062c56b1 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -42,6 +42,7 @@ import { arrayValueStore, arrayValueType, currentArrayIndexPresent, + primitiveElement, } from "../array-values.js"; import { typeKey } from "../../type-mapper.js"; import { @@ -1689,6 +1690,8 @@ function flatMapUnionPlan( } const converts = (t: IrType): boolean => { if (typeEquals(t, outElem)) return true; + // A `(T | undefined)[]` result stores T elements (primitiveElement). + if (t.kind === "undefinedT" && primitiveElement(outElem)) return true; if (outElem.kind !== "union") return false; return lowerer.armTag(outElem.unionId, t) >= 0; }; @@ -1754,24 +1757,21 @@ function flatMapUnionHelper( }; if (plan.arms[tag] === "empty") return []; if (plan.arms[tag] === "value") { + // A unit arm's value is the unit itself, which has no narrowed + // representation. + const value: IrExpr = isUnitType(arm) + ? { kind: "unitLit", unit: arm.kind === "nullT" ? "null" : "undefined", type: arm, loc } + : narrowed; return [ arrayValueStore( lowerer, outRef(), outLength(), - 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, - ), + // An undefined value is a present undefined element of a + // primitive array (arrayValueStore). + arm.kind === "undefinedT" && primitiveElement(plan.outElem) + ? value + : lowerer.coerceToExpected(value, plan.outElem), plan.outElem, loc, ), @@ -2347,6 +2347,43 @@ function reduceHelper( return name; } +/** The comparator of a sort over a `(T | undefined)[]`, which stores T + * elements. The comparator only receives present elements (undefined + * elements sort last without a call), so the unannotated parameters of an + * inline comparator take the element type, and the runtime calls it + * without an adapter. */ +function lowerOptionalElementComparator( + lowerer: Lowerer, + argNode: ts.Expression, + comparator: ts.Expression, + elem: IrType, + arrT: IrType & { kind: "array" }, +): IrExpr { + if ( + arrT.optionalElements !== true || + !(ts.isArrowFunction(comparator) || ts.isFunctionExpression(comparator)) || + isJsSourceFile(ts.getSourceFileOfNode(comparator)) + ) + return lowerer.lowerExpr(argNode); + const rebound: { symbol: ts.Symbol; previous: IrType | undefined }[] = []; + for (const param of comparator.parameters.nodes.slice(0, 2)) { + if (param.type || param.initializer || param.dotDotDotToken || !ts.isIdentifier(param.name)) + continue; + const symbol = lowerer.checker.getSymbolAtLocation(param.name); + if (!symbol) continue; + rebound.push({ symbol, previous: lowerer.runtimeOptionalBindingTypes.get(symbol) }); + lowerer.runtimeOptionalBindingTypes.set(symbol, elem); + } + try { + return lowerer.lowerExpr(argNode); + } finally { + for (const { symbol, previous } of rebound) { + if (previous === undefined) lowerer.runtimeOptionalBindingTypes.delete(symbol); + else lowerer.runtimeOptionalBindingTypes.set(symbol, previous); + } + } +} + /** `a.sort(cmp)` / `a.toSorted(cmp)`, desugared to an interned synthetic * function like the other array HOFs. sort mutates and returns the receiver; * toSorted takes its shallow snapshot INSIDE the helper, after the receiver @@ -2435,7 +2472,8 @@ export function lowerArraySortCall( loc, }; } - let fnArg = undefinedArg ?? lowerer.lowerExpr(argNode); + let fnArg = + undefinedArg ?? lowerOptionalElementComparator(lowerer, argNode, unwrappedArg, elem, arrT); // Reusable JS comparators often accept checked values. Adapt the array's // element ABI just as other array callbacks do before sorting. if ( diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 1ca51f526..25a98c8a6 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -35,7 +35,13 @@ import { typeEquals, } from "../../../ir/ir.js"; import { isJsSourceFile, locOf } from "../../program.js"; -import { arrayValueRead, arrayValueStore, arrayValueType } from "../array-values.js"; +import { + arrayValueRead, + arrayValueStore, + arrayValueType, + optionalElementValue, + primitiveElement, +} from "../array-values.js"; import { typeKey } from "../../type-mapper.js"; import { newFnCtx } from "../lowerer.js"; import { iteratorCanStep, iteratorValue } from "../iterator-consumption.js"; @@ -88,7 +94,8 @@ export function lowerArrayValueItems( ]; for (const value of values) { const stored = - lowerer.runtimeOptionalWidening(value.type, elem) !== null + lowerer.runtimeOptionalWidening(value.type, elem) !== null || + optionalElementValue(lowerer, value, elem) ? value : lowerer.coerceToExpected(value, elem); body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc)); @@ -212,7 +219,8 @@ export function lowerArraySpreadItems( } else { const value = lowerer.lowerExpr(node); const stored = - lowerer.runtimeOptionalWidening(value.type, elem) !== null + lowerer.runtimeOptionalWidening(value.type, elem) !== null || + optionalElementValue(lowerer, value, elem) ? value : lowerer.coerceInto(node, value, elem); body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc)); @@ -518,6 +526,20 @@ export function lowerArrayOfCall( if (call.arguments.nodes.some(ts.isSpreadElement)) { return lowerArraySpreadItems(lowerer, call.arguments.nodes, result.elem, result, locOf(call)); } + // A `(T | undefined)[]` (primitiveElement) stores undefined arguments as + // present undefined elements. + if (result.optionalElements === true) { + const nodes = call.arguments.nodes; + const values = nodes.map((arg) => lowerer.lowerExpr(arg)); + if (values.some((value) => optionalElementValue(lowerer, value, result.elem))) + return lowerArrayValueItems(lowerer, values, result.elem, result, locOf(call)); + return { + kind: "arrayLit", + elems: values.map((value, i) => lowerer.coerceInto(nodes[i]!, value, result.elem)), + type: result, + loc: locOf(call), + }; + } return { kind: "arrayLit", elems: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, result.elem)), @@ -959,6 +981,31 @@ export function lowerArrayFromCall( const arrT = lowerer.mapTypeOf(lowerer.typeOf(call)); if (arrT?.kind !== "array") lowerer.badType(call, lowerer.typeOf(call)); const elem = arrT.elem; + // A `(T | undefined)[]` (primitiveElement): n present undefined + // elements. + const elemTs = lowerer.checker.isArrayType(lowerer.typeOf(call)) + ? lowerer.checker.getTypeArguments(lowerer.typeOf(call) as ts.TypeReference)[0] + : undefined; + if ( + primitiveElement(elem) && + elemTs !== undefined && + (elemTs.isUnionType() ? ts.constituentTypes(elemTs) : [elemTs]).some( + (part) => (part.flags & ts.TypeFlags.Undefined) !== 0, + ) + ) { + return { + kind: "arrIntrinsic", + method: "fillUndefined", + receiver: { kind: "arrayNewLen", length: n, type: arrT, loc }, + // To the end: fill clamps an infinite end to the length. + args: [ + numLit(0, loc), + { kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc }, + ], + type: arrT, + loc, + }; + } const absent = elem.kind === "union" ? lowerer.wrappedUndefined(elem, loc) !== null || @@ -1085,7 +1132,10 @@ export function lowerArrayFromCall( // A generator (or an iterator adapted into one) drains in order. if (src.type.kind === "generator" && args.length === 1) { const arrT = lowerer.mapTypeOf(lowerer.typeOf(call)); - const drained = arrT?.kind === "array" ? generatorDrain(lowerer, src, arrT.elem, loc) : null; + const drained = + arrT?.kind === "array" + ? generatorDrain(lowerer, src, arrT.elem, loc, arrT.optionalElements === true) + : null; if (drained) return drained; } lowerer.noLowering( @@ -1159,6 +1209,11 @@ function buildArrayFromArrayFn( type: fnRet, loc, }; + // A result of another type than the element (`T | undefined` into a + // `(T | undefined)[]`, which stores T elements) is held in a local, so + // the store tests and converts one value. + const held = !typeEquals(fnRet, outElem); + const value: IrExpr = held ? varRef("v.0", fnRet, loc) : mapped; return { name, params: [ @@ -1171,6 +1226,7 @@ function buildArrayFromArrayFn( { id: "f.0", name: "f", type: fnT, mutable: true }, { id: "out.0", name: "out", type: outT, mutable: false }, { id: "i.0", name: "i", type: F64, mutable: true }, + ...(held ? [{ id: "v.0", name: "v", type: fnRet, mutable: false }] : []), ], body: [ { @@ -1191,7 +1247,8 @@ function buildArrayFromArrayFn( loc, }, body: [ - arrayValueStore(lowerer, out, i, mapped, outElem, loc), + ...(held ? [{ kind: "varDecl", localId: "v.0", init: mapped, loc } as const] : []), + arrayValueStore(lowerer, out, i, value, outElem, loc), { kind: "assign", localId: "i.0", diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 4ef203edd..f406e57eb 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -32,6 +32,10 @@ import { arrayValueRead, arrayValueStore, arrayValueType, + elementPlaceholder, + insertedValues, + optionalElementValue, + primitiveElement, } from "../array-values.js"; import { typeKey } from "../../type-mapper.js"; import { @@ -125,6 +129,14 @@ function lowerOptionalArrayPush( return { kind: "seqExpr", stmts, result: length(), type: F64, loc }; } +/** The checker type of `node` admits undefined. */ +function checkerTypeAdmitsUndefined(lowerer: Lowerer, node: ts.Expression): boolean { + const type = lowerer.typeOf(node); + return (type.isUnionType() ? ts.constituentTypes(type) : [type]).some( + (part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0, + ); +} + /** `a.splice(start, count, v)` where v may hold undefined at run time. * Receiver, start, count, and v evaluate once, in order; a present v is * inserted as a plain element literal (which a statement-position splice @@ -658,7 +670,10 @@ export function lowerArrayMethodCall( const hasSpread = itemNodes.some(ts.isSpreadElement); const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg)); const statefulItems = itemProbes.some( - (probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null, + (probe) => + probe !== null && + (lowerer.runtimeOptionalWidening(probe.type, elem) !== null || + optionalElementValue(lowerer, probe, elem)), ); const items: IrExpr = hasSpread ? lowerArraySpreadItems(lowerer, itemNodes, elem, receiverIr, loc) @@ -875,6 +890,65 @@ export function lowerArrayMethodCall( loc, }; } + // A `(T | undefined)[]` (primitiveElement) takes undefined values as + // present undefined elements, stored after the insertion. + if ( + (name === "push" || name === "unshift") && + primitiveElement(elem) && + call.arguments.nodes.some((a) => checkerTypeAdmitsUndefined(lowerer, a)) + ) { + const nodes = call.arguments.nodes; + const values = nodes.map((a) => lowerer.lowerExpr(a)); + const inserted = insertedValues( + lowerer, + values, + elem, + (value, i) => lowerer.coerceInto(nodes[i]!, value, elem), + loc, + ); + if (inserted.optional) { + const array = lowerer.declareHiddenLocal("%insertInto", receiver.type); + const arr = varRef(array.id, array.type, loc); + const length = lowerer.declareHiddenLocal("%insertLength", F64); + const count: IrExpr = numLit(values.length, loc); + const first: IrExpr = + name === "push" + ? { + kind: "bin", + op: "-", + left: varRef(length.id, F64, loc), + right: count, + type: F64, + loc, + } + : numLit(0, loc); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: array.id, init: receiver, loc }, + ...inserted.stmts, + { + kind: "varDecl", + localId: length.id, + init: { + kind: "arrIntrinsic", + method: name, + receiver: arr, + args: inserted.args, + type: F64, + loc, + }, + loc, + }, + ...inserted.fill(arr, first), + ], + result: varRef(length.id, F64, loc), + type: F64, + loc, + }; + } + return { kind: "arrIntrinsic", method: name, receiver, args: inserted.args, type: F64, loc }; + } // Inserted values flow into the element slot like an assignment would: // union-element arrays wrap plain arm values (coerceInto is inert // when the types already agree). @@ -996,9 +1070,12 @@ export function lowerArrayMethodCall( const hasSpread = itemNodes.some(ts.isSpreadElement); const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg)); const statefulItems = itemProbes.some( - (probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null, + (probe) => + probe !== null && + (lowerer.runtimeOptionalWidening(probe.type, elem) !== null || + optionalElementValue(lowerer, probe, elem)), ); - if (statefulItems && itemNodes.length === 1) { + if (statefulItems && itemNodes.length === 1 && itemProbes[0]?.type.kind === "union") { return lowerOptionalSpliceItem( lowerer, receiver, @@ -1734,7 +1811,28 @@ function arraySearchHelper( type: valueT, loc, }; - const value = directNeedle ? rawRead : optionalValue; + // A direct needle matches only a present value (`match` tests the state + // first); a present undefined of a `(T | undefined)[]` (primitiveElement) + // must not be read as T. + const value = !directNeedle + ? optionalValue + : primitiveElement(elem) + ? { + kind: "ternary" as const, + cond: { + kind: "bin" as const, + op: "===" as const, + left: stateRef, + right: numLit(1, loc), + type: BOOL, + loc, + }, + then: rawRead, + else_: elementPlaceholder(elem, loc), + type: elem, + loc, + } + : rawRead; const readT = directNeedle ? elem : valueT; const equal = arraySearchEquality( lowerer, diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index d95640978..de027e08c 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -2,6 +2,7 @@ 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"; +import { primitiveElement } from "../array-values.js"; import { varRef } from "../../../ir/build.js"; import { typeKey } from "../../type-mapper.js"; import { @@ -144,12 +145,22 @@ export function lowerArrayCallback( const untyped = (checkerType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; const raw = untyped ? null : lowerer.mapTypeOf(checkerType); const mapped = raw ? lowerer.runtimeOptionalBindingType(param.name, raw) : null; + // A helper that passes present number, string or boolean elements + // (a `(T | undefined)[]` stores T elements) binds a `T | undefined` + // parameter to T, so the callback takes them without an adapter. + const presentElement = + mapped !== null && + primitiveElement(expected) && + mapped.kind === "union" && + lowerer.armTag(mapped.unionId, UNDEFINED_T) >= 0 && + typeEquals(lowerer.stripUndefinedArm(mapped), expected); if ( untyped ? !bindUntyped : mapped === null || (lowerer.coercibleValue(expected, mapped) && - !runtimeOptionalPairVariant(lowerer, expected, mapped)) + !runtimeOptionalPairVariant(lowerer, expected, mapped) && + !presentElement) ) return; const symbol = lowerer.checker.getSymbolAtLocation(param.name); diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index e043daf68..f18291a63 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -1,4 +1,5 @@ import { keyedHelperName } from "../stable-names.js"; +import { primitiveElement } from "../array-values.js"; import { nodeThrowExpr, boolLit, countedFor, numLit, strLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -1137,6 +1138,17 @@ function setFromSeedValue( seed = strCharsCall(lowerer, source, loc); } else if (source.type.kind === "array" && typeEquals(source.type.elem, setT.elem)) { seed = source; + } else if ( + source.type.kind === "array" && + primitiveElement(source.type.elem) && + setT.elem.kind === "union" && + lowerer.armTag(setT.elem.unionId, UNDEFINED_T) >= 0 && + typeEquals(lowerer.stripUndefinedArm(setT.elem), source.type.elem) + ) { + // A `(T | undefined)[]` stores T elements: its present undefined + // elements copy as the Set's undefined. + seed = lowerer.coerceToExpected(source, arrayOf(setT.elem)); + if (!typeEquals(seed.type, arrayOf(setT.elem))) return null; } else if (source.type.kind === "record") { const shape = lowerer.shapes.get(source.type.shapeId); if (!shape?.tuple || !shape.fields.every((field) => typeEquals(field.type, setT.elem))) diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index 668603d47..1c07885ae 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -1,6 +1,7 @@ import { keyedHelperName } from "../stable-names.js"; import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; +import { arrayValueStore, optionalElementValue, primitiveElement } from "../array-values.js"; import { objectEnumerationReceiver } from "../object-enumeration-receiver.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -156,7 +157,14 @@ export function objectIterOverIndexShape( // also holds numbers): each overflow value checks into the element type, // so a value of a dropped arm throws instead of mistyping. let ivNarrows = false; - if (valueT) { + // `T | undefined` values into a `(T | undefined)[]`, which stores T + // elements (primitiveElement): each value is stored with its present + // undefined. + const optionalValues = + member === "values" && + valueT !== null && + optionalElementValue(lowerer, { kind: "unitLit", unit: "undefined", type: iv, loc }, valueT); + if (valueT && !optionalValues) { const ivSurfaces = typeEquals(iv, valueT) || (valueT.kind === "union" && lowerer.armTag(valueT.unionId, iv) >= 0) || @@ -350,7 +358,25 @@ export function objectIterOverIndexShape( loc, }, then: [fieldStmt], - else_: undefinedElement ? [push(pushedOf(undefinedElement))] : null, + else_: undefinedElement + ? [push(pushedOf(undefinedElement))] + : member === "values" && valueT && primitiveElement(valueT) + ? [ + { + kind: "arraySetUndefined", + arr: outRef, + index: { + kind: "arrIntrinsic", + method: "length", + receiver: outRef, + args: [], + type: F64, + loc, + }, + loc, + }, + ] + : null, loc, }; } @@ -374,35 +400,45 @@ export function objectIterOverIndexShape( const kRef = varRef("k.0", STRING, loc); const readValue: IrExpr | null = !valueT ? null - : ivNarrows + : optionalValues ? { - kind: "dynCheck", - value: { - kind: "dynFrom", - value: { - kind: "recordKeyGet", - obj: rRef, - shapeId: argIr.shapeId, - key: kRef, - overflowOnly: true, - type: iv, - loc, - }, - type: DYN, - loc, - }, - type: valueT, - loc, - } - : { kind: "recordKeyGet", obj: rRef, shapeId: argIr.shapeId, key: kRef, overflowOnly: true, - type: valueT, + type: iv, loc, - }; + } + : ivNarrows + ? { + kind: "dynCheck", + value: { + kind: "dynFrom", + value: { + kind: "recordKeyGet", + obj: rRef, + shapeId: argIr.shapeId, + key: kRef, + overflowOnly: true, + type: iv, + loc, + }, + type: DYN, + loc, + }, + type: valueT, + loc, + } + : { + kind: "recordKeyGet", + obj: rRef, + shapeId: argIr.shapeId, + key: kRef, + overflowOnly: true, + type: valueT, + loc, + }; const loopPushed: IrExpr = member === "keys" ? kRef @@ -440,7 +476,26 @@ export function objectIterOverIndexShape( }, loc, }, - push(loopPushed), + ...(optionalValues + ? [ + { kind: "varDecl" as const, localId: "v.0", init: readValue!, loc }, + arrayValueStore( + lowerer, + outRef, + { + kind: "arrIntrinsic", + method: "length", + receiver: outRef, + args: [], + type: F64, + loc, + }, + varRef("v.0", iv, loc), + valueT!, + loc, + ), + ] + : [push(loopPushed)]), ], ), { kind: "return", value: outRef, loc }, @@ -457,6 +512,7 @@ export function objectIterOverIndexShape( { id: "ks.0", name: "ks", type: ksT, mutable: false }, { id: "i.0", name: "i", type: F64, mutable: true }, { id: "k.0", name: "k", type: STRING, mutable: false }, + ...(optionalValues ? [{ id: "v.0", name: "v", type: iv, mutable: false }] : []), ], body, loc, diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index c1b54a417..773f4cf0b 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -31,7 +31,7 @@ import { strCharsCall } from "../containers/array-construction.js"; import { generatorDrain } from "../iterator-adapters.js"; import { isSearchParamsIterable } from "../builtins/url.js"; import { varRef } from "../../../ir/build.js"; -import { arrayValueStore } from "../array-values.js"; +import { arrayValueStore, primitiveElement } from "../array-values.js"; import { classGeneratorIterable } from "../class-iterables.js"; /** JavaScript: whether `expr` is or holds an existing checked-dynamic @@ -102,6 +102,13 @@ function emptyNeverLiteral(lowerer: Lowerer, expr: ts.ArrayLiteralExpression): b * tsc's API doesn't surface it (ternary arms under an array context — * tsc accepts the arm covariantly, but a tagged element representation * must be BUILT as the slot's element type). */ +/** The checker type of `node` admits undefined. */ +function mayHoldUndefined(lowerer: Lowerer, node: ts.Expression): boolean { + const type = lowerer.typeOf(node); + const parts = type.isUnionType() ? ts.constituentTypes(type) : [type]; + return parts.some((part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0); +} + export function lowerArrayLiteral( lowerer: Lowerer, expr: ts.ArrayLiteralExpression, @@ -515,7 +522,13 @@ export function lowerArrayLiteral( // `[...it]` over a generator (or an iterator adapted into one): // drain it once, in order, into a fresh element array. if (src.type.kind === "generator") { - const drained = generatorDrain(lowerer, src, type.elem, locOf(el)); + const drained = generatorDrain( + lowerer, + src, + type.elem, + locOf(el), + type.optionalElements === true, + ); if (drained) src = drained; } // `[...sp]` and `[...sp.entries()]` over a URLSearchParams: its @@ -606,6 +619,9 @@ export function lowerArrayLiteral( let lowered: IrExpr; const directOptionalCandidate = runtimeOptionalElements.has(i) || + // An element that can be undefined in a `(T | undefined)[]` + // (primitiveElement) is stored as a present undefined. + (primitiveElement(type.elem) && mayHoldUndefined(lowerer, el)) || ts.isElementAccessExpression(el) || (ts.isCallExpression(el) && ts.isPropertyAccessExpression(el.expression) && @@ -683,7 +699,8 @@ export function lowerArrayLiteral( continue; } let value = el; - if (!typeEquals(value.type, type.elem)) { + // An undefined element is stored directly (arrayValueStore). + if (!typeEquals(value.type, type.elem) && value.type.kind !== "undefinedT") { const temp = lowerer.declareHiddenLocal("%arrayValue", value.type); body.push({ kind: "varDecl", localId: temp.id, init: value, loc }); value = varRef(temp.id, temp.type, loc); diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 9ac6a04b6..70909ca7b 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -61,6 +61,8 @@ import { arrayValueType, lowerSafeBytesRead, lowerSafeIndexRead, + primitiveElement, + unionArrayValueRead, } from "../array-values.js"; import { lowerOptionalStringIndex, lowerUnionStringIndex } from "../string-index.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -594,6 +596,8 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } if (key.type.kind === "f64") { + const optional = optionalArmIndexRead(lowerer, value, key, locOf(expr)); + if (optional) return lowerer.maybeNarrow(optional, expr); const safe = lowerOptionalArrayIndexRead(lowerer, expr, value, key); if (safe) return safe; } @@ -884,9 +888,14 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre // must be the element type (library ABI checks and the catchable // RangeError contract for an out-of-bounds assert). Where undefined is // representable, the assertion keeps JavaScript's unchecked read. + // A declared `(T | undefined)[]` element (stored as T) keeps its + // optional read: the assertion then narrows it with a catchable + // TypeError, as for a union element. + const declaredOptional = arr.type.optionalElements === true; const provenPresent = ts.isNonNullExpression(expr.parent) && expr.parent.expression === expr && + !declaredOptional && !assertedElementKeepsUndefined(lowerer, expr.parent); if (provenPresent) { return lowerer.maybeNarrow( @@ -2437,3 +2446,39 @@ export function lowerEnvironmentKey(lowerer: Lowerer, node: ts.Expression): IrEx return { kind: "libCall", fn: "dyn.toStringCoerce", args: [key], type: STRING, loc: key.loc }; return lowerRecordPropertyKey(lowerer, key, node); } + +/** A number-keyed read of a union of arrays with a `(T | undefined)[]` arm + * (stored as T elements, primitiveElement) whose element facts admit + * missing values: each arm reads its element by its state + * (unionArrayValueRead), where the keyed read would trap on a present + * undefined. The receiver and key evaluate once. Null for other unions. */ +function optionalArmIndexRead( + lowerer: Lowerer, + value: IrExpr, + key: IrExpr, + loc: SrcLoc, +): IrExpr | null { + if (value.type.kind !== "union") return null; + const arms = lowerer.unions.get(value.type.unionId)?.arms ?? []; + if ( + !arms.some( + (arm) => + arm.kind === "array" && + primitiveElement(arm.elem) && + arrayValueType(lowerer, arm.elem).kind === "union", + ) + ) + return null; + if (unionArrayValueRead(lowerer, value, key, loc) === null) return null; + const stmts: IrStmt[] = []; + const stable = (e: IrExpr, name: string): IrExpr => { + if (e.kind === "varRef" || e.kind === "numLit") return e; + const local = lowerer.declareHiddenLocal(name, e.type); + stmts.push({ kind: "varDecl", localId: local.id, init: e, loc }); + return varRef(local.id, e.type, loc); + }; + const receiver = stable(value, "%indexReceiver"); + const index = stable(key, "%indexKey"); + const read = unionArrayValueRead(lowerer, receiver, index, loc)!; + return stmts.length === 0 ? read : { kind: "seqExpr", stmts, result: read, type: read.type, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index 9f2b1e31f..4909e7f45 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -1,3 +1,4 @@ +import { appendElement, optionalElementValue, primitiveElement } from "./array-values.js"; import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { @@ -964,16 +965,27 @@ function generatorRetypeAdapter( return { kind: "call", callee: name, args: [expr], type: expected, loc }; } +/** A checked-dynamic yielded value for a `(T | undefined)[]` of a + * number, string or boolean T: converted to `T | undefined`, which the + * drain stores as a T element or a present undefined. */ +function optionalYield(lowerer: Lowerer, value: IrExpr, elem: IrType, optional: boolean): IrExpr { + if (!optional || value.type.kind !== "dyn" || !primitiveElement(elem)) return value; + return lowerer.coerceToExpected(value, lowerer.withUndefinedArm(elem)); +} + /** Drain a synchronous generator into a fresh `elem[]` — the eager * consumers of an iterator (`[...it]`, `Array.from(it)`). Each step resumes * the generator once and appends its value; completion stops the drain * without appending the return value, as in JS. Null when the yielded - * values cannot be extracted into the element type. */ + * values cannot be extracted into the element type. `optional`: the + * result is a `(T | undefined)[]`, which stores T elements, so a + * checked-dynamic value may also be undefined. */ export function generatorDrain( lowerer: Lowerer, source: IrExpr, elem: IrType, loc: SrcLoc, + optional = false, ): IrExpr | null { const gen = source.type; if (gen.kind !== "generator" || gen.async || gen.yieldT.kind === "void") return null; @@ -982,14 +994,16 @@ export function generatorDrain( const valueT = shape?.fields.find((field) => field.name === "value")?.type; if (!resultType || !valueT) return null; const arrT: IrType = { kind: "array", elem }; - const key = `genDrain:${typeKey(gen)}:${typeKey(elem)}`; + const key = `genDrain:${typeKey(gen)}:${typeKey(elem)}${optional ? "?" : ""}`; let name = lowerer.valueHelpers.get(key); if (!name && stepDrivable(gen)) { // The record-free drive: test done, then take the yielded value. const genRef = varRef("gen.0", gen, loc) as IrExpr & { type: typeof gen }; const outRef = varRef("out.0", arrT, loc); - const value = lowerer.coerceToExpected(genValue(genRef, loc), elem); - if (!typeEquals(value.type, elem)) return null; + const yielded = optionalYield(lowerer, genValue(genRef, loc), elem, optional); + const present = optionalElementValue(lowerer, yielded, elem); + const value = present ? yielded : lowerer.coerceToExpected(yielded, elem); + if (!present && !typeEquals(value.type, elem)) return null; name = keyedHelperName(lowerer, "%gen.drain", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ @@ -999,6 +1013,7 @@ export function generatorDrain( locals: [ { id: "gen.0", name: "gen", type: gen, mutable: false }, { id: "out.0", name: "out", type: arrT, mutable: false }, + ...(present ? [{ id: "v.0", name: "v", type: value.type, mutable: false }] : []), ], body: [ { @@ -1010,20 +1025,7 @@ export function generatorDrain( { kind: "while", cond: { kind: "unary", op: "!", operand: genStep(genRef, loc), type: BOOL, loc }, - body: [ - { - kind: "exprStmt", - expr: { - kind: "arrIntrinsic", - method: "push", - receiver: outRef, - args: [value], - type: F64, - loc, - }, - loc, - }, - ], + body: appendElement(lowerer, outRef, value, elem, "v.0", loc), loc, }, { kind: "return", value: outRef, loc }, @@ -1050,8 +1052,10 @@ export function generatorDrain( loc, ); if (!raw) return null; - const value = lowerer.coerceToExpected(raw, elem); - if (!typeEquals(value.type, elem)) return null; + const yielded = optionalYield(lowerer, raw, elem, optional); + const present = optionalElementValue(lowerer, yielded, elem); + const value = present ? yielded : lowerer.coerceToExpected(yielded, elem); + if (!present && !typeEquals(value.type, elem)) return null; name = keyedHelperName(lowerer, "%gen.drain", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ @@ -1062,6 +1066,7 @@ export function generatorDrain( { id: "gen.0", name: "gen", type: gen, mutable: false }, { id: "out.0", name: "out", type: arrT, mutable: false }, { id: "step.0", name: "step", type: resultType, mutable: false }, + ...(present ? [{ id: "v.0", name: "v", type: value.type, mutable: false }] : []), ], body: [ { @@ -1101,18 +1106,7 @@ export function generatorDrain( else_: null, loc, }, - { - kind: "exprStmt", - expr: { - kind: "arrIntrinsic", - method: "push", - receiver: outRef, - args: [value], - type: F64, - loc, - }, - loc, - }, + ...appendElement(lowerer, outRef, value, elem, "v.0", loc), ], loc, }, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index af89c61a4..2ddf02dfc 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -6515,6 +6515,17 @@ export class Lowerer { mayBeUndefined: (node) => { if (explicitlyNonNull(node)) return assertedReadMayBeUndefined(node); if (mayBeOptional(node)) return true; + // A value the checker types as possibly undefined: `(T | undefined)[]` + // stores T elements (the type mapper), so its undefined values are + // facts of T's arrays. + const checked = typeOrNull(node); + if ( + checked !== null && + (checked.isUnionType() ? ts.constituentTypes(checked) : [checked]).some( + (part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0, + ) + ) + return true; // `undefined as unknown as T` keeps the undefined value. for ( let inner: ts.Expression = node; @@ -6533,7 +6544,37 @@ export class Lowerer { const symbol = symbolOf(e); return symbol !== null && iterationSymbols.has(symbol) && !provenPresent(e, symbol); }, - callbackReturnsUndefined: (node) => callbackReturnsOptional(node), + iterationMayYieldUndefined: (node) => { + const type = typeOrNull(node); + if (type === null || this.checker.isArrayType(type) || this.checker.isTupleType(type)) + return false; + // The first type argument of Set, Map and Set iterators, + // generators and iterables is the value they yield. + const element = + (type.flags & ts.TypeFlags.Object) !== 0 + ? panicSafe(() => this.checker.getTypeArguments(type as ts.TypeReference)[0], undefined) + : undefined; + if (element === undefined) return false; + return (element.isUnionType() ? ts.constituentTypes(element) : [element]).some( + (part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0, + ); + }, + callbackReturnsUndefined: (node) => { + if (callbackReturnsOptional(node)) return true; + // A callback the checker types as possibly returning undefined + // (`(m) => map.get(m)`): its results are present undefined + // elements of a `(T | undefined)[]`, which stores T elements. + const type = typeOrNull(node); + return ( + type !== null && + this.checker.getCallSignatures(type).some((signature) => { + const result = this.checker.getReturnTypeOfSignature(signature); + return (result.isUnionType() ? ts.constituentTypes(result) : [result]).some( + (part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0, + ); + }) + ); + }, isGlobal: (node, name) => this.isStdlibGlobal(node, name), isString: (node) => { const type = typeOrNull(node); @@ -6594,6 +6635,7 @@ export class Lowerer { const cached = scanSites.get(sf); if (cached !== undefined) return cached; const nodes: ts.Node[] = []; + const values: ts.Node[] = []; ts.walkPreorder(sf, (node) => { if ( ts.isVariableDeclaration(node) || @@ -6609,7 +6651,21 @@ export class Lowerer { ts.isPostfixUnaryExpression(node) ) nodes.push(node); + // The values these sites store, whose checker types mayBeUndefined + // reads: batched here, since dead bodies are scanned too. + if (ts.isArrayLiteralExpression(node)) + for (const element of node.elements.nodes) + values.push(ts.isSpreadElement(element) ? element.expression : element); + else if (ts.isCallExpression(node) || ts.isNewExpression(node)) + for (const arg of node.arguments?.nodes ?? []) + values.push(ts.isSpreadElement(arg) ? arg.expression : arg); + else if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) + values.push(node.right); }); + this.checker.prefetchCollectionTypes(values); scanSites.set(sf, nodes); return nodes; }; diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts index 96d20d2c4..fffe79a51 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts @@ -16,6 +16,9 @@ export interface ElementStateHost { elementAdmitsUndefined(key: string): boolean; /** The value stored by `node` can be `undefined` at runtime. */ mayBeUndefined(node: ts.Expression): boolean; + /** Iterating `node`, an iterable other than an array (a Set, a Map's + * values, a generator), can yield `undefined`. */ + iterationMayYieldUndefined?(node: ts.Expression): boolean; /** A callback argument can return `undefined` at runtime. */ callbackReturnsUndefined(node: ts.Expression): boolean; /** The value may be a plain array-like object, whose missing indices @@ -353,7 +356,11 @@ export class ArrayElementStates { /** One stored value, or the elements a spread copies. */ private valueMayBeUndefined(value: ts.Expression): boolean { - if (ts.isSpreadElement(value)) return this.mayYieldUndefined(value.expression); + if (ts.isSpreadElement(value)) + return ( + this.mayYieldUndefined(value.expression) || + (this.host.iterationMayYieldUndefined?.(value.expression) ?? false) + ); return this.host.mayBeUndefined(value); } diff --git a/packages/compiler/src/frontend/lowering/sanitize-class-types.ts b/packages/compiler/src/frontend/lowering/sanitize-class-types.ts index 77478d76d..6b89b446a 100644 --- a/packages/compiler/src/frontend/lowering/sanitize-class-types.ts +++ b/packages/compiler/src/frontend/lowering/sanitize-class-types.ts @@ -16,6 +16,8 @@ export function sanitizeUnregisteredClassTypes( case "object": return hasClass(type.className) ? type : F64; case "array": + type.elem = rewrite(type.elem); + break; case "set": type.elem = rewrite(type.elem); break; diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 161f73d27..16b77b7f5 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -2341,6 +2341,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { elem = unitOnlyUnion(unions); if (!elem) return null; + // `(T | undefined)[]` of a number, string or boolean T stores T + // elements: an array slot holds a present undefined by its own state, + // so a T[] seen as `(T | undefined)[]` stays the same array. + let optionalElements = false; + if (elem.kind === "union") { + const arms = unions.get(elem.unionId)?.arms; + const present = + arms?.length === 2 ? arms.find((arm) => arm.kind !== "undefinedT") : undefined; + if ( + present !== undefined && + arms!.some((arm) => arm.kind === "undefinedT") && + (present.kind === "f64" || present.kind === "string" || present.kind === "bool") + ) { + elem = present; + optionalElements = true; + } + } // A dyn ELEMENT makes the WHOLE array the checked-dynamic value: // `unknown[]`, `object[]`, and the collapsed `(string | object)[]` // (the plugins-slot shape) — the checked-dynamic tree has real arrays, so length/ @@ -2362,7 +2379,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // drain of the auxiliary-server registries): handles are ordinary // refcounted REF elements (their `_v` adapters, no trace — both drop // their closures at their terminal event). - return arrayOf(elem); + return optionalElements ? { kind: "array", elem, optionalElements: true } : arrayOf(elem); } // Tuples: `[string, string]` maps to an interned RECORD shape flagged // `tuple`, with one field per position named by its index ("0", "1", ...) diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 8fb636608..58edd52d3 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -87,7 +87,16 @@ export type IrType = | { kind: "date" } | { kind: "string" } // heap, refcounted, UTF-8 | { kind: "bool" } - | { kind: "array"; elem: IrType } // heap, refcounted, monomorphic elements + | { + kind: "array"; + elem: IrType; + /** A `(T | undefined)[]` of a number, string or boolean T, stored as + * T elements (which hold a present undefined by their state). Not + * part of the type's identity (typeEquals, typeKey): it only lets a + * checked conversion from a dynamic value accept undefined + * elements. */ + optionalElements?: true; + } // heap, refcounted, monomorphic elements /** ES `Map` — heap, refcounted, insertion-ordered hash map with ONE * runtime representation (ScrMap) and type-directed key/value handling, * exactly the array pattern (never per-instantiation structs). Keys use diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index a694e8d2c..b9996fc6c 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -2050,12 +2050,18 @@ function validateFunction( } case "arrayNewLen": { // Mapper-less Array.from({length: n}): absent-slot fill exists - // only for refcounted element kinds (frontend fences scalars). + // only for refcounted element kinds. Number and boolean slots are + // filled with present undefined values right after + // (`(T | undefined)[]`); the frontend fences other scalar uses. if (e.type.kind !== "array") { err(`arrayNewLen must be array-typed, got ${e.type.kind}`, e.loc); break; } - if (!isRefCounted(e.type.elem)) { + if ( + !isRefCounted(e.type.elem) && + e.type.elem.kind !== "f64" && + e.type.elem.kind !== "bool" + ) { err( `arrayNewLen with non-refcounted ${e.type.elem.kind} elements (no absent value)`, e.loc, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8000-optional-element-arrays-share-storage.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8000-optional-element-arrays-share-storage.ts.json new file mode 100644 index 000000000..8cdd32baf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8000-optional-element-arrays-share-storage.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8000-optional-element-arrays-share-storage.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8001-optional-element-writers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8001-optional-element-writers.ts.json new file mode 100644 index 000000000..fe45249bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8001-optional-element-writers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8001-optional-element-writers.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8000-optional-element-arrays-share-storage.ts b/tests/corpus/8000-optional-element-arrays-share-storage.ts new file mode 100644 index 000000000..fa31fcad2 --- /dev/null +++ b/tests/corpus/8000-optional-element-arrays-share-storage.ts @@ -0,0 +1,61 @@ +// A number[] or string[] seen as `(T | undefined)[]` is the same array, as +// in Node: undefined written through either name is visible through both, +// whether the wider view is a cast, a binding or a parameter. + +const c: number[] = [1.5, 2]; +(c as (number | undefined)[])[1] = undefined; +console.log(c); +const e: number[] = [1.5, 2]; +const f = e as (number | undefined)[]; +f[1] = undefined; +console.log(e, f, e === f); + +function clearFirst(values: (number | undefined)[]): void { + values[0] = undefined; + values.push(undefined, 4); +} +const a: number[] = [1, 2, 3]; +clearFirst(a); +console.log(a, a.length, a.indexOf(4), a.includes(undefined as unknown as number)); + +const names: string[] = ["x"]; +const wider: (string | undefined)[] = names; +wider.push(undefined); +wider.unshift(undefined); +console.log(names, wider === names, names.length, JSON.stringify(names)); + +// The wider arrays themselves: literals, pushes, splices and copies keep +// their undefined elements. +const lit: (number | undefined)[] = [1, undefined, 3]; +const maybe = (i: number): number | undefined => (i % 2 ? i : undefined); +const built: (number | undefined)[] = []; +for (let i = 0; i < 4; i++) built.push(maybe(i)); +built.splice(1, 0, undefined); +console.log(lit, built, lit.indexOf(undefined), built.filter((x): boolean => x === undefined).length); +console.log(built.map((x) => x ?? -1), built.find((x) => x !== undefined), [...built].reverse()); +let sum = 0; +for (const x of built) sum += x ?? 0; +console.log(sum, built.join("-"), JSON.stringify(built)); + +const flags: (boolean | undefined)[] = [true, undefined, false]; +const strs: (string | undefined)[] = ["a", undefined]; +console.log(flags, flags.filter(Boolean).length, strs.map((s) => s?.toUpperCase())); + +// Values from other containers keep their undefined. +const m = new Map([["a", undefined], ["b", 2]]); +const fromMap = [...m.values()]; +const fromSet = Array.from(new Set(["s", undefined])); +const sized = Array.from({ length: 2 }); +console.log(fromMap, fromSet, sized, sized.length); +const dict: { [key: string]: number | undefined } = { a: 1, b: undefined }; +console.log(Object.values(dict)); + +// An asserted element that is undefined throws a catchable TypeError. +function lengthAt(items: (string | undefined)[], i: number): number { + try { + return items[i]!.length; + } catch { + return -1; + } +} +console.log(lengthAt(strs, 0), lengthAt(strs, 1)); diff --git a/tests/corpus/8001-optional-element-writers.ts b/tests/corpus/8001-optional-element-writers.ts new file mode 100644 index 000000000..5dff693c3 --- /dev/null +++ b/tests/corpus/8001-optional-element-writers.ts @@ -0,0 +1,103 @@ +// Every way a value reaches a `(T | undefined)[]` of numbers, strings or +// booleans keeps its undefined: callbacks, generators, Array.of, rest +// packs, destructuring, class fields and copies into Sets and Maps. + +const slots = new Map([["a", 1]]); +const looked = ["x", "a", "y"].map((key) => slots.get(key)); +console.log(looked[0] === undefined, looked, looked.length); +const fromMapped = Array.from(["a", "b"], (key) => slots.get(key)); +console.log(fromMapped[1] === undefined, fromMapped); +const objects: { n?: number }[] = [{ n: 1 }, {}]; +const fields = objects.map((o) => o.n); +function pick(o: { n?: number }): number | undefined { + return o.n; +} +console.log(fields[1] === undefined, objects.map(pick)[1] === undefined, fields); +function wrap(value: T): T[] { + return [value]; +} +console.log(wrap(undefined)[0] === undefined); + +function* maybe(): Generator { + yield undefined; + yield 2; +} +const drained = [...maybe()]; +console.log(drained[0] === undefined, drained, Array.from(maybe())); +const yielding = function* (): Generator { + yield "s"; + yield undefined; +}; +console.log([...yielding()]); +console.log(Array.of(undefined, 1), Array.of(true)); + +function sum(...values: (number | undefined)[]): number { + return values.reduce((total, v) => total + (v ?? 0), 0); +} +const spread: (number | undefined)[] = [undefined, 4]; +console.log(sum(1, undefined, 2), sum(...spread), sum(...spread, undefined, 1)); + +const [first, second = 7, third] = [3, undefined, 1] as (number | undefined)[]; +console.log(first, second, third); +const strings: (string | undefined)[] = ["a", undefined, "a"]; +const seen = new Set(strings); +console.log(seen.size, seen.has(undefined), [...seen]); +for (const [i, s] of strings.entries()) console.log(i, s); +for (const key in strings) console.log("key", key); + +class Holder { + values: (number | undefined)[] = []; + add(value: number | undefined): void { + this.values.push(value); + } + first(): number | undefined { + return this.values[0]; + } +} +const holder = new Holder(); +holder.add(undefined); +holder.add(2); +console.log(holder.first(), holder.values, holder.values.length); + +const xs: (number | undefined)[] = [3, undefined, 1]; +xs.forEach((x, i) => console.log(i, x, typeof x)); +console.log(xs.findIndex((x) => x === undefined), xs.findLast((x) => x !== undefined)); +console.log(xs.some((x) => x === undefined), xs.every((x) => x !== undefined)); +console.log(xs.reduce((acc, x) => acc + (x ?? 10), 0), xs.join("/"), xs.at(1), xs.at(-1)); +console.log(xs.toSorted((a, b) => (a ?? 0) - (b ?? 0)), [...xs].sort()); +xs[1] ??= 5; +console.log(xs); +const flags: (boolean | undefined)[] = [undefined, true]; +console.log(flags.map((b) => !b), flags.indexOf(undefined), flags.includes(true), String(flags)); +const nested: (number | undefined)[][] = [[undefined], [1]]; +console.log(nested.flat(), nested[0]![0]); +const byKey = new Map([["k", [undefined, 1]]]); +console.log(byKey.get("k")![0], byKey); + +// Promise.all over `Promise[]` settles on Node's turn, with +// its undefined values. +const lookup = async (key: string): Promise => { + if (key === "x") throw new Error("missing " + key); + return slots.get(key); +}; +const turns: string[] = []; +const pending: Promise[] = [lookup("a"), lookup("b")]; +void Promise.all(pending).then((vs) => turns.push("all " + vs.join(",") + " " + (vs[1] === undefined))); +const failing: Promise[] = [lookup("a"), lookup("x")]; +Promise.all(failing).then( + () => turns.push("unexpected"), + (error: unknown) => turns.push("rejected " + (error as Error).message), +); +void Promise.all([] as Promise[]).then((vs) => turns.push("empty " + vs.length)); +void Promise.resolve() + .then(() => turns.push("t1")) + .then(() => turns.push("t2")) + .then(() => turns.push("t3")) + .then(() => console.log(turns.join(" "))); + +// Index reads through a union of arrays, and searches with a number +// needle, see the present undefined elements. +const pairs: ((null | undefined)[] | (number | undefined)[])[] = [[null, undefined], [undefined, 0]]; +for (const pair of pairs) console.log(pair[0], pair[1], pair[2]); +const sparse: (number | undefined)[] = [1, , undefined, 1]; +console.log(sparse.lastIndexOf(undefined), sparse.lastIndexOf(1, 2), sparse.indexOf(1, 1)); diff --git a/tests/harness/corpus-costs/8000-optional-element-arrays-share-storage.ts.json b/tests/harness/corpus-costs/8000-optional-element-arrays-share-storage.ts.json new file mode 100644 index 000000000..b3db55643 --- /dev/null +++ b/tests/harness/corpus-costs/8000-optional-element-arrays-share-storage.ts.json @@ -0,0 +1 @@ +{ "plain": 6206, "san": 8487 } diff --git a/tests/harness/corpus-costs/8001-optional-element-writers.ts.json b/tests/harness/corpus-costs/8001-optional-element-writers.ts.json new file mode 100644 index 000000000..31695bc47 --- /dev/null +++ b/tests/harness/corpus-costs/8001-optional-element-writers.ts.json @@ -0,0 +1 @@ +{ "plain": 3690, "san": 4000 } From bfb360a65952bfec75dc2d9d18f61e60794b2d9f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 00:30:36 -0500 Subject: [PATCH 041/273] Add a runner for Node's test/parallel files tests/node-parallel/run.mjs builds files from a Node.js checkout's test/parallel with scriptc, runs each executable in that directory, and compares its exit status with Node's, writing a pass, fail or refused verdict and the first error line per file (results.json, results.md). --common minimal (the default) runs the files against a minimal test/common (common/index.cjs, common/wpt.cjs) whose helpers keep Node's semantics, so a verdict measures the module under test; --common node uses Node's own test/common. --- tests/node-parallel/README.md | 23 +++++ tests/node-parallel/common/index.cjs | 131 ++++++++++++++++++++++++++ tests/node-parallel/common/wpt.cjs | 32 +++++++ tests/node-parallel/run.mjs | 134 +++++++++++++++++++++++++++ 4 files changed, 320 insertions(+) create mode 100644 tests/node-parallel/README.md create mode 100644 tests/node-parallel/common/index.cjs create mode 100644 tests/node-parallel/common/wpt.cjs create mode 100644 tests/node-parallel/run.mjs diff --git a/tests/node-parallel/README.md b/tests/node-parallel/README.md new file mode 100644 index 000000000..35af64464 --- /dev/null +++ b/tests/node-parallel/README.md @@ -0,0 +1,23 @@ +# Node test/parallel files + +`run.mjs` builds files from a Node.js checkout's `test/parallel` with scriptc, runs each executable in that directory, and compares its exit status with Node's. It records a verdict per file: `pass` (same exit status), `fail` (different exit status), or `refused` (the build failed), with the first error line. + +```bash +git clone --depth 1 --branch v24.22.0 https://github.com/nodejs/node /path/to/node +node tests/node-parallel/run.mjs --node /path/to/node --pattern '^test-(url-|whatwg-url-|querystring)' +``` + +Options: + +- `--node`: a Node.js checkout, or its `test` directory. Use the version in `.node-version`. +- `--pattern`: a regular expression over file names in `test/parallel`. +- `--common`: `minimal` (the default) or `node`. + - `minimal` copies the selected files into `/tree/test/parallel` and uses `common/index.cjs` and `common/wpt.cjs` from this directory as `test/common/index.js` and `test/common/wpt.js`, with the checkout's `test/fixtures` and `test/common/fixtures.js`. The minimal common has the helpers these files use (`mustCall`, `mustNotCall`, `skip`, `expectWarning`, `invalidArgTypeHelper`, `spawnPromisified`, `hasIntl`, `isWindows`, ...) with Node's semantics, so a verdict measures the module under test rather than Node's full `test/common`. + - `node` builds the files in place against Node's own `test/common`. +- `--out`: where executables and results go (default `node_modules/.cache/scriptc-node-parallel`). +- `--cli`: the scriptc CLI entry (default `packages/cli/dist/main.js`). +- `--jobs`: concurrent files (default 4). + +Node runs each file with its `// Flags:` line. Results are written to `/results.json` and `/results.md`. + +Before a lane relies on the minimal common, run its files under Node in `/tree/test/parallel`: they should all pass there. diff --git a/tests/node-parallel/common/index.cjs b/tests/node-parallel/common/index.cjs new file mode 100644 index 000000000..b52bba638 --- /dev/null +++ b/tests/node-parallel/common/index.cjs @@ -0,0 +1,131 @@ +"use strict"; +// A minimal test/common for building Node's test/parallel files with scriptc: +// the helpers those files use, with the semantics of Node's +// test/common/index.js. Add a helper here, with Node's semantics, when a +// lane's files need it. +const assert = require("assert"); +const { inspect } = require("util"); + +const hasIntl = true; +const isDebug = false; +const isWindows = process.platform === "win32"; +const mustCallChecks = []; + +function runCallChecks(exitCode) { + if (exitCode !== 0) return; + const failed = mustCallChecks.filter((context) => { + if ("minimum" in context) return context.actual < context.minimum; + return context.actual !== context.exact; + }); + for (const context of failed) { + console.log( + "Mismatched %s function calls. Expected %s, actual %d.", + context.name, + String(context.exact ?? context.minimum), + context.actual, + ); + } + if (failed.length) process.exit(1); +} + +function noop() {} + +function mustCallInner(fn, criteria, field) { + if (typeof fn === "number") { + criteria = fn; + fn = noop; + } else if (fn === undefined) { + fn = noop; + } + if (criteria === undefined) criteria = 1; + const context = { actual: 0, name: fn.name || "" }; + context[field] = criteria; + if (mustCallChecks.length === 0) process.on("exit", runCallChecks); + mustCallChecks.push(context); + return function (...args) { + context.actual++; + return fn.apply(this, args); + }; +} + +function mustCall(fn, exact) { + return mustCallInner(fn, exact, "exact"); +} + +function mustCallAtLeast(fn, minimum) { + return mustCallInner(fn, minimum, "minimum"); +} + +function mustNotCall(msg) { + return function mustNotCall() { + assert.fail(msg || "function should not have been called"); + }; +} + +function skip(msg) { + console.log(`1..0 # Skipped: ${msg}`); + process.exit(0); +} + +function expectWarning(name, expected, code) { + if (typeof expected === "string") expected = [[expected, code]]; + else if (expected.length !== 0 && !Array.isArray(expected[0])) + expected = [[expected[0], expected[1]]]; + const check = mustCall((warning) => { + const [message, warningCode] = expected.shift(); + assert.strictEqual(warning.name, name); + if (typeof message === "string") assert.strictEqual(warning.message, message); + else assert.match(warning.message, message); + assert.strictEqual(warning.code, warningCode); + }, expected.length); + process.on("warning", (warning) => { + if (warning.name !== name) + throw new TypeError(`"${warning.name}" was triggered without being expected.`); + check(warning); + }); +} + +function invalidArgTypeHelper(input) { + if (input == null) return ` Received ${input}`; + if (typeof input === "function") return ` Received function ${input.name}`; + if (typeof input === "object") { + const name = input.constructor?.name; + if (typeof name === "string" && name !== "") return ` Received an instance of ${name}`; + return ` Received ${inspect(input, { depth: -1 })}`; + } + let inspected = inspect(input, { colors: false }); + if (inspected.length > 28) inspected = `${inspected.slice(inspected, 0, 25)}...`; + return ` Received type ${typeof input} (${inspected})`; +} + +function spawnPromisified(...args) { + const { spawn } = require("child_process"); + let stderr = ""; + let stdout = ""; + const child = spawn(...args); + child.stderr.setEncoding("utf8"); + child.stderr.on("data", (data) => { + stderr += data; + }); + child.stdout.setEncoding("utf8"); + child.stdout.on("data", (data) => { + stdout += data; + }); + return new Promise((resolve, reject) => { + child.on("close", (code, signal) => resolve({ code, signal, stderr, stdout })); + child.on("error", (code, signal) => reject({ code, signal, stderr, stdout })); + }); +} + +module.exports = { + hasIntl, + isDebug, + isWindows, + mustCall, + mustCallAtLeast, + mustNotCall, + skip, + expectWarning, + invalidArgTypeHelper, + spawnPromisified, +}; diff --git a/tests/node-parallel/common/wpt.cjs b/tests/node-parallel/common/wpt.cjs new file mode 100644 index 000000000..eeb7d9770 --- /dev/null +++ b/tests/node-parallel/common/wpt.cjs @@ -0,0 +1,32 @@ +"use strict"; +// The harness object of Node's test/common/wpt.js, without its WPT runner. +const assert = require("assert"); + +const harness = { + test: (fn, desc) => { + try { + fn(); + } catch (err) { + console.error(`In ${desc}:`); + throw err; + } + }, + assert_equals: assert.strictEqual, + assert_true: (value, message) => assert.strictEqual(value, true, message), + assert_false: (value, message) => assert.strictEqual(value, false, message), + assert_throws: (code, func, desc) => { + assert.throws( + func, + function (err) { + return typeof err === "object" && "name" in err && err.name.startsWith(code.name); + }, + desc, + ); + }, + assert_array_equals: assert.deepStrictEqual, + assert_unreached(desc) { + assert.fail(`Reached unreachable code: ${desc}`); + }, +}; + +module.exports = { harness }; diff --git a/tests/node-parallel/run.mjs b/tests/node-parallel/run.mjs new file mode 100644 index 000000000..4f2230c6b --- /dev/null +++ b/tests/node-parallel/run.mjs @@ -0,0 +1,134 @@ +// Builds Node's test/parallel files with scriptc and runs them next to Node. +// See README.md for the options. +import { spawn } from "node:child_process"; +import { + cpSync, + existsSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { parseArgs } from "node:util"; + +const here = dirname(fileURLToPath(import.meta.url)); +const repo = resolve(here, "../.."); +const { values } = parseArgs({ + options: { + node: { type: "string" }, + pattern: { type: "string" }, + common: { type: "string", default: "minimal" }, + out: { type: "string", default: join(repo, "node_modules/.cache/scriptc-node-parallel") }, + cli: { type: "string", default: join(repo, "packages/cli/dist/main.js") }, + jobs: { type: "string", default: "4" }, + }, +}); +if (!values.node || !values.pattern || !["minimal", "node"].includes(values.common)) { + console.error( + "usage: node tests/node-parallel/run.mjs --node --pattern [--common minimal|node] [--out ] [--cli ] [--jobs ]", + ); + process.exit(2); +} +const nodeTest = existsSync(join(values.node, "test", "parallel")) + ? join(values.node, "test") + : values.node; +const pattern = new RegExp(values.pattern); +const out = resolve(values.out); +mkdirSync(out, { recursive: true }); + +// The minimal lane copies the selected files into a tree whose test/common is +// this directory's index.cjs and wpt.cjs, next to the checkout's fixtures and +// common/fixtures.js. +let testRoot = nodeTest; +const files = readdirSync(join(nodeTest, "parallel")) + .filter((name) => name.endsWith(".js") && pattern.test(name)) + .sort(); +if (values.common === "minimal") { + testRoot = join(out, "tree", "test"); + rmSync(join(out, "tree"), { recursive: true, force: true }); + mkdirSync(join(testRoot, "parallel"), { recursive: true }); + mkdirSync(join(testRoot, "common"), { recursive: true }); + for (const name of files) + cpSync(join(nodeTest, "parallel", name), join(testRoot, "parallel", name)); + symlinkSync(join(nodeTest, "fixtures"), join(testRoot, "fixtures")); + cpSync(join(nodeTest, "common", "fixtures.js"), join(testRoot, "common", "fixtures.js")); + cpSync(join(here, "common", "index.cjs"), join(testRoot, "common", "index.js")); + cpSync(join(here, "common", "wpt.cjs"), join(testRoot, "common", "wpt.js")); +} + +function run(cmd, args, opts) { + return new Promise((done) => { + const child = spawn(cmd, args, { ...opts, stdio: ["ignore", "pipe", "pipe"] }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (d) => (stdout += d)); + child.stderr.on("data", (d) => (stderr += d)); + const timer = setTimeout(() => child.kill("SIGKILL"), opts.timeout); + child.on("close", (code, signal) => { + clearTimeout(timer); + done({ code, signal, stdout, stderr }); + }); + }); +} + +const firstCause = (text) => + ( + text.split("\n").find((line) => /error|Error|assert|SC\d{4}/.test(line)) ?? + text.split("\n")[0] ?? + "" + ) + .replaceAll(testRoot, "test") + .trim() + .slice(0, 300); + +async function one(file) { + const dir = join(testRoot, "parallel"); + const source = readFileSync(join(dir, file), "utf8"); + const flags = (/^\/\/ Flags: (.*)$/m.exec(source)?.[1] ?? "").split(/\s+/).filter(Boolean); + const node = await run(process.execPath, [...flags, file], { cwd: dir, timeout: 120_000 }); + const exe = join(out, "bin", `${file}.exe`); + mkdirSync(join(out, "bin"), { recursive: true }); + const build = await run(process.execPath, [values.cli, "build", file, "-o", exe], { + cwd: dir, + timeout: 900_000, + }); + const result = { file, flags: flags.join(" "), nodeExit: node.code }; + if (build.code !== 0) { + result.verdict = "refused"; + result.cause = firstCause(build.stdout + build.stderr); + return result; + } + const native = await run(exe, [], { cwd: dir, timeout: 120_000 }); + result.exit = native.code ?? native.signal; + result.verdict = native.code === node.code ? "pass" : "fail"; + if (result.verdict !== "pass") result.cause = firstCause(native.stderr + native.stdout); + return result; +} + +const results = []; +const queue = [...files]; +await Promise.all( + Array.from({ length: Number(values.jobs) }, async () => { + for (let file = queue.shift(); file !== undefined; file = queue.shift()) { + const result = await one(file); + results.push(result); + console.log(`${result.verdict.padEnd(8)} ${file}${result.cause ? ` ${result.cause}` : ""}`); + } + }), +); +results.sort((a, b) => (a.file < b.file ? -1 : 1)); +const count = (verdict) => results.filter((r) => r.verdict === verdict).length; +const table = [ + "| file | verdict | first cause |", + "| --- | --- | --- |", + ...results.map((r) => `| ${r.file} | ${r.verdict} | ${(r.cause ?? "").replaceAll("|", "\\|")} |`), + "", + `pass ${count("pass")}, fail ${count("fail")}, refused ${count("refused")} of ${results.length} (test/common: ${values.common})`, +]; +writeFileSync(join(out, "results.json"), `${JSON.stringify(results, null, 2)}\n`); +writeFileSync(join(out, "results.md"), `${table.join("\n")}\n`); +console.log(table.at(-1)); From d99fc851e66ffc3eadb95b30b226dbdcce5745c0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 01:22:42 -0500 Subject: [PATCH 042/273] Type untyped JavaScript Maps, Sets and arrays from their uses In a JavaScript file, `const m = new Map()`, `new Set()` and `[]` type as Map, Set and any[], so every use lowered to the checked-dynamic collection operations: boxed keys and values, generic method dispatch, and dynamic arithmetic on what they returned. js-collection-types.ts proves types for such bindings from their uses. A `const` qualifies when every reference is a direct use of the collection: - for a Map: get/has/delete/clear, size, and set as a statement; - for a Set: has/delete/clear, size, and add as a statement; - for an array: push/pop/shift/at/includes/indexOf/join and length. The value then never leaves the binding. Its key, value or element type is the one primitive (string, number or boolean) of every expression the program writes. That is found by a fixed point that types `m.get(k) ?? d`, arithmetic, comparisons and constants defined from them, and it requires lookups of the same type. Any other reference (passing, returning, storing, exporting, iterating, inspecting, an optional call), a direct `eval` in the file, or any doubt keeps the binding untyped. A proven binding answers the lowerer's typeOf with a carrier type declared in scriptc-overrides.d.ts, which only the lowering program includes. `new Map()` and `new Set()` adopt it, so the binding lowers as the typed Map, Set or T[], as a JSDoc @type would make it. 7985 covers the typed cases and every refusal. --- .../compiler/ambient/scriptc-overrides.d.ts | 21 + .../frontend/lowering/js-collection-types.ts | 519 ++++++++++++++++++ .../src/frontend/lowering/lower-classes.ts | 10 +- .../compiler/src/frontend/lowering/lowerer.ts | 8 + .../compiler/test/js-collection-types.test.ts | 62 +++ .../7985-untyped-collection-types.mjs.json | 6 + .../corpus/7985-untyped-collection-types.mjs | 116 ++++ .../7985-untyped-collection-types.mjs.json | 1 + 8 files changed, 741 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/js-collection-types.ts create mode 100644 packages/compiler/test/js-collection-types.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7985-untyped-collection-types.mjs.json create mode 100644 tests/corpus/7985-untyped-collection-types.mjs create mode 100644 tests/harness/corpus-costs/7985-untyped-collection-types.mjs.json diff --git a/packages/compiler/ambient/scriptc-overrides.d.ts b/packages/compiler/ambient/scriptc-overrides.d.ts index 2dafbcdf0..376eba9d5 100644 --- a/packages/compiler/ambient/scriptc-overrides.d.ts +++ b/packages/compiler/ambient/scriptc-overrides.d.ts @@ -70,3 +70,24 @@ interface PromiseConstructor { reject: (reason: Error) => void; }; } + +/* Carriers of the types the lowerer gives untyped JavaScript collections + * whose uses prove them (js-collection-types.ts). Nothing reads them at run + * time. */ +declare namespace __scriptcInferredCollections { + const map_string_string: Map; + const map_string_number: Map; + const map_string_boolean: Map; + const map_number_string: Map; + const map_number_number: Map; + const map_number_boolean: Map; + const map_boolean_string: Map; + const map_boolean_number: Map; + const map_boolean_boolean: Map; + const set_string: Set; + const set_number: Set; + const set_boolean: Set; + const array_string: string[]; + const array_number: number[]; + const array_boolean: boolean[]; +} diff --git a/packages/compiler/src/frontend/lowering/js-collection-types.ts b/packages/compiler/src/frontend/lowering/js-collection-types.ts new file mode 100644 index 000000000..2be3c067f --- /dev/null +++ b/packages/compiler/src/frontend/lowering/js-collection-types.ts @@ -0,0 +1,519 @@ +/* Types of untyped JavaScript collections that their uses prove. + * + * In a JavaScript file, `const m = new Map()`, `new Set()` and `[]` type as + * Map, Set and any[] to the checker, so every use lowers to + * the checked-dynamic collection operations. When every reference of such a + * `const` is a direct use of the collection, the value never leaves the + * binding. If its keys, values or elements are then each one primitive type + * wherever the program writes and looks them up, the binding lowers as the + * typed Map, Set or T[], as a JSDoc @type would make it: + * + * - Map: `m.get(k)`, `m.has(k)`, `m.delete(k)`, `m.clear()`, `m.size`, and + * `m.set(k, v)` as a statement (its result is the map). + * - Set: `s.add(k)` as a statement, `s.has(k)`, `s.delete(k)`, `s.clear()`, + * `s.size`. + * - Array: `a.push(...)`, `a.pop()`, `a.shift()`, `a.at(i)`, `a.includes(v)`, + * `a.indexOf(v)`, `a.join(separator)`, `a.length`. + * + * Any other reference (passing, returning, storing, exporting, iterating, + * spreading, inspecting the collection, an optional call), a direct `eval` + * in the file, or any doubt about a type keeps the binding untyped. + * + * The types come from carrier declarations in scriptc-overrides.d.ts, which + * only the lowering program includes. */ + +import * as ts from "../ts7/adapter.js"; +import { isJsSourceFile } from "../program.js"; +import { overridesDtsPath } from "../dts-paths.js"; +import type { Lowerer } from "./lowerer.js"; + +type Primitive = "string" | "number" | "boolean"; +const PRIMITIVES: readonly Primitive[] = ["string", "number", "boolean"]; + +type Collection = "map" | "set" | "array"; + +interface Candidate { + readonly kind: Collection; + readonly declaration: ts.VariableDeclaration; + /** Every identifier that names the binding, its declaration's included. */ + readonly names: ts.Identifier[]; + /** Map keys or Set members written by set/add. */ + readonly keyWrites: ts.Expression[]; + /** Map values written by set. Array elements written by push. */ + readonly valueWrites: ts.Expression[]; + /** Arguments compared with keys or elements: get/has/delete, includes/indexOf. */ + readonly lookups: ts.Expression[]; + /** Array indexes passed to at(). */ + readonly indexes: ts.Expression[]; + /** Separators passed to join(). */ + readonly separators: ts.Expression[]; + key: Primitive | undefined; + value: Primitive | undefined; +} + +const MAP_LOOKUPS = new Set(["get", "has", "delete"]); +const SET_LOOKUPS = new Set(["has", "delete"]); +const ARRAY_READS = new Set(["pop", "shift"]); +const ARRAY_SEARCHES = new Set(["includes", "indexOf"]); + +/** The inferred type of the binding a `new Map()` or `new Set()` initializes, + * which the construction adopts. */ +export function initializedCollectionType( + lowerer: Lowerer, + expr: ts.NewExpression, +): ts.Type | null { + const declaration = expr.parent; + return declaration !== undefined && + ts.isVariableDeclaration(declaration) && + declaration.initializer === expr && + ts.isIdentifier(declaration.name) + ? inferredCollectionType(lowerer, declaration.name) + : null; +} + +/** The inferred collection type of an identifier that names a proven + * binding, or null. Computed for every JavaScript file of the program on + * first use. */ +export function inferredCollectionType(lowerer: Lowerer, node: ts.Identifier): ts.Type | null { + const types = (lowerer.inferredCollectionTypes ??= inferProgram(lowerer)); + return types.size === 0 ? null : (types.get(node) ?? null); +} + +function inferProgram(lowerer: Lowerer): Map { + const out = new Map(); + const carriers = carrierTypes(lowerer); + if (carriers === null) return out; + for (const file of lowerer.program.getSourceFiles()) { + if (file.isDeclarationFile || !isJsSourceFile(file)) continue; + for (const candidate of inferFile(lowerer, file)) { + const name = + candidate.kind === "map" + ? `map_${candidate.key}_${candidate.value}` + : candidate.kind === "set" + ? `set_${candidate.key}` + : `array_${candidate.value}`; + const type = carriers.get(name); + if (type === undefined) continue; + for (const id of candidate.names) out.set(id, type); + } + } + return out; +} + +/** The carrier declarations' types, by name (`map_string_number`, + * `set_number`, `array_string`, ...), or null when the lowering program does + * not include them. */ +function carrierTypes(lowerer: Lowerer): Map | null { + const file = lowerer.program.getSourceFile(overridesDtsPath()); + if (file === undefined) return null; + const out = new Map(); + for (const statement of file.statements.nodes) { + if ( + !ts.isModuleDeclaration(statement) || + !ts.isIdentifier(statement.name) || + statement.name.text !== "__scriptcInferredCollections" || + statement.body === undefined || + !ts.isModuleBlock(statement.body) + ) + continue; + for (const member of statement.body.statements.nodes) { + if (!ts.isVariableStatement(member)) continue; + for (const declaration of member.declarationList.declarations.nodes) + if (ts.isIdentifier(declaration.name)) + out.set(declaration.name.text, lowerer.checker.getTypeAtLocation(declaration.name)); + } + } + return out.size > 0 ? out : null; +} + +function inferFile(lowerer: Lowerer, file: ts.SourceFile): Candidate[] { + const candidates = new Map(); + let directEval = false; + const declarations = (node: ts.Node): void => { + if ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === "eval" + ) + directEval = true; + if (ts.isVariableDeclaration(node)) { + const kind = candidateKind(lowerer, node); + const symbol = kind !== null ? lowerer.checker.getSymbolAtLocation(node.name) : undefined; + if (kind !== null && symbol !== undefined) + candidates.set(symbol, { + kind, + declaration: node, + names: [node.name as ts.Identifier], + keyWrites: [], + valueWrites: [], + lookups: [], + indexes: [], + separators: [], + key: undefined, + value: undefined, + }); + } + ts.forEachChild(node, declarations); + }; + declarations(file); + if (directEval || candidates.size === 0) return []; + + const names = new Set( + [...candidates.values()].map((c) => (c.declaration.name as ts.Identifier).text), + ); + const refused = new Set(); + const references = (node: ts.Node): void => { + if (ts.isIdentifier(node) && names.has(node.text)) { + const symbol = referencedSymbol(lowerer, node); + const candidate = symbol !== undefined ? candidates.get(symbol) : undefined; + if (symbol !== undefined && candidate !== undefined && node !== candidate.declaration.name) { + if (recordUse(candidate, node)) candidate.names.push(node); + else refused.add(symbol); + } + } + ts.forEachChild(node, references); + }; + references(file); + for (const symbol of refused) candidates.delete(symbol); + const proven = [...candidates.values()].filter( + (c) => c.valueWrites.length > 0 || c.keyWrites.length > 0, + ); + solve(lowerer, proven); + return proven.filter((c) => + c.kind === "map" ? c.key && c.value : c.kind === "set" ? c.key : c.value, + ); +} + +/** The collection a `const` declaration starts empty, when the checker + * types it as an untyped Map, Set or array. */ +function candidateKind(lowerer: Lowerer, declaration: ts.VariableDeclaration): Collection | null { + if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined) return null; + const list = declaration.parent; + if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) return null; + const statement = list.parent; + if ( + ts.isVariableStatement(statement) && + ts.getModifiers(statement)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) + ) + return null; + const init = declaration.initializer; + let kind: Collection | null = null; + if (ts.isArrayLiteralExpression(init) && init.elements.nodes.length === 0) kind = "array"; + else if ( + ts.isNewExpression(init) && + init.typeArguments === undefined && + (init.arguments?.nodes.length ?? 0) === 0 + ) { + if (lowerer.isStdlibGlobal(init.expression, "Map")) kind = "map"; + else if (lowerer.isStdlibGlobal(init.expression, "Set")) kind = "set"; + } + if (kind === null) return null; + // A JSDoc type, or anything else the checker already knows, stands. + const type = lowerer.checker.getTypeAtLocation(declaration.name); + const args = + kind === "array" + ? lowerer.checker.isArrayType(type) + ? lowerer.checker.getTypeArguments(type as ts.TypeReference) + : [] + : lowerer.checker.getTypeArguments(type as ts.TypeReference); + const arity = kind === "map" ? 2 : 1; + if (args.length !== arity || !args.every((a) => (a.flags & ts.TypeFlags.Any) !== 0)) return null; + return kind; +} + +function referencedSymbol(lowerer: Lowerer, id: ts.Identifier): ts.Symbol | undefined { + const parent = id.parent; + if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) + return lowerer.checker.getShorthandAssignmentValueSymbol(parent); + return lowerer.checker.getSymbolAtLocation(id); +} + +/** Records a reference's use, or answers false when the reference is not + * one of the collection uses this inference proves. */ +function recordUse(candidate: Candidate, id: ts.Identifier): boolean { + const access = id.parent; + if ( + access === undefined || + !ts.isPropertyAccessExpression(access) || + access.expression !== id || + access.questionDotToken !== undefined + ) + return false; + const name = access.name.text; + const call = access.parent; + if (call !== undefined && ts.isCallExpression(call) && call.expression === access) { + if (call.questionDotToken !== undefined) return false; + const args = call.arguments.nodes; + if (args.some(ts.isSpreadElement)) return false; + switch (candidate.kind) { + case "map": + if (MAP_LOOKUPS.has(name) && args.length === 1) + return (candidate.lookups.push(args[0]!), true); + if (name === "clear") return args.length === 0; + if (name === "set" && args.length === 2 && isStatement(call)) { + candidate.keyWrites.push(args[0]!); + candidate.valueWrites.push(args[1]!); + return true; + } + return false; + case "set": + if (SET_LOOKUPS.has(name) && args.length === 1) + return (candidate.lookups.push(args[0]!), true); + if (name === "clear") return args.length === 0; + if (name === "add" && args.length === 1 && isStatement(call)) + return (candidate.keyWrites.push(args[0]!), true); + return false; + case "array": + if (name === "push") return (candidate.valueWrites.push(...args), true); + if (ARRAY_READS.has(name)) return args.length === 0; + if (name === "at" && args.length === 1) return (candidate.indexes.push(args[0]!), true); + if (ARRAY_SEARCHES.has(name) && args.length === 1) + return (candidate.lookups.push(args[0]!), true); + if (name === "join" && args.length <= 1) return (candidate.separators.push(...args), true); + return false; + } + } + return (candidate.kind === "array" ? name === "length" : name === "size") && isPlainRead(access); +} + +function isStatement(call: ts.CallExpression): boolean { + let node: ts.Node = call; + while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent; + return node.parent !== undefined && ts.isExpressionStatement(node.parent); +} + +/** A read that neither writes nor deletes the property. */ +function isPlainRead(access: ts.PropertyAccessExpression): boolean { + let node: ts.Node = access; + while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + if (parent === undefined) return false; + if ( + ts.isBinaryExpression(parent) && + parent.left === node && + isAssignment(parent.operatorToken.kind) + ) + return false; + if ( + (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) && + (parent.operator === ts.SyntaxKind.PlusPlusToken || + parent.operator === ts.SyntaxKind.MinusMinusToken) + ) + return false; + return !ts.isDeleteExpression(parent); +} + +function isAssignment(kind: ts.SyntaxKind): boolean { + return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment; +} + +/** Decides each candidate's key and value types: a fixed point in which a + * decided candidate's reads type the expressions built from them. */ +function solve(lowerer: Lowerer, candidates: Candidate[]): void { + const bySymbol = new Map(); + for (const c of candidates) { + const symbol = lowerer.checker.getSymbolAtLocation(c.declaration.name); + if (symbol) bySymbol.set(symbol, c); + } + for (let changed = true; changed;) { + changed = false; + for (const c of candidates) { + if (c.kind !== "array" && c.key === undefined) { + const key = uniform(lowerer, bySymbol, c.keyWrites); + if (key !== null) { + c.key = key; + changed = true; + } + } + if (c.kind !== "set" && c.value === undefined) { + // Each candidate type is assumed for this collection's own reads. + const accepted = PRIMITIVES.filter((assumed) => { + c.value = assumed; + const found = uniform(lowerer, bySymbol, c.valueWrites); + c.value = undefined; + return found === assumed; + }); + if (accepted.length === 1) { + c.value = accepted[0]; + changed = true; + } + } + } + } + for (const c of candidates) { + const element = c.kind === "array" ? c.value : c.key; + // Lookups compare with the stored type; an index is a number. + const lookupsAgree = + element !== undefined && + c.lookups.every((e) => primitiveOf(lowerer, bySymbol, e, new Set()) === element) && + c.indexes.every((e) => primitiveOf(lowerer, bySymbol, e, new Set()) === "number") && + c.separators.every((e) => primitiveOf(lowerer, bySymbol, e, new Set()) === "string"); + if (!lookupsAgree) { + c.key = undefined; + c.value = undefined; + } + } +} + +/** The one primitive type of every expression, or null. */ +function uniform( + lowerer: Lowerer, + bySymbol: Map, + exprs: readonly ts.Expression[], +): Primitive | null { + let found: Primitive | null = null; + for (const e of exprs) { + const p = primitiveOf(lowerer, bySymbol, e, new Set()); + if (p === null || (found !== null && p !== found)) return null; + found = p; + } + return found; +} + +function checkerPrimitive(type: ts.Type): Primitive | null { + const flags = type.flags; + if (flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral)) return "string"; + if (flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral)) return "number"; + if (flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) return "boolean"; + return null; +} + +/** The primitive type `e` always evaluates to, or null. The checker answers + * where it knows; where it types `e` as `any`, the value's structure does: + * a read of a decided candidate, arithmetic and comparisons over such + * values, and constants defined from them. */ +function primitiveOf( + lowerer: Lowerer, + bySymbol: Map, + e: ts.Expression, + visiting: Set, +): Primitive | null { + const type = lowerer.checker.getTypeAtLocation(e); + if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) === 0) return checkerPrimitive(type); + const of = (x: ts.Expression): Primitive | null => primitiveOf(lowerer, bySymbol, x, visiting); + if (ts.isParenthesizedExpression(e)) return of(e.expression); + if (ts.isPrefixUnaryExpression(e)) { + if (e.operator === ts.SyntaxKind.ExclamationToken) return "boolean"; + return e.operator !== ts.SyntaxKind.PlusPlusToken && + e.operator !== ts.SyntaxKind.MinusMinusToken && + of(e.operand) === "number" + ? "number" + : null; + } + if (ts.isConditionalExpression(e)) { + const yes = of(e.whenTrue); + return yes !== null && yes === of(e.whenFalse) ? yes : null; + } + if (ts.isBinaryExpression(e)) { + const op = e.operatorToken.kind; + switch (op) { + case ts.SyntaxKind.LessThanToken: + case ts.SyntaxKind.GreaterThanToken: + case ts.SyntaxKind.LessThanEqualsToken: + case ts.SyntaxKind.GreaterThanEqualsToken: + case ts.SyntaxKind.EqualsEqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + case ts.SyntaxKind.EqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsToken: + return "boolean"; + case ts.SyntaxKind.QuestionQuestionToken: + case ts.SyntaxKind.BarBarToken: { + // `m.get(k) ?? d` and `a.pop() || d`: the stored type or a default of it. + const stored = storedRead(lowerer, bySymbol, e.left); + const fallback = of(e.right); + return stored !== null && stored === fallback ? stored : null; + } + case ts.SyntaxKind.PlusToken: { + const left = of(e.left); + const right = of(e.right); + if (left === "number" && right === "number") return "number"; + return left !== null && right !== null && (left === "string" || right === "string") + ? "string" + : null; + } + case ts.SyntaxKind.MinusToken: + case ts.SyntaxKind.AsteriskToken: + case ts.SyntaxKind.SlashToken: + case ts.SyntaxKind.PercentToken: + case ts.SyntaxKind.AsteriskAsteriskToken: + case ts.SyntaxKind.BarToken: + case ts.SyntaxKind.AmpersandToken: + case ts.SyntaxKind.CaretToken: + case ts.SyntaxKind.LessThanLessThanToken: + case ts.SyntaxKind.GreaterThanGreaterThanToken: + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + return of(e.left) === "number" && of(e.right) === "number" ? "number" : null; + default: + return null; + } + } + if (ts.isCallExpression(e) || ts.isPropertyAccessExpression(e)) + return candidateRead(lowerer, bySymbol, e); + // A constant defined from such an expression. + if (ts.isIdentifier(e) && !visiting.has(e)) { + const symbol = lowerer.checker.getSymbolAtLocation(e); + const declaration = symbol?.valueDeclaration; + if ( + declaration !== undefined && + ts.isVariableDeclaration(declaration) && + declaration.initializer !== undefined && + ts.isVariableDeclarationList(declaration.parent) && + (declaration.parent.flags & ts.NodeFlags.Const) !== 0 + ) { + visiting.add(e); + const p = primitiveOf(lowerer, bySymbol, declaration.initializer, visiting); + visiting.delete(e); + return p; + } + } + return null; +} + +/** The candidate a use reads, when `e` is `c.member` or `c.member(...)` of a + * candidate `c`. */ +function candidateOf( + lowerer: Lowerer, + bySymbol: Map, + e: ts.Expression, +): { candidate: Candidate; member: string } | null { + const access = ts.isCallExpression(e) ? e.expression : e; + if (!ts.isPropertyAccessExpression(access) || !ts.isIdentifier(access.expression)) return null; + const symbol = lowerer.checker.getSymbolAtLocation(access.expression); + const candidate = symbol !== undefined ? bySymbol.get(symbol) : undefined; + return candidate !== undefined ? { candidate, member: access.name.text } : null; +} + +/** The type of a candidate's read that may answer undefined: `m.get(k)`, + * `a.pop()`, `a.shift()`, `a.at(i)`. */ +function storedRead( + lowerer: Lowerer, + bySymbol: Map, + e: ts.Expression, +): Primitive | null { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isCallExpression(e)) return null; + const read = candidateOf(lowerer, bySymbol, e); + if (read === null) return null; + const { candidate, member } = read; + if (candidate.kind === "map" && member === "get") return candidate.value ?? null; + if (candidate.kind === "array" && (ARRAY_READS.has(member) || member === "at")) + return candidate.value ?? null; + return null; +} + +/** The type of a candidate's read that always answers a value: size, + * length, has, includes, indexOf, push. */ +function candidateRead( + lowerer: Lowerer, + bySymbol: Map, + e: ts.Expression, +): Primitive | null { + const read = candidateOf(lowerer, bySymbol, e); + if (read === null) return null; + const { candidate, member } = read; + if (!ts.isCallExpression(e)) return member === "size" || member === "length" ? "number" : null; + if (member === "has" || member === "includes" || member === "delete") return "boolean"; + if (member === "indexOf" || (candidate.kind === "array" && member === "push")) return "number"; + if (candidate.kind === "array" && member === "join") return "string"; + return null; +} diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index b829b6ddc..c4dd6bce1 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -113,6 +113,7 @@ import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./low import { isAssertClassNew } from "./assert-instances.js"; import { isCjsRequireGlobal } from "./lower-node-module.js"; import { lowerArrayConstructor } from "./containers/array-construction.js"; +import { initializedCollectionType } from "./js-collection-types.js"; import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js"; import { lowerObjectConstructor } from "./containers/indexed-objects.js"; import { knownBufEncoding, lowerBufferConstruct } from "./containers/bytes.js"; @@ -10568,7 +10569,11 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // expression). The CONTEXTUAL type carries the annotation — adopt // it when it is a supported map. TS type arguments keep winning: // their expression type already maps. - if (mapped?.kind !== "map" || contextualSeed) { + const inferred = initializedCollectionType(lowerer, expr); + if (inferred) { + tsType = inferred; + mapped = lowerer.mapTypeOf(inferred); + } else if (mapped?.kind !== "map" || contextualSeed) { const ctx = lowerer.checker.getContextualType(expr); const ctxMapped = ctx ? lowerer.mapTypeOf(ctx) : null; if (ctx && ctxMapped?.kind === "map") { @@ -10708,7 +10713,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // `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")) { - const tsType = lowerer.typeOf(expr); + const inferred = initializedCollectionType(lowerer, expr); + const tsType = inferred ?? lowerer.typeOf(expr); let mapped = lowerer.mapTypeOf(tsType); const fieldType = assignedThisFieldType(lowerer, expr); if (mapped?.kind !== "set" && fieldType?.kind === "set") mapped = fieldType; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index af89c61a4..78d1fafad 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -156,6 +156,7 @@ import { trackClassFieldCreation, } from "./class-dynamic-dispatch.js"; import { guardClassAttributeWrites } from "./class-attributes.js"; +import { inferredCollectionType } from "./js-collection-types.js"; import { guardFrozenWrites, markSloppyRecordWrites } from "./frozen-records.js"; import { applyHandleProperties } from "./handle-properties.js"; import { installBufferValue } from "./containers/bytes.js"; @@ -2846,6 +2847,9 @@ export class Lowerer { /** Parameters read as `any` in the error-path copy of an error-first * callback. */ readonly anySymbols = new Set(); + /** The collection types that uses prove for untyped JavaScript bindings, + * by every identifier naming one (js-collection-types.ts). */ + inferredCollectionTypes: Map | undefined = undefined; /** overload-choice's answers per call, binding and function. */ readonly dynamicOverloadAnswers = new Map(); /** Error parameters of an error-first callback with a known value in @@ -9055,6 +9059,10 @@ export class Lowerer { } typeOf(node: ts.Node): ts.Type { + if (ts.isIdentifier(node)) { + const inferred = inferredCollectionType(this, node); + if (inferred) return inferred; + } if (this.anySymbols.size > 0 && this.readsAnySymbol(node)) return this.checker.getAnyType(); // A checked-dynamic argument that another overload could take at run // time leaves the resolved overload's return a guess, and JS bindings diff --git a/packages/compiler/test/js-collection-types.test.ts b/packages/compiler/test/js-collection-types.test.ts new file mode 100644 index 000000000..88e2006b7 --- /dev/null +++ b/packages/compiler/test/js-collection-types.test.ts @@ -0,0 +1,62 @@ +import { mkdtempSync, readFileSync, rmSync } 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"; + +// Corpus 7985 exercises every refusal: only the collections whose uses +// prove their types lower as typed collections; the rest stay untyped. +test("untyped JavaScript collections take the types their uses prove, and only those", async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-js-collection-types-")); + try { + const entry = join( + import.meta.dirname, + "../../../tests/corpus/7985-untyped-collection-types.mjs", + ); + const output = join(dir, "main.ir.json"); + 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 types = new Map(module.globals.map((g) => [g.name, JSON.stringify(g.type)])); + const map = (key: string, value: string) => + JSON.stringify({ kind: "map", key: { kind: key }, value: { kind: value } }); + const set = (elem: string) => JSON.stringify({ kind: "set", elem: { kind: elem } }); + const array = (elem: string) => JSON.stringify({ kind: "array", elem: { kind: elem } }); + // Untyped collections hold checked-dynamic keys, values and elements. + const typed = (t: string) => !t.includes('"dyn"'); + expect(Object.fromEntries([...types].filter(([, t]) => typed(t)))).toEqual({ + counts: map("string", "f64"), + seen: set("f64"), + stack: array("f64"), + words: array("string"), + byNumber: map("f64", "string"), + nums: array("f64"), + flags: set("bool"), + cache: map("string", "f64"), + "%loaded": '{"kind":"bool"}', + }); + for (const untyped of [ + "strings", + "numbers", + "mixedKeys", + "mixedValues", + "mixedElements", + "passed", + "stored", + "iterated", + "eachSet", + "shown", + "chained", + "optional", + "indexed", + "truncated", + "spread", + "unread", + "exported", + ]) + expect(typed(types.get(untyped)!), untyped).toBe(false); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7985-untyped-collection-types.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7985-untyped-collection-types.mjs.json new file mode 100644 index 000000000..a807063cc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7985-untyped-collection-types.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7985-untyped-collection-types.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/7985-untyped-collection-types.mjs b/tests/corpus/7985-untyped-collection-types.mjs new file mode 100644 index 000000000..a60a2ea04 --- /dev/null +++ b/tests/corpus/7985-untyped-collection-types.mjs @@ -0,0 +1,116 @@ +// Untyped Maps, Sets and arrays whose uses prove their key, value and +// element types compile as typed collections; any other use keeps them +// untyped. Both must answer as Node does, including SameValueZero keys, +// lookups of another type, and every use that refuses the typing. + +// Typed: counters, membership sets and stacks used only through their +// methods. +const counts = new Map(); +const seen = new Set(); +const stack = []; +const words = []; +for (let i = 0; i < 40; i++) { + const key = "k" + (i % 7); + counts.set(key, (counts.get(key) ?? 0) + 1); + seen.add(i % 5); + stack.push(i * 2); + if (stack.length > 6) stack.shift(); + words.push(key); +} +console.log(counts.size, counts.get("k3"), counts.get("missing"), counts.has("k6"), counts.delete("k0"), counts.size); +console.log(seen.size, seen.has(4), seen.has(5), seen.delete(4), seen.has(4)); +console.log(stack.length, stack.at(-1), stack.at(0), stack.pop(), stack.includes(70), stack.indexOf(72), words.join("|").length); + +// SameValueZero keys and NaN elements on typed collections. +const byNumber = new Map(); +byNumber.set(NaN, "nan"); +byNumber.set(-0, "zero"); +byNumber.set(1.5, "x"); +console.log(byNumber.get(NaN), byNumber.get(0), byNumber.get(-0), byNumber.size, byNumber.has(1.5)); +const nums = []; +nums.push(NaN); +nums.push(-0); +console.log(nums.includes(NaN), nums.indexOf(NaN), nums.includes(0), nums.indexOf(0)); +const flags = new Set(); +flags.add(true); +console.log(flags.has(true), flags.has(false), flags.size); + +// Captured by a closure that only uses it. +const cache = new Map(); +const latest = () => cache.get("c") ?? -1; +console.log(latest()); +for (let i = 0; i < 3; i++) cache.set("c", i * 10); +console.log(latest(), cache.size); + +// Lookups of another type: the typing refuses, and the answers stay Node's. +const strings = new Map(); +strings.set("1", "one"); +console.log(strings.get(1), strings.get("1"), strings.has(1)); +const numbers = []; +numbers.push(1); +console.log(numbers.includes("1"), numbers.indexOf("1"), numbers.at("0")); + +// Mixed key, value and element types. +const mixedKeys = new Map(); +mixedKeys.set(1, "number"); +mixedKeys.set("1", "string"); +console.log(mixedKeys.size, mixedKeys.get(1), mixedKeys.get("1")); +const mixedValues = new Map(); +mixedValues.set("a", 1); +mixedValues.set("b", "two"); +console.log(mixedValues.get("a"), mixedValues.get("b")); +const mixedElements = []; +mixedElements.push(1); +mixedElements.push("two"); +console.log(mixedElements.join(","), mixedElements.length); + +// Escapes and other uses that keep the collection untyped. +const passed = new Map(); +passed.set("x", 1); +function sizeOf(m) { + return m.size; +} +console.log(sizeOf(passed)); +function make() { + const made = new Set(); + made.add("y"); + return made; +} +console.log(make().has("y")); +const stored = []; +stored.push(3); +const holder = { list: stored }; +holder.list.push(4); +console.log(stored.length, holder.list.join("+")); +const iterated = new Map(); +iterated.set("a", 1); +iterated.set("b", 2); +for (const [k, v] of iterated) console.log("entry", k, v); +const eachSet = new Set(); +eachSet.add(3); +eachSet.forEach((v) => console.log("member", v)); +const shown = new Map(); +shown.set("s", 1); +console.log(shown); +const chained = new Map(); +chained.set("a", 1).set("b", 2); +console.log(chained.size); +const optional = new Map(); +optional.set("o", 1); +console.log(optional?.get("o"), optional.get?.("o")); +const indexed = []; +indexed.push(1); +indexed[3] = 2; +console.log(indexed.length, indexed.join(",")); +const truncated = []; +truncated.push(1, 2, 3); +truncated.length = 1; +console.log(truncated.length, truncated.at(0)); +const spread = []; +spread.push(5); +console.log(Math.max(...spread)); +const unread = new Map(); +console.log(unread.size, unread.get("none")); +export const exported = new Map(); +exported.set("e", 1); +console.log(exported.get("e")); diff --git a/tests/harness/corpus-costs/7985-untyped-collection-types.mjs.json b/tests/harness/corpus-costs/7985-untyped-collection-types.mjs.json new file mode 100644 index 000000000..6915a85fe --- /dev/null +++ b/tests/harness/corpus-costs/7985-untyped-collection-types.mjs.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 2600 } From 1ddec6df7cc7c81eb934d90675651724f3c15922 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 01:52:11 -0500 Subject: [PATCH 043/273] Freeze the symbol-keyed fields of class instances A frozen class instance kept its symbol-keyed fields writable through typed references, and Object.isFrozen, isSealed and isExtensible reported it unfrozen, because the field write checks and the instance's checked view covered its string-keyed fields only. This held for Object.freeze through an untyped value and for a typed Object.freeze alike. - Writes to a symbol-keyed field of a hierarchy with an integrity word are checked like other field writes, and throw Node's TypeError naming the symbol ("Cannot assign to read only property 'Symbol(tag)' ..."). - The checked view applies the instance's integrity state to its symbol-keyed properties too, and the commit of an untyped freeze, seal or preventExtensions counts them. - The commit removes a symbol-keyed field's entry from the property bag whatever its attributes (scr_dyn_obj_delete_symbol_field), as it does for string-keyed fields, so a sealed view no longer leaves the field's old value in the bag. Corpus 7407 covers freeze and seal of symbol-keyed fields through unknown and through typed calls, on a class and a subclass, and matches Node. --- .../src/backend/llvm/expr-stream-bridges.ts | 8 ++- .../src/frontend/lowering/class-attributes.ts | 40 ++++++++++++--- packages/runtime/src/scr_dyn.h | 3 ++ packages/runtime/src/scr_json.c | 50 +++++++++++++++---- tests/corpus/7407-frozen-class-instances.ts | 38 ++++++++++++++ 5 files changed, 120 insertions(+), 19 deletions(-) diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 757e4349f..357db2fae 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -498,7 +498,9 @@ export function streamTypedRefCommitAdapter( host.declare( meta.def.tracksOwnFields ? `declare void @scr_dyn_symbol_key_placeholder(ptr, ptr)` - : `declare void @scr_dyn_key_delete_computed(ptr, ptr, i1 zeroext)`, + : withAttributes + ? `declare void @scr_dyn_obj_delete_symbol_field(ptr, ptr)` + : `declare void @scr_dyn_key_delete_computed(ptr, ptr, i1 zeroext)`, ); [...symbols.values()].forEach((globalId, index) => lines.push( @@ -506,7 +508,9 @@ export function streamTypedRefCommitAdapter( ` %bag_sym${index} = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %bag_symraw${index})`, meta.def.tracksOwnFields ? ` call void @scr_dyn_symbol_key_placeholder(ptr %bag, ptr %bag_sym${index})` - : ` call void @scr_dyn_key_delete_computed(ptr %bag, ptr %bag_sym${index}, i1 zeroext false)`, + : withAttributes + ? ` call void @scr_dyn_obj_delete_symbol_field(ptr %bag, ptr %bag_sym${index})` + : ` call void @scr_dyn_key_delete_computed(ptr %bag, ptr %bag_sym${index}, i1 zeroext false)`, ` call void @scr_dyn_release_v(ptr %bag_sym${index})`, ), ); diff --git a/packages/compiler/src/frontend/lowering/class-attributes.ts b/packages/compiler/src/frontend/lowering/class-attributes.ts index de7cc8ca7..cdd53efa6 100644 --- a/packages/compiler/src/frontend/lowering/class-attributes.ts +++ b/packages/compiler/src/frontend/lowering/class-attributes.ts @@ -303,6 +303,32 @@ export function lowerClassFieldDefine( }; } +/** The property key of `field` when it is the storage of a symbol-keyed + * field of `info`, as the read-only TypeError names it: the symbol's + * String() form ("Symbol(tag)"), read from the module global that holds + * the symbol. Null for other fields. */ +function symbolFieldKey( + lowerer: Lowerer, + info: ClassInfo, + field: string, + loc: SrcLoc, +): IrExpr | null { + if (info.symbolFields === undefined) return null; + for (const [symbol, name] of info.symbolFields) { + if (name !== field) continue; + const global = lowerer.globalsBySymbol.get(symbol); + if (global?.type.kind !== "symbol") return null; + return { + kind: "libCall", + fn: "sym.toString", + args: [varRef(global.id, global.type, loc)], + type: STRING, + loc, + }; + } + return null; +} + function classDisplayName(info: ClassInfo): string { return info.def.jsName ?? info.decl?.name?.text ?? info.def.name.replace(/^%/, ""); } @@ -367,12 +393,15 @@ export function guardClassAttributeWrites(lowerer: Lowerer, functions: IrFunctio if ( statement.kind !== "fieldSet" || statement.attributesChecked || - !guarded.has(statement.className) || - !isClassOwnEnumerableFieldName(statement.field) + !guarded.has(statement.className) ) return statement; const info = lowerer.classes.get(statement.className)!; const loc = statement.loc; + // A symbol-keyed field is an own property under its symbol. + const symbolKey = symbolFieldKey(lowerer, info, statement.field, loc); + if (!symbolKey && !isClassOwnEnumerableFieldName(statement.field)) return statement; + const key = (): IrExpr => symbolKey ?? strLit(statement.field, loc); const receiver = local(statement.obj.type, loc); const value = local(statement.value.type, loc); const masked = (field: string, bit: number): IrExpr => ({ @@ -405,7 +434,7 @@ export function guardClassAttributeWrites(lowerer: Lowerer, functions: IrFunctio expr: { kind: "libCall", fn: "class.throwNotExtensible", - args: [strLit(statement.field, loc)], + args: [key()], type: VOID, loc, }, @@ -420,10 +449,7 @@ export function guardClassAttributeWrites(lowerer: Lowerer, functions: IrFunctio expr: { kind: "libCall", fn: "class.throwReadOnly", - args: [ - strLit(statement.field, loc), - instanceClassName(lowerer, info, receiver, helpers, loc), - ], + args: [key(), instanceClassName(lowerer, info, receiver, helpers, loc)], type: VOID, loc, }, diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index bf3b8d426..9df51f90a 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1220,6 +1220,9 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict); /* Removes the own property `key` of an ordinary object whatever its * attributes, as scr_dyn_obj_set_field replaces values. */ void scr_dyn_obj_delete_field(ScrDyn *obj, const ScrStr *key); +/* The same for the own symbol-keyed property `symbol` of an ordinary + * object. */ +void scr_dyn_obj_delete_symbol_field(ScrDyn *obj, ScrDyn *symbol); /* This thread's global object box (borrowed). Every spelling of the global * object answers it; its symbol-keyed properties are the global object's * (scr_node_builtin.c). */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 2f8e4b6f8..bc14a8a1e 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2415,10 +2415,14 @@ double scr_dyn_class_view_integrity(const ScrDyn *view, double bits, ScrDyn *bag state |= SCR_CLASS_NOT_EXTENSIBLE; if (bag && bag->kind == SCR_DYN_OBJ) bag->non_extensible = true; bool sealed = true, frozen = true; - for (size_t i = 0; i < view->v.obj.len; i++) { - const ScrDynEntry *entry = &view->v.obj.entries[i]; - if (entry->configurable) sealed = false; - if (!entry->accessor && entry->writable) frozen = false; + /* The string-keyed entries, then the symbol-keyed ones. */ + for (const ScrDyn *table = view; table; table = table == view ? view->symbol_properties : NULL) { + if (table->kind != SCR_DYN_OBJ) continue; + for (size_t i = 0; i < table->v.obj.len; i++) { + const ScrDynEntry *entry = &table->v.obj.entries[i]; + if (entry->configurable) sealed = false; + if (!entry->accessor && entry->writable) frozen = false; + } } if (sealed) state |= SCR_CLASS_SEALED; if (sealed && frozen) state |= SCR_CLASS_FROZEN; @@ -7969,6 +7973,22 @@ bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key) { return s bool scr_dyn_has_own_computed(const ScrDyn *value, const ScrDyn *key) { return scr_dyn_key_probe_computed(value, key, 1); } bool scr_dyn_property_is_enumerable_computed(const ScrDyn *value, const ScrDyn *key) { return scr_dyn_key_probe_computed(value, key, 2); } +static void scr_json_delete_member(ScrDyn *object, const ScrStr *key); + +void scr_dyn_obj_delete_symbol_field(ScrDyn *obj, ScrDyn *symbol) { + if (obj->kind != SCR_DYN_OBJ || !obj->symbol_properties) return; + ScrStr *name = scr_dyn_symbol_storage_key(symbol); + scr_json_delete_member(obj->symbol_properties, name); + scr_str_release(name); + for (size_t i = 0; obj->symbol_keys && i < obj->symbol_keys->v.arr.len; i++) { + if (!scr_dyn_strict_eq(obj->symbol_keys->v.arr.items[i], symbol)) continue; + scr_dyn_release(obj->symbol_keys->v.arr.items[i]); + size_t remaining = --obj->symbol_keys->v.arr.len - i; + memmove(&obj->symbol_keys->v.arr.items[i], &obj->symbol_keys->v.arr.items[i + 1], remaining * sizeof(ScrDyn *)); + break; + } +} + void scr_dyn_key_delete_computed(ScrDyn *value, const ScrDyn *raw_key, bool strict) { ScrDyn *key = scr_dyn_property_key_value(raw_key); if (!key) return; @@ -16914,17 +16934,27 @@ void scr_dyn_class_field_attributes(ScrDyn *view, ScrStr *name, double bits) { if (b & SCR_FIELD_NOT_CONFIGURABLE) entry->configurable = false; } -void scr_dyn_class_view_attributes(ScrDyn *view, double bits) { - unsigned b = (unsigned)bits; - if (b == 0 || view->kind != SCR_DYN_OBJ) return; - view->non_extensible = true; - for (size_t i = 0; i < view->v.obj.len; i++) { - ScrDynEntry *entry = &view->v.obj.entries[i]; +/* The integrity state `b` on the entries of `table`: a view, or the table + * of its symbol-keyed properties. */ +static void scr_dyn_class_table_attributes(ScrDyn *table, unsigned b) { + table->non_extensible = true; + for (size_t i = 0; i < table->v.obj.len; i++) { + ScrDynEntry *entry = &table->v.obj.entries[i]; if (b & (SCR_CLASS_SEALED | SCR_CLASS_FROZEN)) entry->configurable = false; if ((b & SCR_CLASS_FROZEN) && !entry->accessor) entry->writable = false; } } +void scr_dyn_class_view_attributes(ScrDyn *view, double bits) { + unsigned b = (unsigned)bits; + if (b == 0 || view->kind != SCR_DYN_OBJ) return; + scr_dyn_class_table_attributes(view, b); + /* Symbol-keyed fields, and symbol-keyed properties added to the + * instance, take the same state. */ + if (view->symbol_properties && view->symbol_properties->kind == SCR_DYN_OBJ) + scr_dyn_class_table_attributes(view->symbol_properties, b); +} + /* Object.defineProperty on a declared class field. The descriptor applies to * a one-property object holding the field's current value and attributes, * so validation and errors match ordinary objects. Answers that object (+1), diff --git a/tests/corpus/7407-frozen-class-instances.ts b/tests/corpus/7407-frozen-class-instances.ts index 0732b8c35..6a3a14c54 100644 --- a/tests/corpus/7407-frozen-class-instances.ts +++ b/tests/corpus/7407-frozen-class-instances.ts @@ -136,8 +136,46 @@ function added(): void { console.log((p as any).extra, JSON.stringify(p)); } +const tag = Symbol("tag"); + +class Tagged { + [tag] = 1; + name = "n"; + bump(): void { + this[tag] += 1; + } +} + +class Subtagged extends Tagged { + extra = 0; +} + +function symbols(): void { + const t = new Tagged(); + freezeUnknown(t); + console.log(integrity(t), Object.getOwnPropertyDescriptor(t, tag)?.writable); + try { t[tag] = 5; } catch (e) { report(e); } + try { t.bump(); } catch (e) { report(e); } + console.log(t[tag]); + + const s = new Subtagged(); + sealUnknown(s); + s[tag] = 3; + console.log(integrity(s), s[tag], Object.getOwnPropertyDescriptor(s, tag)?.configurable); + freezeUnknown(s); + try { s[tag] = 4; } catch (e) { report(e); } + console.log(integrity(s), s[tag]); + + const u = new Tagged(); + Object.freeze(u); + console.log(integrity(u)); + try { u[tag] = 9; } catch (e) { report(e); } + console.log(u[tag]); +} + frozen(); sealed(); notExtensible(); untouched(); added(); +symbols(); From 03e677a9c4d2d6af3ff71f30451ca94c5b5103b4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 03:14:24 -0500 Subject: [PATCH 044/273] Inherit the base constructor after a prototype's constructor is deleted A constructor read on a class instance through an untyped value answered the class itself whenever the class's prototype object did not hold an own `constructor`. After `delete Object.getPrototypeOf(new Middle()).constructor`, Node reads the property the base class's prototype holds, and past the last class's prototype Object.prototype's. The read now checks the instance's own property, then walks the class's prototypes: each answers its own `constructor`; a prototype without one answers its class unless reflection had populated it, in which case the program deleted the property and the read continues to the base class's prototype, and from the last one to Object.prototype. Reflection sets a per-class flag when the prototype initializes, so the answer does not depend on whether the reflection was compiled before the read. Corpus 8260 gains the deletion case. --- .../lowering/class-dynamic-dispatch.ts | 128 +++++++++--------- .../src/frontend/lowering/class-prototypes.ts | 23 ++++ .../src/frontend/lowering/lower-classes.ts | 4 + tests/corpus/8260-class-prototype-views.mjs | 21 +++ 4 files changed, 111 insertions(+), 65 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index d658fb20a..68e755592 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -52,6 +52,7 @@ import { import { classPrototypeData, hasClassPrototypeData, + prototypeReflectedFlag, observedPrototypeMethod, reflectClassPrototype, } from "./class-prototypes.js"; @@ -4105,34 +4106,13 @@ export class ClassDynamicDispatch { return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc }; } - /** `%dyn.class.bagHasOwn[OrPrototype]`: whether an instance's property bag - * holds `key` as its own property, or, with `prototype`, whether the bag's - * prototype object does. False before the instance has a bag. */ - private readBagHasOwn( - lowerer: Lowerer, - bag: IrExpr, - key: IrExpr, - prototype: boolean, - loc: SrcLoc, - ): IrExpr { - const name = prototype ? "%dyn.class.bagHasOwnOrPrototype" : "%dyn.class.bagHasOwn"; + /** `%dyn.class.bagHasOwn`: whether an instance's property bag holds `key` + * as its own property. False before the instance has a bag. */ + private readBagHasOwn(lowerer: Lowerer, bag: IrExpr, key: IrExpr, loc: SrcLoc): IrExpr { + const name = "%dyn.class.bagHasOwn"; let helper = lowerer.liftedFnNamed(name); if (!helper) { const value = varRef("p.0", DYN, loc); - const keyRef = varRef("p.1", STRING, loc); - const proto = varRef("proto", DYN, loc); - const hasOwn = (target: IrExpr): IrExpr => ({ - kind: "libCall", - fn: "dyn.hasOwn", - args: [target, keyRef], - type: BOOL, - loc, - }); - const returnFalse: IrStmt = { - kind: "return", - value: { kind: "boolLit", value: false, type: BOOL, loc }, - loc, - }; helper = { name, params: [ @@ -4143,47 +4123,28 @@ export class ClassDynamicDispatch { locals: [ { id: "p.0", name: "bag", type: DYN, mutable: false }, { id: "p.1", name: "key", type: STRING, mutable: false }, - ...(prototype ? [{ id: "proto", name: "proto", type: DYN, mutable: false }] : []), ], body: [ { kind: "if", cond: { kind: "dynTest", test: "undefined", value, type: BOOL, loc }, - then: [returnFalse], + then: [ + { kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc }, + ], else_: null, loc, }, - ...(prototype - ? ([ - { - kind: "if", - cond: hasOwn(value), - then: [ - { - kind: "return", - value: { kind: "boolLit", value: true, type: BOOL, loc }, - loc, - }, - ], - else_: null, - loc, - }, - { - kind: "varDecl", - localId: "proto", - init: { kind: "libCall", fn: "dyn.getPrototype", args: [value], type: DYN, loc }, - loc, - }, - { - kind: "if", - cond: { kind: "dynTest", test: "nullish", value: proto, type: BOOL, loc }, - then: [returnFalse], - else_: null, - loc, - }, - { kind: "return", value: hasOwn(proto), loc }, - ] satisfies IrStmt[]) - : [{ kind: "return", value: hasOwn(value), loc } satisfies IrStmt]), + { + kind: "return", + value: { + kind: "libCall", + fn: "dyn.hasOwn", + args: [value, varRef("p.1", STRING, loc)], + type: BOOL, + loc, + }, + loc, + }, ], loc, }; @@ -4193,6 +4154,45 @@ export class ClassDynamicDispatch { return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc }; } + /** The `constructor` an instance of `info` inherits when it has no own + * one. The class's prototype view answers its own `constructor` (a + * replacement, or the class once reflection installed it). A view + * without one answers the class itself unless reflection had populated it + * (prototypeReflectedFlag, set when the view initializes): then the + * program deleted the property, and the read continues to the base + * class's prototype and past the last one to Object.prototype, as in + * Node. */ + private inheritedConstructor( + lowerer: Lowerer, + info: ClassInfo, + key: IrExpr, + loc: SrcLoc, + ): IrExpr { + const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl!), DYN); + const prototype = hasClassPrototypeData(info) ? classPrototypeData(lowerer, info, loc) : null; + if (!prototype) return cls; + const base = info.base; + const next: IrExpr = + base && !base.builtinError && base.decl && !base.generic && !base.localClass + ? this.inheritedConstructor(lowerer, base, key, loc) + : { kind: "dynKeyGet", value: prototype, key, type: DYN, loc }; + return { + kind: "ternary", + cond: { kind: "libCall", fn: "dyn.hasOwn", args: [prototype, key], type: BOOL, loc }, + then: { kind: "dynKeyGet", value: prototype, key, type: DYN, loc }, + else_: { + kind: "ternary", + cond: varRef(prototypeReflectedFlag(lowerer, info), BOOL, loc), + then: next, + else_: cls, + type: DYN, + loc, + }, + type: DYN, + loc, + }; + } + /** The hints for which some boxed class declares a conversion method. A * string conversion calls toString first, so without a declared toString * or Symbol.toPrimitive it answers Object.prototype.toString's @@ -5951,15 +5951,13 @@ export class ClassDynamicDispatch { !info.classDecorators ) { this.constructors.set(info.def.name, info); - const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl), DYN); const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc); const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; // `constructor` is an own property of every class prototype, so an - // ancestor's prototype never answers for an instance of this class: - // the instance's own property answers, then one this class's - // prototype object defines (a reflected prototype, or a write to - // `C.prototype.constructor`), and otherwise the class itself. - const answered = this.readBagHasOwn(lowerer, bag, key, hasClassPrototypeData(info), loc); + // ancestor's prototype answers for an instance of this class only + // after the program deletes the class's own: the instance's own + // property answers, then the class's prototype (inheritedConstructor). + const answered = this.readBagHasOwn(lowerer, bag, key, loc); return [ { kind: "return", @@ -5973,7 +5971,7 @@ export class ClassDynamicDispatch { type: DYN, loc, }, - else_: cls, + else_: this.inheritedConstructor(lowerer, info, key, loc), type: DYN, loc, }, diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 595b64e05..d005e64d1 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -299,6 +299,23 @@ function declaresPrototypeMembers(decl: ts.ClassLikeDeclaration): boolean { ); } +/** The global that is true once the class's prototype view has been + * populated by reflection (reflectClassPrototype): the view then holds an + * own `constructor` unless the program deleted it. A view that was never + * reflected leaves `constructor` implicit. */ +export function prototypeReflectedFlag(lowerer: Lowerer, info: ClassInfo): string { + if (info.prototypeReflectedFlag) return info.prototypeReflectedFlag; + const id = `%g.%prototype.data.${info.def.name}.reflected`; + lowerer.globalsList.push({ + id, + name: `${info.def.jsName ?? info.def.name}.prototype.reflected`, + type: BOOL, + mutable: true, + }); + info.prototypeReflectedFlag = id; + return id; +} + /** Populate the shared prototype view before exposing it to reflection. * Methods and accessors are non-enumerable and retain declaration identity. */ export function reflectClassPrototype( @@ -399,6 +416,12 @@ export function reflectClassPrototype( { key: "value", value: classValueRef(lowerer, info, info.decl) }, writable, ]), + { + kind: "assign", + localId: prototypeReflectedFlag(lowerer, info), + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }, ); const context = newFnCtx(false, null, null, DYN); lowerer.fnStack.push(context); diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index ac1864051..2394b728b 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -481,6 +481,10 @@ export interface ClassInfo { localPrototypeData?: true; prototypeMethodValues?: Map; prototypeReflectionReady?: true; + /** The global that records at run time that the prototype view was + * populated by reflection, so it holds its own `constructor` until the + * program deletes it (prototypeReflectedFlag). */ + prototypeReflectedFlag?: string; prototypeObservedMethodsReady?: true; /** The globals holding each observed method's original prototype value * (set when the prototype data is built), by method name. A typed call diff --git a/tests/corpus/8260-class-prototype-views.mjs b/tests/corpus/8260-class-prototype-views.mjs index e08b8f206..35e55f5f4 100644 --- a/tests/corpus/8260-class-prototype-views.mjs +++ b/tests/corpus/8260-class-prototype-views.mjs @@ -132,3 +132,24 @@ const areaValue = Shape.prototype.area; const disc = new Disc("d"); const rebuilt = disc.rebuild(); console.log(disc.constructor === Disc, rebuilt instanceof Disc, rebuilt.area(), rebuilt.name, typeof areaValue); + +// Deleting a reflected prototype's constructor: instances read the +// constructor their base class's prototype holds, then Object's; a +// replacement answers again. +class Root { + hi() { + return "hi"; + } +} +class Middle extends Root {} +class Tip extends Middle {} +const members = [new Middle(), new Tip(), new Root(), {}]; +const constructorNames = () => members.map((member) => member.constructor.name).join(","); +console.log(constructorNames(), Object.getOwnPropertyNames(Object.getPrototypeOf(new Middle())).join("|")); +delete Object.getPrototypeOf(new Middle()).constructor; +console.log(constructorNames(), Object.getOwnPropertyNames(Object.getPrototypeOf(new Middle())).join("|")); +console.log(new Middle().constructor === Root, new Tip().constructor === Tip); +delete Object.getPrototypeOf(new Root()).constructor; +console.log(constructorNames(), new Middle().constructor === Object); +Middle.prototype.constructor = function Named() {}; +console.log(constructorNames()); From 949c715f7bc52771d1f3978a721a20371ff88c3c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 03:27:00 -0500 Subject: [PATCH 045/273] Keep comparator sorts and untyped-source copies out of units loops A units copy reads the receiver's UTF-16 units through the pointer its guard took, which stays valid only while nothing indexes another non-ASCII string: that evicts the receiver's index entry and frees the units. The classifier admitted every typed-array and Buffer method, so a loop that sorted a typed array with a comparator function value, or copied an untyped source into one (`ta.set(source)`, whose element reads can run getters), got a units copy, and a comparator or getter that indexed other strings made the loop read freed memory. The units copy now admits only the typed-array and Buffer methods that call no program code and read no checked-dynamic value or string. Corpus 7307 sorts with such a comparator and copies from such a source inside scans of a warmed non-ASCII text; the sanitized build of the previous commit reports a heap-use-after-free on it. --- .../backend/llvm/string-unit-versions.test.ts | 40 ++++++++++ .../src/backend/llvm/string-unit-versions.ts | 66 ++++++++++++++++- .../7307-string-unit-loop-evictions.ts.json | 6 ++ .../corpus/7307-string-unit-loop-evictions.ts | 73 +++++++++++++++++++ .../7307-string-unit-loop-evictions.ts.json | 1 + 5 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7307-string-unit-loop-evictions.ts.json create mode 100644 tests/corpus/7307-string-unit-loop-evictions.ts create mode 100644 tests/harness/corpus-costs/7307-string-unit-loop-evictions.ts.json diff --git a/packages/compiler/src/backend/llvm/string-unit-versions.test.ts b/packages/compiler/src/backend/llvm/string-unit-versions.test.ts index 5b0634b1a..e21803948 100644 --- a/packages/compiler/src/backend/llvm/string-unit-versions.test.ts +++ b/packages/compiler/src/backend/llvm/string-unit-versions.test.ts @@ -84,3 +84,43 @@ console.log(words("ab c"), pairs("ab", "cd"), both("ab", "cd"), concatenates("a expect(unitSafeLoop(loopOf(mod, "both"), param("both"), safe)).toBeNull(); expect(unitSafeLoop(loopOf(mod, "concatenates"), param("concatenates"), safe)).toBeNull(); }); + +test("loops that sort with a comparator or copy an untyped source get no units copy", async () => { + // Either can run program code that indexes other strings, which evicts + // the receiver's units while the loop reads them. + const mod = await lower(` +function sorts(s: string, values: Float64Array, compare: (a: number, b: number) => number): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + n += s.charCodeAt(i); + if (i === 3) values.sort(compare); + } + return n; +} +function copies(s: string, values: Float64Array, source: any): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + n += s.charCodeAt(i); + if (i === 3) values.set(source); + } + return n; +} +function sortsNumbers(s: string, values: Float64Array): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + n += s.charCodeAt(i); + if (i === 3) values.sort(); + } + return n; +} +const values = new Float64Array([2, 1]); +console.log(sorts("abcdef", values, (a, b) => a - b), copies("abcdef", values, [4, 3]), sortsNumbers("abcdef", values)); +`); + const safe = unitSafeFunctions(mod); + const param = (fn: string): string => + mod.functions.find((f) => f.name === fn)!.params[0]!.localId; + expect(unitSafeLoop(loopOf(mod, "sorts"), param("sorts"), safe)).toBeNull(); + expect(unitSafeLoop(loopOf(mod, "copies"), param("copies"), safe)).toBeNull(); + // A numeric sort calls no program code. + expect(unitSafeLoop(loopOf(mod, "sortsNumbers"), param("sortsNumbers"), safe)).not.toBeNull(); +}); diff --git a/packages/compiler/src/backend/llvm/string-unit-versions.ts b/packages/compiler/src/backend/llvm/string-unit-versions.ts index 022c3a513..368b8eae8 100644 --- a/packages/compiler/src/backend/llvm/string-unit-versions.ts +++ b/packages/compiler/src/backend/llvm/string-unit-versions.ts @@ -1,4 +1,4 @@ -import type { IrExpr, IrModule, IrStmt } from "../../ir/ir.js"; +import type { IrBytesIntrinsicMethod, IrExpr, IrModule, IrStmt } from "../../ir/ir.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; /* Loop versions over the UTF-16 units of a string that is not ASCII. @@ -15,6 +15,68 @@ import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; const ARRAY_METHODS = new Set(["push", "pop", "length"]); +/* The typed-array and Buffer methods a units copy may run: those that + * call no program code and read no checked-dynamic value or string. A + * comparator sort (sortCompare) calls back into the program, and a copy + * from a checked-dynamic source (setFromDyn) reads its elements through + * getters; either can index another string, which evicts the receiver's + * units while the loop reads them. */ +const UNIT_SAFE_BYTES_METHODS: ReadonlySet = new Set([ + "buffer", + "length", + "byteLength", + "byteOffset", + "get", + "slice", + "subarray", + "copyWithin", + "sortNumeric", + "toReversed", + "with", + "toArray", + "setFrom", + "readNum", + "writeNum", + "readNumVar", + "writeNumVar", + "equals", + "compareBuf", + "indexOfNum", + "lastIndexOfNum", + "includesNum", + "indexOfElem", + "lastIndexOfElem", + "includesElem", + "fill", + "fillNum", + "fillElem", + "copy", + "swap16", + "swap32", + "swap64", + "dataViewNew", + "dvGetUint8", + "dvGetInt8", + "dvGetUint16", + "dvGetInt16", + "dvGetUint32", + "dvGetInt32", + "dvGetFloat16", + "dvGetFloat32", + "dvGetFloat64", + "dvGetBigUint64Number", + "dvGetBigInt64Number", + "dvSetUint8", + "dvSetInt8", + "dvSetUint16", + "dvSetInt16", + "dvSetUint32", + "dvSetInt32", + "dvSetFloat16", + "dvSetFloat32", + "dvSetFloat64", +]); + /** Whether `e` (or statement `s`) uses only operations that index no * string: numbers, booleans, locals, records and class fields, typed * arrays, number arrays and Math. `reads` admits the string reads a @@ -39,7 +101,9 @@ function unitSafe( case "incDec": case "recordGet": case "fieldGet": + break; case "bytesIntrinsic": + if (!UNIT_SAFE_BYTES_METHODS.has(e.method)) return false; break; case "arrayGet": if (e.type.kind !== "f64" && e.type.kind !== "bool") return false; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7307-string-unit-loop-evictions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7307-string-unit-loop-evictions.ts.json new file mode 100644 index 000000000..cb5864986 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7307-string-unit-loop-evictions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7307-string-unit-loop-evictions.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7307-string-unit-loop-evictions.ts b/tests/corpus/7307-string-unit-loop-evictions.ts new file mode 100644 index 000000000..94e98e5b4 --- /dev/null +++ b/tests/corpus/7307-string-unit-loop-evictions.ts @@ -0,0 +1,73 @@ +// A units copy must not run program code that indexes other strings: a +// comparator sort or a copy from an untyped source can evict the scanned +// text's UTF-16 units while the loop reads them. + +function bigText(seed: number): string { + let s = ""; + for (let i = 0; i < 9000; i++) s += String.fromCharCode(0xe9 + ((i * seed) % 7)) + "ab "; + return s; +} + +const others = [bigText(3), bigText(5), bigText(11), bigText(13), bigText(17)]; + +let touched = 0; +function touchOthers(): void { + for (let k = 0; k < others.length; k++) { + const o = others[k]; + for (let j = 0; j < 80; j++) touched += o.charCodeAt((j * 997 + k) % o.length); + } +} + +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 sortedWhileScanning( + s: string, + values: Float64Array, + compare: (a: number, b: number) => number, +): number { + let sum = 0; + for (let i = 0; i < s.length; i++) { + sum += s.charCodeAt(i); + if (i % 4096 === 100) values.sort(compare); + } + return sum; +} + +function copiedWhileScanning(s: string, values: Float64Array, source: any): number { + let sum = 0; + for (let i = 0; i < s.length; i++) { + sum += s.charCodeAt(i); + if (i % 4096 === 100) values.set(source); + } + return sum; +} + +const text = bigText(7); +// Repeated scans make the runtime hold the text's units. +let warm = 0; +for (let r = 0; r < 4; r++) warm += spaces(text); +console.log("warm", warm); + +const values = new Float64Array([3, 1, 2]); +const byValue = (a: number, b: number): number => { + touchOthers(); + return a - b; +}; +console.log("sorted", sortedWhileScanning(text, values, byValue), values.join(",")); +for (let r = 0; r < 2; r++) warm += spaces(text); + +const source: any = { + length: 3, + get 0() { + touchOthers(); + return 9; + }, + 1: 8, + 2: 7, +}; +console.log("copied", copiedWhileScanning(text, values, source), values.join(",")); +console.log("touched", touched > 0, spaces(text)); diff --git a/tests/harness/corpus-costs/7307-string-unit-loop-evictions.ts.json b/tests/harness/corpus-costs/7307-string-unit-loop-evictions.ts.json new file mode 100644 index 000000000..bb380e241 --- /dev/null +++ b/tests/harness/corpus-costs/7307-string-unit-loop-evictions.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3000 } From 4581dd2f123d917ddba44829c0699441acbe8047 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 04:05:54 -0500 Subject: [PATCH 046/273] Keep a Date boxed when a toJSON argument calls its setter A Date local kept as its time value was read as toJSON's receiver before the arguments ran, so a setter in an argument (d.toJSON(d.setTime(0))) did not change the result. Such a local now keeps its Date object, and toJSON reads it after its arguments, as in Node. --- .../src/frontend/lowering/date-values.ts | 6 +++++- tests/corpus/8027-date-setter-order.ts | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/date-values.ts b/packages/compiler/src/frontend/lowering/date-values.ts index f769e0866..8931d0f80 100644 --- a/packages/compiler/src/frontend/lowering/date-values.ts +++ b/packages/compiler/src/frontend/lowering/date-values.ts @@ -43,7 +43,8 @@ const RELATIONAL = new Set([ /** True when a local `const` (or a `let` that is never assigned again) * initialized with `new Date(...)` can keep the Date's time value instead * of a Date object: no use observes the Date's identity. Every use reads - * the time value as a Date method receiver (a setter replaces it), a + * the time value as a Date method receiver (a setter replaces it; the + * arguments of a method that reads it call no setter on it), a * String(), Number() or template operand, a unary + or - operand, a * relational or string `+` operand, or `new Date(d)`'s argument, inside * the declaring function, or inside the block or loop that scopes a @@ -194,6 +195,9 @@ function timeValueUse( ) return null; if (isDateSetter(parent.name.text)) return "set"; + // The method reads the time value after its arguments are evaluated: + // a setter there (`d.toJSON(d.setTime(0))`) changes what it reads. + if (call.arguments.nodes.some((arg) => callsSetterOf(lowerer, arg, symbol))) return null; return TIME_VALUE_METHODS.has(parent.name.text) ? "read" : null; } return readsTimeValue(lowerer, node, parent, symbol) ? "read" : null; diff --git a/tests/corpus/8027-date-setter-order.ts b/tests/corpus/8027-date-setter-order.ts index d0a175c2f..b87f3a656 100644 --- a/tests/corpus/8027-date-setter-order.ts +++ b/tests/corpus/8027-date-setter-order.ts @@ -1,7 +1,8 @@ // Date setters and evaluation order: a setter called without arguments // makes the Date invalid, an argument that calls setTime on the same -// Date subclass instance is seen by the setter, and a comparison or -// concatenation reads its left Date after the right side runs a setter. +// Date subclass instance is seen by the setter, a comparison or +// concatenation reads its left Date after the right side runs a setter, +// and toJSON reads its Date after an argument runs a setter. export {}; process.env.TZ = "UTC"; @@ -51,3 +52,15 @@ const s = new Date(Date.UTC(2026, 0, 1)); console.log(s + (s.setUTCFullYear(1999), "!")); const n = new Date(Date.UTC(2026, 0, 1)); console.log(+n === (n.setTime(5), 5), +n); + +// Locals a function keeps as their time value: a method reads the time +// value after its arguments run a setter. +function locals(): void { + const json = new Date(Date.UTC(2026, 0, 1)); + console.log(json.toJSON(json.setUTCFullYear(1999)), json.toJSON()); + const key = new Date(Date.UTC(2026, 0, 1)); + console.log(key.toJSON(String(key.setTime(Date.UTC(2021, 5, 1))))); + const read = new Date(Date.UTC(2026, 0, 1)); + console.log(read.toJSON(), read.getTime()); +} +locals(); From 1d1fccccd59249a22cabcb04bae88979879b7a06 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 05:56:14 -0500 Subject: [PATCH 047/273] Compare identical references and accept any message in the test262 adapter assert.sameValue, assert.notSameValue and assert.compareArray elements treat identical references as the same value and a reference as different from every primitive. Only two distinct references still refuse, because a conversion at the native boundary can copy a reference but never merge two. Assertion messages may be any value and convert with String, as in the upstream helpers. --- tests/harness/test262.test.ts | 57 +++++++++++++++++++++--- tests/test262/README.md | 2 +- tests/test262/harness.ts | 84 ++++++++++++++++++++--------------- 3 files changed, 101 insertions(+), 42 deletions(-) diff --git a/tests/harness/test262.test.ts b/tests/harness/test262.test.ts index f1b616578..fd35a08fb 100644 --- a/tests/harness/test262.test.ts +++ b/tests/harness/test262.test.ts @@ -237,6 +237,47 @@ assert.notSameValue(1, "1"); status: "fail", source: "assert.throws(TypeError, () => { throw 1; });", }, + { + name: "identical references", + status: "pass", + source: ` +const object = {}; +const list = [1]; +const fn = function () {}; +assert.sameValue(object, object); +assert.sameValue(list, list); +assert.sameValue(fn, fn); +assert.compareArray([object, fn], [object, fn]); +`, + }, + { + name: "a reference is not a primitive", + status: "pass", + source: + "assert.notSameValue({}, 1); assert.notSameValue([], undefined); assert.notSameValue(null, {});", + }, + { name: "reference and primitive mismatch", status: "fail", source: "assert.sameValue([], 0);" }, + { + name: "identical references are not different", + status: "fail", + source: "const object = {}; assert.notSameValue(object, object);", + }, + { + name: "array element reference and primitive mismatch", + status: "fail", + source: "assert.compareArray([{}], [1]);", + }, + { + name: "non-string messages", + status: "pass", + source: + "assert.sameValue(1, 1, false); assert.notSameValue(1, 2, 0); assert(true, null); assert.compareArray([], [], 1);", + }, + { + name: "failure with a non-string message", + status: "fail", + source: "assert.sameValue(1, 2, false);", + }, ]; describe(`Test262 host assertion contract${shardSuffix()}`, () => { @@ -259,8 +300,8 @@ describe(`Test262 host assertion contract${shardSuffix()}`, () => { ["{}", "[1]", "function () {}"], (value) => `reference:${value}`, )) { - test(`reference assertions are explicitly refused: ${value}`, async () => { - const source = `const value = ${value}; try { assert.sameValue(value, value); } catch { }`; + test(`distinct reference assertions are explicitly refused: ${value}`, async () => { + const source = `try { assert.sameValue(${value}, ${value}); } catch { } try { assert.notSameValue(${value}, ${value}); } catch { }`; expect(() => runUpstream(source)).not.toThrow(); const result = await runSource(source, { sanitize }); expect(result, JSON.stringify(result)).toMatchObject({ @@ -270,8 +311,8 @@ describe(`Test262 host assertion contract${shardSuffix()}`, () => { }); } - test("array element identity assertions are refused", async () => { - const source = "const value = {}; try { assert.compareArray([value], [value]); } catch { }"; + test("distinct array element references are refused", async () => { + const source = "try { assert.compareArray([{}], [{}]); } catch { }"; expect(() => runUpstream(source)).not.toThrow(); const result = await runSource(source, { sanitize }); expect(result, JSON.stringify(result)).toMatchObject({ @@ -359,9 +400,15 @@ const propertyControls = shardSelect( }, { name: "reference value", - status: "harness-refusal", + status: "pass", source: "const f = function() {}; const o = { a: f }; verifyProperty(o, 'a', { value: f });", }, + { + name: "distinct reference value", + status: "harness-refusal", + source: + "const o = { a: function() {} }; try { verifyProperty(o, 'a', { value: function() {} }); } catch { }", + }, ], (item) => `property:${item.name}`, ); diff --git a/tests/test262/README.md b/tests/test262/README.md index 2f8a0ed3e..5474fa2c3 100644 --- a/tests/test262/README.md +++ b/tests/test262/README.md @@ -32,7 +32,7 @@ The current profile is `static-adapted-v6`. It adapts strict scripts to standalo The unmodified upstream assertion helper currently reaches scriptc's JavaScript function-expando refusal. Test262 explicitly permits host implementations of harness functions in its [interpretation rules](https://github.com/tc39/test262/blob/7ab7fafa0003f73fc85c1b95d88094d33f7eb8bd/INTERPRETING.md#host-defined-functions). `harness.ts` supplies `assert`, `assert.sameValue`, `assert.notSameValue`, `assert.compareArray`, `assert.throws` for selected Error constructors, `$DONE`, and `Test262Error`. Scalar SameValue and scalar array-element comparisons use the statically compiled `node:assert/strict` implementation. Contract tests compare successful and failing assertions with the original Test262 helpers under Node, including NaN, signed zero, null/undefined, differing primitive types, and premature termination. -Reference-equality assertions terminate with a distinct harness-refusal result: conversion at a native function boundary can copy references, so this adapter cannot faithfully compare every object, array, or function identity. Tests cannot catch this refusal and accidentally pass. Sparse array comparisons are not yet handled faithfully across that boundary. The adapted `propertyHelper.js` admits direct `verifyProperty` calls with an inline descriptor literal or `undefined`; reference-valued comparisons still refuse, while other property helpers and descriptor expressions remain excluded. The adapted `testTypedArray.js` keeps the upstream constructor lists, argument factories and `testWith*` helpers. It omits the `TypedArray` (%TypedArray%) binding, so tests that name it are excluded. Its resizable and immutable buffer factories run only where `ArrayBuffer.prototype` provides `resize` and `transferToImmutable`, as the upstream feature checks intend. Contract tests compare its successful and failing uses with the upstream helper under Node. Assertion aliases, mutation, reflection, additional assertion methods, and other `includes` helpers remain excluded. +Conversion at a native function boundary can copy references, so one object, array, or function may reach the adapter as two distinct references. A copy cannot merge two references into one. `assert.sameValue`, `assert.notSameValue`, and `assert.compareArray` elements therefore treat identical references as the same value and a reference as different from every primitive. Two distinct references terminate with a distinct harness-refusal result, which tests cannot catch and turn into a pass. Assertion messages may be any value; the adapter converts them with `String`, as the upstream helpers do. Sparse array comparisons are not yet handled faithfully across that boundary. The adapted `propertyHelper.js` admits direct `verifyProperty` calls with an inline descriptor literal or `undefined`; reference-valued comparisons follow the same identity rule, while other property helpers and descriptor expressions remain excluded. The adapted `testTypedArray.js` keeps the upstream constructor lists, argument factories and `testWith*` helpers. It omits the `TypedArray` (%TypedArray%) binding, so tests that name it are excluded. Its resizable and immutable buffer factories run only where `ArrayBuffer.prototype` provides `resize` and `transferToImmutable`, as the upstream feature checks intend. Contract tests compare its successful and failing uses with the upstream helper under Node. Assertion aliases, mutation, reflection, additional assertion methods, and other `includes` helpers remain excluded. Raw tests, module variants, agents, resolution negatives, and runtime negatives remain excluded. Parse negatives pass only when a source-matched syntax or vetted JavaScript early-error diagnostic appears; TypeScript type errors and harness-only failures never satisfy a negative expectation. `$262`, dynamic evaluation, observable script-global state, and other unsupported host requirements are excluded as well. The runtime and compiler continue to enforce their existing refusal boundaries for admitted source. diff --git a/tests/test262/harness.ts b/tests/test262/harness.ts index c44d32869..b478b4555 100644 --- a/tests/test262/harness.ts +++ b/tests/test262/harness.ts @@ -17,68 +17,80 @@ export function $DONE(error?: unknown): void { console.log("__scriptc_test262_complete__"); } -function scalar(value: unknown): void { - // Native-to-unknown conversion can copy references (notably arrays). - // Refuse these assertions before comparing altered identities. Terminate +function reference(value: unknown): boolean { + return (typeof value === "object" && value !== null) || typeof value === "function"; +} + +function refuse(): never { + // Native-to-unknown conversion can copy references (notably arrays), so + // two distinct references may be one value in the program. Terminate // instead of throwing so a test's catch block cannot turn this into a pass. - if ((typeof value === "object" && value !== null) || typeof value === "function") { - console.error("SCRIPTC_TEST262_HARNESS: reference equality requires an identity-preserving adapter"); - process.exit(86); + console.error("SCRIPTC_TEST262_HARNESS: reference equality requires an identity-preserving adapter"); + process.exit(86); +} + +function scalar(value: unknown): void { + if (reference(value)) refuse(); +} + +/** The upstream helpers accept any message value and convert it to a string. */ +function text(message: unknown, fallback: string): string { + return message === undefined ? fallback : String(message); +} + +/** SameValue of two operands. A copy made at the conversion boundary can only + * separate one reference into two, so identical references are the same value + * and a reference is never the same value as a primitive. Two distinct + * references refuse. */ +function same(actual: unknown, expected: unknown): boolean { + if (reference(actual) || reference(expected)) { + if (actual === expected) return true; + if (reference(actual) && reference(expected)) refuse(); + return false; + } + try { + nodeAssert.strictEqual(actual, expected); + return true; + } catch { + return false; } } -export function assert(value: unknown, message = "Expected true"): void { - if (value !== true) throw new Test262Error(message); +export function assert(value: unknown, message?: unknown): void { + if (value !== true) throw new Test262Error(text(message, "Expected true")); } export namespace assert { - export function sameValue(actual: unknown, expected: unknown, message = "Expected SameValue"): void { - scalar(actual); - scalar(expected); - try { - nodeAssert.strictEqual(actual, expected); - } catch { - throw new Test262Error(message); - } + export function sameValue(actual: unknown, expected: unknown, message?: unknown): void { + if (!same(actual, expected)) throw new Test262Error(text(message, "Expected SameValue")); } - export function notSameValue(actual: unknown, expected: unknown, message = "Expected different values"): void { - scalar(actual); - scalar(expected); - try { - nodeAssert.notStrictEqual(actual, expected); - } catch { - throw new Test262Error(message); - } + export function notSameValue(actual: unknown, expected: unknown, message?: unknown): void { + if (same(actual, expected)) throw new Test262Error(text(message, "Expected different values")); } - export function throws(expectedErrorConstructor: unknown, func: () => unknown, message = "Expected exception"): void { + export function throws(expectedErrorConstructor: unknown, func: () => unknown, message?: unknown): void { const outcome = throwsOutcome(expectedErrorConstructor, func); if (outcome === "invalid") throw new Test262Error("assert.throws requires a function to run"); if (outcome === "unsupported") { console.error("SCRIPTC_TEST262_HARNESS: assert.throws needs an Error instance"); process.exit(86); } - if (outcome !== "pass") throw new Test262Error(message); + if (outcome !== "pass") throw new Test262Error(text(message, "Expected exception")); } - export function compareArray(actual: unknown, expected: unknown, message = "Expected matching array contents"): void { + export function compareArray(actual: unknown, expected: unknown, message?: unknown): void { + const failure = text(message, "Expected matching array contents"); if (!Array.isArray(actual) || !Array.isArray(expected)) { scalar(actual); scalar(expected); - throw new Test262Error(message); + throw new Test262Error(failure); } - if (actual.length !== expected.length) throw new Test262Error(message); + if (actual.length !== expected.length) throw new Test262Error(failure); for (let i = 0; i < actual.length; i++) { const left: unknown = i in actual ? actual[i] : undefined; const right: unknown = i in expected ? expected[i] : undefined; - scalar(left); - scalar(right); - try { - nodeAssert.strictEqual(left, right); - } catch { - throw new Test262Error(message); - } + if (!same(left, right)) throw new Test262Error(failure); } } } From 4c0572c49e9fa7d72a7fa24ac28d6f24c157def1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 06:25:57 -0500 Subject: [PATCH 048/273] Decode an undefined TextDecoder input as no bytes In Node, `decoder.decode(undefined)` returns "" like `decode()`, and flushes bytes pending from streamed input. scriptc refused a statically undefined argument and an argument typed `Uint8Array | undefined` with SC2020, and `decode(x!)` on an undefined value threw a TypeError. The decode input lowering now evaluates a statically undefined argument for its effects and decodes an empty Uint8Array, and reads the undefined arm of `Uint8Array | undefined` as an empty Uint8Array, in both the decoder-value and the `new TextDecoder(label).decode(...)` paths. A non-null assertion passed to decode keeps the undefined it removes, instead of the checked narrowing other optional standard library parameters keep. Inputs that may be null are refused as before (`decode(null)` throws a TypeError in Node). --- .../frontend/lowering/builtins/text-codecs.ts | 26 ++++++++++++++-- .../src/frontend/lowering/lower-exprs.ts | 15 +++++++++ .../text-decoder-undefined-input.ts.json | 6 ++++ ...-non-null-assertion-nullish-destination.ts | 4 +-- tests/corpus/text-decoder-undefined-input.ts | 31 +++++++++++++++++++ .../text-decoder-undefined-input.ts.json | 1 + 6 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/text-decoder-undefined-input.ts.json create mode 100644 tests/corpus/text-decoder-undefined-input.ts create mode 100644 tests/harness/corpus-costs/text-decoder-undefined-input.ts.json diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index 8d47c8e3a..34cc7aa14 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -16,6 +16,7 @@ import { type IrType, STRING, type SrcLoc, + typeEquals, } from "../../../ir/ir.js"; import { lowerBuiltinValuePreservingUndefined, @@ -24,6 +25,27 @@ import { } from "./arguments.js"; import { type BuiltinModuleFn } from "../surfaces.js"; +/** The input of `decoder.decode(input)`. An undefined input decodes no bytes, + * as `decode()` does in Node: a statically undefined argument (after its + * effects) and the undefined arm of `Uint8Array | undefined` read as an + * empty Uint8Array. */ +function lowerDecodeInput(lowerer: Lowerer, node: ts.Expression, loc: SrcLoc): IrExpr { + const empty: IrExpr = { kind: "bytesNew", source: null, type: BYTES_U8, loc }; + const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, node); + if (undefinedArg) return defaultAfterUndefined(undefinedArg, empty); + const value = lowerer.lowerExpr(node); + if (value.type.kind === "union") { + const def = lowerer.unions.get(value.type.unionId); + if ( + def?.arms.length === 2 && + def.arms.some((arm) => arm.kind === "undefinedT") && + def.arms.some((arm) => typeEquals(arm, BYTES_U8)) + ) + return { kind: "nullish", left: value, right: empty, type: BYTES_U8, loc }; + } + return value; +} + /** StringDecoder writes preserve pending bytes; end(input) performs the * final write and flushes the state. String input passes through unchanged. */ export function lowerStringDecoderMethodCall( @@ -413,7 +435,7 @@ function lowerStoredTextCodecCall( : { kind: "bytesNew", source: null, type: BYTES_U8, loc } : cls === "TextEncoder" ? lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING) - : lowerer.lowerExpr(call.arguments.nodes[0]!); + : lowerDecodeInput(lowerer, call.arguments.nodes[0]!, loc); if ( cls === "TextDecoder" && (arg.type.kind === "dyn" || (arg.type.kind === "bytes" && arg.type.elem !== "u8")) @@ -622,7 +644,7 @@ export function lowerTextCodecCall( ); } const argNode = call.arguments.nodes[0]!; - let arg = lowerer.lowerExpr(argNode); + let arg = lowerDecodeInput(lowerer, argNode, loc); if (arg.type.kind === "dyn" || (arg.type.kind === "bytes" && arg.type.elem !== "u8")) { arg = { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 033f482d1..8d9b9d410 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -975,6 +975,18 @@ function assertionDestinationHoldsNullish( return constituentTypes(contextual).some((type) => (type.flags & nullish) !== 0); } +/** Whether `declaration` is TextDecoder's `decode` method. */ +function isTextDecoderDecode(declaration: ts.Node): boolean { + if (!ts.isMethodSignatureDeclaration(declaration) && !ts.isMethodDeclaration(declaration)) + return false; + if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "decode") return false; + const owner = declaration.parent; + return ( + (ts.isInterfaceDeclaration(owner) || ts.isClassDeclaration(owner)) && + owner.name?.text === "TextDecoder" + ); +} + /** Whether `argument` of `call` binds an optional parameter of a standard * library declaration (`decoder.decode(input?)`). The undefined such a * parameter's type holds comes from its optionality, and the builtin @@ -989,6 +1001,9 @@ function optionalStdlibParameter( const declaration = lowerer.checker.getResolvedSignature(call)?.declaration; if (declaration === undefined || !lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration))) return false; + // TextDecoder.decode reads an undefined input as no bytes, as Node does + // (builtins/text-codecs.ts lowerDecodeInput). + if (isTextDecoderDecode(declaration)) return false; const index = call.arguments?.nodes.indexOf(argument) ?? -1; const parameter = declaration.parameters?.nodes[index]; return ( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/text-decoder-undefined-input.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/text-decoder-undefined-input.ts.json new file mode 100644 index 000000000..a1f5f49d9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/text-decoder-undefined-input.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/text-decoder-undefined-input.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7189-non-null-assertion-nullish-destination.ts b/tests/corpus/7189-non-null-assertion-nullish-destination.ts index 632655cff..4cd7e291f 100644 --- a/tests/corpus/7189-non-null-assertion-nullish-destination.ts +++ b/tests/corpus/7189-non-null-assertion-nullish-destination.ts @@ -46,8 +46,8 @@ console.log(String(names.get("a")!), String(names.get("b")!)); console.log(Number(amounts.get("a")!), Number(amounts.get("b")!)); console.log(kindOf(names.get("a")!), kindOf(amounts.get("a")!), kindOf(names.get("b")!)); -// An optional parameter of a standard library method keeps the checked -// narrowing: `decode(input?)` reads the asserted Uint8Array. +// TextDecoder.decode reads the asserted Uint8Array; an undefined input +// decodes no bytes (text-decoder-undefined-input.ts). const chunks = new Map([["a", new TextEncoder().encode("chunk")]]); interface Read { done: boolean; diff --git a/tests/corpus/text-decoder-undefined-input.ts b/tests/corpus/text-decoder-undefined-input.ts new file mode 100644 index 000000000..6a3be354c --- /dev/null +++ b/tests/corpus/text-decoder-undefined-input.ts @@ -0,0 +1,31 @@ +// TextDecoder.decode(undefined) decodes no bytes, as decode() does: it +// returns "" and, after streamed input, flushes the pending bytes. +const decoder = new TextDecoder(); +const encoded = new TextEncoder().encode("bytes"); +const inputs = new Map([ + ["present", encoded], + ["absent", undefined], +]); + +console.log(JSON.stringify(decoder.decode(undefined))); +console.log(JSON.stringify(decoder.decode(inputs.get("present"))), JSON.stringify(decoder.decode(inputs.get("absent")))); +console.log(JSON.stringify(decoder.decode(inputs.get("absent")!)), JSON.stringify(decoder.decode(inputs.get("missing")!))); +console.log(JSON.stringify(new TextDecoder("utf-8").decode(undefined))); +console.log(JSON.stringify(new TextDecoder("utf-8").decode(inputs.get("absent")))); + +let calls = 0; +function nothing(): undefined { + calls++; + return undefined; +} +console.log(JSON.stringify(decoder.decode(nothing())), calls); + +interface Chunk { + done: boolean; + value?: Uint8Array; +} +const chunks: Chunk[] = [{ done: false, value: new Uint8Array([0xe2, 0x82]) }, { done: true }]; +const streaming = new TextDecoder(); +let text = ""; +for (const chunk of chunks) text += streaming.decode(chunk.value, { stream: !chunk.done }); +console.log(JSON.stringify(text), text.length); diff --git a/tests/harness/corpus-costs/text-decoder-undefined-input.ts.json b/tests/harness/corpus-costs/text-decoder-undefined-input.ts.json new file mode 100644 index 000000000..b156827d6 --- /dev/null +++ b/tests/harness/corpus-costs/text-decoder-undefined-input.ts.json @@ -0,0 +1 @@ +{ "plain": 1937, "san": 2233 } From b6050f96bd04ea373b5ea18f4f8c3581a19d5c83 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 06:40:00 -0500 Subject: [PATCH 049/273] Read the runtime ABI marker in the native toolchain fixture from the compiler - The native toolchain test's fixture runtime pack hard-coded ABI v37, so with this branch's marker (v40) the toolchain refused the pack and the test saw exit status 1. It now takes RUNTIME_ABI_VERSION and RUNTIME_ABI_MARKER from runtime-abi.ts, as the native driver test does. --- tests/harness/self-hosting-native-toolchain.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c5476328e..4b4af5ac6 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -9,6 +9,10 @@ import { nativeCodegenTarget } from "../../packages/compiler/src/backend/targets import type { NativeLinkFeatures } from "../../packages/compiler/src/backend/native-link-info.js"; import type { RuntimePackManifest } from "../../packages/compiler/src/backend/runtime-pack-core.js"; import type { NativeToolchainManifest } from "../../packages/compiler/src/native/toolchain.js"; +import { + RUNTIME_ABI_MARKER, + RUNTIME_ABI_VERSION, +} from "../../packages/compiler/src/backend/runtime-abi.js"; const root = join(import.meta.dirname, "../.."); const entry = join(root, "tests/fixtures/self-hosting/native-toolchain.ts"); @@ -109,7 +113,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: RUNTIME_ABI_VERSION, marker: RUNTIME_ABI_MARKER }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From 854f3ae8103d5573f9c39e829f850b8a7f76661f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 06:41:00 -0500 Subject: [PATCH 050/273] Construct readline.Interface and builtin classes bound through require() members in JavaScript --- .../lowering/builtins/module-bindings.ts | 26 ++++++++++ .../frontend/lowering/builtins/readline.ts | 36 ++++++++----- .../src/frontend/lowering/lower-classes.ts | 16 ++++-- packages/compiler/src/frontend/tsc-codes.ts | 6 +++ ...30-readline-interface-constructor.cjs.json | 6 +++ .../8231-builtin-member-binding-new.cjs.json | 6 +++ .../8230-readline-interface-constructor.cjs | 51 +++++++++++++++++++ .../8231-builtin-member-binding-new.cjs | 35 +++++++++++++ ...30-readline-interface-constructor.cjs.json | 1 + .../8231-builtin-member-binding-new.cjs.json | 1 + 10 files changed, 167 insertions(+), 17 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8230-readline-interface-constructor.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8231-builtin-member-binding-new.cjs.json create mode 100644 tests/corpus/8230-readline-interface-constructor.cjs create mode 100644 tests/corpus/8231-builtin-member-binding-new.cjs create mode 100644 tests/harness/corpus-costs/8230-readline-interface-constructor.cjs.json create mode 100644 tests/harness/corpus-costs/8231-builtin-member-binding-new.cjs.json diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts index 5465aeafa..4ded700fe 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts @@ -185,6 +185,32 @@ export function builtinMemberRequireDecl( return spec !== null && canonicalBuiltinModule(spec) !== null; } +/** The member's own symbol for a variable bound to a builtin module's + * member (`const SD = require("string_decoder").StringDecoder`, + * builtinMemberRequireDecl) when the member is a class, so the variable + * resolves as a named import of the class does. The symbol itself + * otherwise. */ +export function builtinMemberBindingTarget( + lowerer: Lowerer, + symbol: ts.Symbol | null, +): ts.Symbol | null { + const decl = symbol?.valueDeclaration; + if ( + decl === undefined || + !ts.isVariableDeclaration(decl) || + !builtinMemberRequireDecl(decl.name, decl.initializer) + ) + return symbol; + const init = decl.initializer as ts.PropertyAccessExpression; + let member = lowerer.checker.getSymbolAtLocation(init.name); + if (member && member.flags & ts.SymbolFlags.Alias) + member = lowerer.checker.getAliasedSymbol(member); + // Only a class member: `require("events").EventEmitter` is a static + // property of the exported class, which builtinEmitterInfoOf follows + // from the variable itself. + return member?.declarations.some(ts.isClassDeclaration) ? member : symbol; +} + /** The supported builtin module when `decl` destructures a builtin * NAMESPACE binding (`const crypto = require('crypto'); const { * createSign, sign: mySign } = crypto;` — test/common/crypto.js's diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index aca209017..c2b6984ce 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -302,6 +302,15 @@ function interfaceClassModule(lowerer: Lowerer, node: ts.Expression): string | n : null; } +/** `new readline.Interface(...)` and `new readlinePromises.Interface(...)`: + * the constructor createInterface calls, with the same arguments. Null for + * other constructors. */ +export function lowerReadlineInterfaceNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { + const module = interfaceClassModule(lowerer, expr.expression); + if (module === null) return null; + return lowerReadlineCreateCall(lowerer, expr, locOf(expr), module === "readline/promises"); +} + /** `rl instanceof readline.Interface` and `rl instanceof * readlinePromises.Interface` over a readline Interface: an interface is * an instance of the class of the module that created it (Node's @@ -1059,10 +1068,12 @@ function lowerOutput(lowerer: Lowerer, node: ts.Expression): IrExpr | "stdout" | * goes through rl.terminal with the completer. */ export function lowerReadlineCreateCall( lowerer: Lowerer, - expr: ts.CallExpression, + expr: ts.CallExpression | ts.NewExpression, loc: SrcLoc, promises: boolean, ): IrExpr { + const argNodes = expr.arguments?.nodes ?? []; + const what = ts.isNewExpression(expr) ? "new Interface" : "createInterface"; let input: IrExpr | null = null; let output: IrExpr | "stdout" | "none" = "none"; let crlf: IrExpr = numLit(0, loc); @@ -1112,39 +1123,38 @@ export function lowerReadlineCreateCall( }); terminal = { kind: "varRef", localId: local.id, type: BOOL, loc }; }; - const optsNode = expr.arguments.nodes[0]; + const optsNode = argNodes[0]; if (optsNode === undefined) { lowerer.noLowering( - "createInterface without arguments", + `${what} without arguments`, expr, "pass { input: process.stdin, output: process.stdout } or createInterface(input, output)", ); } if (!ts.isObjectLiteralExpression(optsNode)) { // createInterface(input, output?, completer?, terminal?) - if (expr.arguments.nodes.length > 4) { + if (argNodes.length > 4) { lowerer.noLowering( - `createInterface with ${expr.arguments.nodes.length} arguments`, - expr.arguments.nodes[4]!, + `${what} with ${argNodes.length} arguments`, + argNodes[4]!, "pass createInterface(input, output, completer, terminal) or one options object", ); } input = lowerInput(lowerer, optsNode); - if (expr.arguments.nodes[1] !== undefined) - output = lowerOutput(lowerer, expr.arguments.nodes[1]); - const completerNode = expr.arguments.nodes[2]; + if (argNodes[1] !== undefined) output = lowerOutput(lowerer, argNodes[1]); + const completerNode = argNodes[2]; if ( completerNode !== undefined && !(ts.isIdentifier(completerNode) && completerNode.text === "undefined") ) { setCompleter(completerNode); } - if (expr.arguments.nodes[3] !== undefined) setTerminal(expr.arguments.nodes[3]); + if (argNodes[3] !== undefined) setTerminal(argNodes[3]); } else { - if (expr.arguments.nodes.length > 1) { + if (argNodes.length > 1) { lowerer.noLowering( - "createInterface with more than one argument", - expr.arguments.nodes[1]!, + `${what} with more than one argument`, + argNodes[1]!, "pass every option in the one options object", ); } diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index b829b6ddc..8bd5e57d7 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -18,8 +18,11 @@ import { superIntoResultSlot, } from "./constructor-returns.js"; import { checkedIterableSpread } from "./checked-iterable-spread.js"; -import { lowerReadlineUiNew } from "./builtins/readline.js"; -import { builtinMemberRequireDecl } from "./builtins/module-bindings.js"; +import { lowerReadlineInterfaceNew, lowerReadlineUiNew } from "./builtins/readline.js"; +import { + builtinMemberBindingTarget, + builtinMemberRequireDecl, +} from "./builtins/module-bindings.js"; import { checkedClassUnionAssertion } from "./class-assertions.js"; import { collectClassMethodMutations } from "./class-method-mutations.js"; import { InternalCompilerError } from "../../errors.js"; @@ -9819,7 +9822,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { } const worker = lowerWorkerNew(lowerer, expr); if (worker) return worker; - const readlineUi = lowerReadlineUiNew(lowerer, expr); + const readlineUi = lowerReadlineUiNew(lowerer, expr) ?? lowerReadlineInterfaceNew(lowerer, expr); if (readlineUi) return readlineUi; const channel = lowerMessageChannelNew(lowerer, expr) ?? lowerBroadcastChannelNew(lowerer, expr); if (channel) return channel; @@ -10132,7 +10135,12 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // (a no-op for every non-import= binding). if (ts.isIdentifier(expr.expression)) fenceEarlyAliasUse(lowerer, expr.expression, expr); const identifier = ts.isIdentifier(expr.expression) ? expr.expression : expr.expression.name; - const symbol = ts.isIdentifier(identifier) ? lowerer.resolveValueSymbol(identifier) : null; + // `const SD = require("string_decoder").StringDecoder; new SD()` + // constructs the member, as a named import of it does. + const symbol = builtinMemberBindingTarget( + lowerer, + ts.isIdentifier(identifier) ? lowerer.resolveValueSymbol(identifier) : null, + ); if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Array")) { return lowerArrayConstructor(lowerer, expr, expr.arguments?.nodes ?? []); } diff --git a/packages/compiler/src/frontend/tsc-codes.ts b/packages/compiler/src/frontend/tsc-codes.ts index e6c0be7cd..8816615cf 100644 --- a/packages/compiler/src/frontend/tsc-codes.ts +++ b/packages/compiler/src/frontend/tsc-codes.ts @@ -128,6 +128,12 @@ export const JS_RELAXED_TSC_CODES: ReadonlySet = new Set([ // JavaScript subclasses may widen fields or add method parameters. // Class lowering checks native storage and override ABIs at each use. 2415, + // a private or protected constructor (`new readline.Interface(...)`, + // whose declaration is protected): JavaScript has no constructor + // accessibility, and Node constructs the class. Each `new` meets the + // lowerer's per-site constructor fences. + 2673, + 2674, // unresolvable modules: Node throws WHEN the require executes — the // binding types as any and reached uses fence instead of passing unchecked 2307, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8230-readline-interface-constructor.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8230-readline-interface-constructor.cjs.json new file mode 100644 index 000000000..05573974c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8230-readline-interface-constructor.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8230-readline-interface-constructor.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8231-builtin-member-binding-new.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8231-builtin-member-binding-new.cjs.json new file mode 100644 index 000000000..1f54f4886 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8231-builtin-member-binding-new.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8231-builtin-member-binding-new.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/8230-readline-interface-constructor.cjs b/tests/corpus/8230-readline-interface-constructor.cjs new file mode 100644 index 000000000..6d7eea0e4 --- /dev/null +++ b/tests/corpus/8230-readline-interface-constructor.cjs @@ -0,0 +1,51 @@ +// `new readline.Interface(...)`: Node's createInterface calls this +// constructor with the same arguments, and a JavaScript file may call it +// directly. Its declaration is protected, which JavaScript does not +// enforce. +"use strict"; +const readline = require("readline"); +const readlinePromises = require("readline/promises"); +const { Interface } = require("readline"); +const { PassThrough } = require("stream"); + +// The options form. +const a = new PassThrough(); +const rlA = new readline.Interface({ input: a, terminal: false }); +console.log("instanceof", rlA instanceof readline.Interface); +const linesA = []; +rlA.on("line", (line) => { + linesA.push(line); +}); +rlA.on("close", () => { + console.log("options", JSON.stringify(linesA)); +}); +a.write("one\ntwo\n"); +a.end("three"); + +// The positional form, through a destructured binding. +const b = new PassThrough(); +const outB = new PassThrough(); +const rlB = new Interface(b, outB); +const linesB = []; +rlB.on("line", (line) => { + linesB.push(line); +}); +rlB.on("close", () => { + console.log("positional", JSON.stringify(linesB)); +}); +b.write("four\r\nfive\n"); +b.end(); + +// readline/promises' Interface. +const c = new PassThrough(); +const rlC = new readlinePromises.Interface({ input: c, crlfDelay: Infinity }); +console.log("promises instanceof", rlC instanceof readlinePromises.Interface); +const linesC = []; +rlC.on("line", (line) => { + linesC.push(line); +}); +rlC.on("close", () => { + console.log("promises", JSON.stringify(linesC)); +}); +c.end("six\nseven"); + diff --git a/tests/corpus/8231-builtin-member-binding-new.cjs b/tests/corpus/8231-builtin-member-binding-new.cjs new file mode 100644 index 000000000..1549425f7 --- /dev/null +++ b/tests/corpus/8231-builtin-member-binding-new.cjs @@ -0,0 +1,35 @@ +// `new` of a builtin class bound through a member of a require() result +// (`const SD = require("string_decoder").StringDecoder`, the spelling of +// Node's own tests): the binding resolves to the class, as the +// destructured `const { StringDecoder } = require(...)` does. +"use strict"; +const SD = require("string_decoder").StringDecoder; +const Readable = require("stream").Readable; +const PassThrough = require("stream").PassThrough; +const Transform = require("stream").Transform; + +const decoder = new SD("utf8"); +console.log(decoder.encoding); +console.log(JSON.stringify(decoder.write(Buffer.from([0xe2, 0x82])))); +console.log(decoder.end(Buffer.from([0xac]))); +const hex = new SD("hex"); +console.log(hex.encoding, hex.write(Buffer.from("hi"))); +console.log(new SD().encoding); + +const r = new Readable({ read() {} }); +r.on("data", (chunk) => console.log("readable", String(chunk))); +r.on("end", () => console.log("readable end")); +r.push("abc"); +r.push(null); + +const p = new PassThrough(); +p.on("data", (chunk) => console.log("passthrough", String(chunk))); +p.end("xyz"); + +const upper = new Transform({ + transform(chunk, encoding, callback) { + callback(null, String(chunk).toUpperCase()); + }, +}); +upper.on("data", (chunk) => console.log("transform", String(chunk))); +upper.end("shout"); diff --git a/tests/harness/corpus-costs/8230-readline-interface-constructor.cjs.json b/tests/harness/corpus-costs/8230-readline-interface-constructor.cjs.json new file mode 100644 index 000000000..9b459042f --- /dev/null +++ b/tests/harness/corpus-costs/8230-readline-interface-constructor.cjs.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3000 } diff --git a/tests/harness/corpus-costs/8231-builtin-member-binding-new.cjs.json b/tests/harness/corpus-costs/8231-builtin-member-binding-new.cjs.json new file mode 100644 index 000000000..54e11997a --- /dev/null +++ b/tests/harness/corpus-costs/8231-builtin-member-binding-new.cjs.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 2500 } From a5eca18f1eb62446a109069f75aae5077683d9e5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 07:22:00 -0500 Subject: [PATCH 051/273] Read fs.promises as a module, end CommonJS entries at a top-level return, and check fs read and write arguments as Node does fs.promises read as a member of the fs module (a binding, an inline require('fs').promises, destructuring) is the node:fs/promises namespace, as in Node. It failed at run time before. A return at the top level of the entry CommonJS module ends its evaluation, as in Node's module wrapper: preflight admits TS1108 in CommonJS JavaScript files. A CommonJS module the program requires with a top-level return is refused with SC1090, because the exports assigned after the return would stay undefined, which their storage cannot hold. fs.ReadStream(...) and fs.WriteStream(...) called without new construct their stream, as Node's stream constructors do. fs.read, fs.readSync and fs.write check their arguments in Node's order and with Node's errors: the fd, the buffer, the offset, `length |= 0`, number and bigint positions (validatePosition), the empty-buffer error and the buffer window. fs.read accepts a Proxy options object. A readSync call whose JavaScript arguments are untyped or bigint takes the same checks at run time (fs.readSyncDyn). fs.write threw no error for an invalid fd, offset or length before. --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../lowering/builtins/filesystem-calls.ts | 24 +- .../frontend/lowering/builtins/fs-streams.ts | 31 ++- .../lowering/builtins/module-bindings.ts | 4 +- .../src/frontend/lowering/lower-calls.ts | 10 +- .../src/frontend/lowering/lower-modules.ts | 36 +++ .../compiler/src/frontend/lowering/lowerer.ts | 11 +- packages/compiler/src/frontend/program.ts | 22 +- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 1 + .../corpus/8270-fs-promises-member.cjs.json | 6 + .../corpus/8271-cjs-top-level-return.cjs.json | 6 + .../8272-fs-stream-call-without-new.cjs.json | 6 + ...273-fs-read-write-argument-checks.cjs.json | 6 + .../main.js.json | 7 + packages/runtime/src/scr_bytes.h | 2 + packages/runtime/src/scr_bytes_io.c | 244 ++++++++++++++---- packages/runtime/src/scr_lib.c | 40 +-- tests/corpus/8270-fs-promises-member.cjs | 25 ++ tests/corpus/8271-cjs-top-level-return.cjs | 20 ++ .../8272-fs-stream-call-without-new.cjs | 21 ++ .../8273-fs-read-write-argument-checks.cjs | 69 +++++ .../cjs-required-top-level-return/lib.js | 5 + .../cjs-required-top-level-return/main.js | 8 + .../package.json | 3 + .../cjs-required-top-level-return/main.js.txt | 6 + .../8270-fs-promises-member.cjs.json | 1 + .../8271-cjs-top-level-return.cjs.json | 1 + .../8272-fs-stream-call-without-new.cjs.json | 1 + ...273-fs-read-write-argument-checks.cjs.json | 1 + 30 files changed, 551 insertions(+), 68 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8270-fs-promises-member.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8271-cjs-top-level-return.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8272-fs-stream-call-without-new.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8273-fs-read-write-argument-checks.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-required-top-level-return/main.js.json create mode 100644 tests/corpus/8270-fs-promises-member.cjs create mode 100644 tests/corpus/8271-cjs-top-level-return.cjs create mode 100644 tests/corpus/8272-fs-stream-call-without-new.cjs create mode 100644 tests/corpus/8273-fs-read-write-argument-checks.cjs create mode 100644 tests/diagnostics/cjs-required-top-level-return/lib.js create mode 100644 tests/diagnostics/cjs-required-top-level-return/main.js create mode 100644 tests/diagnostics/cjs-required-top-level-return/package.json create mode 100644 tests/harness/__snapshots__/cjs-required-top-level-return/main.js.txt create mode 100644 tests/harness/corpus-costs/8270-fs-promises-member.cjs.json create mode 100644 tests/harness/corpus-costs/8271-cjs-top-level-return.cjs.json create mode 100644 tests/harness/corpus-costs/8272-fs-stream-call-without-new.cjs.json create mode 100644 tests/harness/corpus-costs/8273-fs-read-write-argument-checks.cjs.json diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 7a16c37e8..40daa8205 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -523,6 +523,7 @@ export const LIB_FN_SYMS: Record = { "fs.openSync": "scr_fs_open", "fs.openNumericSync": "scr_fs_open_numeric", "fs.readSync": "scr_fs_read_sync", + "fs.readSyncDyn": "scr_fs_read_sync_dyn", "fs.writeSync": "scr_fs_write_sync", "fs.writeStrSync": "scr_fs_write_str_sync", "fs.closeSync": "scr_fs_close", diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts index 0bbc3b3fa..6b5e2d8ae 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts @@ -2,7 +2,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 { isJsSourceFile, locOf } from "../../program.js"; import { type BuiltinModuleFn, FS_WRITE_FILE_DOCUMENTED_OPTIONS, @@ -403,6 +403,28 @@ export function lowerFsReadSyncCall( } lowerer.noLowering("readSync with a non-literal options argument", optionsNode, supported); } + // JavaScript arguments outside the typed signature (an untyped fd or + // buffer, a bigint or untyped position) take Node's argument checks at + // run time, then the same read. + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { + const nodes = expr.arguments.nodes; + const typed = [F64, BYTES_U8, F64, F64, F64].every((want, i) => { + const node = nodes[i]; + if (node === undefined) return true; + let value: ts.Expression = node; + while (ts.isParenthesizedExpression(value)) value = value.expression; + if (i === 4 && value.kind === ts.SyntaxKind.NullKeyword) return true; + const t = lowerer.mapTypeOf(lowerer.typeOf(node)); + return t !== null && (want.kind === "bytes" ? t.kind === "bytes" : t.kind === "f64"); + }); + if (!typed) { + const args = [0, 1, 2, 3, 4].map((i): IrExpr => { + const node = nodes[i]; + return node === undefined ? dynUndefinedExpr(loc) : lowerer.lowerExprExpecting(node, DYN); + }); + return { kind: "libCall", fn: "fs.readSyncDyn", args, type: F64, loc }; + } + } const args: IrExpr[] = [ lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64), lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8), diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts index 498f9b6d4..6c6f8fa79 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts @@ -4,7 +4,7 @@ * and everything inherited goes through the stream spoke. */ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; -import { locOf } from "../../program.js"; +import { isJsSourceFile, locOf } from "../../program.js"; import { isNodeTypesPath } from "../../dts-paths.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; import { @@ -178,3 +178,32 @@ export function lowerFsStreamNew( locOf(expr), ); } + +/** `fs.ReadStream(path, options)` / `fs.WriteStream(...)` called without + * `new` in JavaScript: Node's stream constructors construct when called as + * functions, so the call is the `new` form. Null for other calls. */ +export function lowerFsStreamCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr | null { + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null; + const callee = expr.expression; + const name = ts.isPropertyAccessExpression(callee) + ? callee.name + : ts.isIdentifier(callee) + ? callee + : null; + if (name === null || !ts.isIdentifier(name)) return null; + const symbol = lowerer.checker.getSymbolAtLocation(name); + const resolved = + symbol && symbol.flags & ts.SymbolFlags.Alias + ? lowerer.checker.getAliasedSymbol(symbol) + : symbol; + const cls = fsStreamClassOf(lowerer, resolved); + if (cls === null) return null; + return fsStreamCreateExpr( + lowerer, + expr.arguments.nodes, + cls === "ReadStream", + `fs.${cls}`, + expr, + locOf(expr), + ); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts index 5465aeafa..5bac5651a 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts @@ -213,7 +213,9 @@ export function builtinNamespaceDestructureModuleOf( if (ts.isCallExpression(init)) { const cr = createRequireSpecOf(lowerer, init); module = cr !== null && cr.spec !== null ? canonicalBuiltinModule(cr.spec) : null; - } else if (ts.isIdentifier(init)) { + } else if (ts.isIdentifier(init) || ts.isPropertyAccessExpression(init)) { + // `const { open } = fs.promises` / `= require("fs").promises`: a member + // that is itself a supported module. module = lowerer.builtinNamespaceModuleOf(init); } if (module === null) return null; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 14565aebc..884c106b7 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -24,7 +24,11 @@ import { import { lowerMockTimersElementCall } from "./lower-test.js"; import { objectEnumerationReceiver } from "./object-enumeration-receiver.js"; import { InternalCompilerError } from "../../errors.js"; -import { isFsStreamMember, lowerFsStreamMethodCall } from "./builtins/fs-streams.js"; +import { + isFsStreamMember, + lowerFsStreamCall, + lowerFsStreamMethodCall, +} from "./builtins/fs-streams.js"; import * as ts from "../ts7/adapter.js"; import { isZlibStreamMember, @@ -375,6 +379,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const buffer = lowerBufferConstruct(lowerer, expr); if (buffer) return { kind: "dynFrom", value: buffer, type: DYN, loc }; } + { + const stream = lowerFsStreamCall(lowerer, expr); + if (stream) return stream; + } // A package shim's runtime intrinsic (a bodiless `declare function` // named scriptcUnixDgram* in a shipped shim): a direct runtime call. if (ts.isIdentifier(expr.expression) && isShimIntrinsicName(expr.expression.text)) { diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 19f7243ed..5c30c2250 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -1346,6 +1346,25 @@ export function collectGlobals( // "m.". const rawTag = lowerer.fileTag.get(sf) ?? ""; const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, ""); + // A `return` at the top level of a CommonJS module ends its evaluation + // (preflight admits it). In the entry module that ends the program's + // top-level code. A module the program requires would leave the exports + // assigned after the return undefined, which its typed export storage + // cannot hold, so it is refused there. + if (rawTag !== "" && isJsSourceFile(sf)) { + const early = topLevelReturnOf(sf); + if (early !== null) { + try { + lowerer.unsupported( + "SC1090", + early, + "top-level return statements in CommonJS modules the program requires (only the entry module can end its evaluation early)", + ); + } catch (e) { + if (!(e instanceof PoisonError)) throw e; + } + } + } // Expando function members (`foo.bar = 12` anywhere in the file) // register their module globals first — reads inside function bodies // collected earlier in this pass must resolve them (lower-expando.ts). @@ -3579,3 +3598,20 @@ export function buildMain(lowerer: Lowerer): IrFunction { loc, }; } + +/** The first `return` statement at the top level of `sf` (outside every + * function body and class), which a CommonJS module may contain. */ +function topLevelReturnOf(sf: ts.SourceFile): ts.ReturnStatement | null { + let found: ts.ReturnStatement | null = null; + const visit = (node: ts.Node): void => { + if (found !== null || ts.isFunctionLike(node)) return; + if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return; + if (ts.isReturnStatement(node)) { + found = node; + return; + } + ts.forEachChild(node, visit); + }; + for (const stmt of sf.statements.nodes) visit(stmt); + return found; +} diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 78980d322..eb7928dd4 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -13766,7 +13766,7 @@ export class Lowerer { if ( ts.isPropertyAccessExpression(expr) && !expr.questionDotToken && - ts.isIdentifier(expr.expression) + (ts.isIdentifier(expr.expression) || requireSpecOf(expr.expression) !== null) ) { const outer = this.builtinNamespaceModuleOf(expr.expression); if (outer === null) return null; @@ -13971,6 +13971,15 @@ export class Lowerer { `library functions as values (call '${ts.getTextOfNode(expr)}' directly)`, ); } + // A member that is a supported module in its own right (`fs.promises` + // is node:fs/promises, as in Node) reads as that module's namespace. + { + const sub = this.builtinNamespaceModuleOf(expr); + if (sub !== null) { + const type: IrType = { kind: "moduleNs", moduleId: `builtin:${sub}` }; + return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc }; + } + } this.noLowering( `${bi.module}.${bi.member}`, expr, diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index bf75127c4..f9be2b832 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -1379,9 +1379,16 @@ function suppressedJsStrictness7(d: ts.Diagnostic): boolean { return ts.flattenDiagnosticMessageText(d, "\n").includes("'any'"); } // The deprecated Buffer(...) call without `new`, which Node's Buffer - // function accepts (containers/bytes.ts lowers it). + // function accepts (containers/bytes.ts lowers it), and fs.ReadStream(...) + // and fs.WriteStream(...), which construct their stream + // (builtins/fs-streams.ts). if (d.code === 2348) { - return ts.flattenDiagnosticMessageText(d, "\n").includes("'BufferConstructor'"); + const text = ts.flattenDiagnosticMessageText(d, "\n"); + return ( + text.includes("'BufferConstructor'") || + text.includes("'typeof ReadStream'") || + text.includes("'typeof WriteStream'") + ); } return false; } @@ -5358,6 +5365,16 @@ function preflight7(load: LoadResult): { return new RegExp(`\\btypeof\\s*\\(?\\s*${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(sf.text); }; + /** A CommonJS file runs inside Node's module wrapper function, so a + * `return` at its top level ends the module's evaluation (TS1108 in a + * plain script). The lowering ends the module initializer there. */ + const cjsTopLevelReturnSuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => { + if (d.code !== 1108 || d.fileName === undefined || !isJsSourceFileName(d.fileName)) + return false; + const sf = p.getSourceFile(d.fileName); + return sf !== undefined && isCjsJsFile(sf, p); + }; + const workspaceImplicitAnySuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => { if (d.code !== 7016 || d.fileName === undefined || d.pos === undefined || d.end === undefined) return false; @@ -5404,6 +5421,7 @@ function preflight7(load: LoadResult): { !isNpmStaticSubclassArgument(p, d) && !workspaceImplicitAnySuppressed(p, d) && !typeofTestedNameSuppressed(p, d) && + !cjsTopLevelReturnSuppressed(p, d) && !jsdocTypeSuppressed(p, d, commentDup), ); }; diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 390c7fbc3..fb24f6596 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -1028,6 +1028,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fs.openSync", "fs.openNumericSync", "fs.readSync", + "fs.readSyncDyn", "fs.writeSync", "fs.writeStrSync", "fs.closeSync", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 216e46a4d..a1d969667 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1195,6 +1195,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "fs.openSync": { argTypes: [STRING, STRING], result: F64 }, "fs.openNumericSync": { argTypes: [STRING, F64, F64], result: F64 }, "fs.readSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 }, + "fs.readSyncDyn": { argTypes: [DYN, DYN, DYN, DYN, DYN], result: F64 }, "fs.writeSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 }, "fs.writeStrSync": { argTypes: [F64, STRING, F64, STRING], result: F64 }, // fs.watch's callback func type is program-dependent (zero params, or diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8270-fs-promises-member.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8270-fs-promises-member.cjs.json new file mode 100644 index 000000000..eb3938040 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8270-fs-promises-member.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8270-fs-promises-member.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8271-cjs-top-level-return.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8271-cjs-top-level-return.cjs.json new file mode 100644 index 000000000..2fcecbe67 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8271-cjs-top-level-return.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8271-cjs-top-level-return.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8272-fs-stream-call-without-new.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8272-fs-stream-call-without-new.cjs.json new file mode 100644 index 000000000..7fdb114e4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8272-fs-stream-call-without-new.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8272-fs-stream-call-without-new.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8273-fs-read-write-argument-checks.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8273-fs-read-write-argument-checks.cjs.json new file mode 100644 index 000000000..60e64d369 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8273-fs-read-write-argument-checks.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8273-fs-read-write-argument-checks.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-required-top-level-return/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-required-top-level-return/main.js.json new file mode 100644 index 000000000..d8eeb4f76 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-required-top-level-return/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/diagnostics/cjs-required-top-level-return/lib.js", + "/tests/diagnostics/cjs-required-top-level-return/main.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 7c0a935cc..6efb970f8 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -524,6 +524,8 @@ ScrDyn *scr_fs_lchmod_sync_chk(const ScrDyn *path, const ScrDyn *mode); ScrPromise *scr_fsp_lchmod_chk(const ScrDyn *path, const ScrDyn *mode); void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, const ScrDyn *length, const ScrDyn *position, const ScrStr *fence); +double scr_fs_read_sync_dyn(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, + const ScrDyn *length, const ScrDyn *position); void scr_fs_stream_opts_chk(const ScrDyn *path, const ScrDyn *opts, const ScrStr *fence); /* The fs streams' option pieces (scr_fs_stream.c): the getOptions, * options.fd and path checks above without the fence; the checked path diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 253cad1da..bebc2a479 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -638,6 +638,8 @@ void scr_fs_cb_schedule(const ScrDyn *callback, ScrDyn *result, const ScrDyn *bu static bool scr_fs_read_validate(const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *); static bool scr_fs_encoding_chk(const ScrDyn *); static bool scr_fs_int_range_chk(const ScrDyn *, const char *, double, double, const char *); +static bool scr_fs_length_int32(const ScrDyn *, double *); +static bool scr_fs_write_validate(const ScrDyn *, const ScrDyn *, const ScrDyn *); static bool scr_fs_mode_chk(const ScrDyn *, const char *); /* Where Node's signature handling finds the callback: a fixed position, or @@ -856,7 +858,7 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc * (below); every other form checks it here, before its path. */ bool late = !strcmp(op, "access") || !strcmp(op, "copyFile") || !strcmp(op, "chmod") || !strcmp(op, "open") || !strcmp(op, "mkdir") || !strcmp(op, "fchmod") || !strcmp(op, "ftruncate") || !strcmp(op, "truncate") || - !strcmp(op, "read"); + !strcmp(op, "read") || !strcmp(op, "write"); if (!late && !scr_fs_cb_chk(callback, legacy ? "callback" : "cb")) { scr_arr_release(vectors); return NULL; } size_t count = timestamp ? argc : slot < argc ? slot : argc; #define ARG(index) scr_fs_cb_arg(args, count, index) @@ -889,7 +891,8 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc const ScrDyn *params = count == 3 ? ARG(2) : scr_dyn_bytes_is(ARG(1), SCR_BYTES_U8) ? NULL : ARG(1); bool object = !params || scr_fs_dyn_absent(params) || params->kind == SCR_DYN_OBJ || params->kind == SCR_DYN_BYTES || - (params->kind == SCR_DYN_TYPED_REF && !scr_dyn_ref_is_array(params)); + (params->kind == SCR_DYN_TYPED_REF && !scr_dyn_ref_is_array(params)) || + (params->kind == SCR_DYN_PROXY && scr_dyn_is_object(params)); if (!object) { scr_dyn_arg_type_fail("options", "of type object", params); return NULL; @@ -902,6 +905,22 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc if (!scr_fs_cb_chk(callback, "cb")) return NULL; } if (!strcmp(op, "read") && count >= 4 && !scr_fs_read_validate(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4))) return NULL; + /* write checks its fd first, then the buffer form's callback, offset and + * length (validateOffsetLengthWrite), or the string form's data and + * callback. */ + if (!strcmp(op, "write")) { + if (!scr_fs_int_range_chk(ARG(0), "fd", 0, 2147483647.0, ">= 0 && <= 2147483647")) return NULL; + if (ARG(1)->kind == SCR_DYN_BYTES) { + if (!scr_fs_cb_chk(callback, "cb") || !scr_fs_write_validate(ARG(1), ARG(2), ARG(3))) return NULL; + } else { + if (ARG(1)->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("buffer", "of type string or an instance of Buffer, TypedArray, or DataView", + ARG(1)); + return NULL; + } + if (!scr_fs_cb_chk(callback, "cb")) return NULL; + } + } bool descriptor = timestamp_fd || vector || newControl || !strcmp(op, "close") || !strcmp(op, "fstat") || !strcmp(op, "fsync") || !strcmp(op, "read") || !strcmp(op, "write") || !strcmp(op, "fchown"); /* truncate checks len, then the callback, before the open its request * starts with. */ @@ -1100,7 +1119,10 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc return NULL; } else { double offset = ownedBuffer ? scr_fs_cb_option_number(options, "offset", 0) : scr_fs_cb_number(ARG(2), "offset", 0); - double length = ownedBuffer ? scr_fs_cb_option_number(options, "length", bytes->len - offset) : scr_fs_cb_number(ARG(3), "length", bytes->len - offset); + double length = bytes->len - offset; + if (ownedBuffer) length = scr_fs_cb_option_number(options, "length", length); + else if (readOp && count >= 4) scr_fs_length_int32(ARG(3), &length); + else length = scr_fs_cb_number(ARG(3), "length", length); double position = ownedBuffer ? scr_fs_cb_option_number(options, "position", -1) : scr_fs_cb_number(ARG(4), "position", -1); if (!scr_exc_pending()) result = scr_dyn_new_num(readOp ? scr_fs_read_sync(fd, bytes, offset, length, position) : scr_fs_write_sync(fd, bytes, offset, length, position)); } @@ -1877,68 +1899,134 @@ static bool scr_fs_int_range_chk(const ScrDyn *v, const char *name, double min, return true; } -static bool scr_fs_read_validate(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, - const ScrDyn *length, const ScrDyn *position) { - if (buffer->kind != SCR_DYN_BYTES) { - scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer); +/* `length |= 0`: ToInt32 of the length argument (a non-numeric value is + * 0). False with the exception pending when ToNumber throws. */ +static bool scr_fs_length_int32(const ScrDyn *length, double *out) { + double n; + if (length->kind == SCR_DYN_NUM) { + n = length->v.num; + } else if (!scr_dyn_number_coerce_js(length, &n)) { return false; } + if (!isfinite(n)) { + *out = 0; + return true; + } + double m = fmod(trunc(n), 4294967296.0); + if (m < 0) m += 4294967296.0; + *out = m >= 2147483648.0 ? m - 4294967296.0 : m; + return true; +} + +/* A bigint read position in [-1n, 2n ** 63n - 1n - BigInt(length)] + * (validatePosition); false with ERR_OUT_OF_RANGE pending otherwise. The + * received value groups the digits past 2^32 in threes, as Node's + * ERR_OUT_OF_RANGE does for bigints. */ +static bool scr_fs_bigint_position_chk(ScrBigInt *position, double length) { + ScrBigInt *top = scr_bigint_from_i64(INT64_MAX); + ScrBigInt *span = scr_bigint_from_f64(length); + ScrBigInt *max = scr_bigint_sub(top, span); + scr_bigint_release(top); + scr_bigint_release(span); + ScrBigInt *low = scr_bigint_from_i64(-1); + bool ok = scr_bigint_cmp_f64(position, low) >= 0 && scr_bigint_cmp_f64(position, max) <= 0; + scr_bigint_release(low); + if (ok) { + scr_bigint_release(max); + return true; + } + ScrStr *limit = scr_bigint_to_string(max, 10); + scr_bigint_release(max); + ScrStr *decimal = scr_bigint_to_string(position, 10); + if (!limit || !decimal) { + if (limit) scr_str_release(limit); + if (decimal) scr_str_release(decimal); + return false; + } + ScrBigInt *bound = scr_bigint_from_i64((int64_t)1 << 32); + ScrBigInt *negBound = scr_bigint_from_i64(-((int64_t)1 << 32)); + bool group = scr_bigint_cmp_f64(position, bound) > 0 || scr_bigint_cmp_f64(position, negBound) < 0; + scr_bigint_release(bound); + scr_bigint_release(negBound); + /* addNumericalSeparator: underscores every three digits from the right, + * after an optional sign. */ + char digits[200]; + size_t len = decimal->len < 120 ? decimal->len : 120, out = 0; + size_t start = len > 0 && decimal->data[0] == '-' ? 1 : 0; + for (size_t i = 0; i < len; i++) { + if (group && i > start && (len - i) % 3 == 0) digits[out++] = '_'; + digits[out++] = (char)decimal->data[i]; + } + digits[out] = '\0'; + char msg[384]; + int n = snprintf(msg, sizeof msg, + "The value of \"position\" is out of range. It must be >= -1 && <= %.*s. Received %sn", + (int)limit->len, (const char *)limit->data, digits); + scr_str_release(limit); + scr_str_release(decimal); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, n < 0 ? 0 : (size_t)n, "ERR_OUT_OF_RANGE"); + return false; +} + + +/* fs.read(fd, buffer, offset, length, position, cb)'s argument checks, in + * Node's order: the fd, the buffer, the offset (an integer >= 0), the + * length as `length |= 0`, the position (validatePosition, against that + * length), then for a non-empty read the empty-buffer and + * validateOffsetLengthRead checks. */ +static bool scr_fs_read_validate(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, + const ScrDyn *length, const ScrDyn *position) { if (fd->kind != SCR_DYN_NUM) { scr_dyn_arg_type_fail("fd", "of type number", fd); return false; } + if (buffer->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer); + return false; + } double buflen = scr_bytes_byte_len(buffer->v.bytes); + double off = 0; if (!scr_fs_dyn_absent(offset)) { - /* validateInteger's MAX_SAFE range first, the buffer bound second — - * Node renders each with its own max. */ if (!scr_fs_int_range_chk(offset, "offset", 0, 9007199254740991.0, ">= 0 && <= 9007199254740991")) { return false; } - if (offset->v.num > buflen) { - char range[64]; - snprintf(range, sizeof range, ">= 0 && <= %.0f", buflen); - if (!scr_fs_int_range_chk(offset, "offset", 0, buflen, range)) return false; - } + off = offset->v.num; } + double count = 0; + if (!scr_fs_length_int32(length, &count)) return false; if (!scr_fs_dyn_absent(position)) { - if (position->kind != SCR_DYN_NUM) { + if (position->kind == SCR_DYN_BIGINT) { + if (!scr_fs_bigint_position_chk(position->v.bigint, count)) return false; + } else if (position->kind != SCR_DYN_NUM) { scr_dyn_arg_type_fail("position", "of type bigint or integer", position); return false; - } - if (!scr_fs_int_range_chk(position, "position", -1, 9007199254740991.0, - ">= -1 && <= 9007199254740991")) { + } else if (!scr_fs_int_range_chk(position, "position", -1, 9007199254740991.0, + ">= -1 && <= 9007199254740991")) { return false; } } - double off = offset->kind == SCR_DYN_NUM ? offset->v.num : 0; - if (!scr_fs_dyn_absent(length)) { - if (length->kind != SCR_DYN_NUM || !(isfinite(length->v.num) && trunc(length->v.num) == length->v.num) || - length->v.num < 0) { - /* Node renders the bare ">= 0" form here (checkPosition's cousin). */ - if (length->kind == SCR_DYN_NUM) { - char recv[48], msg[160]; - scr_num_received(length->v.num, recv); - const char *shape = (isfinite(length->v.num) && trunc(length->v.num) == length->v.num) - ? "It must be >= 0." - : "It must be an integer."; - int len = snprintf(msg, sizeof msg, - "The value of \"length\" is out of range. %s Received %s", shape, recv); - scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); - return false; - } - scr_dyn_arg_type_fail("length", "of type number", length); - return false; - } - if (length->v.num > buflen - off) { - char recv[48], msg[160]; - scr_num_received(length->v.num, recv); - int len = snprintf(msg, sizeof msg, - "The value of \"length\" is out of range. It must be <= %.0f. Received %s", - buflen - off, recv); - scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); - return false; + if (count == 0) return true; + if (buflen == 0) { + if (scr_bytes_is_buffer(buffer->v.bytes)) { + static const char empty[] = + "The argument 'buffer' is empty and cannot be written. Received "; + scr_throw_error_msg_code(SCR_ERR_TYPE, empty, sizeof empty - 1, "ERR_INVALID_ARG_VALUE"); + } else { + scr_dyn_arg_value_fail("buffer", "is empty and cannot be written", buffer); } + return false; + } + if (count < 0 || off + count > buflen) { + char recv[48], range[64], msg[192]; + scr_num_received(count, recv); + if (count < 0) snprintf(range, sizeof range, ">= 0"); + else snprintf(range, sizeof range, "<= %.0f", buflen - off); + int len = snprintf(msg, sizeof msg, + "The value of \"length\" is out of range. It must be %s. Received %s", range, + recv); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); + return false; } return true; } @@ -1991,3 +2079,69 @@ bool scr_fs_stream_mode(const ScrDyn *value, double *mode) { } return scr_fs_cb_file_mode(value, "mode", mode); } + +/* fs.readSync(fd, buffer, offset, length[, position]) with arguments the + * compiler could not type (JavaScript): Node's checks (validateBuffer + * first, then the fd, the offset, `length |= 0` and the position, which + * may be a bigint), then the typed read. */ +double scr_fs_read_sync_dyn(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, + const ScrDyn *length, const ScrDyn *position) { + if (buffer->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer); + return 0; + } + if (!scr_fs_read_validate(fd, buffer, offset, length, position)) return 0; + double count = 0; + if (!scr_fs_length_int32(length, &count)) return 0; + if (count == 0) return 0; + double at = scr_fs_dyn_absent(position) ? -1 + : position->kind == SCR_DYN_BIGINT ? scr_bigint_to_f64(position->v.bigint) + : position->v.num; + ScrBytes *bytes = scr_dyn_bytes_unbox(buffer); + double read = scr_fs_read_sync(fd->v.num, bytes, scr_fs_dyn_absent(offset) ? 0 : offset->v.num, + count, at); + scr_bytes_release(bytes); + return read; +} + +/* ERR_OUT_OF_RANGE for the number `n` of argument `name`. Always false. */ +static bool scr_fs_num_range_fail(const char *name, const char *range, double n) { + char recv[48], msg[192]; + scr_num_received(n, recv); + int len = snprintf(msg, sizeof msg, "The value of \"%s\" is out of range. It must be %s. Received %s", + name, range, recv); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); + return false; +} + +/* fs.write(fd, buffer, offset, length, position, cb)'s buffer checks: the + * offset (an integer >= 0 unless omitted, null or the callback), a + * non-number length defaulting to the rest of the buffer, then + * validateOffsetLengthWrite. An options object in the offset slot is read + * by the write itself. */ +static bool scr_fs_write_validate(const ScrDyn *buffer, const ScrDyn *offset, const ScrDyn *length) { + if (offset->kind == SCR_DYN_OBJ || offset->kind == SCR_DYN_TYPED_REF || offset->kind == SCR_DYN_PROXY || + offset->kind == SCR_DYN_NULL) + return true; + double buflen = scr_bytes_byte_len(buffer->v.bytes); + double off = 0; + if (!scr_fs_dyn_absent(offset) && offset->kind != SCR_DYN_FUNC) { + if (!scr_fs_int_range_chk(offset, "offset", 0, 9007199254740991.0, ">= 0 && <= 9007199254740991")) + return false; + off = offset->v.num; + } + double len = length->kind == SCR_DYN_NUM ? length->v.num : buflen - off; + char range[64]; + if (off > buflen) { + snprintf(range, sizeof range, "<= %.0f", buflen); + return scr_fs_num_range_fail("offset", range, off); + } + if (len > buflen - off) { + snprintf(range, sizeof range, "<= %.0f", buflen - off); + return scr_fs_num_range_fail("length", range, len); + } + if (len < 0) return scr_fs_num_range_fail("length", ">= 0", len); + if (!(isfinite(len) && trunc(len) == len)) return scr_fs_num_range_fail("length", "an integer", len); + if (len > 2147483647.0) return scr_fs_num_range_fail("length", ">= 0 && <= 2147483647", len); + return true; +} diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index fb3524c81..b7deae018 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -4236,27 +4236,35 @@ double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length, scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE"); return 0; } - /* Node returns after the offset and position validation when the - * requested length normalizes to zero: buffer-window bounds and fd are - * not consulted. Preserve the existing size_t coercion's [0, 1) case. */ - if (length >= 0 && length < 1) return 0; - if (offset > (double)bytelen) { - numbuf[scr_f64_to_str(offset, numbuf)] = 0; + /* `length |= 0`, then Node's order: a zero length reads nothing after + * the offset and position checks; otherwise the buffer must not be + * empty and the window must fit it (validateOffsetLengthRead). */ + if (!isfinite(length)) { + length = 0; + } else { + double wrapped = fmod(trunc(length), 4294967296.0); + if (wrapped < 0) wrapped += 4294967296.0; + length = wrapped >= 2147483648.0 ? wrapped - 4294967296.0 : wrapped; + } + if (length == 0) return 0; + if (bytelen == 0) { + static const char empty[] = + "The argument 'buffer' is empty and cannot be written. Received "; + scr_throw_error_msg_code(SCR_ERR_TYPE, empty, sizeof empty - 1, "ERR_INVALID_ARG_VALUE"); + return 0; + } + if (length < 0 || offset + length > (double)bytelen) { + char recv[48], range[48]; + scr_num_received(length, recv); + if (length < 0) snprintf(range, sizeof range, ">= 0"); + else snprintf(range, sizeof range, "<= %.0f", (double)bytelen - offset); mlen = snprintf(msg, sizeof msg, - "The value of \"offset\" is out of range. It must be >= 0 && <= 9007199254740991. Received %s", - numbuf); + "The value of \"length\" is out of range. It must be %s. Received %s", range, + recv); scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE"); return 0; } size_t off = (size_t)offset; - if (length < 0 || (double)length > (double)(bytelen - off)) { - numbuf[scr_f64_to_str(length, numbuf)] = 0; - mlen = snprintf(msg, sizeof msg, - "The value of \"length\" is out of range. It must be <= %zu. Received %s", - bytelen - off, numbuf); - scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE"); - return 0; - } size_t want = (size_t)length; uint8_t *bounce = buf->shared ? malloc(want ? want : 1) : NULL; if (buf->shared && !bounce) scr_trap("scriptc: out of memory\n"); diff --git a/tests/corpus/8270-fs-promises-member.cjs b/tests/corpus/8270-fs-promises-member.cjs new file mode 100644 index 000000000..86448f012 --- /dev/null +++ b/tests/corpus/8270-fs-promises-member.cjs @@ -0,0 +1,25 @@ +'use strict'; +// fs.promises is node:fs/promises, as in Node: through a binding, an +// inline require, destructuring and a direct member call. +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scriptc-fsp-')); +const file = path.join(dir, 'data.txt'); +const fsPromises = fs.promises; +const { readFile, stat } = require('fs').promises; +const { open } = fs.promises; + +(async () => { + await fsPromises.writeFile(file, 'abc'); + await fs.promises.appendFile(file, 'def'); + console.log(await readFile(file, 'utf8')); + console.log((await stat(file)).size); + const handle = await open(file, 'r'); + const { bytesRead, buffer } = await handle.read(Buffer.alloc(3), 0, 3, 1); + console.log(bytesRead, buffer.toString()); + await handle.close(); + await fsPromises.rm(dir, { recursive: true }); + console.log(fs.existsSync(dir)); +})(); diff --git a/tests/corpus/8271-cjs-top-level-return.cjs b/tests/corpus/8271-cjs-top-level-return.cjs new file mode 100644 index 000000000..9f5cc6d23 --- /dev/null +++ b/tests/corpus/8271-cjs-top-level-return.cjs @@ -0,0 +1,20 @@ +'use strict'; +// A return at the top level of the entry CommonJS module ends its +// evaluation, as in Node's module wrapper: from a block, a loop and a try +// statement, whose finally block still runs. +const fs = require('fs'); + +console.log('start'); +for (const name of ['a', 'b', 'c']) { + if (name === 'c') { + try { + console.log('returning at', name); + process.on('exit', (code) => console.log('exit', code)); + if (!fs.existsSync('/scriptc-missing-path')) return; + } finally { + console.log('finally runs'); + } + } + console.log('visited', name); +} +console.log('not reached'); diff --git a/tests/corpus/8272-fs-stream-call-without-new.cjs b/tests/corpus/8272-fs-stream-call-without-new.cjs new file mode 100644 index 000000000..91fe0a6c0 --- /dev/null +++ b/tests/corpus/8272-fs-stream-call-without-new.cjs @@ -0,0 +1,21 @@ +'use strict'; +// fs.ReadStream and fs.WriteStream called without new construct their +// stream, as in Node. +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scriptc-streams-')); +const target = path.join(dir, 'out.txt'); +const writer = fs.WriteStream(target); +writer.on('finish', () => { + const reader = fs.ReadStream(target, { encoding: 'utf8' }); + let text = ''; + reader.on('data', (chunk) => (text += chunk)); + reader.on('end', () => { + console.log(text); + fs.rmSync(dir, { recursive: true }); + }); +}); +writer.write('hello '); +writer.end('streams'); diff --git a/tests/corpus/8273-fs-read-write-argument-checks.cjs b/tests/corpus/8273-fs-read-write-argument-checks.cjs new file mode 100644 index 000000000..623d5286f --- /dev/null +++ b/tests/corpus/8273-fs-read-write-argument-checks.cjs @@ -0,0 +1,69 @@ +'use strict'; +// fs.read, fs.readSync and fs.write check their arguments in Node's order +// and with Node's errors: the fd, the buffer, the offset, `length |= 0`, +// number and bigint positions, and the buffer window. +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scriptc-fs-args-')); +const file = path.join(dir, 'data.txt'); +fs.writeFileSync(file, 'abcdef'); +const fd = fs.openSync(file, 'r+'); +const noop = () => {}; + +function attempt(name, fn) { + try { + fn(); + console.log(name, 'ok'); + } catch (error) { + console.log(name, error.name, error.code, error.message); + } +} + +attempt('read offset -1', () => fs.read(fd, Buffer.alloc(3), -1, 3, 0, noop)); +attempt('read offset NaN', () => fs.read(fd, Buffer.alloc(3), NaN, 3, 0, noop)); +attempt('read length -1', () => fs.read(fd, Buffer.alloc(3), 0, -1, 0, noop)); +attempt('read length 4', () => fs.read(fd, Buffer.alloc(3), 0, 4, 0, noop)); +attempt('read length 1.5', () => fs.read(fd, Buffer.alloc(3), 0, 1.5, 0, noop)); +attempt('read length "x"', () => fs.read(fd, Buffer.alloc(3), 0, 'x', 0, noop)); +attempt('read position 1.5', () => fs.read(fd, Buffer.alloc(3), 0, 3, 1.5, noop)); +attempt('read position -2', () => fs.read(fd, Buffer.alloc(3), 0, 3, -2, noop)); +attempt('read position ""', () => fs.read(fd, Buffer.alloc(3), 0, 3, '', noop)); +attempt('read position 2n ** 63n', () => fs.read(fd, Buffer.alloc(3), 0, 3, 2n ** 63n, noop)); +attempt('read position -2n', () => fs.read(fd, Buffer.alloc(3), 0, 3, -2n, noop)); +attempt('read empty buffer', () => fs.read(fd, Buffer.alloc(0), 0, 1, 0, noop)); +attempt('read fd true', () => fs.read(true, Buffer.alloc(3), 0, 3, 0, noop)); +attempt('read buffer number', () => fs.read(fd, 3, 0, 3, 0, noop)); + +const read = (position) => { + const buffer = Buffer.alloc(3); + const count = fs.readSync(fd, buffer, 0, 3, position); + return `${count} ${buffer.toString()}`; +}; +attempt('readSync position 1n', () => console.log(read(1n))); +attempt('readSync position 0n', () => console.log(read(0n))); +attempt('readSync position 2n ** 63n', () => read(2n ** 63n)); +attempt('readSync offset 4', () => fs.readSync(fd, Buffer.alloc(3), 4, 1, 0)); +attempt('readSync buffer number', () => fs.readSync(fd, 3, 0, 1, 0)); +attempt('readSync fd null', () => fs.readSync(null, Buffer.alloc(3), 0, 1, 0)); +console.log(fs.readSync(fd, Buffer.alloc(3), 0, 0.5, 0), fs.readSync(fd, Buffer.alloc(3), 0, 2.5, 0)); + +attempt('write fd null', () => fs.write(null, Buffer.alloc(1), 0, 1, noop)); +attempt('write fd "1"', () => fs.write('1', 'x', noop)); +attempt('write offset -1', () => fs.write(fd, Buffer.alloc(0), -1, noop)); +attempt('write offset NaN', () => fs.write(fd, Buffer.from('abcd'), NaN, 4, 0, noop)); +attempt('write offset 5', () => fs.write(fd, Buffer.from('abcd'), 5, noop)); +attempt('write length 5', () => fs.write(fd, Buffer.from('abcd'), 0, 5, 0, noop)); +attempt('write length -1', () => fs.write(fd, Buffer.alloc(0), 0, -1, noop)); +attempt('write data number', () => fs.write(fd, 4, noop)); +attempt('write no callback', () => fs.write(fd, Buffer.from('a'), 0, 1, 0)); + +fs.read(fd, Buffer.alloc(2), 0, 2, 2n, (error, count, buffer) => { + console.log('read 2n', error, count, buffer.toString()); + fs.write(fd, Buffer.from('XY'), 0, 2, 0, (error2, written) => { + console.log('wrote', error2, written, fs.readFileSync(file, 'utf8')); + fs.closeSync(fd); + fs.rmSync(dir, { recursive: true }); + }); +}); diff --git a/tests/diagnostics/cjs-required-top-level-return/lib.js b/tests/diagnostics/cjs-required-top-level-return/lib.js new file mode 100644 index 000000000..62e017964 --- /dev/null +++ b/tests/diagnostics/cjs-required-top-level-return/lib.js @@ -0,0 +1,5 @@ +'use strict'; + +exports.ready = true; +if (process.env.SCRIPTC_SKIP_LIB === '1') return; +exports.value = 42; diff --git a/tests/diagnostics/cjs-required-top-level-return/main.js b/tests/diagnostics/cjs-required-top-level-return/main.js new file mode 100644 index 000000000..0b62093c2 --- /dev/null +++ b/tests/diagnostics/cjs-required-top-level-return/main.js @@ -0,0 +1,8 @@ +// A top-level return in a CommonJS module the program requires: the +// exports assigned after it may stay undefined, which the module's typed +// export storage cannot hold, so the return is refused. The entry module's +// own top-level return compiles (tests/corpus/8271-cjs-top-level-return.cjs). +'use strict'; + +const lib = require('./lib.js'); +console.log(lib.ready, lib.value); diff --git a/tests/diagnostics/cjs-required-top-level-return/package.json b/tests/diagnostics/cjs-required-top-level-return/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/tests/diagnostics/cjs-required-top-level-return/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/harness/__snapshots__/cjs-required-top-level-return/main.js.txt b/tests/harness/__snapshots__/cjs-required-top-level-return/main.js.txt new file mode 100644 index 000000000..db3d72b88 --- /dev/null +++ b/tests/harness/__snapshots__/cjs-required-top-level-return/main.js.txt @@ -0,0 +1,6 @@ +cjs-required-top-level-return/lib.js:4:43 - error SC1090: top-level return statements in CommonJS modules the program requires (only the entry module can end its evaluation early) are not supported yet + + 3 | exports.ready = true; + 4 | if (process.env.SCRIPTC_SKIP_LIB === '1') return; + | ^~~~~~~ + 5 | exports.value = 42; \ No newline at end of file diff --git a/tests/harness/corpus-costs/8270-fs-promises-member.cjs.json b/tests/harness/corpus-costs/8270-fs-promises-member.cjs.json new file mode 100644 index 000000000..04488e737 --- /dev/null +++ b/tests/harness/corpus-costs/8270-fs-promises-member.cjs.json @@ -0,0 +1 @@ +{ "plain": 4040, "san": 4008 } diff --git a/tests/harness/corpus-costs/8271-cjs-top-level-return.cjs.json b/tests/harness/corpus-costs/8271-cjs-top-level-return.cjs.json new file mode 100644 index 000000000..33a1c426c --- /dev/null +++ b/tests/harness/corpus-costs/8271-cjs-top-level-return.cjs.json @@ -0,0 +1 @@ +{ "plain": 1907, "san": 2109 } diff --git a/tests/harness/corpus-costs/8272-fs-stream-call-without-new.cjs.json b/tests/harness/corpus-costs/8272-fs-stream-call-without-new.cjs.json new file mode 100644 index 000000000..dab2504e6 --- /dev/null +++ b/tests/harness/corpus-costs/8272-fs-stream-call-without-new.cjs.json @@ -0,0 +1 @@ +{ "plain": 2099, "san": 2373 } diff --git a/tests/harness/corpus-costs/8273-fs-read-write-argument-checks.cjs.json b/tests/harness/corpus-costs/8273-fs-read-write-argument-checks.cjs.json new file mode 100644 index 000000000..1989bfb5f --- /dev/null +++ b/tests/harness/corpus-costs/8273-fs-read-write-argument-checks.cjs.json @@ -0,0 +1 @@ +{ "plain": 2379, "san": 2450 } From 5ad3576c1e71e49750ad854cb12b4a7be36b7d66 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 07:39:14 -0500 Subject: [PATCH 052/273] Hold a Map.get result as the entry's reference until the map can change A local initialized from Map.get of a reference value retained the value and released it at the end of its scope. In the common update const cur = counts.get(key); if (cur === undefined) counts.set(key, new Counter()); else cur.num++; the map is written only on the branch that does not read the local, so the entry keeps the value alive for every read. A stable local whose every use comes before any statement that may change a reference edge (ReferenceEffects) now holds the entry's own reference: no retain on the read and no release at scope exit. An if statement passes its condition's state to both branches and joins them; any other statement that names the local is judged whole. Number keys and inline string keys take this path, for nullable results and for the stack-boxed results of union locals. --- .../src/backend/llvm/emit-function.ts | 18 ++- .../src/backend/llvm/emit-statements.ts | 15 +- packages/compiler/src/backend/llvm/emitter.ts | 3 + .../src/backend/llvm/expr-containers.ts | 59 ++++++++ .../src/backend/llvm/map-number-lookup.ts | 25 ++-- .../backend/llvm/map-read-lifetimes.test.ts | 10 +- .../src/backend/llvm/map-read-lifetimes.ts | 96 ++++++++++++- .../test/map-get-borrow-emission.test.ts | 58 ++++++++ .../tests/corpus/8181-map-get-borrows.ts.json | 6 + tests/corpus/8181-map-get-borrows.ts | 130 ++++++++++++++++++ .../corpus-costs/8181-map-get-borrows.ts.json | 1 + 11 files changed, 405 insertions(+), 16 deletions(-) create mode 100644 packages/compiler/test/map-get-borrow-emission.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8181-map-get-borrows.ts.json create mode 100644 tests/corpus/8181-map-get-borrows.ts create mode 100644 tests/harness/corpus-costs/8181-map-get-borrows.ts.json diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 8f306035a..4f04df59c 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -20,7 +20,12 @@ import { constructorClass } from "../../ir/final-fields.js"; import { findLoopArrayBorrows } from "./loop-array-borrows.js"; import { findCallArrayReads, findLocalArrayReads } from "./local-array-reads.js"; import { findScalarStringSlices, findScratchStringSlices } from "./string-slices.js"; -import { findMapReadLifetimes, findReadNumberMapWrites } from "./map-read-lifetimes.js"; +import { + findMapReadBorrows, + findMapReadLifetimes, + findReadNumberMapWrites, +} from "./map-read-lifetimes.js"; +import { borrowableMapGet } from "./expr-containers.js"; import { findMapEntryFusions } from "./map-entry-fusion.js"; import { findLocalStackUnions } from "./stack-unions.js"; import { findLocalStackObjects, findReassignedStackObjects } from "./stack-objects.js"; @@ -193,6 +198,17 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { fn, emitter.boxedUnionsById, emitter.callLifetimes, + emitter.referenceEffects, + ); + const stable = emitter.callLifetimes.bindings.get(fn.name); + emitter.borrowedMapGets = findMapReadBorrows( + fn, + emitter.referenceEffects, + (decl) => + decl.init !== null && + stable?.has(decl.localId) === true && + !emitter.mapReadLifetimes.locals.has(decl.localId) && + borrowableMapGet(emitter, decl.init), ); emitter.readNumberMapWrites = findReadNumberMapWrites(fn); emitter.mapEntryFusion = emitter.debug === null ? findMapEntryFusions(fn) : null; diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 55e31219c..fdcc54b55 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -32,7 +32,7 @@ import { allocateLocalUnion, storeLocalUnion } from "./local-union-storage.js"; import { boxAccess, boxNewCall, elemAccess } from "./shapes.js"; import { dynHoleRead } from "./loop-array-borrows.js"; import { endsWithJump, matchStringSelfConcat } from "../../ir/analysis.js"; -import { charAtUnitRead, emitDiscardedSplice } from "./expr-containers.js"; +import { charAtUnitRead, emitBorrowedMapGet, emitDiscardedSplice } from "./expr-containers.js"; import { InternalCompilerError } from "../../errors.js"; import { emitDenseArrayGet, @@ -104,6 +104,19 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } const b = emitter.binding(s.localId); + if ( + emitter.borrowedMapGets.has(s.localId) && + b.kind === "local" && + s.init?.kind === "mapIntrinsic" + ) { + // The map entry's own reference: no retain now, no release at scope + // exit (findMapReadBorrows). + const borrowed = emitBorrowedMapGet(emitter, s.init); + if (borrowed) { + B.line(`store ptr ${borrowed.name}, ptr ${b.slot}`); + break; + } + } if (emitter.walkBorrows.has(s.localId) && b.kind === "local") { // A borrowed walk pointer: no retain now, no release at scope exit. const v = s.init === null ? "null" : emitter.emitReadReceiver(s.init).name; diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 6760f3b74..53cb99c9e 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1145,6 +1145,9 @@ export class LlEmitter { readonly checkedNarrows: CheckedNarrows; callArrayReads = new Map(); mapReadLifetimes: MapReadLifetimes = { locals: new Map(), arguments: new Map() }; + /** Locals of the current function that hold a Map.get result as the + * entry's own reference (findMapReadBorrows, borrowableMapGet). */ + borrowedMapGets: ReadonlySet = new Set(); readNumberMapWrites: ReadonlySet = new Set(); /** The current function's fused string-key Map get-then-set pairs (map-entry-fusion.ts). */ mapEntryFusion: MapEntryFusion | null = null; diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 279b8c044..906f03b76 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -1127,6 +1127,65 @@ export function emitArrIntrinsic( } } +/** Whether `e` is a Map.get that emitBorrowedMapGet can emit without a + * retain: a typed map with number or inline string keys, a reference value, + * and a nullable result (the value's pointer, or the absent arm's). */ +export function borrowableMapGet(host: LlvmEmitterContext, e: IrExpr): boolean { + if (e.kind !== "mapIntrinsic" || e.method !== "get" || e.args.length !== 1) return false; + if (!borrowsMapReadInputs(e) || e.receiver.type.kind !== "map") return false; + const { key, value } = e.receiver.type; + if (!isRefCounted(value) || value.kind === "union" || key.kind === "union") return false; + if (e.type.kind !== "union" || !host.nullableUnions.has(e.type.unionId)) return false; + const arg = e.args[0]!; + const access = mapKeyAccess(key); + return ( + (access === "f64" && arg.type.kind === "f64") || + (access === "str" && arg.type.kind === "string") + ); +} + +/** A borrowableMapGet read as the entry's own reference: no retain, and + * no owner. The caller proves that nothing can change the map while the + * value is used (findMapReadBorrows). Null, before emitting anything, for + * a key held as a split span, which takes the runtime's owned lookup. */ +export function emitBorrowedMapGet( + host: LlvmEmitterContext, + e: IrExpr & { kind: "mapIntrinsic" }, +): LlValue | null { + const arg = e.args[0]!; + if (arg.kind === "varRef" && host.splitSpans.get(arg.localId) !== undefined) return null; + const B = host.B; + const mapType = e.receiver.type as IrType & { kind: "map" }; + const r = host.emitStableReceiver(e.receiver, e.args); + const k = emitMapLookupKey(host, e.args[0]!, true); + const numberKey = inlineNumberKey(k); + const stringKey = entryStringKey(k); + const value = mapType.value; + let raw: string; + if (numberKey !== null) raw = emitNumberMapGetRef(host, r.name, numberKey, value, true); + else if (stringKey !== null) + raw = emitStringMapGetRef( + host, + r.name, + stringKey, + value, + compactMapValues(host, mapType.key, value), + true, + ); + else throw new InternalCompilerError("llvm emitter bug: borrowed Map.get without an inline key"); + const resultType = e.type as IrType & { kind: "union" }; + const absent = host.unitInstanceRef( + resultType.unionId, + undefinedArmTag(resultType, host.unionsById), + ); + if (absent === "null") return { name: raw, type: e.type }; + const isnull = B.tmp(), + t = B.tmp(); + B.line(`${isnull} = icmp eq ptr ${raw}, null`); + B.line(`${t} = select i1 ${isnull}, ptr ${absent}, ptr ${raw}`); + return { name: t, type: e.type }; +} + export function wrapNullable( host: LlvmEmitterContext, raw: string, diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.ts b/packages/compiler/src/backend/llvm/map-number-lookup.ts index 6d219b50a..64bc3516e 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.ts @@ -122,14 +122,16 @@ export function emitStringMapGetScalar( } /** Map.get of a reference value: a +1 value, or null when absent — the - * contract of scr_map_get_f64_ref. */ + * contract of scr_map_get_f64_ref. `borrow`: the entry's own reference, + * without a retain, for a reader that finishes before the map can change. */ export function emitNumberMapGetRef( host: LlvmEmitterContext, map: string, key: string, value: IrType, + borrow = false, ): string { - return emitMapEntryGetRef(host, emitNumberMapEntry(host, map, key), value); + return emitMapEntryGetRef(host, emitNumberMapEntry(host, map, key), value, false, borrow); } /** Map.get of a reference value under a string key, with scr_map_get_str_ref's @@ -141,16 +143,25 @@ export function emitStringMapGetRef( key: string, value: IrType, compact = false, + borrow = false, ): string { - return emitMapEntryGetRef(host, emitStringMapEntry(host, map, key, compact), value, compact); + return emitMapEntryGetRef( + host, + emitStringMapEntry(host, map, key, compact), + value, + compact, + borrow, + ); } -/** The +1 value of a found entry, or null for a missing (null) entry. */ +/** The +1 value of a found entry (its own reference when `borrow`), or + * null for a missing (null) entry. */ function emitMapEntryGetRef( host: LlvmEmitterContext, entry: string, value: IrType, compact = false, + borrow = false, ): string { const B = host.B; const found = B.tmp(); @@ -160,10 +171,8 @@ function emitMapEntryGetRef( join = B.newLabel("mapn.join"); B.condBr(found, hit, miss); B.startBlock(hit); - const retained = host.retainFrame( - compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr"), - value, - ); + const loaded = compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr"); + const retained = borrow ? loaded : host.retainFrame(loaded, value); B.br(join); B.startBlock(miss); B.br(join); diff --git a/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts b/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts index 3725db060..d91e2b211 100644 --- a/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts +++ b/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts @@ -120,7 +120,12 @@ test("local map results keep scalar and owned reference payloads without heap wr expect(ir).toContain("call ptr @sc_map_entry_str("); expect(ir).not.toContain("@scr_map_get_"); } - if (value === STRING) expect(ir).toContain("@scr_str_release"); + // Nothing can change the map before the tag test, so a reference + // result is the entry's own (findMapReadBorrows): no retain or release. + if (value === STRING) { + expect(ir).not.toContain("@scr_str_retain"); + expect(ir).not.toContain("@scr_str_release"); + } } } }); @@ -132,7 +137,8 @@ test("nullable record results are the looked-up pointer itself on both ABIs", () expect(ir).not.toContain("%ScrUnion"); expect(ir).not.toContain("@scr_union_"); expect(ir).toMatch(/icmp (?:eq|ne) ptr %\w+, null/); - expect(ir).toContain("@sc_rrelease_"); + // The pointer is the entry's own reference until the tag test. + expect(ir).not.toContain("@sc_rrelease_"); } }); diff --git a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts index 6b58d314a..0f22f9e05 100644 --- a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts @@ -9,6 +9,7 @@ import { } from "../../ir/ir.js"; import { everyExpr, everyStmt, everyStmtList } from "../../ir/traverse.js"; import type { CallLifetimes } from "./call-lifetimes.js"; +import type { ReferenceEffects } from "./reference-effects.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { emitNumberMapGetRef, @@ -68,6 +69,10 @@ export function borrowsMapMutationReceiver(expr: IrExpr): boolean { export interface LocalMapRead { /** The Map.get expression itself. */ expr: IrExpr; + /** A local's reference result may stay the entry's own reference: every + * use of the local runs before anything that can change a reference + * edge (findMapReadBorrows), so the entry still holds the value. */ + borrowed?: boolean; type: IrType; receiver: IrExpr; key: IrExpr; @@ -126,6 +131,7 @@ export function findMapReadLifetimes( fn: IrFunction, unions: ReadonlyMap, lifetimes: CallLifetimes, + effects?: ReferenceEffects, ): MapReadLifetimes { const result: MapReadLifetimes = { locals: new Map(), arguments: new Map() }; if (fn.async || fn.generator) return result; @@ -150,6 +156,85 @@ export function findMapReadLifetimes( return true; }, }); + if (effects) { + const borrowed = findMapReadBorrows(fn, effects, (decl) => { + const read = result.locals.get(decl.localId); + return read !== undefined && isRefCounted(read.value); + }); + for (const id of borrowed) result.locals.get(id)!.borrowed = true; + } + return result; +} + +/** The locals, declared by statements `candidate` accepts, that can hold a + * map entry's own reference instead of a +1 copy. The statements after the + * declaration run in order, and every one that names the local must + * preserve reference edges (ReferenceEffects) and run before any statement + * that may not, so no write, deletion or unknown call can release the value + * while the local is read. An `if` passes its condition's state to both + * branches and joins them; any other statement that names the local is + * judged whole. A later pass of an enclosing loop declares the local again, + * so only the rest of its own statement list matters. An update such as + * `const c = m.get(k); if (c === undefined) m.set(k, v); else c.n++` keeps + * the borrow: the write runs only on the branch that does not read `c`. */ +export function findMapReadBorrows( + fn: IrFunction, + effects: ReferenceEffects, + candidate: (decl: IrStmt & { kind: "varDecl" }) => boolean, +): Set { + const result = new Set(); + if (fn.async || fn.generator) return result; + const names = (node: IrStmt | IrExpr, id: string, isStmt: boolean): boolean => { + let found = false; + const visitor = { + stmt: () => !found, + expr: (expr: IrExpr) => { + if (expr.kind === "varRef" && expr.localId === id) found = true; + return !found; + }, + }; + if (isStmt) everyStmt(node as IrStmt, visitor); + else everyExpr(node as IrExpr, visitor); + return found; + }; + // Whether an edge may have changed after `list`, or null when a statement + // names the local after such a change (or changes edges itself). + const walk = (list: readonly IrStmt[], id: string, changed: boolean): boolean | null => { + for (const stmt of list) { + if (stmt.kind === "if") { + const preserving = effects.preserves(stmt.cond); + if (names(stmt.cond, id, false) && (changed || !preserving)) return null; + const condition = changed || !preserving; + const then = walk(stmt.then, id, condition); + const otherwise = stmt.else_ === null ? condition : walk(stmt.else_, id, condition); + if (then === null || otherwise === null) return null; + changed = then || otherwise; + continue; + } + if (stmt.kind === "block") { + const inner = walk(stmt.body, id, changed); + if (inner === null) return null; + changed = inner; + continue; + } + const preserving = effects.preservesScope([stmt]); + if (names(stmt, id, true) && (changed || !preserving)) return null; + changed ||= !preserving; + } + return changed; + }; + const visit = (list: readonly IrStmt[]): void => { + list.forEach((stmt, index) => { + if ( + stmt.kind === "varDecl" && + candidate(stmt) && + walk(list.slice(index + 1), stmt.localId, false) !== null + ) + result.add(stmt.localId); + for (const nested of nestedStmtLists(stmt)) visit(nested); + }); + }; + visit(fn.body); return result; } @@ -303,13 +388,14 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead): let owner: StackMapRead["owner"] = null; const number = inlineNumberKey(key); const string = entryStringKey(key); + const borrow = read.borrowed === true; if (number !== null) { if (isRefCounted(read.value)) { - const raw = emitNumberMapGetRef(host, receiver.name, number, read.value); + const raw = emitNumberMapGetRef(host, receiver.name, number, read.value, borrow); B.line(`${found} = icmp ne ptr ${raw}, null`); B.line(`store i64 0, ptr ${payload}`); B.line(`store ptr ${raw}, ptr ${payload}`); - owner = { slot: payload, type: read.value }; + if (!borrow) owner = { slot: payload, type: read.value }; } else { // Booleans are stored as 0/1, already the widened union payload. const scalar = emitNumberMapGetScalar(host, receiver.name, number); @@ -326,8 +412,10 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead): let raw: string; const mapType = read.receiver.type; const compact = mapType.kind === "map" && compactMapValues(host, mapType.key, mapType.value); + // The runtime's other key kinds return +1, which the frame then owns. + const borrowed = borrow && stringKey !== null; if (stringKey !== null) - raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value, compact); + raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value, compact, borrowed); else { raw = B.tmp(); host.declare(`declare ptr @scr_map_get_${access}_ref(ptr, ${keyType})`); @@ -336,7 +424,7 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead): B.line(`${found} = icmp ne ptr ${raw}, null`); B.line(`store i64 0, ptr ${payload}`); B.line(`store ptr ${raw}, ptr ${payload}`); - owner = { slot: payload, type: read.value }; + if (!borrowed) owner = { slot: payload, type: read.value }; } else { const scalar = read.value.kind === "f64" ? "f64" : "bool"; B.line(`store i64 0, ptr ${payload}`); diff --git a/packages/compiler/test/map-get-borrow-emission.test.ts b/packages/compiler/test/map-get-borrow-emission.test.ts new file mode 100644 index 000000000..950fe35c2 --- /dev/null +++ b/packages/compiler/test/map-get-borrow-emission.test.ts @@ -0,0 +1,58 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +test("a Map.get local read before any reference write holds the entry's reference", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-map-get-borrow-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile( + entry, + ` +class Counter { + num = 1; +} +function count(m: Map, keys: number[]): void { + for (const key of keys) { + const cur = m.get(key); + if (cur === undefined) m.set(key, new Counter()); + else cur.num++; + } +} +function named(m: Map, key: string): number { + const cur = m.get(key); + return cur === undefined ? 0 : cur.num; +} +function replaced(m: Map): number { + const old = m.get(1); + m.set(1, new Counter()); + return old === undefined ? 0 : old.num; +} +const numbers = new Map(); +count(numbers, [1, 2, 1]); +console.log(named(new Map([["a", new Counter()]]), "a"), replaced(numbers)); +`, + ); + 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")); + const ll = emitLlvmModule(mod); + const fn = (name: string): string => + new RegExp(`^define internal [^\\n]*@sc_bf_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(ll)![0]; + // The update writes the map only where the entry is absent, and a + // string key's lookup is inline: neither read retains the value. + for (const name of ["count", "named"]) { + expect(fn(name)).not.toContain("@sc_retain_Counter"); + expect(fn(name)).not.toContain("@sc_release_Counter"); + } + // A write between the read and its use keeps the read's own reference. + expect(fn("replaced")).toContain("@sc_retain_Counter"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8181-map-get-borrows.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8181-map-get-borrows.ts.json new file mode 100644 index 000000000..39f43aa85 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8181-map-get-borrows.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8181-map-get-borrows.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8181-map-get-borrows.ts b/tests/corpus/8181-map-get-borrows.ts new file mode 100644 index 000000000..be546cf73 --- /dev/null +++ b/tests/corpus/8181-map-get-borrows.ts @@ -0,0 +1,130 @@ +// Map.get results held in locals across the operations that can and cannot +// release the entry's value: an update on the absent branch, a write, delete +// or clear before a later read, a call that may change the map, loops, +// string keys, and values that are stored, returned or compared. +class Counter { + num: number; + label: string; + constructor(num: number, label: string) { + this.num = num; + this.label = label; + } +} + +function count(keys: number[]): Map { + const counts = new Map(); + for (const key of keys) { + const cur = counts.get(key); + if (cur === undefined) counts.set(key, new Counter(1, `k${key}`)); + else cur.num++; + } + return counts; +} + +function report(counts: Map): string { + const parts: string[] = []; + for (const [key, value] of counts) parts.push(`${key}:${value.num}:${value.label}`); + return parts.join(" "); +} + +console.log("count", report(count([3, 1, 3, 2, 3, 1, 9]))); + +// A write replaces the entry's value while the old one is still read. +function replaceThenRead(m: Map): string { + const old = m.get(1); + m.set(1, new Counter(100, "new")); + return old === undefined ? "none" : `${old.num}:${old.label}`; +} +const replaced = new Map([[1, new Counter(5, "old")]]); +console.log("replace", replaceThenRead(replaced), replaced.get(1)!.label); + +// Deleting and clearing release the value before the read. +function deleteThenRead(m: Map): string { + const held = m.get(2); + m.delete(2); + return held === undefined ? "none" : held.label; +} +function clearThenRead(m: Map): string { + const held = m.get(2); + m.clear(); + return held === undefined ? "none" : `${held.label}:${m.size}`; +} +console.log("delete", deleteThenRead(new Map([[2, new Counter(1, "two")]]))); +console.log("clear", clearThenRead(new Map([[2, new Counter(1, "two")]]))); + +// A call that may change the map runs between the read and the use. +let shared = new Map([[7, new Counter(7, "seven")]]); +function mutate(): void { + shared.set(7, new Counter(70, "seventy")); + shared = new Map(); +} +function callBetween(): string { + const held = shared.get(7); + mutate(); + return held === undefined ? "none" : `${held.num}:${held.label}`; +} +console.log("call", callBetween()); + +// Reads on both branches, a write on one of them, and a read after the join. +function branches(m: Map, k: number, write: boolean): string { + const held = m.get(k); + if (held === undefined) return "absent"; + if (write) m.set(k, new Counter(0, "zero")); + else held.num += 10; + return `${held.num}:${held.label}`; +} +const branchMap = new Map([[4, new Counter(4, "four")]]); +console.log("branches", branches(branchMap, 4, false), branches(branchMap, 4, true), branches(branchMap, 5, true)); + +// The value escapes into an array and a returned object. +function escape(m: Map): Counter[] { + const kept: Counter[] = []; + for (let k = 0; k < 4; k++) { + const value = m.get(k % 2); + if (value !== undefined) kept.push(value); + } + m.clear(); + return kept; +} +const escapes = escape(new Map([[0, new Counter(0, "zero")], [1, new Counter(1, "one")]])); +console.log("escape", escapes.map((c) => c.label).join(",")); +function returned(m: Map): Counter | undefined { + const value = m.get(3); + return value; +} +const returnMap = new Map([[3, new Counter(3, "three")]]); +const got = returned(returnMap); +returnMap.clear(); +console.log("returned", got?.label); + +// String keys, including keys built at run time. +function words(text: string): string { + const seen = new Map(); + for (const word of text.split(" ")) { + const entry = seen.get(word); + if (entry === undefined) seen.set(word, new Counter(1, word.toUpperCase())); + else entry.num++; + } + const parts: string[] = []; + for (const [word, entry] of seen) parts.push(`${word}=${entry.num}/${entry.label}`); + return parts.join(" "); +} +console.log("words", words("a b a c b a")); +const prefix = "k"; +const byName = new Map([["k1", new Counter(1, "one")]]); +const named = byName.get(prefix + "1"); +byName.set("k1", new Counter(2, "two")); +console.log("named", named?.label, byName.get("k1")?.label); + +// A loop whose body writes the map after the read on every pass. +function loopWrites(m: Map): number { + let total = 0; + for (let i = 0; i < 5; i++) { + const value = m.get(i % 2); + if (value !== undefined) total += value.num; + m.set(i % 2, new Counter(i, "loop")); + if (value !== undefined) total += value.num * 100; + } + return total; +} +console.log("loop", loopWrites(new Map([[0, new Counter(1, "a")], [1, new Counter(2, "b")]]))); diff --git a/tests/harness/corpus-costs/8181-map-get-borrows.ts.json b/tests/harness/corpus-costs/8181-map-get-borrows.ts.json new file mode 100644 index 000000000..d5d1c5f65 --- /dev/null +++ b/tests/harness/corpus-costs/8181-map-get-borrows.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3500 } From ab9cfd7e91cf2844ff6033d60a54ac299ebb5916 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 07:53:04 -0500 Subject: [PATCH 053/273] Speculate integer loops through pure helper calls, powers of two and byte reads - A speculated write may call a function whose body is one return of arithmetic, Math.floor and Math.pow over its number parameters. The fast copy evaluates the expanded body; the double copy still calls the function. Union tests on the parameters (`number | undefined`) fold against the wrapped argument. - `Math.pow(2, n)` and `Math.floor(a / 2^n)` run as checked i64 shifts: with n an integer in [0, 52], both equal the double results for every safe integer a. - Element reads of integer typed arrays are speculative leaves, read before any check, as string and array reads are. - A write's other calls of program functions are hoisted: the fast copy evaluates each once into a local of its own, and a failed check resumes at a copy of the write that re-runs only the arithmetic over those locals. This applies only when no call can write a local the write reads. - k-nucleotide's key chain (`key = bitwiseTrimLeft(key, m) * 4 + seq[i]`) now runs in i64. --- .../src/backend/llvm/emit-statements.ts | 37 +- packages/compiler/src/backend/llvm/emitter.ts | 44 ++ .../backend/llvm/speculative-integers.test.ts | 100 +++++ .../src/backend/llvm/speculative-integers.ts | 383 +++++++++++++++++- .../7757-speculative-key-chains.ts.json | 6 + tests/corpus/7757-speculative-key-chains.ts | 67 +++ .../7757-speculative-key-chains.ts.json | 1 + 7 files changed, 613 insertions(+), 25 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7757-speculative-key-chains.ts.json create mode 100644 tests/corpus/7757-speculative-key-chains.ts create mode 100644 tests/harness/corpus-costs/7757-speculative-key-chains.ts.json diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 55e31219c..ab87b3439 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -2136,6 +2136,7 @@ export function emitLoopCopies( ); }, (id) => emitter.speculativeLocal(id) || emitter.speculativeInvariantGlobal(id), + emitter.speculationContext(), ) : null; if (!plan) { @@ -2193,6 +2194,20 @@ export function emitLoopCopies( B.line(`${both} = and i1 ${ok}, ${trying}`); ok = both; } + // Hoisted leaves live in locals of their own, which the resume copy of + // their write reads (see SpeculativeValue). + for (const { leaves } of plan.values.values()) + for (const leaf of leaves) { + emitter.currentLocals.set(leaf.localId, { + id: leaf.localId, + name: "speculated leaf", + type: leaf.expr.type, + mutable: true, + }); + B.entryAllocas.push( + `${emitter.binding(leaf.localId).slot} = alloca double ; speculated leaf`, + ); + } const fast = B.newLabel("spec.fast"), slow = B.newLabel("spec.slow"); B.condBr(ok, fast, slow); @@ -2218,6 +2233,7 @@ export function emitLoopCopies( throw new InternalCompilerError("llvm emitter bug: speculative loop copy without a header"); B.startBlock(B.newLabel("spec.tail")); emitter.speculationResume = resume; + emitter.speculationResumeValues = plan.values; emitter.speculationHeader = () => { B.br(header!); B.startBlock(B.newLabel("spec.unused")); @@ -2226,6 +2242,7 @@ export function emitLoopCopies( emitCopy(exit); } finally { emitter.speculationResume = null; + emitter.speculationResumeValues = null; emitter.speculationHeader = null; } B.startBlock(fast); @@ -2274,6 +2291,7 @@ export function emitLoopCopies( deferred, bails, ran, + values: plan.values, }; try { const leave = B.newLabel("spec.leave"); @@ -2531,9 +2549,22 @@ export function emitSpeculativeWrite( // at once: a bailing path then owns nothing, and reads have no effect // whose reordering before the arithmetic could be observed. const reads = new Map(); - if (target.value !== null) { + const rewritten = emitter.speculation!.values?.get(s); + const value = rewritten?.value ?? target.value; + if (rewritten !== undefined) { + // Hoisted leaves run first, in the write's order, and keep their + // doubles for the resume copy. emitter.frames.push([]); - for (const read of speculativeReads(target.value)) reads.set(read, emitter.emitExpr(read)); + for (const leaf of rewritten.leaves) { + const evaluated = emitter.emitExpr(leaf.expr); + B.line(`store double ${evaluated.name}, ptr ${emitter.binding(leaf.localId).slot}`); + reads.set(leaf.ref, evaluated); + } + emitter.releaseTemps(emitter.frames.pop()!); + } + if (value !== null) { + emitter.frames.push([]); + for (const read of speculativeReads(value)) reads.set(read, emitter.emitExpr(read)); emitter.releaseTemps(emitter.frames.pop()!); } emitter.frames.push([]); @@ -2548,7 +2579,7 @@ export function emitSpeculativeWrite( B.line(`${biased} = add i64 ${result}, ${Number.MAX_SAFE_INTEGER}`); B.line(`${outside} = icmp ugt i64 ${biased}, ${2 * Number.MAX_SAFE_INTEGER}`); bail(outside); - } else result = emitSpeculativeInteger(emitter, target.value, bail, reads, resume === undefined); + } else result = emitSpeculativeInteger(emitter, value!, bail, reads, resume === undefined); // Speculable values read only numbers past this point. if (emitter.frames.pop()!.length > 0) throw new InternalCompilerError("llvm emitter bug: speculative write owns a temporary"); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 6760f3b74..4080084de 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -338,6 +338,13 @@ import { } from "./shapes.js"; import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlValue } from "./expr-context.js"; import { INLINE_LITERAL_BYTES } from "./string-literal-equality.js"; +import { + inlinableNumberFunction, + withSpeculativeValue, + type InlinableFunction, + type SpeculationContext, + type SpeculativeValue, +} from "./speculative-integers.js"; import { emitFfiCallbackDefs, emitLibDefs, @@ -1435,11 +1442,41 @@ export class LlEmitter { * integer loop has neither. */ bails?: string; ran?: string; + /** Writes that speculate a rewritten value (rewriteSpeculable). */ + values?: ReadonlyMap; } | 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; + /** The rewritten values of the writes `speculationResume` resumes at. */ + speculationResumeValues: ReadonlyMap | null = null; + /** Hoisted speculative leaves numbered so far in the module. */ + private speculativeLeaves = 0; + private readonly inlinableFunctions = new Map(); + + /** What speculative loop planning may assume here (SpeculationContext). */ + speculationContext(): SpeculationContext { + return { + inlinable: (callee) => { + if (!this.inlinableFunctions.has(callee)) { + const fn = this.fnByName.get(callee); + this.inlinableFunctions.set(callee, fn ? inlinableNumberFunction(fn) : undefined); + } + return this.inlinableFunctions.get(callee); + }, + unaliased: (id) => { + const local = this.currentLocals.get(id); + return ( + local !== undefined && + !local.boxed && + !this.captureIds.has(id) && + !this.lazyCaptureBoxes.has(id) + ); + }, + leafId: () => `%spec.leaf.${this.speculativeLeaves++}`, + }; + } /** 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. */ @@ -5591,6 +5628,13 @@ export class LlEmitter { resume.placed = true; this.B.br(resume.label); this.B.startBlock(resume.label); + // A write with hoisted leaves re-executes its arithmetic over the + // leaves' stored doubles: each leaf ran once, in the fast copy. + const rewritten = this.speculationResumeValues?.get(s); + if (rewritten !== undefined && rewritten.leaves.length > 0) { + this.emitStmt(withSpeculativeValue(s, rewritten.value)); + return; + } } if (this.speculation?.writes.has(s)) { this.emitSpeculativeWrite(s, this.speculation.resume.get(s)); diff --git a/packages/compiler/src/backend/llvm/speculative-integers.test.ts b/packages/compiler/src/backend/llvm/speculative-integers.test.ts index 3acfec0fb..86055b1b6 100644 --- a/packages/compiler/src/backend/llvm/speculative-integers.test.ts +++ b/packages/compiler/src/backend/llvm/speculative-integers.test.ts @@ -1,9 +1,11 @@ import { expect, test } from "vitest"; import { F64, type IrExpr, type IrStmt } from "../../ir/ir.js"; import { + inlinableNumberFunction, isSpeculableValue, planProvenIntegerLoop, planSpeculativeLoop, + rewriteSpeculable, speculativeReads, } from "./speculative-integers.js"; @@ -266,3 +268,101 @@ test("bounded cursors with proven writes are shadowed in a proven integer loop", planProvenIntegerLoop(forLoop([...body, jump]) as never, cursors, all, () => false, all), ).toBeNull(); }); + +const libCall = (fn: string, args: IrExpr[]): IrExpr => + ({ kind: "libCall", fn, args, type: F64, loc }) as never; +const call = (callee: string, args: IrExpr[]): IrExpr => ({ + kind: "call", + callee, + args, + type: F64, + loc, +}); +const pow2 = (n: IrExpr): IrExpr => libCall("math.pow", [num(2), n]); +/** `function trim(v, n) { return v - Math.floor(v / Math.pow(2, n)) * Math.pow(2, n); }` */ +const trim = { + name: "trim", + params: [ + { localId: "v", name: "v", type: F64 }, + { localId: "n", name: "n", type: F64 }, + ], + returnType: F64, + locals: [], + body: [ + { + kind: "return", + value: bin( + "-", + ref("v"), + bin("*", libCall("math.floor", [bin("/", ref("v"), pow2(ref("n")))]), pow2(ref("n"))), + ), + loc, + }, + ], + loc, +} as never; + +test("calls of small pure number functions expand into a speculated write", () => { + const inlinable = inlinableNumberFunction(trim)!; + expect(inlinable.params).toEqual(["v", "n"]); + const context = { inlinable: (callee: string) => (callee === "trim" ? inlinable : undefined) }; + // key = trim(key, m) * 4 + c + const value = bin("+", bin("*", call("trim", [ref("key"), ref("m")]), num(4)), ref("c")); + expect(isSpeculableValue(value)).toBe(false); + const rewritten = rewriteSpeculable(value, context)!; + expect(rewritten.leaves).toEqual([]); + expect(JSON.stringify(rewritten.value)).not.toContain('"call"'); + const write = assign("key", value); + const plan = planSpeculativeLoop( + loop([write, increment("i")]) as never, + all, + () => false, + () => false, + all, + context, + )!; + expect(plan.writes.has(write)).toBe(true); + expect(plan.values.get(write)).toEqual(rewritten); + // `m`, an input of the expanded body, is checked on entry with the others. + expect([...plan.locals.keys()].sort()).toEqual(["c", "i", "key", "m", "n"]); +}); + +test("Math.floor needs a power-of-two divisor, and expansion pure arguments", () => { + const context = { inlinable: () => inlinableNumberFunction(trim) }; + expect(rewriteSpeculable(libCall("math.floor", [bin("/", ref("a"), num(8))]), {})).not.toBeNull(); + expect(rewriteSpeculable(libCall("math.floor", [bin("/", ref("a"), num(6))]), {})).toBeNull(); + expect(rewriteSpeculable(libCall("math.floor", [bin("/", ref("a"), ref("b"))]), {})).toBeNull(); + // An argument that is itself a call is evaluated once: no expansion. + expect(rewriteSpeculable(call("trim", [call("other", []), ref("m")]), context)).toBeNull(); + const effectful = { + ...(trim as object), + body: [{ kind: "exprStmt", expr: call("log", []), loc }], + }; + expect(inlinableNumberFunction(effectful as never)).toBeUndefined(); +}); + +test("other leaves are hoisted only when no call can change the write's locals", () => { + let next = 0; + const leafId = (): string => `%leaf.${next++}`; + const value = bin( + "%", + bin("+", bin("*", ref("acc"), num(7)), call("noisy", [ref("i")])), + num(1000003), + ); + const hoisted = rewriteSpeculable(value, { unaliased: () => true, leafId })!; + expect(hoisted.leaves.map((leaf) => leaf.expr)).toEqual([call("noisy", [ref("i")])]); + expect(rewriteSpeculable(value, { unaliased: (id) => id !== "acc", leafId })).toBeNull(); + // Without leaf ids nothing is hoisted. + expect(rewriteSpeculable(value, { unaliased: () => true })).toBeNull(); + const write = assign("acc", value); + const plan = planSpeculativeLoop( + loop([write, increment("i")]) as never, + (id) => !id.startsWith("%leaf"), + () => false, + () => false, + (id) => !id.startsWith("%leaf"), + { unaliased: () => true, leafId }, + )!; + expect(plan.values.get(write)!.leaves).toHaveLength(1); + expect([...plan.locals.keys()].some((id) => id.startsWith("%leaf"))).toBe(false); +}); diff --git a/packages/compiler/src/backend/llvm/speculative-integers.ts b/packages/compiler/src/backend/llvm/speculative-integers.ts index 00fabb788..69066a5b4 100644 --- a/packages/compiler/src/backend/llvm/speculative-integers.ts +++ b/packages/compiler/src/backend/llvm/speculative-integers.ts @@ -1,5 +1,6 @@ -import type { IrExpr, IrStmt } from "../../ir/ir.js"; +import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; +import { InternalCompilerError } from "../../errors.js"; import { f64Lit } from "./common.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { exactInteger, widenInteger } from "./integer-values.js"; @@ -30,6 +31,273 @@ export interface SpeculativeLoopPlan { * than through its condition, `break` or a failed check: no calls, no * throwing operations, no labeled jumps and no `return`. */ observerFree: boolean; + /** The writes that speculate a rewritten value (rewriteSpeculable). */ + values: Map; +} + +/** A write's value as speculation evaluates it: calls of small pure number + * functions expanded in place, and other number leaves (`leaves`) replaced + * by references to fresh locals. The fast copy evaluates each leaf once, + * stores it to its local and checks it as an integer; a failed check + * resumes at a copy of the write that reads the locals, so a leaf is never + * evaluated twice. */ +export interface SpeculativeValue { + value: IrExpr; + leaves: { localId: string; expr: IrExpr; ref: IrExpr }[]; +} + +/** What the planner may assume about the code around a loop. */ +export interface SpeculationContext { + /** The parameters and the returned expression of `callee` when it is a + * small pure number function (inlinableNumberFunction). */ + inlinable?: (callee: string) => InlinableFunction | undefined; + /** A local that no call can write: not captured, boxed or global. */ + unaliased?: (localId: string) => boolean; + /** A fresh local id for a hoisted leaf. */ + leafId?: () => string; +} + +export interface InlinableFunction { + params: readonly string[]; + body: IrExpr; +} + +/** `s` (a speculated write) writing `value` instead. */ +export function withSpeculativeValue(s: IrStmt, value: IrExpr): IrStmt { + switch (s.kind) { + case "assign": + return { ...s, value }; + case "varDecl": + return { ...s, init: value }; + case "exprStmt": + if (s.expr.kind === "assignExpr") return { ...s, expr: { ...s.expr, value } }; + break; + default: + break; + } + throw new InternalCompilerError(`llvm emitter bug: speculated ${s.kind} with a rewritten value`); +} + +/** Calls nest at most this deep in one expansion. */ +const MAX_INLINE_DEPTH = 4; +/** An expansion larger than this stays a call. */ +const MAX_INLINE_NODES = 64; + +/** A function that only returns arithmetic over its number parameters: + * `function trim(v, n) { return v - Math.floor(v / Math.pow(2, n)) * Math.pow(2, n); }`. + * Its call evaluates nothing but its arguments, so a speculated write may + * expand it in place. */ +export function inlinableNumberFunction(fn: IrFunction): InlinableFunction | undefined { + if ( + fn.async || + fn.generator || + (fn.captures?.length ?? 0) > 0 || + (fn.classCaptures?.length ?? 0) > 0 || + fn.returnType.kind !== "f64" || + fn.params.some((p) => p.type.kind !== "f64" && p.type.kind !== "union") || + fn.body.length !== 1 || + fn.body[0]!.kind !== "return" || + fn.body[0]!.value === undefined + ) + return undefined; + const params = fn.params.map((p) => p.localId); + const known = new Set(params); + let nodes = 0; + // A parameter of a union type (`number | undefined`) is read only + // through its tag test and narrowing, which a wrapped argument folds. + const union = (e: IrExpr): boolean => e.kind === "varRef" && known.has(e.localId); + const pure = (e: IrExpr): boolean => { + if (++nodes > MAX_INLINE_NODES) return false; + if (e.kind === "boolLit") return true; + if (e.kind === "unionIsTag") return union(e.value); + if (e.kind === "unionNarrow") return e.type.kind === "f64" && union(e.value); + if (e.type.kind !== "f64") return false; + switch (e.kind) { + case "numLit": + return true; + case "varRef": + return known.has(e.localId); + case "ternary": + return pure(e.cond) && pure(e.then) && pure(e.else_); + case "bin": + return (INTEGER_OPS.has(e.op) || e.op === "/") && pure(e.left) && pure(e.right); + case "unary": + return e.op === "-" && pure(e.operand); + case "libCall": + return (e.fn === "math.floor" || e.fn === "math.pow") && e.args.every(pure); + case "call": + return e.args.every(pure); + default: + return false; + } + }; + const body = fn.body[0]!.value!; + return pure(body) ? { params, body } : undefined; +} + +/** `e` with each parameter reference replaced by its argument. A tag test + * or narrowing of a wrapped argument folds, and so does a ternary over a + * folded test. */ +function substitute(e: IrExpr, args: ReadonlyMap): IrExpr { + switch (e.kind) { + case "varRef": + return args.get(e.localId) ?? e; + case "unionIsTag": { + const value = substitute(e.value, args); + if (value.kind !== "unionWrap" || value.unionId !== e.unionId) return { ...e, value }; + return { + kind: "boolLit", + value: (value.tag === e.tag) !== (e.negated === true), + type: e.type, + loc: e.loc, + }; + } + case "unionNarrow": { + const value = substitute(e.value, args); + return value.kind === "unionWrap" && value.unionId === e.unionId && value.tag === e.tag + ? value.value + : { ...e, value }; + } + case "ternary": { + const cond = substitute(e.cond, args); + if (cond.kind === "boolLit") return substitute(cond.value ? e.then : e.else_, args); + return { ...e, cond, then: substitute(e.then, args), else_: substitute(e.else_, args) }; + } + case "bin": + return { ...e, left: substitute(e.left, args), right: substitute(e.right, args) }; + case "unary": + return { ...e, operand: substitute(e.operand, args) }; + case "libCall": + case "call": + return { ...e, args: e.args.map((a) => substitute(a, args)) }; + default: + return e; + } +} + +/** Whether `e` is a power of two speculation can shift by: + * `Math.pow(2, n)` or a literal 2^k with k in [0, 52]. */ +function powerOfTwo(e: IrExpr): boolean { + if (e.kind === "numLit") { + const k = Math.log2(e.value); + return Number.isInteger(k) && k >= 0 && k <= 52; + } + return ( + e.kind === "libCall" && + e.fn === "math.pow" && + e.args.length === 2 && + e.args[0]!.kind === "numLit" && + e.args[0]!.value === 2 + ); +} + +/** Typed-array element kinds whose elements are integers. */ +const INTEGER_ELEMENTS = new Set(["u8", "u8c", "i8", "u16", "i16", "u32", "i32"]); + +/** A typed-array element read of an integer kind with no effect: an + * out-of-range read gives NaN, which fails its check. Float elements would + * only bail. */ +function isSpeculativeByteRead(e: IrExpr): boolean { + return ( + e.kind === "bytesIntrinsic" && + e.method === "get" && + e.type.kind === "f64" && + e.receiver.type.kind === "bytes" && + INTEGER_ELEMENTS.has(e.receiver.type.elem) && + e.args.length === 1 && + isEffectFree(e.receiver) && + isEffectFree(e.args[0]!) + ); +} + +/** Rewrite a write's value that isSpeculableValue refuses into one it can + * speculate (SpeculativeValue), or null. Expansion and the new operations + * (`Math.pow(2, n)`, `Math.floor(a / 2^n)`, byte reads) evaluate nothing + * but numbers, so the fast copy may evaluate them in any order. Hoisted + * leaves run in the write's evaluation order before the arithmetic; that + * reorders nothing only when the write reads no heap and every local it + * reads is one no call can write. */ +export function rewriteSpeculable( + value: IrExpr, + context: SpeculationContext, +): SpeculativeValue | null { + const leaves: SpeculativeValue["leaves"] = []; + let reads = false; + const locals: string[] = []; + const rewrite = (e: IrExpr, depth: number): IrExpr | null => { + if (e.type.kind !== "f64") return null; + switch (e.kind) { + case "numLit": + return Number.isSafeInteger(e.value) && !Object.is(e.value, -0) ? e : null; + case "varRef": + locals.push(e.localId); + return e; + case "bin": { + if (!ARITHMETIC_OPS.has(e.op)) break; + const left = rewrite(e.left, depth); + const right = left && rewrite(e.right, depth); + return left && right ? { ...e, left, right } : null; + } + case "unary": { + if (e.op !== "-") break; + const operand = rewrite(e.operand, depth); + return operand ? { ...e, operand } : null; + } + case "libCall": { + if (powerOfTwo(e) && e.kind === "libCall") { + const exponent = rewrite(e.args[1]!, depth); + return exponent ? { ...e, args: [e.args[0]!, exponent] } : null; + } + const quotient = e.args[0]; + if ( + e.fn === "math.floor" && + e.args.length === 1 && + quotient?.kind === "bin" && + quotient.op === "/" && + powerOfTwo(quotient.right) + ) { + const left = rewrite(quotient.left, depth); + const right = left && rewrite(quotient.right, depth); + return left && right ? { ...e, args: [{ ...quotient, left, right }] } : null; + } + break; + } + case "call": { + const fn = depth < MAX_INLINE_DEPTH ? context.inlinable?.(e.callee) : undefined; + // Arguments are evaluated before the body: an expansion may read + // them in another order, or twice, only when they are pure. + const plain = (a: IrExpr): boolean => + isSpeculableValue(a, false) || + (a.kind === "unionWrap" && isSpeculableValue(a.value, false)); + if (fn !== undefined && fn.params.length === e.args.length && e.args.every(plain)) { + const args = new Map(fn.params.map((id, i) => [id, e.args[i]!])); + return rewrite(substitute(fn.body, args), depth + 1); + } + break; + } + default: + if (isSpeculativeRead(e) || isSpeculativeByteRead(e)) { + reads = true; + return e; + } + break; + } + // Only calls of program functions are hoisted: other leaves (a string's + // length, a Map read) are cheap where they stand, and would turn every + // loop that adds one into a second copy. + if (context.leafId === undefined || e.kind !== "call") return null; + const ref: IrExpr = { kind: "varRef", localId: context.leafId(), type: e.type, loc: e.loc }; + leaves.push({ localId: (ref as { localId: string }).localId, expr: e, ref }); + return ref; + }; + const rewritten = rewrite(value, 0); + if (rewritten === null) return null; + if (leaves.length > 0) { + const leafIds = new Set(leaves.map((leaf) => leaf.localId)); + if (reads || !locals.every((id) => leafIds.has(id) || context.unaliased?.(id) === true)) + return null; + } + return { value: rewritten, leaves }; } type SpeculativeLoop = Extract; @@ -170,13 +438,16 @@ function isEffectFree(e: IrExpr): boolean { } } -/** The speculative reads of a speculable value in evaluation order. */ +/** The speculative reads of a speculable value in evaluation order, those + * of a rewritten value (rewriteSpeculable) included. */ export function speculativeReads(e: IrExpr, out: IrExpr[] = []): IrExpr[] { - if (e.kind === "bin" && ARITHMETIC_OPS.has(e.op)) { + if (e.kind === "bin" && (ARITHMETIC_OPS.has(e.op) || e.op === "/")) { speculativeReads(e.left, out); speculativeReads(e.right, out); } else if (e.kind === "unary" && e.op === "-") speculativeReads(e.operand, out); - else if (isSpeculativeRead(e)) out.push(e); + else if (e.kind === "libCall" && (e.fn === "math.floor" || e.fn === "math.pow")) + for (const arg of e.args) speculativeReads(arg, out); + else if (isSpeculativeRead(e) || isSpeculativeByteRead(e)) out.push(e); return out; } @@ -195,6 +466,8 @@ function hasProduct(e: IrExpr): boolean { if (e.kind === "bin") return e.op === "*" || e.op === "%" || hasProduct(e.left) || hasProduct(e.right); if (e.kind === "unary") return hasProduct(e.operand); + // Math.floor(a / 2^n) and Math.pow(2, n) replace a division and a call. + if (e.kind === "libCall") return e.fn === "math.floor" || e.fn === "math.pow"; return false; } @@ -202,6 +475,7 @@ function hasArithmetic(e: IrExpr): boolean { if (e.kind === "bin") return ARITHMETIC_OPS.has(e.op) || hasArithmetic(e.left) || hasArithmetic(e.right); if (e.kind === "unary") return e.op === "-" || hasArithmetic(e.operand); + if (e.kind === "libCall") return e.fn === "math.floor" || e.fn === "math.pow"; return false; } @@ -236,8 +510,11 @@ export function planSpeculativeLoop( integral: (localId: string) => boolean = () => false, proven: (e: IrExpr) => boolean = () => false, invariantEligible: (localId: string) => boolean = eligible, + context: SpeculationContext = {}, ): SpeculativeLoopPlan | null { const writes = new Map(); + const values = new Map(); + const leafIds = new Set(); const unspeculable = new Set(); const reads = new Set(); // Locals with at least one read the static analysis does not prove. @@ -263,11 +540,9 @@ export function planSpeculativeLoop( // make the body's work integer. let inCondition = false; - const expr = (e: IrExpr): boolean => { - if (rejected) return false; - nodes++; - if (!OBSERVER_FREE_EXPRESSIONS.has(e.kind) || (e.kind === "bin" && e.left.type.kind !== "f64")) - observerFree = false; + // What the integer writes read and consume. A rewritten value is noted + // here alone: its original, which `expr` walks, has its effects. + const note = (e: IrExpr): void => { if (e.kind === "varRef") { reads.add(e.localId); if (!proven(e)) unproven.add(e.localId); @@ -281,6 +556,20 @@ export function planSpeculativeLoop( } else if (e.kind === "arrayGet" || e.kind === "arrayHas" || e.kind === "arrayState") consume(e.index); else if (e.kind === "bytesIntrinsic" || e.kind === "strIntrinsic") e.args.forEach(consume); + }; + // An expanded call is emitted in the fast copy: it counts toward the + // duplicated code. + const noteAll = (e: IrExpr): boolean => { + nodes++; + note(e); + return everyExprChild(e, noteAll, () => true); + }; + const expr = (e: IrExpr): boolean => { + if (rejected) return false; + nodes++; + if (!OBSERVER_FREE_EXPRESSIONS.has(e.kind) || (e.kind === "bin" && e.left.type.kind !== "f64")) + observerFree = false; + note(e); return everyExprChild(e, expr, nested); }; // Statements inside expressions (sequence expressions) run mid-expression, @@ -307,7 +596,11 @@ export function planSpeculativeLoop( if (s.kind === "arraySet" || s.kind === "bytesSet") consume(s.index); const target = speculativeWriteTarget(s); if (target) { - if (target.value === null || isSpeculableValue(target.value)) { + const rewritten = + target.value === null || isSpeculableValue(target.value) + ? null + : rewriteSpeculable(target.value, context); + if (target.value === null || isSpeculableValue(target.value) || rewritten !== null) { const list = writes.get(target.localId) ?? []; list.push(s); writes.set(target.localId, list); @@ -315,6 +608,11 @@ export function planSpeculativeLoop( reads.add(target.localId); if (s.kind !== "exprStmt" || !proven(s.expr)) unproven.add(target.localId); } else expr(target.value); + if (rewritten !== null) { + values.set(s, rewritten); + for (const leaf of rewritten.leaves) leafIds.add(leaf.localId); + noteAll(rewritten.value); + } return !rejected; } unspeculable.add(target.localId); @@ -341,25 +639,28 @@ export function planSpeculativeLoop( // write runs: speculated itself, already an integer, or loop-invariant // (checked on entry). A value computed from doubles the loop writes // would only fail its check on every entry. + const valueOf = (s: IrStmt): IrExpr | null => + values.get(s)?.value ?? speculativeWriteTarget(s)!.value; const grounded = (e: IrExpr): boolean => { if (e.kind === "varRef") { const id = e.localId; return ( + leafIds.has(id) || proven(e) || locals.has(id) || integral(id) || (!writes.has(id) && !unspeculable.has(id) && invariantEligible(id)) ); } - if (isSpeculativeRead(e)) return true; + if (isSpeculativeRead(e) || isSpeculativeByteRead(e)) return true; return everyExprChild(e, grounded, () => false); }; for (let changed = true; changed;) { changed = false; for (const id of [...locals.keys()]) { for (const s of writes.get(id)!) { - const target = speculativeWriteTarget(s)!; - if (target.value !== null && !grounded(target.value)) { + const value = valueOf(s); + if (value !== null && !grounded(value)) { locals.delete(id); changed = true; break; @@ -376,6 +677,7 @@ export function planSpeculativeLoop( if (e.kind === "varRef") { const id = e.localId; if ( + !leafIds.has(id) && !proven(e) && !locals.has(id) && !writes.has(id) && @@ -388,11 +690,11 @@ export function planSpeculativeLoop( return everyExprChild(e, invariant, () => true); }; for (const s of speculated) { - const target = speculativeWriteTarget(s)!; - if (target.value === null) arithmetic = true; + const value = valueOf(s); + if (value === null) arithmetic = true; else { - if (hasArithmetic(target.value)) arithmetic = true; - invariant(target.value); + if (hasArithmetic(value)) arithmetic = true; + invariant(value); } } if (!arithmetic) return null; @@ -401,13 +703,12 @@ export function planSpeculativeLoop( [...locals.keys()].some((id) => multiplied.has(id)) || [...speculated].some((s) => { const target = speculativeWriteTarget(s)!; - return ( - target.value !== null && - (hasProduct(target.value) || isVariableStep(target.localId, target.value)) - ); + const value = valueOf(s); + return value !== null && (hasProduct(value) || isVariableStep(target.localId, value)); }); if (!heavy) return null; - return { locals, writes: speculated, observerFree }; + for (const s of values.keys()) if (!speculated.has(s)) values.delete(s); + return { locals, writes: speculated, observerFree, values }; } /** A proven integer loop. The bindings integer-ranges.ts bounds over a for @@ -682,6 +983,19 @@ export function emitSpeculativeInteger( bail(negativeZero); return result; } + case "libCall": { + // A rewritten value's Math.pow(2, n) and Math.floor(a / 2^n) + // (rewriteSpeculable): with n an integer in [0, 52], 2^n is exact and + // so is a / 2^n for a safe integer a, whose floor the arithmetic shift + // computes, also for negative a. No result is -0. + if (e.fn === "math.pow") return shiftOne(host, exponentOf(host, e, bail, reads, integral)); + if (e.fn !== "math.floor" || e.args[0]?.kind !== "bin") break; + const a = emitSpeculativeInteger(host, e.args[0].left, bail, reads, integral); + const n = exponentOf(host, e.args[0].right, bail, reads, integral); + const result = B.tmp(); + B.line(`${result} = ashr i64 ${a}, ${n}`); + return result; + } case "unary": { if (e.op !== "-") break; const a = emitSpeculativeInteger(host, e.operand, bail, reads, integral); @@ -699,6 +1013,31 @@ export function emitSpeculativeInteger( return speculativeLeaf(host, reads.get(e) ?? host.emitExpr(e), e, bail); } +/** The exponent of a power of two (Math.pow(2, n) or a literal 2^k) as an + * i64 in [0, 52]; other exponents bail. */ +function exponentOf( + host: LlvmEmitterContext, + e: IrExpr, + bail: (condition: string) => void, + reads: ReadonlyMap, + integral: boolean, +): string { + if (e.kind === "numLit") return String(Math.log2(e.value)); + if (e.kind !== "libCall") + throw new InternalCompilerError("llvm emitter bug: speculative power of two"); + const n = emitSpeculativeInteger(host, e.args[1]!, bail, reads, integral); + const outside = host.B.tmp(); + host.B.line(`${outside} = icmp ugt i64 ${n}, 52`); + bail(outside); + return n; +} + +function shiftOne(host: LlvmEmitterContext, n: string): string { + const result = host.B.tmp(); + host.B.line(`${result} = shl i64 1, ${n}`); + return result; +} + /** The safe i64 of an evaluated number: its proven integer, or a checked * conversion. */ function speculativeLeaf( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7757-speculative-key-chains.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7757-speculative-key-chains.ts.json new file mode 100644 index 000000000..274151b18 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7757-speculative-key-chains.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7757-speculative-key-chains.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7757-speculative-key-chains.ts b/tests/corpus/7757-speculative-key-chains.ts new file mode 100644 index 000000000..bdbd12e1e --- /dev/null +++ b/tests/corpus/7757-speculative-key-chains.ts @@ -0,0 +1,67 @@ +// Speculative integer loops over writes that call small pure number +// functions, use Math.pow(2, n) and Math.floor(a / 2^n), read bytes, or +// call other functions: every fallback gives Node's numbers. +function trim(value: number, length: number): number { + return value - Math.floor(value / Math.pow(2, length)) * Math.pow(2, length); +} +function maybeTrim(value: number | undefined, length: number): number { + return (value ?? 0) - Math.floor((value ?? 0) / Math.pow(2, length)) * Math.pow(2, length); +} + +function chain(codes: Uint8Array, length: number): number { + const mask = length * 2 - 2; + let key = 0; + let sum = 0; + for (let i = 0; i < codes.length; ++i) { + sum = (sum + key) % 1000000007; + key = trim(key, mask) * 4 + codes[i]!; + } + return sum * 31 + key; +} + +function optionalChain(codes: Uint8Array, length: number): number { + let key = 0; + let sum = 0; + for (let i = 0; i < codes.length; ++i) { + sum = (sum * 3 + key) % 1000003; + key = maybeTrim(key, length * 2) * 4 + codes[i]!; + } + return sum + key; +} + +// Exponents past 52, negative and fractional values, and NaN reads leave +// the integer copy and finish in the double one. +function shifts(start: number, exponent: number, steps: number): number[] { + const out: number[] = []; + let v = start; + for (let i = 0; i < steps; i++) { + v = Math.floor(v / Math.pow(2, exponent)) * 3 + i * 7; + out.push(v); + } + return out; +} + +let calls = 0; +function noisy(x: number): number { + calls++; + return x % 5 === 0 ? x / 2 : x * 2; +} +// A call that the write cannot expand runs once per iteration, also on +// the iteration whose check fails. +function hoisted(n: number): number { + let acc = 1; + for (let i = 0; i < n; i++) acc = (acc * 7 + noisy(i)) % 1000003; + return acc; +} + +const codes = new Uint8Array(5000); +let seed = 7; +for (let i = 0; i < codes.length; i++) { + seed = (seed * 1103515245 + 12345) % 2147483648; + codes[i] = seed % 4; +} +for (const length of [1, 2, 3, 6, 12, 18, 27]) console.log(length, chain(codes, length), optionalChain(codes, length)); +console.log(shifts(1000, 3, 6), shifts(-1000, 2, 6), shifts(5, 53, 3), shifts(7.5, 1, 3), shifts(2 ** 52, 1, 3)); +console.log(hoisted(20), calls); +const short = codes.subarray(0, 3); +console.log(chain(short, 2), chain(new Uint8Array(0), 2)); diff --git a/tests/harness/corpus-costs/7757-speculative-key-chains.ts.json b/tests/harness/corpus-costs/7757-speculative-key-chains.ts.json new file mode 100644 index 000000000..be69de5f7 --- /dev/null +++ b/tests/harness/corpus-costs/7757-speculative-key-chains.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 2900 } From d9f1457cd55dd99cf60f630eb1d610a1545207f3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 08:07:52 -0500 Subject: [PATCH 054/273] Run CommonJS top-level return, untyped assert forms, and untyped instanceof Object as Node does --- .../limitations/builtin-apis/assertions.mdx | 2 + .../limitations/builtin-apis/prototypes.mdx | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 4 + .../lowering/expressions/relational.ts | 15 +- .../src/frontend/lowering/lower-assert.ts | 316 +++++++++++++++++- .../src/frontend/lowering/lower-calls.ts | 15 +- .../src/frontend/lowering/lower-stmts.ts | 40 +++ .../compiler/src/frontend/lowering/lowerer.ts | 8 +- .../src/frontend/lowering/surfaces.ts | 6 + packages/compiler/src/frontend/program.ts | 11 + packages/compiler/src/frontend/tsc-codes.ts | 4 + packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 8 + .../8062-assert-message-values.cjs.json | 6 + .../8065-cjs-top-level-return/main.js.json | 7 + .../corpus/8066-object-hasown-delete.cjs.json | 6 + .../8067-assert-throws-validators.cjs.json | 6 + .../8076-assert-instance-identity.cjs.json | 6 + .../8078-instanceof-object-function.cjs.json | 6 + .../8190-assert-function-values.cjs.json | 6 + .../8191-assert-function-values.ts.json | 6 + packages/runtime/src/scr_assert.c | 314 +++++++++++++++++ packages/runtime/src/scr_assert.h | 13 + packages/runtime/src/scr_json.c | 72 ++++ tests/corpus/8062-assert-message-values.cjs | 41 +++ .../corpus/8065-cjs-top-level-return/early.js | 6 + .../corpus/8065-cjs-top-level-return/main.js | 16 + .../8065-cjs-top-level-return/package.json | 3 + tests/corpus/8066-object-hasown-delete.cjs | 23 ++ .../corpus/8067-assert-throws-validators.cjs | 22 ++ .../corpus/8076-assert-instance-identity.cjs | 50 +++ .../8078-instanceof-object-function.cjs | 55 +++ tests/corpus/8190-assert-function-values.cjs | 30 ++ tests/corpus/8191-assert-function-values.ts | 16 + tests/coverage-fixtures/startup-fences.js | 4 +- tests/diagnostics/assert-fences.ts | 8 +- .../__snapshots__/assert-fences.ts.txt | 8 +- tests/harness/coverage.test.ts | 4 +- 38 files changed, 1121 insertions(+), 44 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8062-assert-message-values.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8065-cjs-top-level-return/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8066-object-hasown-delete.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8067-assert-throws-validators.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8076-assert-instance-identity.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8078-instanceof-object-function.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8190-assert-function-values.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8191-assert-function-values.ts.json create mode 100644 tests/corpus/8062-assert-message-values.cjs create mode 100644 tests/corpus/8065-cjs-top-level-return/early.js create mode 100644 tests/corpus/8065-cjs-top-level-return/main.js create mode 100644 tests/corpus/8065-cjs-top-level-return/package.json create mode 100644 tests/corpus/8066-object-hasown-delete.cjs create mode 100644 tests/corpus/8067-assert-throws-validators.cjs create mode 100644 tests/corpus/8076-assert-instance-identity.cjs create mode 100644 tests/corpus/8078-instanceof-object-function.cjs create mode 100644 tests/corpus/8190-assert-function-values.cjs create mode 100644 tests/corpus/8191-assert-function-values.ts diff --git a/docs/content/limitations/builtin-apis/assertions.mdx b/docs/content/limitations/builtin-apis/assertions.mdx index f7f5c9354..381fc3802 100644 --- a/docs/content/limitations/builtin-apis/assertions.mdx +++ b/docs/content/limitations/builtin-apis/assertions.mdx @@ -3,6 +3,8 @@ When `strictEqual` compares function values, `actual` and `expected` read as `undefined`; when `assert.throws` or `assert.rejects` reports an error that is not an instance of the expected class, or that does not match an expected object, `expected` reads as `undefined`. Values whose type can be cyclic, such as recursive records, also read as `undefined`. `assert.AssertionError` used as a value outside `instanceof` throws `SC2020`. + The comparison methods, `ok`, `fail` and `ifError` are function values that assert as the methods do; called through a stored value, `ok` without a message reports `value == true`, where Node quotes the call, and `fail` takes at most one argument. + `throws`, `rejects`, `doesNotThrow`, `doesNotReject`, `match` and `doesNotMatch` as values throw `SC2020`. `assert.deepEqual` and `assert.notDeepEqual` follow Node's loose comparison, including Maps, Sets, Dates, regular expressions, typed arrays, and errors; operands whose type can be cyclic or holds functions inside a structure are refused. `assert.partialDeepStrictEqual` follows Node's partial comparison over the same values, with the same refusals. Checked-dynamic values compared deeply have no cycle detection. diff --git a/docs/content/limitations/builtin-apis/prototypes.mdx b/docs/content/limitations/builtin-apis/prototypes.mdx index 170b11793..ce7bec74c 100644 --- a/docs/content/limitations/builtin-apis/prototypes.mdx +++ b/docs/content/limitations/builtin-apis/prototypes.mdx @@ -6,3 +6,4 @@ 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. + `instanceof Object` and `instanceof Function` on untyped values follow the prototype chain as in Node, so an object built with `Object.create(null)` is not an `Object`. diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 7a16c37e8..46798e275 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -37,6 +37,7 @@ export const ASSERT_VERDICT_LIB_FNS: ReadonlyMap = new Ma export const LIB_FN_SYMS: Record = { "assert.expectsErrDyn": "scr_assert_expects_err_dyn", + "assert.throwsValidate": "scr_assert_throws_validate", "insp.jsonDyn": "scr_util_format_j", "http.reqH2StreamOrThrow": "scr_http_req_h2_stream_or_throw", "intl.segmenterNew": "scr_intl_segmenter_new", @@ -399,6 +400,9 @@ export const LIB_FN_SYMS: Record = { "assert.eqBool": "scr_assert_eq_bool", "assert.eqSym": "scr_assert_eq_sym", "assert.eqDyn": "scr_assert_eq_dyn", + "assert.messageValue": "scr_assert_message_value", + "assert.methodValue": "scr_assert_method_value", + "assert.messageClear": "scr_assert_message_clear", "assert.looseResult": "scr_assert_loose_result", "assert.deepResult": "scr_assert_deep_result", "assert.looseDeepResult": "scr_assert_loose_deep_result", diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 1651bf0d9..5cd63013e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -2178,23 +2178,10 @@ 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. */ + * when `C` is a runtime constructor with no instance test. */ 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-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index b996abed7..39ec9c959 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -51,11 +51,14 @@ import { STRING, type SrcLoc, VOID, + isDynTypedRefType, isUnitType, typeEquals, typeKey, } from "../../ir/ir.js"; import { boolLit, countedFor, dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js"; +import { checkedClassInstanceOf } from "./class-construction.js"; +import { isJsSourceFile } from "../program.js"; /** node:assert/strict binds the loose NAMES to the strict comparisons — * Node's own aliasing (`strict.equal === assert.strictEqual`). */ @@ -73,6 +76,63 @@ function canonicalAssertMember(module: string, member: string): string { return member; } +/** The assert methods that are function values (scr_assert_method_value). */ +const ASSERT_FUNCTION_VALUES: ReadonlySet = new Set([ + "ok", + "fail", + "equal", + "notEqual", + "strictEqual", + "notStrictEqual", + "deepEqual", + "notDeepEqual", + "deepStrictEqual", + "notDeepStrictEqual", + "partialDeepStrictEqual", + "ifError", +]); + +/** The assert method a `const` binding holds (`const ok = assert.ok`, + * `const eq = strictEqual`), or null. */ +export function assertFunctionAliasOf( + lowerer: Lowerer, + ident: ts.Identifier, +): { module: string; member: string } | null { + const decl = lowerer.checker.getSymbolAtLocation(ident)?.valueDeclaration; + if ( + !decl || + !ts.isVariableDeclaration(decl) || + !ts.isIdentifier(decl.name) || + !ts.isVariableDeclarationList(decl.parent) || + (decl.parent.flags & ts.NodeFlags.Const) === 0 || + !decl.initializer + ) + return null; + let init = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + const bi = ts.isPropertyAccessExpression(init) + ? lowerer.builtinMemberOf(init) + : ts.isIdentifier(init) + ? lowerer.builtinImportOf(init) + : null; + if (!bi || (bi.module !== "assert" && bi.module !== "assert/strict")) return null; + return ASSERT_FUNCTION_VALUES.has(canonicalAssertMember(bi.module, bi.member)) ? bi : null; +} + +/** An assert method read as a value (`emitter.on("x", assert.fail)`, + * `const eq = assert.strictEqual`): an untyped function that asserts as + * the method does, the same value for every read. Null for other + * members. */ +export function assertFunctionValue( + bi: { module: string; member: string }, + loc: SrcLoc, +): IrExpr | null { + if (bi.module !== "assert" && bi.module !== "assert/strict") return null; + const member = canonicalAssertMember(bi.module, bi.member); + if (!ASSERT_FUNCTION_VALUES.has(member)) return null; + return { kind: "libCall", fn: "assert.methodValue", args: [strLit(member, loc)], type: DYN, loc }; +} + /* Node's AssertionError carries actual, expected and operator. Where the * verdict is computed in compiled code, the failure branch records them * (assert.record) before the throwing call, so a passing assertion boxes @@ -166,11 +226,22 @@ function requireStatementPosition(lowerer: Lowerer, call: ts.CallExpression, wha ); } +/** The message the runtime reads from a registered value + * (SCR_ASSERT_VALUE_MESSAGE in scr_assert.h). */ +const VALUE_MESSAGE = "\u0001scriptc:assert-message-value"; + +/** A message argument that is not a string, met while one assertion + * lowers (withValueMessage). */ +let valueMessage: { value: IrExpr; has: string; text: string } | null = null; + /** The optional trailing message argument: Node accepts a string or an * Error (throwing the Error itself) — the STRING form lowers; a literal * `undefined` counts as omitted (Node's `message == null` test). Returns * the libCall's (msg, hasMsg) pair — a "" dummy when omitted, so the - * runtime can distinguish an omitted message from an empty one. */ + * runtime can distinguish an omitted message from an empty one. Any other + * value that crosses into the checked-dynamic tree (an untyped + * `failures.join('')`, an Error) is registered with the runtime at the call + * instead (withValueMessage). */ function lowerMessageArg( lowerer: Lowerer, node: ts.Expression | undefined, @@ -180,14 +251,112 @@ function lowerMessageArg( return { msg: { kind: "strLit", value: "", type: STRING, loc }, hasMsg: boolLit(false, loc) }; } const msg = lowerer.lowerExpr(node); - if (msg.type.kind !== "string") { + if (msg.type.kind === "string") return { msg, hasMsg: boolLit(true, loc) }; + const value = + valueMessage === null && (msg.type.kind === "dyn" || lowerer.dynConvertible(msg.type)) + ? lowerer.coerceToExpected(msg, DYN) + : null; + if (value === null || value.type.kind !== "dyn") { lowerer.noLowering( `assert messages of type '${lowerer.fmt(msg.type)}'`, node, "only string messages lower (Node also accepts an Error to throw — construct and throw it directly)", ); } - return { msg, hasMsg: boolLit(true, loc) }; + const has = lowerer.declareHiddenLocal("assert.hasMessage", BOOL); + const text = lowerer.declareHiddenLocal("assert.messageText", STRING); + valueMessage = { value, has: has.id, text: text.id }; + return { msg: varRef(text.id, STRING, loc), hasMsg: varRef(has.id, BOOL, loc) }; +} + +/** Lowers one assertion; a message value met on the way is evaluated after + * the assertion's other arguments, as Node evaluates them. A null or + * undefined message is omitted (Node's `message == null` test); a truthy + * one is registered with the runtime, which the failure reads; any other + * value is the empty message, which Node's comparison diffs treat as no + * message too (createErrDiff tests the message's truthiness). */ +function withValueMessage( + lowerer: Lowerer, + expr: ts.CallExpression, + loc: SrcLoc, + lower: () => IrExpr | null, +): IrExpr | null { + const outer = valueMessage; + valueMessage = null; + try { + const lowered = lower(); + const message = valueMessage as { value: IrExpr; has: string; text: string } | null; + if (lowered === null || message === null) return lowered; + // assert.ok's statements evaluate the value first; its call is last. + const before = lowered.kind === "seqExpr" ? lowered.stmts : []; + const call = lowered.kind === "seqExpr" ? lowered.result : lowered; + if (call.kind !== "libCall") { + lowerer.noLowering( + "assert messages that are not strings in this assertion form", + expr, + "pass a string message", + ); + } + const stmts: IrStmt[] = [...before]; + const args = call.args.map((arg, index) => + arg.kind === "varRef" && (arg.localId === message.has || arg.localId === message.text) + ? arg + : stableValue(lowerer, stmts, arg, `assert.arg${index}`, loc), + ); + const local = lowerer.declareHiddenLocal("assert.message", DYN); + const value = varRef(local.id, DYN, loc); + stmts.push( + { kind: "varDecl", localId: local.id, init: message.value, loc }, + { + kind: "varDecl", + localId: message.has, + init: { kind: "dynTest", test: "nullish", negated: true, value, type: BOOL, loc }, + loc, + }, + { + kind: "varDecl", + localId: message.text, + init: { + kind: "ternary", + cond: { kind: "dynTest", test: "truthy", value, type: BOOL, loc }, + then: strLit(VALUE_MESSAGE, loc), + else_: strLit("", loc), + type: STRING, + loc, + }, + loc, + }, + { + kind: "if", + cond: { kind: "dynTest", test: "truthy", value, type: BOOL, loc }, + then: [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "assert.messageValue", + args: [value, { kind: "dynTest", test: "error", value, type: BOOL, loc }], + type: VOID, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + { kind: "exprStmt", expr: { ...call, args }, loc }, + ); + return { + kind: "seqExpr", + stmts, + result: { kind: "libCall", fn: "assert.messageClear", args: [], type: VOID, loc }, + type: VOID, + loc, + }; + } finally { + valueMessage = outer; + } } /** The module-function dispatch — the spoke's entry, called from both the @@ -201,6 +370,15 @@ export function lowerAssertModuleCall( loc: SrcLoc, ): IrExpr | null { if (bi.module !== "assert" && bi.module !== "assert/strict") return null; + return withValueMessage(lowerer, expr, loc, () => lowerAssertMember(lowerer, expr, bi, loc)); +} + +function lowerAssertMember( + lowerer: Lowerer, + expr: ts.CallExpression, + bi: { module: string; member: string }, + loc: SrcLoc, +): IrExpr | null { switch (canonicalAssertMember(bi.module, bi.member)) { case "ok": return lowerAssertOk(lowerer, expr, loc); @@ -880,7 +1058,7 @@ export function lowerAssertDirectCall( 'use the default import: import assert from "node:assert")', ); } - return lowerAssertOk(lowerer, expr, loc); + return withValueMessage(lowerer, expr, loc, () => lowerAssertOk(lowerer, expr, loc)); } /** assert(value[, message]) / assert.ok(value[, message]): the truthiness @@ -889,7 +1067,24 @@ export function lowerAssertDirectCall( * the same bytes at compile time). */ function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { requireStatementPosition(lowerer, expr, ts.getTextOfNode(expr.expression)); - if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { + if (expr.arguments.nodes.length === 0) { + // Node's message for a call without arguments. + const text = "No value argument passed to `assert.ok()`"; + return { + kind: "seqExpr", + stmts: [recordOnFailure(lowerer, boolLit(true, loc), null, boolLit(true, loc), "==", loc)], + result: { + kind: "libCall", + fn: "assert.ok", + args: [boolLit(false, loc), strLit(text, loc), boolLit(true, loc)], + type: VOID, + loc, + }, + type: VOID, + loc, + }; + } + if (expr.arguments.nodes.length > 2) { lowerer.noLowering( `${ts.getTextOfNode(expr.expression)} with ${expr.arguments.nodes.length} arguments`, expr, @@ -1056,9 +1251,22 @@ function lowerAssertEqual( // would answer false where Node can answer true (the value's own // earlier crossing) — a miscompare, never lowered. Bytes fence both // ways (the dyn copy cannot carry the Buffer/Uint8Array brand the - // checker type knows); class instances and everything non-convertible - // keep the fence too. - if (a.type.kind === "dyn" || b.type.kind === "dyn") { + // checker type knows); everything non-convertible keeps the fence too. + // A class instance crosses as a reference to the instance itself, so + // strict compares are honest reference equality, between two instances + // too (assert.strictEqual(e, e.setMaxListeners(1))). + const instanceType = (t: IrType): boolean => + isDynTypedRefType(t) && !lowerer.isSubclassOf(t.className, "%Error"); + const instance = (e: IrExpr): boolean => + !deep && + (instanceType(e.type) || + (e.type.kind === "union" && + (lowerer.unions + .get(e.type.unionId) + ?.arms.every((arm) => instanceType(arm) || isUnitType(arm)) ?? + false))) && + lowerer.dynConvertible(e.type); + if (a.type.kind === "dyn" || b.type.kind === "dyn" || (instance(a) && instance(b))) { const box = (e: IrExpr, node: ts.Expression): IrExpr => { if (e.type.kind === "dyn") return e; const k = e.type.kind; @@ -1067,6 +1275,18 @@ function lowerAssertEqual( k === "f64" || k === "string" || k === "bool" || + // A union of scalars and units crosses by value, like its arms. + (e.type.kind === "union" && + (lowerer.unions + .get(e.type.unionId) + ?.arms.every( + (arm) => + arm.kind === "f64" || + arm.kind === "string" || + arm.kind === "bool" || + isUnitType(arm), + ) ?? + false)) || (k === "func" && lowerer.dynConvertible(e.type)) || // Runtime handles cross by REFERENCE (identity preserved), so the // strict compare is honest: assert.strictEqual(this, server) — @@ -1082,6 +1302,9 @@ function lowerAssertEqual( // same). The deep walk compares the retained error data without // re-encoding and losing identity (SEMANTICS.md). (k === "object" && e.type.kind === "object" && e.type.className === "%Error") || + instance(e) || + // A symbol crosses as itself, so strict compares are identity. + (!deep && k === "symbol") || (deep && k !== "bytes" && k !== "object" && lowerer.dynConvertible(e.type)) || // Under skipPrototype the prototype a class instance or Buffer // brand would carry is not compared. @@ -1470,17 +1693,29 @@ function classifyThrowsExpected( displayName: rec ? rec.lib : info.def.name.replace(/^%/, ""), }; } - if ( - (surface === "assert.doesNotThrow" || surface === "assert.doesNotReject") && - expectedT?.kind === "func" - ) { + if (expectedT?.kind === "func") { const value = lowerer.lowerExpr(node); + // An untyped value there (a dynamic-tier function's result) is checked + // at run time like any untyped expected value. + if (value.type.kind === "jsval" || value.type.kind === "dyn") { + const boxed = lowerer.coerceToExpected(value, DYN); + if (boxed.type.kind === "dyn") return { form: "errValue", value: boxed }; + } + // A JavaScript function accepts any argument; a typed one must take + // unknown values (the caught value need not be an Error). if ( value.type.kind === "func" && - value.type.params.every((param) => param.kind === "dyn") && + (value.type.params.every((param) => param.kind === "dyn") || + isJsSourceFile(ts.getSourceFileOfNode(node))) && lowerer.dynConvertible(value.type) ) { - return { form: "predicate", value: lowerer.coerceToExpected(value, DYN) }; + // The boxed function carries its source name (Node's message names + // a named validation function). + const fnName = jsFuncNameOf(node); + return { + form: "predicate", + value: { kind: "dynFrom", value, ...(fnName !== null ? { fnName } : {}), type: DYN, loc }, + }; } lowerer.noLowering( `${surface} with this validation callback`, @@ -1546,7 +1781,9 @@ function classifyThrowsExpected( // tracingChannel suite's shape): the value crosses as a dyn error and // the runtime compares per Node's expectsError key walk. A // checked-dynamic expected rides directly; a static %Error boxes. - if (expectedT?.kind === "dyn") { + // An untyped JavaScript value (a parameter) too: a function there is + // checked at run time (assertThrowsHelper). + if (expectedT?.kind === "dyn" || expectedT === null) { const value = lowerer.lowerExpr(node); if (value.type.kind === "dyn") return { form: "errValue", value }; } @@ -1943,6 +2180,46 @@ function assertThrowsHelper( return { e: strLit("", loc), has: false }; })(); + // expectedException's function branch: a function with a prototype that + // the caught value is an instance of passes; anything else is called as + // a validation function, whose result must be true. + const validateStmts = (fn: IrExpr): IrStmt[] => { + const actual = (): IrExpr => ({ kind: "caughtToDyn", value: caughtRef(), type: DYN, loc }); + const prototype: IrExpr = { + kind: "dynKeyGet", + value: fn, + key: strLit("prototype", loc), + optional: true, + type: DYN, + loc, + }; + return [ + ifStmt( + { kind: "dynTest", test: "undefined", negated: true, value: prototype, type: BOOL, loc }, + [ + ifStmt( + checkedClassInstanceOf( + lowerer, + actual(), + fn, + "assert.throws against this native constructor is not supported yet", + loc, + ), + [doReturn], + ), + ], + ), + lib("assert.throwsValidate", [ + actual(), + fn, + strLit(mode === "rejects" ? "rejects" : "throws", loc), + m(), + hm(), + ]), + doReturn, + ]; + }; + let catchBody: IrStmt[]; if (mode === "dnr" || mode === "dnt") { const actual = (): IrExpr => ({ kind: "caughtToDyn", value: caughtRef(), type: DYN, loc }); @@ -2116,7 +2393,13 @@ function assertThrowsHelper( // caught value crosses as a dyn value (identity-preserving) and // the runtime walks the expected's keys — a mismatch throws the // deep-equal AssertionError. + // A function there is a validation function (or an error class + // value) as in the predicate form. catchBody = [ + ifStmt( + { kind: "dynTest", test: "function", value: varRef("ev.0", DYN, loc), type: BOOL, loc }, + validateStmts(varRef("ev.0", DYN, loc)), + ), lib("assert.expectsErrDyn", [ { kind: "caughtToDyn", value: caughtRef(), type: DYN, loc }, varRef("ev.0", DYN, loc), @@ -2127,7 +2410,8 @@ function assertThrowsHelper( ]; break; case "predicate": - throw new InternalCompilerError("assert validation callback only enters a no-error helper"); + catchBody = validateStmts(varRef("predicate.0", DYN, loc)); + break; } } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 14565aebc..a75796cba 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,3 +1,4 @@ +import { assertFunctionAliasOf } from "./lower-assert.js"; import { keyedHelperName } from "./stable-names.js"; import { perKeyOrder } from "./key-order-witness.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; @@ -1052,6 +1053,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { } const bi = lowerer.builtinImportOf(expr.expression); if (bi) return lowerBuiltinCall(lowerer, expr, bi, loc); + // A constant bound to an assert method (`const ok = assert.ok`) called + // directly is that assertion, with the message Node quotes the call for. + { + const alias = assertFunctionAliasOf(lowerer, expr.expression); + const served = alias ? lowerer.lowerAssertModuleCall(expr, alias, loc) : null; + if (served) return served; + } // The assert module binding called DIRECTLY (`assert(x)` — a default // import or the CJS `const assert = require("assert")`): Node's // module object IS assert.ok; namespace-import bindings fence inside @@ -8703,7 +8711,12 @@ function lowerObjectStaticCall( (probed && isDynTypedRefType(probed.type)) || (probed?.type.kind === "object" && lowerer.errorHierarchyClassOf(probed.type.className)) || (probed && constructor && (constructor.callableBase || hasRuntimeStatics(constructor))) || - (probed?.type.kind === "func" && lowerer.dynConvertible(probed.type)) + (probed?.type.kind === "func" && lowerer.dynConvertible(probed.type)) || + // null, undefined and unions of untyped JavaScript values: the same + // probe, whose ToObject throws Node's TypeError on nullish. + (probed && + (isUnitType(probed.type) || probed.type.kind === "union") && + lowerer.dynConvertible(probed.type)) ) { const loc = locOf(call); const receiver = lowerer.coerceToExpected(probed, DYN); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index c50676863..ddb764305 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1626,6 +1626,44 @@ const THROWN_AS_DYN: ReadonlySet = new Set([ "regex", ]); +/** A `return` at the top level of a CommonJS module (preflight admits it + * there only): it ends the module's evaluation, and its value is + * evaluated and discarded, as Node's module wrapper does. Top-level + * bindings and export assignments after it would be left uninitialized + * where Node leaves them in their temporal dead zone or absent, so a + * module with any keeps a refusal. Null for a return inside a function. */ +function lowerModuleLevelReturn(lowerer: Lowerer, stmt: ts.ReturnStatement): IrStmt | null { + let top: ts.Node = stmt; + while (top.parent !== undefined && !ts.isSourceFile(top.parent)) { + if (ts.isFunctionLike(top.parent) || ts.isClassStaticBlockDeclaration(top.parent)) return null; + top = top.parent; + } + const sf = top.parent; + if (sf === undefined || !ts.isSourceFile(sf)) return null; + const stmts = sf.statements.nodes; + const later = stmts.slice(stmts.indexOf(top as ts.Statement) + 1); + if ( + later.some( + (s) => + ts.isVariableStatement(s) || ts.isClassDeclaration(s) || cjsExportAssignmentOf(s) !== null, + ) + ) { + lowerer.unsupported( + "SC1090", + stmt, + "a top-level return in a CommonJS module that declares variables or classes, or assigns exports, after it", + ); + } + const loc = locOf(stmt); + const ret: IrStmt = { kind: "return", value: null, loc }; + if (!stmt.expression) return ret; + return { + kind: "block", + body: [{ kind: "exprStmt", expr: lowerer.lowerExpr(stmt.expression), loc }, ret], + loc, + }; +} + export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null { if (ownPrototypeResetStatement(lowerer, stmt)) return { kind: "block", body: [], loc: locOf(stmt) }; @@ -1735,6 +1773,8 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt if (ts.isForOfStatement(stmt)) return lowerer.lowerForOf(stmt); if (ts.isLabeledStatement(stmt)) return lowerLabeled(lowerer, stmt); if (ts.isReturnStatement(stmt)) { + const moduleReturn = lowerModuleLevelReturn(lowerer, stmt); + if (moduleReturn) return moduleReturn; // Implicit-any instance RETURN INFERENCE (function-returns.ts // resolveInferredReturn): the value lowers BARE and the statement // records itself — the post-pass settles the instance's return type diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 78980d322..8ef5a42a4 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -593,7 +593,11 @@ import { lowerTestMethodCall, lowerTestCtxProperty, } from "./lower-test.js"; -import { lowerAssertModuleCall, lowerAssertDirectCall } from "./lower-assert.js"; +import { + assertFunctionValue, + lowerAssertModuleCall, + lowerAssertDirectCall, +} from "./lower-assert.js"; import type { AssertInstanceOptions } from "./assert-instances.js"; import { lowerUtilModuleCall } from "./lower-inspect.js"; import { lowerSystemErrorMapValue } from "./lower-system-errors.js"; @@ -13985,6 +13989,8 @@ export class Lowerer { * deliberately narrower than table membership because many table rows * are only dispatch sentinels for call-site-specific lowering. */ lowerBuiltinCallableValue(bi: { module: string; member: string }, loc: SrcLoc): IrExpr | null { + const assertion = assertFunctionValue(bi, loc); + if (assertion) return assertion; if (bi.module === "util" && bi.member === "types") return utilTypesModuleValue(this, loc); if (bi.module === "util/types" && builtinModuleFnOf(this, bi.module, bi.member)) return lowerUtilTypeValue(this, bi.member, loc); diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 90acabce6..310d6f50b 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -873,6 +873,12 @@ export const OBJECT_CALLABLE_VALUES: Record result: DYN, valueParams: exactValueParams(DYN, DYN), }, + hasOwn: { + fn: "dyn.hasOwnComputed", + params: [DYN, DYN], + result: BOOL, + valueParams: exactValueParams(DYN, DYN), + }, keys: { fn: "dyn.objKeys", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) }, values: { fn: "dyn.objValues", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) }, entries: { fn: "dyn.objEntries", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) }, diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index bf75127c4..664759d54 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -5358,6 +5358,16 @@ function preflight7(load: LoadResult): { return new RegExp(`\\btypeof\\s*\\(?\\s*${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(sf.text); }; + // A `return` at the top level of a CommonJS module (TS1108): Node runs + // the module as a function body, so the return ends the module's + // evaluation. ES modules and TypeScript keep the error. + const cjsTopLevelReturnSuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => { + if (d.code !== 1108 || d.fileName === undefined || !isJsSourceFileName(d.fileName)) + return false; + const sf = p.getSourceFile(d.fileName); + return sf !== undefined && isCjsJsFile7(sf, p); + }; + const workspaceImplicitAnySuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => { if (d.code !== 7016 || d.fileName === undefined || d.pos === undefined || d.end === undefined) return false; @@ -5404,6 +5414,7 @@ function preflight7(load: LoadResult): { !isNpmStaticSubclassArgument(p, d) && !workspaceImplicitAnySuppressed(p, d) && !typeofTestedNameSuppressed(p, d) && + !cjsTopLevelReturnSuppressed(p, d) && !jsdocTypeSuppressed(p, d, commentDup), ); }; diff --git a/packages/compiler/src/frontend/tsc-codes.ts b/packages/compiler/src/frontend/tsc-codes.ts index e6c0be7cd..e8ffc29fb 100644 --- a/packages/compiler/src/frontend/tsc-codes.ts +++ b/packages/compiler/src/frontend/tsc-codes.ts @@ -132,6 +132,10 @@ export const JS_RELAXED_TSC_CODES: ReadonlySet = new Set([ // binding types as any and reached uses fence instead of passing unchecked 2307, 2792, + // `delete` of a property the inference typed required (2790): JS deletes + // any configurable property, and the lowering checks each delete's + // receiver itself + 2790, // useUnknownInCatchVariables' annotation demand ("'e' is of type // 'unknown'", "Object is of type 'unknown'"): a JS catch clause CANNOT // annotate its variable, so the demand is unsatisfiable in source — diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 390c7fbc3..1924dc7b2 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -976,6 +976,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "assert.noErrorPredicate", "assert.regexDynTest", "assert.expectsErrDyn", + "assert.throwsValidate", "assert.ifErrorErr", "assert.ifErrorF64", "assert.ifErrorStr", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 216e46a4d..25a86a82b 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4410,6 +4410,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ // The equality quartet over checked-dynamic operands (the frontend // boxes a static side into the checked-dynamic tree first). "assert.eqDyn": { argTypes: [DYN, DYN, BOOL, BOOL, STRING, BOOL], result: VOID }, + /** A message argument that is not a string, registered for the next + * failing assertion (scr_assert_message_value). */ + "assert.messageValue": { argTypes: [DYN, BOOL], result: VOID }, + /** An assert method as a function value, by canonical name. */ + "assert.methodValue": { argTypes: [STRING], result: DYN }, + "assert.messageClear": { argTypes: [], result: VOID }, "assert.looseResult": { argTypes: [BOOL, BOOL, STRING, STRING, STRING, BOOL], result: VOID }, // The throws(fn, {shape}) accumulator: begin/slot calls never throw; // shapeEnd throws the Comparison diff. The error slot is the @@ -4435,6 +4441,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * against the caught value's; a mismatch throws the deep-equal * AssertionError (scr_assert.c). MAY THROW by design. */ "assert.expectsErrDyn": { argTypes: [DYN, DYN, STRING, BOOL], result: VOID }, + /** A validation function against the caught value (Node's expectedException). */ + "assert.throwsValidate": { argTypes: [DYN, DYN, STRING, STRING, BOOL], result: VOID }, // assert.ifError's typed entries (always throw; unit args never // lower). The error slot is program-dependent — any %Error-hierarchy // class, root or subclass (the insp.error precedent, null slot). diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8062-assert-message-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8062-assert-message-values.cjs.json new file mode 100644 index 000000000..78ba99298 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8062-assert-message-values.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8062-assert-message-values.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8065-cjs-top-level-return/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8065-cjs-top-level-return/main.js.json new file mode 100644 index 000000000..22edecf3f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8065-cjs-top-level-return/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/8065-cjs-top-level-return/early.js", + "/tests/corpus/8065-cjs-top-level-return/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8066-object-hasown-delete.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8066-object-hasown-delete.cjs.json new file mode 100644 index 000000000..bf862b8f2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8066-object-hasown-delete.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8066-object-hasown-delete.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8067-assert-throws-validators.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8067-assert-throws-validators.cjs.json new file mode 100644 index 000000000..89f59e97c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8067-assert-throws-validators.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8067-assert-throws-validators.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8076-assert-instance-identity.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8076-assert-instance-identity.cjs.json new file mode 100644 index 000000000..b4212e234 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8076-assert-instance-identity.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8076-assert-instance-identity.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8078-instanceof-object-function.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8078-instanceof-object-function.cjs.json new file mode 100644 index 000000000..90af753d1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8078-instanceof-object-function.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8078-instanceof-object-function.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8190-assert-function-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8190-assert-function-values.cjs.json new file mode 100644 index 000000000..730dd6360 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8190-assert-function-values.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8190-assert-function-values.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8191-assert-function-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8191-assert-function-values.ts.json new file mode 100644 index 000000000..0837dc2ee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8191-assert-function-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8191-assert-function-values.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_assert.c b/packages/runtime/src/scr_assert.c index 4917b1291..738a7729c 100644 --- a/packages/runtime/src/scr_assert.c +++ b/packages/runtime/src/scr_assert.c @@ -212,7 +212,56 @@ bool scr_assert_skip_prototype(void) { return (g_instance_options & SCR_ASSERT_SKIP_PROTOTYPE) != 0; } +/* A message argument that is not a string (an untyped value, or an Error + * to throw): the compiled call registers it before the assertion and passes + * SCR_ASSERT_VALUE_MESSAGE as its message, which the failure paths compose + * like any custom message. A failure whose message starts with it throws + * the Error itself (innerFail's `message instanceof Error`) or puts + * String(value) in its place with generatedMessage `!value`, as Node's + * AssertionError does. Matching the sentinel keeps a registration that no + * failure consumed from reaching another assertion. */ +static SCR_TL ScrDyn *g_message_value; +static SCR_TL bool g_message_error; + +void scr_assert_message_value(ScrDyn *value, bool error) { + if (g_message_value) scr_dyn_release(g_message_value); + g_message_value = scr_dyn_retain(value); + g_message_error = error; +} + +void scr_assert_message_clear(void) { + if (g_message_value) scr_dyn_release(g_message_value); + g_message_value = NULL; +} + void scr_assert_fail(ScrStr *message, bool generated) { + static const char sentinel[] = SCR_ASSERT_VALUE_MESSAGE; + const size_t n = sizeof sentinel - 1; + if (g_message_value && message->len >= n && memcmp(message->data, sentinel, n) == 0) { + ScrDyn *value = g_message_value; + g_message_value = NULL; + if (g_message_error) { + scr_str_release(message); + scr_assert_record(NULL, NULL, NULL); + g_instance_options = 0; + scr_dyn_throw(value); + return; + } + ScrStr *text = scr_dyn_string_coerce_js(value); + generated = !scr_dyn_truthy(value); + scr_dyn_release(value); + if (!text) { + scr_str_release(message); + return; + } + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_write(&b, text->data, text->len); + scr_jb_write(&b, message->data + n, message->len - n); + scr_str_release(text); + scr_str_release(message); + message = scr_jb_finish(&b); + } ScrError *e = scr_assert_error(message, generated, scr_assert_diff_full() ? "full" : "simple"); g_instance_options = 0; scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); @@ -2703,6 +2752,60 @@ bool scr_assert_no_error_predicate(ScrDyn *predicate, ScrDyn *actual) { return matched; } +/* assert.throws/rejects with a validation function (expectedException's + * function branch, after its instanceof check): the function runs with a + * fresh object as `this` and the caught value, and any result but true + * fails, with Node's generated message unless the call passed one. The + * caught value and the function are the AssertionError's actual and + * expected. Borrows all. */ +void scr_assert_throws_validate(ScrDyn *actual, ScrDyn *validate, ScrStr *op, ScrStr *msg, + bool has_msg) { + ScrDyn *receiver = scr_dyn_new_obj(); + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(validate, &actual, 1, "expected"); + scr_dyn_this_pop(); + scr_dyn_release(receiver); + if (!result) return; + if (result->kind == SCR_DYN_BOOL && result->v.b) { + scr_dyn_release(result); + return; + } + ScrStr *message; + bool generated = !(has_msg && msg->len > 0); + if (!generated) { + message = scr_str_retain(msg); + } else { + ScrAssertBuf b = {0}; + ab_cstr(&b, "The "); + ScrDyn *name = scr_dyn_fn_get(validate, "name", 4); + if (name && name->kind == SCR_DYN_STR && name->v.str->len > 0) { + ab_cstr(&b, "\""); + ab_bytes(&b, name->v.str->data, name->v.str->len); + ab_cstr(&b, "\" "); + } + if (name) scr_dyn_release(name); + ab_cstr(&b, "validation function is expected to return \"true\". Received "); + ScrStr *shown = scr_assert_cf_inspect(result); + ab_bytes(&b, shown->data, shown->len); + scr_str_release(shown); + if (scr_dyn_ref_is_error(actual)) { + ScrStr *text = scr_dyn_string_coerce_js(actual); + if (!text) { + scr_dyn_release(result); + scr_str_release(ab_take(&b)); + return; + } + ab_cstr(&b, "\n\nCaught error:\n\n"); + ab_bytes(&b, text->data, text->len); + scr_str_release(text); + } + message = ab_take(&b); + } + scr_dyn_release(result); + scr_assert_record_dyn(actual, validate, op); + scr_assert_fail(message, generated); +} + /* ── assert.ifError ─────────────────────────────────────────────────── * Node throws for ANY value but null/undefined (falsy included); the * frontend routes null/undefined to a no-op and everything else here. @@ -2879,3 +2982,214 @@ ScrError *scr_assert_error_new(ScrDyn *options) { scr_assert_attach_values(e, generated, actual, expected, op, diff); return e; } + +/* ── assert functions as values ────────────────────────────────────────── + * `assert.fail` stored or passed as a function (`emitter.on("x", + * assert.fail)`): one function per method, which takes its arguments as + * untyped values and asserts as the method does. */ + +/* Node's message argument: absent or null/undefined (a generated message), + * a string, an Error (thrown when the assertion fails), or another value + * (String(value)). The value is registered as scr_assert_fail reads it; the + * caller clears it after the assertion. +1 into *msg. */ +static void scr_av_message(ScrDyn *const *args, size_t argc, size_t i, ScrStr **msg, bool *has) { + const ScrDyn *m = i < argc ? args[i] : NULL; + if (!m || m->kind == SCR_DYN_UNDEF || m->kind == SCR_DYN_NULL) { + *msg = scr_str_new("", 0); + *has = false; + return; + } + *has = true; + if (m->kind == SCR_DYN_STR) { + *msg = scr_str_retain(m->v.str); + return; + } + bool error = scr_dyn_ref_is_error(m) || + (m->kind == SCR_DYN_OBJ && scr_dyn_obj_get(m, "%error", 6) != NULL); + scr_assert_message_value((ScrDyn *)m, error); + *msg = scr_str_new(SCR_ASSERT_VALUE_MESSAGE, sizeof SCR_ASSERT_VALUE_MESSAGE - 1); +} + +static const ScrDyn *scr_av_arg(ScrDyn *const *args, size_t argc, size_t i) { + return i < argc ? args[i] : scr_dyn_undefined(); +} + +static ScrDyn *scr_av_done(ScrStr *msg) { + scr_str_release(msg); + scr_assert_message_clear(); + return scr_dyn_retain(scr_dyn_undefined()); +} + +/* assert(value, message) and assert.ok: Node's generated message quotes + * the call's source, which a function value has no fixed site for; the + * message here is AssertionError's own `value == true`. */ +static ScrDyn *scr_av_ok(ScrClosure *c, ScrDyn *const *args, size_t argc) { + (void)c; + const ScrDyn *value = scr_av_arg(args, argc, 0); + if (scr_dyn_truthy(value)) return scr_dyn_retain(scr_dyn_undefined()); + scr_assert_record(scr_dyn_retain((ScrDyn *)value), scr_dyn_new_bool(true), "=="); + if (argc == 0) { + static const char text[] = "No value argument passed to `assert.ok()`"; + scr_assert_fail(scr_str_new(text, sizeof text - 1), true); + return NULL; + } + ScrStr *msg; + bool has; + scr_av_message(args, argc, 1, &msg, &has); + if (has) { + scr_assert_fail(scr_str_retain(msg), false); + return scr_av_done(msg); + } + ScrStr *shown = scr_assert_cf_inspect(value); + ScrAssertBuf b = {0}; + ab_str(&b, shown); + ab_cstr(&b, " == true"); + scr_str_release(shown); + scr_assert_fail(ab_take(&b), true); + return scr_av_done(msg); +} + +/* assert.fail(message): "Failed" without a message. */ +static ScrDyn *scr_av_fail(ScrClosure *c, ScrDyn *const *args, size_t argc) { + (void)c; + if (argc > 1) { + static const char text[] = "assert.fail with more than one argument through a function value is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, text, sizeof text - 1, "SC2020"); + return NULL; + } + ScrStr *msg; + bool has; + scr_av_message(args, argc, 0, &msg, &has); + scr_assert_record(scr_dyn_retain(scr_dyn_undefined()), scr_dyn_retain(scr_dyn_undefined()), "fail"); + if (has) scr_assert_fail(scr_str_retain(msg), false); + else scr_assert_fail(scr_str_new("Failed", 6), true); + return scr_av_done(msg); +} + +/* equal / notEqual: Node's loose comparison, NaN equal to NaN. */ +static ScrDyn *scr_av_loose(ScrDyn *const *args, size_t argc, bool negated) { + const ScrDyn *a = scr_av_arg(args, argc, 0), *b = scr_av_arg(args, argc, 1); + bool nan = a->kind == SCR_DYN_NUM && b->kind == SCR_DYN_NUM && isnan(a->v.num) && isnan(b->v.num); + bool equal = nan || scr_dyn_abstract_eq(a, b); + if (scr_exc_pending()) return NULL; + if (negated ? !equal : equal) return scr_dyn_retain(scr_dyn_undefined()); + ScrStr *msg; + bool has; + scr_av_message(args, argc, 2, &msg, &has); + scr_assert_record(scr_dyn_retain((ScrDyn *)a), scr_dyn_retain((ScrDyn *)b), negated ? "!=" : "=="); + ScrStr *ia = scr_assert_cf_inspect(a), *ib = scr_assert_cf_inspect(b); + scr_assert_loose_result(equal, negated, ia, ib, msg, has); + scr_str_release(ia); + scr_str_release(ib); + return scr_av_done(msg); +} + +static ScrDyn *scr_av_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_loose(a, n, false); +} +static ScrDyn *scr_av_not_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_loose(a, n, true); +} + +/* The strict and deep comparisons, through the untyped assert paths. */ +enum { SCR_AV_STRICT, SCR_AV_DEEP_STRICT, SCR_AV_DEEP, SCR_AV_PARTIAL }; + +static ScrDyn *scr_av_compare(ScrDyn *const *args, size_t argc, int form, bool negated) { + ScrDyn *a = (ScrDyn *)scr_av_arg(args, argc, 0), *b = (ScrDyn *)scr_av_arg(args, argc, 1); + ScrStr *msg; + bool has; + scr_av_message(args, argc, 2, &msg, &has); + if (form == SCR_AV_DEEP) scr_assert_loose_deep_dyn(a, b, negated, msg, has); + else if (form == SCR_AV_PARTIAL) scr_assert_partial_deep_dyn(a, b, msg, has); + else scr_assert_eq_dyn(a, b, negated, form == SCR_AV_DEEP_STRICT, msg, has); + return scr_av_done(msg); +} + +static ScrDyn *scr_av_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_compare(a, n, SCR_AV_STRICT, false); +} +static ScrDyn *scr_av_not_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_compare(a, n, SCR_AV_STRICT, true); +} +static ScrDyn *scr_av_deep_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_compare(a, n, SCR_AV_DEEP_STRICT, false); +} +static ScrDyn *scr_av_not_deep_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_compare(a, n, SCR_AV_DEEP_STRICT, true); +} +static ScrDyn *scr_av_deep_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_compare(a, n, SCR_AV_DEEP, false); +} +static ScrDyn *scr_av_not_deep_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_compare(a, n, SCR_AV_DEEP, true); +} +static ScrDyn *scr_av_partial_deep_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { + (void)c; + return scr_av_compare(a, n, SCR_AV_PARTIAL, false); +} + +static ScrDyn *scr_av_if_error(ScrClosure *c, ScrDyn *const *args, size_t argc) { + (void)c; + scr_assert_iferror_dyn(scr_av_arg(args, argc, 0)); + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); +} + +static const struct { + const char *name; + ScrDynThunk thunk; + uint32_t arity; +} scr_av_methods[] = { + {"ok", &scr_av_ok, 0}, + {"fail", &scr_av_fail, 1}, + {"equal", &scr_av_equal, 3}, + {"notEqual", &scr_av_not_equal, 3}, + {"strictEqual", &scr_av_strict_equal, 3}, + {"notStrictEqual", &scr_av_not_strict_equal, 3}, + {"deepEqual", &scr_av_deep_equal, 3}, + {"notDeepEqual", &scr_av_not_deep_equal, 3}, + {"deepStrictEqual", &scr_av_deep_strict_equal, 3}, + {"notDeepStrictEqual", &scr_av_not_deep_strict_equal, 3}, + {"partialDeepStrictEqual", &scr_av_partial_deep_strict_equal, 3}, + {"ifError", &scr_av_if_error, 1}, +}; +#define SCR_AV_COUNT (sizeof scr_av_methods / sizeof scr_av_methods[0]) +static SCR_TL ScrDyn *scr_av_values[SCR_AV_COUNT]; +static SCR_TL bool scr_av_registered; + +static void scr_av_cleanup(void) { + for (size_t i = 0; i < SCR_AV_COUNT; i++) { + scr_dyn_release(scr_av_values[i]); + scr_av_values[i] = NULL; + } + scr_av_registered = false; +} + +/* The function value of an assert method by its canonical name (the frontend + * maps assert/strict's aliases), +1; one value per method, so stored copies + * compare equal. */ +ScrDyn *scr_assert_method_value(ScrStr *name) { + for (size_t i = 0; i < SCR_AV_COUNT; i++) { + if (strlen(scr_av_methods[i].name) != name->len || + memcmp(scr_av_methods[i].name, name->data, name->len) != 0) + continue; + if (!scr_av_values[i]) { + if (!scr_av_registered) { + scr_av_registered = true; + scr_atexit(scr_av_cleanup); + } + scr_av_values[i] = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_av_methods[i].thunk, + scr_av_methods[i].arity, "native:assert", + scr_av_methods[i].name); + } + return scr_dyn_retain(scr_av_values[i]); + } + return scr_dyn_retain(scr_dyn_undefined()); +} diff --git a/packages/runtime/src/scr_assert.h b/packages/runtime/src/scr_assert.h index 8037f08ec..57158a69a 100644 --- a/packages/runtime/src/scr_assert.h +++ b/packages/runtime/src/scr_assert.h @@ -24,6 +24,19 @@ void scr_assert_fail(ScrStr *message, bool generated); /* takes ownership; alway * consumes them; a passing call clears them. */ void scr_assert_set_options(int options); void scr_assert_clear_options(void); +/* A message argument that is not a string, for the next assertion that + * fails with SCR_ASSERT_VALUE_MESSAGE as its message (scr_assert.c): an + * Error (`error`) is thrown as it is, anything else reports String(value). + * Borrows value; clear drops a registration the call did not consume. */ +#define SCR_ASSERT_VALUE_MESSAGE "\x01scriptc:assert-message-value" +void scr_assert_message_value(struct ScrDyn *value, bool error); +void scr_assert_message_clear(void); +/* An assert method as a function value, by canonical name (+1). */ +struct ScrDyn *scr_assert_method_value(ScrStr *name); +/* assert.throws/rejects with a validation function: its result must be + * true (Node's expectedException), else the AssertionError. Borrows all. */ +void scr_assert_throws_validate(struct ScrDyn *actual, struct ScrDyn *validate, ScrStr *op, + ScrStr *msg, bool has_msg); bool scr_assert_diff_full(void); bool scr_assert_skip_prototype(void); void scr_assert_record(ScrDyn *actual, ScrDyn *expected, const char *op); /* values move in */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index eb03ab6cc..ecc49e8cf 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4665,6 +4665,71 @@ int scr_dyn_has_instance_hook(const ScrDyn *value, const ScrDyn *callee) { return truthy ? 1 : 0; } +/* `value instanceof Object`: whether Object.prototype is on the value's + * prototype chain. Primitives answer false, and so does an object whose + * chain ends in null first (Object.create(null), a null-prototype record). + * Objects without a chain the runtime walks (class instances, native + * handles, errors, promises) inherit Object.prototype. False with the + * exception pending when a prototype read throws. */ +static bool scr_dyn_instanceof_object(const ScrDyn *value) { + switch (value->kind) { + case SCR_DYN_UNDEF: + case SCR_DYN_NULL: + case SCR_DYN_BOOL: + case SCR_DYN_NUM: + case SCR_DYN_STR: + case SCR_DYN_BIGINT: + case SCR_DYN_SYMBOL: + return false; + default: + break; + } + ScrDyn *current = scr_dyn_retain((ScrDyn *)value); + for (;;) { + if (current == scr_builtin_object_prototype) { + scr_dyn_release(current); + return true; + } + ScrDyn *next = NULL; + if (current->kind == SCR_DYN_FUNC) { + /* A function's prototype is Function.prototype, whose own is + * Object.prototype, unless the program set it. */ + if (scr_closure_identity(current->v.fn.clo)->props) { + ScrDyn *table = scr_dyn_fn_properties(current); + next = table->prototype ? scr_dyn_retain(table->prototype) + : table->null_proto ? scr_dyn_new_null() : NULL; + scr_dyn_release(table); + } + } else if (current->kind == SCR_DYN_OBJ || current->kind == SCR_DYN_ARR || + (current->kind == SCR_DYN_TYPED_REF && + strncmp(current->v.typed_ref.type_key, "object:", 7) != 0)) { + next = scr_dyn_get_prototype(current); + if (!next) { + scr_dyn_release(current); + return false; /* the read threw: pending */ + } + } + scr_dyn_release(current); + if (!next) return true; + if (next->kind == SCR_DYN_NULL) { + scr_dyn_release(next); + return false; + } + current = next; + } +} + +/* `value instanceof Function`: every function, unless the program set its + * prototype to null or to a non-function. */ +static bool scr_dyn_instanceof_function(const ScrDyn *value) { + if (value->kind != SCR_DYN_FUNC) return false; + if (!scr_closure_identity(value->v.fn.clo)->props) return true; + ScrDyn *table = scr_dyn_fn_properties((ScrDyn *)value); + bool result = table->prototype ? scr_dyn_instanceof_function(table->prototype) : !table->null_proto; + scr_dyn_release(table); + return result; +} + 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"; @@ -4673,6 +4738,13 @@ bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const Sc } int hooked = scr_dyn_has_instance_hook(value, callee); if (hooked >= 0) return hooked == 1; + if (callee->v.fn.sig && callee->v.fn.name) { + if (strcmp(callee->v.fn.sig, "native:constructor") == 0 && strcmp(callee->v.fn.name, "Object") == 0) + return scr_dyn_instanceof_object(value); + if (strcmp(callee->v.fn.sig, "native:function-constructor") == 0 && + strcmp(callee->v.fn.name, "Function") == 0) + return scr_dyn_instanceof_function(value); + } /* An error constructor held as a value (`inst instanceof params.Parent` * with Parent === Error): the error kind's interval, and for Error itself * objects inheriting Error.prototype. */ diff --git a/tests/corpus/8062-assert-message-values.cjs b/tests/corpus/8062-assert-message-values.cjs new file mode 100644 index 000000000..7f84ccd37 --- /dev/null +++ b/tests/corpus/8062-assert-message-values.cjs @@ -0,0 +1,41 @@ +'use strict'; +// node:assert messages that are not string literals: an untyped string, an +// Error to throw, null/undefined (omitted) and falsy values, and +// scalar-union operands against untyped values. +const assert = require('assert'); + +function attempt(label, fn) { + try { + fn(); + console.log(label, 'passed'); + } catch (e) { + console.log(label, e.name, JSON.stringify(e.message), e.generatedMessage, e.code); + } +} + +const failures = []; +for (const x of ['a', 'b']) if (x === 'c') failures.push(`\n${x}`); +attempt('joined passes', () => assert.strictEqual(failures.length, 0, failures.join(''))); +const untyped = JSON.parse('"untyped message"'); +attempt('untyped', () => assert.strictEqual(1, 2, untyped)); +attempt('untyped deep', () => assert.deepStrictEqual([1], [2], untyped)); +attempt('untyped notStrict', () => assert.notStrictEqual(1, 1, untyped)); +attempt('error message', () => assert.strictEqual(1, 2, new TypeError('own error'))); +attempt('error deep', () => assert.deepStrictEqual({ a: 1 }, { a: 2 }, new RangeError('own range'))); +const none = JSON.parse('null'); +attempt('null message', () => assert.strictEqual(1, 2, none)); +const zero = JSON.parse('0'); +attempt('zero message deep', () => assert.deepStrictEqual([1], [2], zero)); +attempt('passing error message', () => assert.strictEqual(1, 1, new Error('unused'))); +attempt('later string message', () => assert.strictEqual(1, 2, 'plain')); + +// Operand order: both operands evaluate before the message. +const order = []; +const mark = (label, value) => (order.push(label), value); +attempt('order', () => assert.strictEqual(mark('a', 1), mark('b', 2), mark('m', JSON.parse('"m"')))); +console.log(order.join(',')); + +const row = [['a', 'b', true], ['c', 'd', 'x']]; +for (const [, , expected] of row) { + attempt('union operand', () => assert.strictEqual(JSON.parse(JSON.stringify(expected)), expected)); +} diff --git a/tests/corpus/8065-cjs-top-level-return/early.js b/tests/corpus/8065-cjs-top-level-return/early.js new file mode 100644 index 000000000..8d7de4ba3 --- /dev/null +++ b/tests/corpus/8065-cjs-top-level-return/early.js @@ -0,0 +1,6 @@ +'use strict'; +exports.ready = true; +console.log('early module start', typeof helper); +if (exports.ready) return; +console.log('early module end'); +function helper() {} diff --git a/tests/corpus/8065-cjs-top-level-return/main.js b/tests/corpus/8065-cjs-top-level-return/main.js new file mode 100644 index 000000000..7743eaac5 --- /dev/null +++ b/tests/corpus/8065-cjs-top-level-return/main.js @@ -0,0 +1,16 @@ +'use strict'; +// A CommonJS module may end its evaluation with a top-level return: the +// rest of the module does not run, while timers, exit handlers and the +// modules it already required keep their effects. +const early = require('./early.js'); +console.log('early exports', early.ready); +setTimeout(() => console.log('timer ran'), 1); +process.on('exit', () => console.log('exit handler')); +for (const value of [1, 2, 3]) { + if (value === 2) return 'ignored value'; + console.log('value', value); +} +console.log('not reached'); +function hoisted() { + return 'hoisted'; +} diff --git a/tests/corpus/8065-cjs-top-level-return/package.json b/tests/corpus/8065-cjs-top-level-return/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/tests/corpus/8065-cjs-top-level-return/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/corpus/8066-object-hasown-delete.cjs b/tests/corpus/8066-object-hasown-delete.cjs new file mode 100644 index 000000000..11668b521 --- /dev/null +++ b/tests/corpus/8066-object-hasown-delete.cjs @@ -0,0 +1,23 @@ +'use strict'; +// Object.hasOwn over nullish receivers and as a value, and `delete` of +// properties JavaScript inference types as required. +const o = { a: 1, b: undefined }; +console.log(Object.hasOwn(o, 'a'), Object.hasOwn(o, 'b'), Object.hasOwn(o, 'c')); +console.log(Object.hasOwn([1, 2], 1), Object.hasOwn([1, 2], 2), Object.hasOwn(Object.create({ x: 1 }), 'x')); +for (const value of [null, undefined]) { + try { + Object.hasOwn(value, 'a'); + } catch (e) { + console.log(e.name, e.message); + } +} +const pick = Math.random() < 2 ? { k: 1 } : null; +console.log(Object.hasOwn(pick, 'k')); +const hasOwn = Object.hasOwn; +console.log(hasOwn({ k: 1 }, 'k'), hasOwn({}, 'k'), [{ a: 1 }, {}].map((x) => hasOwn(x, 'a')).join()); +const record = { a: 1, b: 2 }; +delete record.a; +console.log(Object.keys(record).join(), Object.hasOwn(record, 'a'), record.a); +const table = { one: 1, two: 2 }; +for (const key of Object.keys(table)) if (key === 'one') delete table[key]; +console.log(JSON.stringify(table)); diff --git a/tests/corpus/8067-assert-throws-validators.cjs b/tests/corpus/8067-assert-throws-validators.cjs new file mode 100644 index 000000000..189f4221a --- /dev/null +++ b/tests/corpus/8067-assert-throws-validators.cjs @@ -0,0 +1,22 @@ +'use strict'; +// assert.throws and assert.rejects with validation functions, constructor +// functions, and untyped expected values (Node's expectedException). +const assert = require('assert'); +function show(label, fn) { + try { fn(); console.log(label, 'passed'); } catch (e) { console.log(label, e.name, JSON.stringify(e.message), e.generatedMessage, e.operator, e.actual && e.actual.message); } +} +function check(err) { return err.message === 'x'; } +show('named false', () => assert.throws(() => { throw new Error('y'); }, check)); +show('named true', () => assert.throws(() => { throw new Error('x'); }, check)); +show('custom message', () => assert.throws(() => { throw new Error('y'); }, check, 'custom')); +show('returns 1', () => assert.throws(() => { throw new Error('y'); }, () => 1)); +show('non-error thrown', () => assert.throws(() => { throw 'str'; }, (v) => v === 'str')); +show('non-error fails', () => assert.throws(() => { throw 42; }, () => undefined)); +function MyErr(m) { this.message = m; } +show('constructor', () => assert.throws(() => { throw new MyErr('q'); }, MyErr)); +function untyped(expected) { assert.throws(() => { throw new Error('u'); }, expected); } +show('untyped validator', () => untyped((e) => e.message === 'u')); +show('untyped validator fails', () => untyped(function v() { return false; })); +show('untyped object', () => untyped({ message: 'u' })); +assert.rejects(Promise.reject(new Error('r')), (e) => e.message === 'r').then(() => console.log('rejects passed')); +assert.rejects(Promise.reject(new Error('r')), () => false).catch((e) => console.log('rejects', JSON.stringify(e.message), e.operator)); diff --git a/tests/corpus/8076-assert-instance-identity.cjs b/tests/corpus/8076-assert-instance-identity.cjs new file mode 100644 index 000000000..e8cedf3d6 --- /dev/null +++ b/tests/corpus/8076-assert-instance-identity.cjs @@ -0,0 +1,50 @@ +'use strict'; +// assert.strictEqual and notStrictEqual compare class instances by +// reference: two instances, an instance and the same instance read back +// through an untyped value, and an instance a method returns. Symbols +// compare by identity too. +const assert = require('assert'); +const EventEmitter = require('events'); + +class Box { + constructor(v) { + this.v = v; + } + self() { + return this; + } +} + +const b = new Box(1); +const c = new Box(1); +assert.strictEqual(b, b.self()); +assert.notStrictEqual(b, c); +const e = new EventEmitter(); +assert.strictEqual(e, e.setMaxListeners(1)); +const untyped = [e, b][0]; +assert.strictEqual(untyped, e); +assert.strictEqual(e, untyped); +assert.notStrictEqual(untyped, b); + +try { + assert.strictEqual(b, c); +} catch (error) { + console.log(error.code); + console.log(error.message); +} +try { + assert.notStrictEqual(b, b.self(), 'same box'); +} catch (error) { + console.log(error.code, error.message); +} +// A symbol read back through an untyped value is the same symbol. +const symbol = Symbol('s'); +const read = Reflect.get({ k: symbol }, 'k'); +assert.strictEqual(read, symbol); +assert.notStrictEqual(read, Symbol('s')); +try { + assert.strictEqual(read, Symbol('other')); +} catch (error) { + console.log(error.code, error.message.split('\n')[0]); +} +console.log('done'); diff --git a/tests/corpus/8078-instanceof-object-function.cjs b/tests/corpus/8078-instanceof-object-function.cjs new file mode 100644 index 000000000..9a72de00f --- /dev/null +++ b/tests/corpus/8078-instanceof-object-function.cjs @@ -0,0 +1,55 @@ +'use strict'; +// `x instanceof Object` and `x instanceof Function` on untyped values follow +// Node: objects are instances of Object unless their prototype chain ends +// in null first, and functions (classes and arrows included) are instances +// of Function. +class Shape {} +class Square extends Shape {} + +const nullProto = Object.create(null); +const underNull = Object.create(nullProto); +const regrown = Object.create(null); +Object.setPrototypeOf(regrown, {}); +const orphan = function orphan() {}; +Object.setPrototypeOf(orphan, null); +function declared() {} + +const values = { + 'plain object': {}, + array: [1, 2], + 'Object.create(null)': nullProto, + 'object under a null-prototype object': underNull, + 'Object.create(null) given a prototype': regrown, + 'Object.create(plain object)': Object.create({ a: 1 }), + 'parsed JSON': JSON.parse('{"a":[1]}'), + 'function declaration': declared, + 'bound function': declared.bind(null), + 'arrow function': () => 1, + 'async function': async () => {}, + class: Square, + 'class instance': new Square(), + 'function with a null prototype': orphan, + map: new Map(), + date: new Date(0), + 'regular expression': /x/, + error: new TypeError('t'), + 'typed array': new Uint8Array(2), + promise: Promise.resolve(1), + number: 1, + string: 's', + boolean: false, + null: null, + undefined: undefined, + symbol: Symbol('s'), + bigint: 10n, +}; + +for (const [label, value] of Object.entries(values)) { + console.log(`${label}: Object ${value instanceof Object}, Function ${value instanceof Function}`); +} + +function describe(value) { + if (!(value instanceof Object)) return 'primitive or null-prototype'; + return value instanceof Function ? 'function' : 'object'; +} +console.log([nullProto, {}, describe, 0].map(describe).join(', ')); diff --git a/tests/corpus/8190-assert-function-values.cjs b/tests/corpus/8190-assert-function-values.cjs new file mode 100644 index 000000000..b91434899 --- /dev/null +++ b/tests/corpus/8190-assert-function-values.cjs @@ -0,0 +1,30 @@ +'use strict'; +// assert methods are function values: stored, passed and called through +// aliases, they assert as the methods do, and every read is the same +// function. A constant bound to a method and called directly reports +// the call as Node quotes it. +const assert = require('assert'); +const { strictEqual, deepStrictEqual } = require('assert'); +const strict = require('assert/strict'); +const EventEmitter = require('events'); +console.log(typeof assert.fail, assert.fail === assert.fail, strict.equal === assert.strictEqual, strictEqual === assert.strictEqual); +const ok = assert.ok; +ok(1); +const tryIt = (label, fn) => { try { fn(); console.log(label, 'passed'); } catch (e) { console.log(label, e.name, e.code, JSON.stringify(e.message), e.generatedMessage); } }; +tryIt('ok', () => ok(0)); +tryIt('ok none', () => ok()); +tryIt('ok msg', () => ok(false, 'nope')); +tryIt('ok err', () => ok(false, new TypeError('own'))); +tryIt('fail', () => assert.fail.call(null)); +tryIt('fail msg', () => (0, assert.fail)('boom')); +tryIt('equal', () => assert.equal.call(null, 1, '1')); +tryIt('equal fail', () => [assert.equal][0](1, 2)); +tryIt('notEqual fail', () => [assert.notEqual][0](1, '1')); +tryIt('strict', () => strictEqual(1, 2)); +tryIt('deep', () => deepStrictEqual({ a: 1 }, { a: 1 })); +tryIt('deep fail', () => [deepStrictEqual][0]({ a: 1 }, { a: 2 }, 'differs')); +tryIt('ifError', () => [assert.ifError][0](null)); +const checks = [assert.ok, assert.fail, assert.strictEqual]; +checks.forEach((check, i) => console.log(i, typeof check, check === checks[i], check.name)); +[1, 'x', true].forEach((value) => assert.ok(value)); +console.log('done'); diff --git a/tests/corpus/8191-assert-function-values.ts b/tests/corpus/8191-assert-function-values.ts new file mode 100644 index 000000000..8ef24153b --- /dev/null +++ b/tests/corpus/8191-assert-function-values.ts @@ -0,0 +1,16 @@ +// assert methods read as values in TypeScript: a constant alias called +// directly, a stored value, and a method passed as a callback. +import assert from "node:assert"; +const eq = assert.strictEqual; +eq(1, 1); +const fail = assert.fail; +const checks: unknown[] = [assert.ok, assert.fail]; +console.log(typeof checks[0], checks[1] === fail); +try { + eq(1, 2, "one is not two"); +} catch (error) { + console.log((error as Error).message); +} +const run = (fn: (v: unknown) => void) => fn(true); +run(assert.ok); +console.log("done"); diff --git a/tests/coverage-fixtures/startup-fences.js b/tests/coverage-fixtures/startup-fences.js index 22aad4e0a..5e1d00b62 100644 --- a/tests/coverage-fixtures/startup-fences.js +++ b/tests/coverage-fixtures/startup-fences.js @@ -2,8 +2,8 @@ // a fence inside a function runs when the function is called. "use strict"; var value = JSON.parse("{}"); -console.log(value instanceof Object); +console.log(value instanceof WeakRef); function check(other) { - return other instanceof Object; + return other instanceof WeakRef; } console.log(check([])); diff --git a/tests/diagnostics/assert-fences.ts b/tests/diagnostics/assert-fences.ts index 823c64c40..7cf96bc5b 100644 --- a/tests/diagnostics/assert-fences.ts +++ b/tests/diagnostics/assert-fences.ts @@ -5,8 +5,8 @@ // (typed arrays, class instances), namespace-object calls, unsupported // throws/rejects expectations (properties outside the static error // surface, empty shapes, non-literal objects, doesNotReject shapes — -// Node's own TypeError), ifError over uncomparable types, and assert -// functions as values. Every reached site terminates in a named +// Node's own TypeError), ifError over uncomparable types, and assert.throws +// as a value. Every reached site terminates in a named // diagnostic — never a miscompare, never silence. import assert from "node:assert"; import * as assertNs from "node:assert"; @@ -47,8 +47,8 @@ assert.throws(() => { throw new Error("x"); }, {}); // Node rejects the empty sh void assert.doesNotReject(async () => {}, { name: "TypeError" }); // Node's own TypeError (function or RegExp only) assert.ifError(new Map()); // no honest ifError rendering for this type -const eq = assert.strictEqual; // assert functions as values -eq(1, 1); +const throwsFn = assert.throws; // assert.throws as a value: it takes the function to run +throwsFn(() => {}); interface Tree { label: string; kids: Tree[] } const tree: Tree = { label: "t", kids: [] }; diff --git a/tests/harness/__snapshots__/assert-fences.ts.txt b/tests/harness/__snapshots__/assert-fences.ts.txt index 2ab78f117..041c20e9b 100644 --- a/tests/harness/__snapshots__/assert-fences.ts.txt +++ b/tests/harness/__snapshots__/assert-fences.ts.txt @@ -104,12 +104,12 @@ assert-fences.ts:48:16 - error SC2020: 'assert.ifError over 'Map hint: numbers, strings, booleans, Errors, null/undefined, and unions of those are the lowered forms -assert-fences.ts:50:12 - error SC2020: 'assert.strictEqual' is part of the standard library types but has no scriptc lowering yet +assert-fences.ts:50:18 - error SC2020: 'assert.throws' is part of the standard library types but has no scriptc lowering yet 49 | - 50 | const eq = assert.strictEqual; // assert functions as values - | ^~~~~~~~~~~~~~~~~~ - 51 | eq(1, 1); + 50 | const throwsFn = assert.throws; // assert.throws as a value: it takes the function to run + | ^~~~~~~~~~~~~ + 51 | throwsFn(() => {}); hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index e1831c609..1046ca6af 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -221,10 +221,10 @@ test("deferred fences in a module's top-level code report as running at startup" ]); 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/, + /runs at startup {3}1 site .*\n.*'instanceof' right-hand sides other than classes declared in the program\n\s+at tests\/coverage-fixtures\/startup-fences\.js:5:30/, ); 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/, + /deferred to runtime {3}1 site .*\n.*'instanceof' right-hand sides other than classes declared in the program\n\s+at tests\/coverage-fixtures\/startup-fences\.js:7:27/, ); const envelope = coverageEnvelope(coverage, { compilerVersion: "test", sourceTexts }); expect(envelope.diagnostics.map((d) => [d.scope, d.line, d.startup])).toEqual([ From cd3c48d2b0d7dff2a3b910770e65b6d05df40980 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 08:19:25 -0500 Subject: [PATCH 055/273] Fence fs.readSyncDyn with fs.readSync The readSync form whose arguments are checked at run time lowers to fs.readSyncDyn, which the determinism attestation demotes on. List it as an alias of fs.readSync so the readSync fence witnesses it. --- packages/compiler/src/frontend/lowering/surfaces.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 90acabce6..15606cc3a 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2127,6 +2127,8 @@ export const BUILTIN_MODULE_FN_ALIASES: Record< ], // The utf8 string overload; the table row is the Buffer-window form. writeSync: ["fs.writeStrSync"], + // The form whose arguments Node checks at run time. + readSync: ["fs.readSyncDyn"], // The { recursive, mode } option lowerings. mkdirSync: ["fs.mkdirModeSync", "fs.mkdirRecursiveSync", "fs.mkdirRecursiveModeSync"], // The { recursive, force } and maxRetries/retryDelay option lowerings. From 878f03288630b2097c7102de0146e016214d74f9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 08:35:30 -0500 Subject: [PATCH 056/273] Crypto parity: RSA key generation lengths, RSA-PSS keys and encrypted private keys --- .../limitations/builtin-apis/crypto.mdx | 9 +- .../ambient/scriptc-node-fallback.d.ts | 31 +- packages/compiler/src/ir/runtime-features.ts | 11 +- .../corpus/8165-rsa-keygen-lengths.cjs.json | 6 + .../tests/corpus/8166-rsa-pss-keys.cjs.json | 6 + .../8167-encrypted-private-keys.cjs.json | 6 + packages/runtime/src/scr_cipher.c | 45 +- packages/runtime/src/scr_cipher_names.h | 100 ++ packages/runtime/src/scr_lib.c | 5 + packages/runtime/src/scr_lib.h | 3 + packages/runtime/src/scr_pki.c | 1387 ++++++++++++++++- packages/runtime/src/scr_webcrypto_pki.c | 3 +- tests/corpus/8165-rsa-keygen-lengths.cjs | 40 + tests/corpus/8166-rsa-pss-keys.cjs | 104 ++ tests/corpus/8167-encrypted-private-keys.cjs | 114 ++ .../8165-rsa-keygen-lengths.cjs.json | 1 + .../corpus-costs/8166-rsa-pss-keys.cjs.json | 1 + .../8167-encrypted-private-keys.cjs.json | 1 + 18 files changed, 1787 insertions(+), 86 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8165-rsa-keygen-lengths.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8166-rsa-pss-keys.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8167-encrypted-private-keys.cjs.json create mode 100644 packages/runtime/src/scr_cipher_names.h create mode 100644 tests/corpus/8165-rsa-keygen-lengths.cjs create mode 100644 tests/corpus/8166-rsa-pss-keys.cjs create mode 100644 tests/corpus/8167-encrypted-private-keys.cjs create mode 100644 tests/harness/corpus-costs/8165-rsa-keygen-lengths.cjs.json create mode 100644 tests/harness/corpus-costs/8166-rsa-pss-keys.cjs.json create mode 100644 tests/harness/corpus-costs/8167-encrypted-private-keys.cjs.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 329aa6809..bbe9cca21 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -31,18 +31,21 @@ `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, Ed25519, and X25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. + RSA, RSA-PSS, EC, Ed25519, and X25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. `generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback. - RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error. + RSA keys generate with modulus lengths from 512 to 8192 bits, an odd length from 2048 bits yielding one bit less as in Node, and odd public exponents up to 4294967295; longer moduli that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error. The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1. Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 and X25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate. EC, Ed25519, and X25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points. Raw X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error. They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`. `crypto.sign`, `crypto.verify`, `createSign`, and `createVerify` support RSA PKCS#1 v1.5 and PSS padding and ECDSA with DER and IEEE P1363 encodings over SHA-1 and SHA-2 digests, and Ed25519 through `crypto.sign` and `crypto.verify` with a `null` or `undefined` algorithm. + RSA-PSS keys follow their RSASSA-PSS-params as Node does: PSS padding only, the key's digest and MGF1 digest, and salts of at least the key's length, with OpenSSL's errors; their details name the parameters, they export as PKCS#8 and SubjectPublicKeyInfo only, and digests other than SHA-1 and SHA-2 in their options throw a coded SC2020 error. 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`, `x448`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused. + Private keys export encrypted (`cipher`, `passphrase`) with aes-128, aes-192, and aes-256 in CBC and ECB modes and with des-ede3-cbc, PKCS#8 as PBES2 with PBKDF2-HMAC-SHA256 and PKCS#1 and SEC1 as traditional PEM, and the ciphers OpenSSL refuses for a form throw its errors; a cipher option can also be an OID; other ciphers Node encrypts with throw a coded SC2020 error. + Encrypted private keys import with a `passphrase` from PBES2 with HMAC-SHA PRFs and the CBC and ECB ciphers, and from traditional PEM with the CBC ciphers, with Node's errors for a missing or wrong passphrase; other schemes and ciphers throw a coded SC2020 error once a passphrase is given. + The other key types (`dsa`, `ed448`, `x448`, and the rest) and wasm32-wasi builds throw a coded SC2020 error or are refused. Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report. `publicEncrypt`/`privateDecrypt` remain unsupported. The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript. diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 1fa787e35..2c056c148 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -2914,6 +2914,8 @@ declare module "crypto" { export interface AsymmetricKeyExportOptions { type: "pkcs1" | "spki" | "pkcs8" | "sec1"; format: T; + cipher?: string | undefined; + passphrase?: string | Buffer | undefined; } export interface AsymmetricKeyDetails { modulusLength?: number; @@ -2938,6 +2940,7 @@ declare module "crypto" { format?: "pem" | "der" | undefined; type?: "pkcs1" | "pkcs8" | "sec1" | undefined; encoding?: string | undefined; + passphrase?: string | Buffer | undefined; } export interface PublicKeyInput { key: string | Buffer; @@ -2974,12 +2977,22 @@ declare module "crypto" { modulusLength: number; publicExponent?: number | undefined; publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF }; - privateKeyEncoding: { type: "pkcs1" | "pkcs8"; format: PrivF }; + privateKeyEncoding: { + type: "pkcs1" | "pkcs8"; + format: PrivF; + cipher?: string | undefined; + passphrase?: string | undefined; + }; } export interface ECKeyPairOptions { namedCurve: string; publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF }; - privateKeyEncoding: { type: "sec1" | "pkcs8"; format: PrivF }; + privateKeyEncoding: { + type: "sec1" | "pkcs8"; + format: PrivF; + cipher?: string | undefined; + passphrase?: string | undefined; + }; } export function generateKeyPairSync( type: "rsa", @@ -3007,7 +3020,12 @@ declare module "crypto" { ): KeyPairKeyObjectResult; export interface ED25519KeyPairOptions { publicKeyEncoding: { type: "spki"; format: PubF }; - privateKeyEncoding: { type: "pkcs8"; format: PrivF }; + privateKeyEncoding: { + type: "pkcs8"; + format: PrivF; + cipher?: string | undefined; + passphrase?: string | undefined; + }; } export function generateKeyPairSync( type: "ed25519", @@ -3020,7 +3038,12 @@ declare module "crypto" { export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult; export interface X25519KeyPairOptions { publicKeyEncoding: { type: "spki"; format: PubF }; - privateKeyEncoding: { type: "pkcs8"; format: PrivF }; + privateKeyEncoding: { + type: "pkcs8"; + format: PrivF; + cipher?: string | undefined; + passphrase?: string | undefined; + }; } export function generateKeyPairSync( type: "x25519", diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 85b21af7b..1cae265d5 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -875,10 +875,11 @@ const RUNTIME_FEATURE_RULES = { mod.functions.some( (fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn", ), - // The legacy url functions, crypto.createCipheriv and - // ecdh.setPublicKey emit their deprecation warnings through the - // dyn-async unit (WebCrypto's encrypt and decrypt link scr_cipher.c - // too); scr_wasm.c boxes its handles and promises through it. + // The legacy url functions, crypto.createCipheriv, + // ecdh.setPublicKey and generateKeyPair's rsa-pss options emit their + // deprecation warnings through the 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, @@ -889,6 +890,8 @@ const RUNTIME_FEATURE_RULES = { "webcrypto.wrapKey", "webcrypto.unwrapKey", "crypto.ecdhNew", + "crypto.generateKeyPairCb", + "crypto.generateKeyPairSync", ], prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."], expr: (node, tables) => diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8165-rsa-keygen-lengths.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8165-rsa-keygen-lengths.cjs.json new file mode 100644 index 000000000..45346fc86 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8165-rsa-keygen-lengths.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8165-rsa-keygen-lengths.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8166-rsa-pss-keys.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8166-rsa-pss-keys.cjs.json new file mode 100644 index 000000000..623419ef3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8166-rsa-pss-keys.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8166-rsa-pss-keys.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8167-encrypted-private-keys.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8167-encrypted-private-keys.cjs.json new file mode 100644 index 000000000..e5e03f8e9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8167-encrypted-private-keys.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8167-encrypted-private-keys.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index a2db11434..33780fdfc 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -12,6 +12,7 @@ * 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_cipher_names.h" #include "scr_runtime.h" #include @@ -114,52 +115,12 @@ static const ScCipherSpec sc_specs[] = { 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}, -}; +} sc_legacy[] = {SCR_CIPHER_LEGACY_ENTRIES}; /* crypto.getCiphers() on Node 24.22 (OpenSSL 3.5). Names in this list that * have no implementation throw a coded SC2020 error instead of Node's * ERR_CRYPTO_UNKNOWN_CIPHER. */ -static const char *const sc_known[] = { - "aes-128-cbc", "aes-128-cbc-cts", "aes-128-ccm", "aes-128-cfb", "aes-128-cfb1", "aes-128-cfb8", - "aes-128-ctr", "aes-128-ecb", "aes-128-gcm", "aes-128-gcm-siv", "aes-128-ocb", "aes-128-ofb", - "aes-128-siv", "aes-128-wrap", "aes-128-wrap-inv", "aes-128-wrap-pad", "aes-128-wrap-pad-inv", - "aes-128-xts", "aes-192-cbc", "aes-192-cbc-cts", "aes-192-ccm", "aes-192-cfb", "aes-192-cfb1", - "aes-192-cfb8", "aes-192-ctr", "aes-192-ecb", "aes-192-gcm", "aes-192-gcm-siv", "aes-192-ocb", - "aes-192-ofb", "aes-192-siv", "aes-192-wrap", "aes-192-wrap-inv", "aes-192-wrap-pad", - "aes-192-wrap-pad-inv", "aes-256-cbc", "aes-256-cbc-cts", "aes-256-ccm", "aes-256-cfb", - "aes-256-cfb1", "aes-256-cfb8", "aes-256-ctr", "aes-256-ecb", "aes-256-gcm", "aes-256-gcm-siv", - "aes-256-ocb", "aes-256-ofb", "aes-256-siv", "aes-256-wrap", "aes-256-wrap-inv", - "aes-256-wrap-pad", "aes-256-wrap-pad-inv", "aes-256-xts", "aes128", "aes128-wrap", - "aes128-wrap-inv", "aes128-wrap-pad", "aes128-wrap-pad-inv", "aes192", "aes192-wrap", - "aes192-wrap-inv", "aes192-wrap-pad", "aes192-wrap-pad-inv", "aes256", "aes256-wrap", - "aes256-wrap-inv", "aes256-wrap-pad", "aes256-wrap-pad-inv", "aria-128-cbc", "aria-128-ccm", - "aria-128-cfb", "aria-128-cfb1", "aria-128-cfb8", "aria-128-ctr", "aria-128-ecb", "aria-128-gcm", - "aria-128-ofb", "aria-192-cbc", "aria-192-ccm", "aria-192-cfb", "aria-192-cfb1", "aria-192-cfb8", - "aria-192-ctr", "aria-192-ecb", "aria-192-gcm", "aria-192-ofb", "aria-256-cbc", "aria-256-ccm", - "aria-256-cfb", "aria-256-cfb1", "aria-256-cfb8", "aria-256-ctr", "aria-256-ecb", "aria-256-gcm", - "aria-256-ofb", "aria128", "aria192", "aria256", "camellia-128-cbc", "camellia-128-cbc-cts", - "camellia-128-cfb", "camellia-128-cfb1", "camellia-128-cfb8", "camellia-128-ctr", - "camellia-128-ecb", "camellia-128-ofb", "camellia-192-cbc", "camellia-192-cbc-cts", - "camellia-192-cfb", "camellia-192-cfb1", "camellia-192-cfb8", "camellia-192-ctr", - "camellia-192-ecb", "camellia-192-ofb", "camellia-256-cbc", "camellia-256-cbc-cts", - "camellia-256-cfb", "camellia-256-cfb1", "camellia-256-cfb8", "camellia-256-ctr", - "camellia-256-ecb", "camellia-256-ofb", "camellia128", "camellia192", "camellia256", "chacha20", - "chacha20-poly1305", "des-ede", "des-ede-cbc", "des-ede-cfb", "des-ede-ecb", "des-ede-ofb", - "des-ede3", "des-ede3-cbc", "des-ede3-cfb", "des-ede3-cfb1", "des-ede3-cfb8", "des-ede3-ecb", - "des-ede3-ofb", "des3", "des3-wrap", "id-aes128-ccm", "id-aes128-gcm", "id-aes128-wrap", - "id-aes128-wrap-pad", "id-aes192-ccm", "id-aes192-gcm", "id-aes192-wrap", "id-aes192-wrap-pad", - "id-aes256-ccm", "id-aes256-gcm", "id-aes256-wrap", "id-aes256-wrap-pad", - "id-smime-alg-cms3deswrap", "sm4", "sm4-cbc", "sm4-ccm", "sm4-cfb", "sm4-cfb128", "sm4-ctr", - "sm4-ecb", "sm4-gcm", "sm4-ofb", "sm4-ofb128", "sm4-xts", -}; +static const char *const sc_known[] = {SCR_CIPHER_KNOWN_NAMES}; enum { SC_NO_TAG = -1 }; enum { SC_TAG_UNKNOWN, SC_TAG_SET_BY_USER, SC_TAG_COMPUTED }; diff --git a/packages/runtime/src/scr_cipher_names.h b/packages/runtime/src/scr_cipher_names.h new file mode 100644 index 000000000..38b31e06e --- /dev/null +++ b/packages/runtime/src/scr_cipher_names.h @@ -0,0 +1,100 @@ +/* crypto.getCiphers() on Node 24.22 (OpenSSL 3.5), the names a cipher + * option can give. The cipher and pki units each include it: names + * without an implementation there throw a coded SC2020 error instead of + * Node's ERR_CRYPTO_UNKNOWN_CIPHER. */ +#ifndef SCR_CIPHER_NAMES_H +#define SCR_CIPHER_NAMES_H + +#define SCR_CIPHER_KNOWN_NAMES \ + "aes-128-cbc", "aes-128-cbc-cts", "aes-128-ccm", "aes-128-cfb", "aes-128-cfb1", "aes-128-cfb8", \ + "aes-128-ctr", "aes-128-ecb", "aes-128-gcm", "aes-128-gcm-siv", "aes-128-ocb", "aes-128-ofb", \ + "aes-128-siv", "aes-128-wrap", "aes-128-wrap-inv", "aes-128-wrap-pad", "aes-128-wrap-pad-inv", \ + "aes-128-xts", "aes-192-cbc", "aes-192-cbc-cts", "aes-192-ccm", "aes-192-cfb", "aes-192-cfb1", \ + "aes-192-cfb8", "aes-192-ctr", "aes-192-ecb", "aes-192-gcm", "aes-192-gcm-siv", "aes-192-ocb", \ + "aes-192-ofb", "aes-192-siv", "aes-192-wrap", "aes-192-wrap-inv", "aes-192-wrap-pad", \ + "aes-192-wrap-pad-inv", "aes-256-cbc", "aes-256-cbc-cts", "aes-256-ccm", "aes-256-cfb", \ + "aes-256-cfb1", "aes-256-cfb8", "aes-256-ctr", "aes-256-ecb", "aes-256-gcm", "aes-256-gcm-siv", \ + "aes-256-ocb", "aes-256-ofb", "aes-256-siv", "aes-256-wrap", "aes-256-wrap-inv", \ + "aes-256-wrap-pad", "aes-256-wrap-pad-inv", "aes-256-xts", "aes128", "aes128-wrap", \ + "aes128-wrap-inv", "aes128-wrap-pad", "aes128-wrap-pad-inv", "aes192", "aes192-wrap", \ + "aes192-wrap-inv", "aes192-wrap-pad", "aes192-wrap-pad-inv", "aes256", "aes256-wrap", \ + "aes256-wrap-inv", "aes256-wrap-pad", "aes256-wrap-pad-inv", "aria-128-cbc", "aria-128-ccm", \ + "aria-128-cfb", "aria-128-cfb1", "aria-128-cfb8", "aria-128-ctr", "aria-128-ecb", "aria-128-gcm", \ + "aria-128-ofb", "aria-192-cbc", "aria-192-ccm", "aria-192-cfb", "aria-192-cfb1", "aria-192-cfb8", \ + "aria-192-ctr", "aria-192-ecb", "aria-192-gcm", "aria-192-ofb", "aria-256-cbc", "aria-256-ccm", \ + "aria-256-cfb", "aria-256-cfb1", "aria-256-cfb8", "aria-256-ctr", "aria-256-ecb", "aria-256-gcm", \ + "aria-256-ofb", "aria128", "aria192", "aria256", "camellia-128-cbc", "camellia-128-cbc-cts", \ + "camellia-128-cfb", "camellia-128-cfb1", "camellia-128-cfb8", "camellia-128-ctr", \ + "camellia-128-ecb", "camellia-128-ofb", "camellia-192-cbc", "camellia-192-cbc-cts", \ + "camellia-192-cfb", "camellia-192-cfb1", "camellia-192-cfb8", "camellia-192-ctr", \ + "camellia-192-ecb", "camellia-192-ofb", "camellia-256-cbc", "camellia-256-cbc-cts", \ + "camellia-256-cfb", "camellia-256-cfb1", "camellia-256-cfb8", "camellia-256-ctr", \ + "camellia-256-ecb", "camellia-256-ofb", "camellia128", "camellia192", "camellia256", "chacha20", \ + "chacha20-poly1305", "des-ede", "des-ede-cbc", "des-ede-cfb", "des-ede-ecb", "des-ede-ofb", \ + "des-ede3", "des-ede3-cbc", "des-ede3-cfb", "des-ede3-cfb1", "des-ede3-cfb8", "des-ede3-ecb", \ + "des-ede3-ofb", "des3", "des3-wrap", "id-aes128-ccm", "id-aes128-gcm", "id-aes128-wrap", \ + "id-aes128-wrap-pad", "id-aes192-ccm", "id-aes192-gcm", "id-aes192-wrap", "id-aes192-wrap-pad", \ + "id-aes256-ccm", "id-aes256-gcm", "id-aes256-wrap", "id-aes256-wrap-pad", \ + "id-smime-alg-cms3deswrap", "sm4", "sm4-cbc", "sm4-ccm", "sm4-cfb", "sm4-cfb128", "sm4-ctr", \ + "sm4-ecb", "sm4-gcm", "sm4-ofb", "sm4-ofb128", "sm4-xts", + +/* The ciphers OpenSSL 3 keeps in its legacy provider, which Node 24 does + * not load, with their IV lengths: OpenSSL knows their names, and using + * them fails with its "unsupported" error. */ +#define SCR_CIPHER_LEGACY_ENTRIES \ + {"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}, + +/* The OID texts OpenSSL's providers register as cipher aliases, with the + * name each fetches (crypto.getCipherInfo(oid).name on Node 24.22). */ +#define SCR_CIPHER_OID_ALIASES \ + {"2.16.840.1.101.3.4.1.1", "aes-128-ecb"}, {"2.16.840.1.101.3.4.1.2", "aes-128-cbc"}, \ + {"2.16.840.1.101.3.4.1.3", "aes-128-ofb"}, {"2.16.840.1.101.3.4.1.4", "aes-128-cfb"}, \ + {"2.16.840.1.101.3.4.1.5", "id-aes128-wrap"}, {"2.16.840.1.101.3.4.1.6", "id-aes128-gcm"}, \ + {"2.16.840.1.101.3.4.1.7", "id-aes128-ccm"}, {"2.16.840.1.101.3.4.1.8", "id-aes128-wrap-pad"}, \ + {"2.16.840.1.101.3.4.1.21", "aes-192-ecb"}, {"2.16.840.1.101.3.4.1.22", "aes-192-cbc"}, \ + {"2.16.840.1.101.3.4.1.23", "aes-192-ofb"}, {"2.16.840.1.101.3.4.1.24", "aes-192-cfb"}, \ + {"2.16.840.1.101.3.4.1.25", "id-aes192-wrap"}, {"2.16.840.1.101.3.4.1.26", "id-aes192-gcm"}, \ + {"2.16.840.1.101.3.4.1.27", "id-aes192-ccm"}, {"2.16.840.1.101.3.4.1.28", "id-aes192-wrap-pad"}, \ + {"2.16.840.1.101.3.4.1.41", "aes-256-ecb"}, {"2.16.840.1.101.3.4.1.42", "aes-256-cbc"}, \ + {"2.16.840.1.101.3.4.1.43", "aes-256-ofb"}, {"2.16.840.1.101.3.4.1.44", "aes-256-cfb"}, \ + {"2.16.840.1.101.3.4.1.45", "id-aes256-wrap"}, {"2.16.840.1.101.3.4.1.46", "id-aes256-gcm"}, \ + {"2.16.840.1.101.3.4.1.47", "id-aes256-ccm"}, {"2.16.840.1.101.3.4.1.48", "id-aes256-wrap-pad"}, \ + {"1.2.410.200046.1.1.1", "aria-128-ecb"}, {"1.2.410.200046.1.1.2", "aria-128-cbc"}, \ + {"1.2.410.200046.1.1.3", "aria-128-cfb"}, {"1.2.410.200046.1.1.4", "aria-128-ofb"}, \ + {"1.2.410.200046.1.1.5", "aria-128-ctr"}, {"1.2.410.200046.1.1.6", "aria-192-ecb"}, \ + {"1.2.410.200046.1.1.7", "aria-192-cbc"}, {"1.2.410.200046.1.1.8", "aria-192-cfb"}, \ + {"1.2.410.200046.1.1.9", "aria-192-ofb"}, {"1.2.410.200046.1.1.10", "aria-192-ctr"}, \ + {"1.2.410.200046.1.1.11", "aria-256-ecb"}, {"1.2.410.200046.1.1.12", "aria-256-cbc"}, \ + {"1.2.410.200046.1.1.13", "aria-256-cfb"}, {"1.2.410.200046.1.1.14", "aria-256-ofb"}, \ + {"1.2.410.200046.1.1.15", "aria-256-ctr"}, {"1.2.410.200046.1.1.34", "aria-128-gcm"}, \ + {"1.2.410.200046.1.1.35", "aria-192-gcm"}, {"1.2.410.200046.1.1.36", "aria-256-gcm"}, \ + {"1.2.410.200046.1.1.37", "aria-128-ccm"}, {"1.2.410.200046.1.1.38", "aria-192-ccm"}, \ + {"1.2.410.200046.1.1.39", "aria-256-ccm"}, {"0.3.4401.5.3.1.9.1", "camellia-128-ecb"}, \ + {"0.3.4401.5.3.1.9.3", "camellia-128-ofb"}, {"0.3.4401.5.3.1.9.4", "camellia-128-cfb"}, \ + {"0.3.4401.5.3.1.9.9", "camellia-128-ctr"}, {"0.3.4401.5.3.1.9.21", "camellia-192-ecb"}, \ + {"0.3.4401.5.3.1.9.23", "camellia-192-ofb"}, {"0.3.4401.5.3.1.9.24", "camellia-192-cfb"}, \ + {"0.3.4401.5.3.1.9.29", "camellia-192-ctr"}, {"0.3.4401.5.3.1.9.41", "camellia-256-ecb"}, \ + {"0.3.4401.5.3.1.9.43", "camellia-256-ofb"}, {"0.3.4401.5.3.1.9.44", "camellia-256-cfb"}, \ + {"0.3.4401.5.3.1.9.49", "camellia-256-ctr"}, {"1.2.156.10197.1.104.1", "sm4-ecb"}, \ + {"1.2.156.10197.1.104.2", "sm4-cbc"}, {"1.2.156.10197.1.104.3", "sm4-ofb"}, \ + {"1.2.156.10197.1.104.4", "sm4-cfb"}, {"1.2.156.10197.1.104.7", "sm4-ctr"}, \ + {"1.2.156.10197.1.104.8", "sm4-gcm"}, {"1.2.156.10197.1.104.9", "sm4-ccm"}, \ + {"1.2.156.10197.1.104.10", "sm4-xts"}, {"1.2.392.200011.61.1.1.1.2", "camellia-128-cbc"}, \ + {"1.2.392.200011.61.1.1.1.3", "camellia-192-cbc"}, \ + {"1.2.392.200011.61.1.1.1.4", "camellia-256-cbc"}, {"1.2.840.113549.3.2", "rc2-cbc"}, \ + {"1.2.840.113549.3.4", "rc4"}, {"1.2.840.113549.3.7", "des-ede3-cbc"}, \ + {"1.2.840.113549.1.9.16.3.6", "id-smime-alg-cms3deswrap"}, {"1.3.14.3.2.6", "des-ecb"}, \ + {"1.3.14.3.2.7", "des-cbc"}, {"1.3.14.3.2.8", "des-ofb"}, {"1.3.14.3.2.9", "des-cfb"}, \ + {"1.3.14.3.2.17", "des-ede"}, {"1.3.111.2.1619.0.1.1", "aes-128-xts"}, \ + {"1.3.111.2.1619.0.1.2", "aes-256-xts"}, {"1.3.6.1.4.1.3029.1.2", "bf-cbc"}, \ + {"1.3.6.1.4.1.188.7.1.1.2", "idea-cbc"}, {"1.2.410.200004.1.3", "seed-ecb"}, \ + {"1.2.410.200004.1.4", "seed-cbc"}, {"1.2.410.200004.1.5", "seed-cfb"}, \ + {"1.2.410.200004.1.6", "seed-ofb"}, {"1.2.840.113533.7.66.10", "cast5-cbc"}, + +#endif diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index fb3524c81..b7736bc35 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -8137,6 +8137,11 @@ static bool scr_digest_alg(const char *name, size_t len, ScrDigestAlg *out) { return false; } +bool scr_digest_known(const char *name, size_t len) { + ScrDigestAlg alg; + return scr_digest_alg(name, len, &alg); +} + /* createHash() and crypto.hash() also accept "dss1" in any case. */ static bool scr_hash_alg(const char *name, size_t len, ScrDigestAlg *out) { if (scr_digest_alg(name, len, out)) return true; diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index fa447ff41..7ba9e2907 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -63,6 +63,9 @@ bool scr_lib_fork_info(double *target, uintptr_t *read_handle, * direct startup never filters user arguments. */ bool scr_lib_should_collapse_reexec_arg(ScrStr *cmd, ScrArr *args); ScrStr *scr_process_platform(void); /* +1 interned ("darwin", "linux", ...) */ +/* Whether OpenSSL, as Node 24 links it, names a digest `name` (its names, + * aliases and object identifiers, case-insensitively). */ +bool scr_digest_known(const char *name, size_t len); ScrStr *scr_process_cwd(void); /* +1 fresh (getcwd) */ bool scr_stdio_write(int fd, const void *data, size_t len); diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index a1d5b98a6..908129243 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -1,7 +1,7 @@ /* node:crypto asymmetric keys and signatures over the vendored mbedTLS * public-key layer: createPrivateKey, createPublicKey, generateKeyPairSync - * and generateKeyPair for "rsa", "ec", "ed25519" and "x25519" keys, - * crypto.diffieHellman (ECDH and X25519), the PrivateKeyObject and + * and generateKeyPair for "rsa", "rsa-pss", "ec", "ed25519" and "x25519" + * keys, crypto.diffieHellman (ECDH and X25519), the PrivateKeyObject and * PublicKeyObject handles (type, asymmetricKeyType, * asymmetricKeyDetails, export(), equals()), crypto.sign and crypto.verify, * and the Sign and Verify objects of createSign and createVerify. Keys read @@ -10,7 +10,8 @@ * 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. + * sig.js. Private keys export and import encrypted as PBES2 PKCS#8 and as + * traditional PEM. Other key types throw a coded SC2020 error. * All arguments are borrowed. */ #include "scr_runtime.h" @@ -23,6 +24,7 @@ #include "mbedtls/asn1.h" #include "mbedtls/asn1write.h" #include "mbedtls/base64.h" +#include "mbedtls/cipher.h" #include "mbedtls/bignum.h" #include "mbedtls/ecdh.h" #include "mbedtls/ecdsa.h" @@ -30,11 +32,13 @@ #include "mbedtls/md.h" #include "mbedtls/oid.h" #include "mbedtls/pk.h" +#include "mbedtls/pkcs5.h" #include "mbedtls/platform_util.h" #include "mbedtls/rsa.h" #include "mbedtls/x509_crt.h" #include "scr_pki.h" +#include "scr_cipher_names.h" /* ── errors ───────────────────────────────────────────────────────────── */ @@ -187,6 +191,13 @@ typedef struct SpKey { * bytes as given, before clamping, and the public key */ uint8_t ed_seed[32]; uint8_t ed_pub[32]; + /* An RSA-PSS key (id-RSASSA-PSS): signatures only, with PSS padding. + * With RSASSA-PSS-params it signs only with pss_md and MGF1 over + * pss_mgf1, and salts of at least pss_salt bytes. */ + bool rsa_pss; + bool pss_params; + mbedtls_md_type_t pss_md, pss_mgf1; + int pss_salt; } SpKey; static void *sp_key_retain(void *h) { @@ -245,6 +256,27 @@ static bool sp_is_ed(const SpKey *k) { return k->ed25519; } static bool sp_is_x(const SpKey *k) { return k->x25519; } +/* The PSS fields of `from` on `to`. */ +static void sp_pss_copy(SpKey *to, const SpKey *from) { + to->rsa_pss = from->rsa_pss; + to->pss_params = from->pss_params; + to->pss_md = from->pss_md; + to->pss_mgf1 = from->pss_mgf1; + to->pss_salt = from->pss_salt; +} + +/* Node's name for a digest RSASSA-PSS-params can name. */ +static const char *sp_pss_md_name(mbedtls_md_type_t md) { + switch (md) { + case MBEDTLS_MD_SHA1: return "sha1"; + case MBEDTLS_MD_SHA224: return "sha224"; + case MBEDTLS_MD_SHA256: return "sha256"; + case MBEDTLS_MD_SHA384: return "sha384"; + case MBEDTLS_MD_SHA512: return "sha512"; + default: return NULL; + } +} + /* An Ed25519 or X25519 key: 32-byte private and public keys, no details. */ static bool sp_is_okp(const SpKey *k) { return k->ed25519 || k->x25519; } @@ -341,10 +373,102 @@ static int sp_write_ed_der(const uint8_t *prefix, size_t prefix_len, const uint8 return (int)n; } +/* A digest AlgorithmIdentifier with NULL parameters, written backwards + * at *c; the written length or a negative error. */ +static int sp_write_md_alg(unsigned char **c, unsigned char *start, mbedtls_md_type_t md) { + const char *oid; + size_t oid_len; + int r, len = 0; + if ((r = mbedtls_oid_get_oid_by_md(md, &oid, &oid_len)) != 0) return r; + if ((r = mbedtls_asn1_write_null(c, start)) < 0) return r; + len += r; + if ((r = mbedtls_asn1_write_oid(c, start, oid, oid_len)) < 0) return r; + len += r; + if ((r = mbedtls_asn1_write_len(c, start, (size_t)len)) < 0) return r; + len += r; + if ((r = mbedtls_asn1_write_tag(c, start, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) return r; + return len + r; +} + +/* An RSA-PSS key's AlgorithmIdentifier, as OpenSSL writes it: id-RSASSA-PSS + * with no parameters for a key without restrictions, or RSASSA-PSS-params + * omitting the fields at their defaults (SHA-1, MGF1 with SHA-1, salt 20). */ +static int sp_write_pss_alg(unsigned char **c, unsigned char *start, const SpKey *k) { + int r, len = 0; + if (k->pss_params) { + int params = 0; + if (k->pss_salt != 20) { + int field = 0; + if ((r = mbedtls_asn1_write_int(c, start, k->pss_salt)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_len(c, start, (size_t)field)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_tag(c, start, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 2)) < 0) + return r; + params += field + r; + } + if (k->pss_mgf1 != MBEDTLS_MD_SHA1) { + int field = 0; + if ((r = sp_write_md_alg(c, start, k->pss_mgf1)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_oid(c, start, MBEDTLS_OID_MGF1, MBEDTLS_OID_SIZE(MBEDTLS_OID_MGF1))) < 0) + return r; + field += r; + if ((r = mbedtls_asn1_write_len(c, start, (size_t)field)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_tag(c, start, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_len(c, start, (size_t)field)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_tag(c, start, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 1)) < 0) + return r; + params += field + r; + } + if (k->pss_md != MBEDTLS_MD_SHA1) { + int field = 0; + if ((r = sp_write_md_alg(c, start, k->pss_md)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_len(c, start, (size_t)field)) < 0) return r; + field += r; + if ((r = mbedtls_asn1_write_tag(c, start, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0)) < 0) + return r; + params += field + r; + } + if ((r = mbedtls_asn1_write_len(c, start, (size_t)params)) < 0) return r; + params += r; + if ((r = mbedtls_asn1_write_tag(c, start, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) return r; + len += params + r; + } + if ((r = mbedtls_asn1_write_oid(c, start, MBEDTLS_OID_RSASSA_PSS, MBEDTLS_OID_SIZE(MBEDTLS_OID_RSASSA_PSS))) < 0) + return r; + len += r; + if ((r = mbedtls_asn1_write_len(c, start, (size_t)len)) < 0) return r; + len += r; + if ((r = mbedtls_asn1_write_tag(c, start, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) return r; + return len + r; +} + +static int sp_write_pkcs1_public_der(SpKey *k, unsigned char *buf, size_t size); + static int sp_write_spki_der(SpKey *k, unsigned char *buf, size_t size) { if (sp_is_okp(k)) return sp_write_ed_der(sp_is_x(k) ? sp_x_spki_prefix : sp_ed_spki_prefix, 12, k->ed_pub, buf, size); - return mbedtls_pk_write_pubkey_der(&k->pk, buf, size); + if (!k->rsa_pss) return mbedtls_pk_write_pubkey_der(&k->pk, buf, size); + /* SubjectPublicKeyInfo ::= SEQUENCE { AlgorithmIdentifier, BIT STRING + * RSAPublicKey } */ + unsigned char inner[SP_DER_MAX]; + int n = sp_write_pkcs1_public_der(k, inner, sizeof inner); + if (n < 0) return n; + unsigned char *c = buf + size; + int r, len = 0; + if ((r = mbedtls_asn1_write_bitstring(&c, buf, inner + sizeof inner - n, (size_t)n * 8)) < 0) return r; + len += r; + if ((r = sp_write_pss_alg(&c, buf, k)) < 0) return r; + len += r; + if ((r = mbedtls_asn1_write_len(&c, buf, (size_t)len)) < 0) return r; + len += r; + if ((r = mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) return r; + return len + r; } /* RSAPublicKey (PKCS#1) DER. */ @@ -414,6 +538,11 @@ static int sp_write_pkcs8_der(SpKey *k, unsigned char *buf, size_t size) { const char *oid; size_t oid_len; size_t alg_len = 0; + if (k->rsa_pss) { + if ((r = sp_write_pss_alg(&c, buf, k)) < 0) goto done; + len += (size_t)r; + goto version; + } if (sp_is_rsa(k)) { if ((r = mbedtls_asn1_write_null(&c, buf)) < 0) goto done; alg_len += (size_t)r; @@ -439,6 +568,7 @@ static int sp_write_pkcs8_der(SpKey *k, unsigned char *buf, size_t size) { alg_len += (size_t)r; len += alg_len; } +version: 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; @@ -476,6 +606,622 @@ static ScrStr *sp_pem(const char *label, const unsigned char *der, size_t len) { return s; } +/* ── encrypted private keys ─────────────────────────────────────────── + * PKCS#8 EncryptedPrivateKeyInfo as OpenSSL 3 writes it (PBES2 with + * PBKDF2-HMAC-SHA256 over a 16-byte salt and 2048 iterations), and the + * traditional PEM encryption of PKCS#1 and SEC1 keys (Proc-Type and + * DEK-Info headers, the key from EVP_BytesToKey with MD5), for the ciphers + * below. Reading takes PBES2 with any HMAC-SHA PRF and these ciphers, and + * traditional PEM with the CBC ciphers through mbedTLS. */ + +typedef struct { + const char *name; /* OpenSSL's name */ + const char *pem; /* its DEK-Info name */ + mbedtls_cipher_type_t type; + const char *oid; + size_t oid_len, key_len, iv_len; + bool ecb; + /* What OpenSSL does when PKCS#8 or traditional PEM encryption takes + * the cipher, an SpKeyCipherError. */ + unsigned char pkcs8_error, traditional_error; +} SpKeyCipher; + +typedef enum { + SP_KC_OK, /* it encrypts */ + SP_KC_NO_OID, + SP_KC_EVP_CIPHER, + SP_KC_PARAMETER, + SP_KC_INPUT_LENGTH, + SP_KC_ZERO_CONTENT, + SP_KC_OPERATION, + SP_KC_PEM_CIPHER, + SP_KC_UNSUPPORTED, +} SpKeyCipherError; + +static const struct { + const char *packed, *library, *reason, *code; +} sp_key_cipher_errors[] = { + [SP_KC_NO_OID] = {"0680006C", "asn1 encoding routines", "cipher has no object identifier", + "ERR_OSSL_ASN1_CIPHER_HAS_NO_OBJECT_IDENTIFIER"}, + [SP_KC_EVP_CIPHER] = {"0300006B", "digital envelope routines", "unsupported cipher", + "ERR_OSSL_EVP_UNSUPPORTED_CIPHER"}, + [SP_KC_PARAMETER] = {"0300007A", "digital envelope routines", "cipher parameter error", + "ERR_OSSL_EVP_CIPHER_PARAMETER_ERROR"}, + [SP_KC_INPUT_LENGTH] = {"1C8000E6", "Provider routines", "invalid input length", "ERR_OSSL_INVALID_INPUT_LENGTH"}, + [SP_KC_ZERO_CONTENT] = {"068000DE", "asn1 encoding routines", "illegal zero content", + "ERR_OSSL_ASN1_ILLEGAL_ZERO_CONTENT"}, + [SP_KC_OPERATION] = {"1C800066", "Provider routines", "cipher operation failed", + "ERR_OSSL_CIPHER_OPERATION_FAILED"}, + [SP_KC_PEM_CIPHER] = {"04800071", "PEM routines", "unsupported cipher", "ERR_OSSL_PEM_UNSUPPORTED_CIPHER"}, + [SP_KC_UNSUPPORTED] = {"0308010C", "digital envelope routines", "unsupported", "ERR_OSSL_EVP_UNSUPPORTED"}, +}; + +#define SP_AES_OID(last) "\x60\x86\x48\x01\x65\x03\x04\x01" last, 9 +static const SpKeyCipher sp_key_ciphers[] = { + {"aes-128-cbc", "AES-128-CBC", MBEDTLS_CIPHER_AES_128_CBC, SP_AES_OID("\x02"), 16, 16, false}, + {"aes-192-cbc", "AES-192-CBC", MBEDTLS_CIPHER_AES_192_CBC, SP_AES_OID("\x16"), 24, 16, false}, + {"aes-256-cbc", "AES-256-CBC", MBEDTLS_CIPHER_AES_256_CBC, SP_AES_OID("\x2a"), 32, 16, false}, + {"aes-128-ecb", "AES-128-ECB", MBEDTLS_CIPHER_AES_128_ECB, SP_AES_OID("\x01"), 16, 0, true, SP_KC_OK, SP_KC_PEM_CIPHER}, + {"aes-192-ecb", "AES-192-ECB", MBEDTLS_CIPHER_AES_192_ECB, SP_AES_OID("\x15"), 24, 0, true, SP_KC_OK, SP_KC_PEM_CIPHER}, + {"aes-256-ecb", "AES-256-ECB", MBEDTLS_CIPHER_AES_256_ECB, SP_AES_OID("\x29"), 32, 0, true, SP_KC_OK, SP_KC_PEM_CIPHER}, + {"des-ede3-cbc", "DES-EDE3-CBC", MBEDTLS_CIPHER_DES_EDE3_CBC, "\x2a\x86\x48\x86\xf7\x0d\x03\x07", 8, 24, 8, false}, +}; +static const struct { + const char *alias, *name; +} sp_key_cipher_aliases[] = { + {"aes128", "aes-128-cbc"}, {"aes192", "aes-192-cbc"}, {"aes256", "aes-256-cbc"}, {"des3", "des-ede3-cbc"}, +}; + +/* The names OpenSSL 3.5 refuses to encrypt a private key with in one form + * or both, from Node 24 (the legacy-provider names fail in both). Other + * names Node knows encrypt there and are refused here. */ +#define SP_KC_FAIL(name, pkcs8, traditional) \ + {name, NULL, MBEDTLS_CIPHER_NONE, NULL, 0, 0, 0, false, SP_KC_##pkcs8, SP_KC_##traditional} +static const SpKeyCipher sp_key_cipher_failures[] = { + SP_KC_FAIL("aes-128-cbc-cts", NO_OID, OK), SP_KC_FAIL("aes-128-ccm", EVP_CIPHER, OK), + SP_KC_FAIL("aes-128-ctr", NO_OID, OK), SP_KC_FAIL("aes-128-gcm", PARAMETER, OK), + SP_KC_FAIL("aes-128-gcm-siv", NO_OID, OK), SP_KC_FAIL("aes-128-ocb", NO_OID, OK), + SP_KC_FAIL("aes-128-siv", NO_OID, PEM_CIPHER), + SP_KC_FAIL("aes-128-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("aes-128-wrap-inv", NO_OID, INPUT_LENGTH), + SP_KC_FAIL("aes-128-wrap-pad-inv", NO_OID, OK), SP_KC_FAIL("aes-128-xts", EVP_CIPHER, OK), + SP_KC_FAIL("aes-192-cbc-cts", NO_OID, OK), SP_KC_FAIL("aes-192-ccm", EVP_CIPHER, OK), + SP_KC_FAIL("aes-192-ctr", NO_OID, OK), SP_KC_FAIL("aes-192-gcm", PARAMETER, OK), + SP_KC_FAIL("aes-192-gcm-siv", NO_OID, OK), SP_KC_FAIL("aes-192-ocb", NO_OID, OK), + SP_KC_FAIL("aes-192-siv", NO_OID, PEM_CIPHER), + SP_KC_FAIL("aes-192-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("aes-192-wrap-inv", NO_OID, INPUT_LENGTH), + SP_KC_FAIL("aes-192-wrap-pad-inv", NO_OID, OK), SP_KC_FAIL("aes-256-cbc-cts", NO_OID, OK), + SP_KC_FAIL("aes-256-ccm", EVP_CIPHER, OK), SP_KC_FAIL("aes-256-ctr", NO_OID, OK), + SP_KC_FAIL("aes-256-gcm", PARAMETER, OK), SP_KC_FAIL("aes-256-gcm-siv", NO_OID, OK), + SP_KC_FAIL("aes-256-ocb", NO_OID, OK), SP_KC_FAIL("aes-256-siv", NO_OID, PEM_CIPHER), + SP_KC_FAIL("aes-256-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("aes-256-wrap-inv", NO_OID, INPUT_LENGTH), + SP_KC_FAIL("aes-256-wrap-pad-inv", NO_OID, OK), SP_KC_FAIL("aes-256-xts", EVP_CIPHER, OK), + SP_KC_FAIL("aes128-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("aes128-wrap-inv", NO_OID, INPUT_LENGTH), + SP_KC_FAIL("aes128-wrap-pad-inv", NO_OID, OK), + SP_KC_FAIL("aes192-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("aes192-wrap-inv", NO_OID, INPUT_LENGTH), + SP_KC_FAIL("aes192-wrap-pad-inv", NO_OID, OK), + SP_KC_FAIL("aes256-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("aes256-wrap-inv", NO_OID, INPUT_LENGTH), + SP_KC_FAIL("aes256-wrap-pad-inv", NO_OID, OK), SP_KC_FAIL("aria-128-ccm", EVP_CIPHER, OK), + SP_KC_FAIL("aria-128-cfb1", NO_OID, OK), SP_KC_FAIL("aria-128-cfb8", NO_OID, OK), + SP_KC_FAIL("aria-128-ecb", OK, PEM_CIPHER), SP_KC_FAIL("aria-128-gcm", PARAMETER, OK), + SP_KC_FAIL("aria-192-ccm", EVP_CIPHER, OK), SP_KC_FAIL("aria-192-cfb1", NO_OID, OK), + SP_KC_FAIL("aria-192-cfb8", NO_OID, OK), SP_KC_FAIL("aria-192-ecb", OK, PEM_CIPHER), + SP_KC_FAIL("aria-192-gcm", PARAMETER, OK), SP_KC_FAIL("aria-256-ccm", EVP_CIPHER, OK), + SP_KC_FAIL("aria-256-cfb1", NO_OID, OK), SP_KC_FAIL("aria-256-cfb8", NO_OID, OK), + SP_KC_FAIL("aria-256-ecb", OK, PEM_CIPHER), SP_KC_FAIL("aria-256-gcm", PARAMETER, OK), + SP_KC_FAIL("camellia-128-cbc-cts", NO_OID, OK), SP_KC_FAIL("camellia-128-cfb1", NO_OID, OK), + SP_KC_FAIL("camellia-128-cfb8", NO_OID, OK), SP_KC_FAIL("camellia-128-ecb", OK, PEM_CIPHER), + SP_KC_FAIL("camellia-192-cbc-cts", NO_OID, OK), SP_KC_FAIL("camellia-192-cfb1", NO_OID, OK), + SP_KC_FAIL("camellia-192-cfb8", NO_OID, OK), SP_KC_FAIL("camellia-192-ecb", OK, PEM_CIPHER), + SP_KC_FAIL("camellia-256-cbc-cts", NO_OID, OK), SP_KC_FAIL("camellia-256-cfb1", NO_OID, OK), + SP_KC_FAIL("camellia-256-cfb8", NO_OID, OK), SP_KC_FAIL("camellia-256-ecb", OK, PEM_CIPHER), + SP_KC_FAIL("chacha20", NO_OID, OK), SP_KC_FAIL("chacha20-poly1305", NO_OID, OK), + SP_KC_FAIL("des-ede", OK, PEM_CIPHER), SP_KC_FAIL("des-ede-cbc", NO_OID, OK), + SP_KC_FAIL("des-ede-cfb", NO_OID, OK), SP_KC_FAIL("des-ede-ecb", OK, PEM_CIPHER), + SP_KC_FAIL("des-ede-ofb", NO_OID, OK), SP_KC_FAIL("des-ede3", NO_OID, PEM_CIPHER), + SP_KC_FAIL("des-ede3-cfb", ZERO_CONTENT, OK), SP_KC_FAIL("des-ede3-cfb1", ZERO_CONTENT, OK), + SP_KC_FAIL("des-ede3-cfb8", ZERO_CONTENT, OK), SP_KC_FAIL("des-ede3-ecb", NO_OID, PEM_CIPHER), + SP_KC_FAIL("des-ede3-ofb", NO_OID, OK), SP_KC_FAIL("des3-wrap", OPERATION, PEM_CIPHER), + SP_KC_FAIL("id-aes128-ccm", EVP_CIPHER, OK), SP_KC_FAIL("id-aes128-gcm", PARAMETER, OK), + SP_KC_FAIL("id-aes128-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("id-aes192-ccm", EVP_CIPHER, OK), SP_KC_FAIL("id-aes192-gcm", PARAMETER, OK), + SP_KC_FAIL("id-aes192-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("id-aes256-ccm", EVP_CIPHER, OK), SP_KC_FAIL("id-aes256-gcm", PARAMETER, OK), + SP_KC_FAIL("id-aes256-wrap", INPUT_LENGTH, INPUT_LENGTH), + SP_KC_FAIL("id-smime-alg-cms3deswrap", OPERATION, PEM_CIPHER), SP_KC_FAIL("sm4-ccm", NO_OID, OK), + SP_KC_FAIL("sm4-ecb", OK, PEM_CIPHER), SP_KC_FAIL("sm4-gcm", NO_OID, OK), + SP_KC_FAIL("sm4-xts", NO_OID, OK), SP_KC_FAIL("des", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("des-cbc", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("des-ecb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("des-cfb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("des-cfb1", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("des-cfb8", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("des-ofb", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("desx", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("desx-cbc", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("bf", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("bf-cbc", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("bf-ecb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("bf-cfb", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("bf-ofb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("blowfish", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("cast", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("cast-cbc", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("cast5-cbc", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("cast5-ecb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("cast5-cfb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("cast5-ofb", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("rc2", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("rc2-cbc", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("rc2-ecb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("rc2-cfb", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("rc2-ofb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("rc2-40-cbc", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("rc2-64-cbc", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("rc4", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("rc4-40", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("rc4-hmac-md5", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("seed", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("seed-cbc", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("seed-ecb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("seed-cfb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("seed-ofb", UNSUPPORTED, UNSUPPORTED), SP_KC_FAIL("idea", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("idea-cbc", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("idea-ecb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("idea-cfb", UNSUPPORTED, UNSUPPORTED), + SP_KC_FAIL("idea-ofb", UNSUPPORTED, UNSUPPORTED), +}; +#undef SP_KC_FAIL + +static bool sp_ascii_eq_ci(const char *a, size_t len, const char *b) { + if (strlen(b) != len) return false; + for (size_t i = 0; i < len; i++) { + char c = a[i]; + if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); + if (c != b[i]) return false; + } + return true; +} + +/* The cipher an export names, by a name or the OID text of one: true with + * *out set (an entry of sp_key_ciphers, or of sp_key_cipher_failures); + * false with Node's + * ERR_CRYPTO_UNKNOWN_CIPHER pending, or the SC2020 refusal for a cipher + * Node encrypts with that keys here do not. */ +static bool sp_key_cipher(const ScrStr *name, const SpKeyCipher **out) { + const char *data = name->data; + size_t len = name->len; + static const struct { + const char *oid, *name; + } oids[] = {SCR_CIPHER_OID_ALIASES}; + for (size_t i = 0; i < sizeof oids / sizeof oids[0]; i++) { + if (strlen(oids[i].oid) == len && memcmp(data, oids[i].oid, len) == 0) { + data = oids[i].name; + len = strlen(data); + } + } + const char *wanted = NULL; + for (size_t i = 0; i < sizeof sp_key_cipher_aliases / sizeof sp_key_cipher_aliases[0]; i++) + if (sp_ascii_eq_ci(data, len, sp_key_cipher_aliases[i].alias)) wanted = sp_key_cipher_aliases[i].name; + for (size_t i = 0; i < sizeof sp_key_ciphers / sizeof sp_key_ciphers[0]; i++) { + if (wanted ? strcmp(wanted, sp_key_ciphers[i].name) == 0 : sp_ascii_eq_ci(data, len, sp_key_ciphers[i].name)) { + *out = &sp_key_ciphers[i]; + return true; + } + } + for (size_t i = 0; i < sizeof sp_key_cipher_failures / sizeof sp_key_cipher_failures[0]; i++) { + if (sp_ascii_eq_ci(data, len, sp_key_cipher_failures[i].name)) { + *out = &sp_key_cipher_failures[i]; + return true; + } + } + static const char *const known[] = {SCR_CIPHER_KNOWN_NAMES}; + for (size_t i = 0; i < sizeof known / sizeof known[0]; i++) { + if (sp_ascii_eq_ci(data, len, known[i])) { + sp_not_supported("private keys encrypted with this cipher"); + return false; + } + } + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNKNOWN_CIPHER", "Unknown cipher"); + return false; +} + +/* Whether encryption in the form (PKCS#8, or traditional PEM) can take + * the cipher for `len` bytes of key: false with OpenSSL's error pending, + * or the SC2020 refusal for a cipher OpenSSL encrypts with that keys here + * do not. The key wrap ciphers fail only on a length that is not a + * multiple of 8; AES key wrap then encrypts, and Node's 3DES key wrap + * corrupts its heap. */ +static bool sp_key_cipher_ready(const SpKeyCipher *ci, bool pkcs8, size_t len) { + unsigned char error = pkcs8 ? ci->pkcs8_error : ci->traditional_error; + if ((error == SP_KC_INPUT_LENGTH || error == SP_KC_OPERATION) && len % 8 == 0) error = SP_KC_OK; + if (error != SP_KC_OK) { + sp_throw_openssl(sp_key_cipher_errors[error].packed, sp_key_cipher_errors[error].library, + sp_key_cipher_errors[error].reason, sp_key_cipher_errors[error].code, true); + return false; + } + if (ci->type == MBEDTLS_CIPHER_NONE) { + sp_not_supported("private keys encrypted with this cipher"); + return false; + } + return true; +} + +/* `in` through the cipher with PKCS#7 padding, as OpenSSL's EVP layer + * pads CBC and ECB: the output (+1 malloc'd, *out_len), or NULL when + * decryption finds bad padding. */ +static unsigned char *sp_key_cipher_run(const SpKeyCipher *ci, bool encrypt, const unsigned char *key, + const unsigned char *iv, const unsigned char *in, size_t in_len, + size_t *out_len) { + const mbedtls_cipher_info_t *info = mbedtls_cipher_info_from_type(ci->type); + size_t block = mbedtls_cipher_info_get_block_size(info); + size_t padded = encrypt ? (in_len / block + 1) * block : in_len; + if (!encrypt && (in_len == 0 || in_len % block != 0)) return NULL; + unsigned char *src = malloc(padded ? padded : 1), *out = malloc(padded ? padded : 1); + if (!src || !out) scr_trap("scriptc: out of memory\n"); + memcpy(src, in, in_len); + if (encrypt) memset(src + in_len, (int)(padded - in_len), padded - in_len); + mbedtls_cipher_context_t ctx; + mbedtls_cipher_init(&ctx); + int r = mbedtls_cipher_setup(&ctx, info); + if (r == 0) r = mbedtls_cipher_setkey(&ctx, key, (int)ci->key_len * 8, encrypt ? MBEDTLS_ENCRYPT : MBEDTLS_DECRYPT); + if (r == 0 && !ci->ecb) r = mbedtls_cipher_set_padding_mode(&ctx, MBEDTLS_PADDING_NONE); + if (r == 0 && !ci->ecb) r = mbedtls_cipher_set_iv(&ctx, iv, ci->iv_len); + size_t written = 0; + for (size_t i = 0; r == 0 && i < padded; i += block) { + size_t n = 0; + r = mbedtls_cipher_update(&ctx, src + i, block, out + written, &n); + written += n; + } + if (r == 0 && !ci->ecb) { + size_t n = 0; + r = mbedtls_cipher_finish(&ctx, out + written, &n); + written += n; + } + mbedtls_cipher_free(&ctx); + mbedtls_platform_zeroize(src, padded); + free(src); + if (r == 0 && !encrypt) { + unsigned char pad = written ? out[written - 1] : 0; + bool ok = pad >= 1 && pad <= block && pad <= written; + for (size_t i = 0; ok && i < pad; i++) ok = out[written - 1 - i] == pad; + if (ok) written -= pad; + else r = -1; + } + if (r != 0) { + mbedtls_platform_zeroize(out, padded); + free(out); + return NULL; + } + *out_len = written; + return out; +} + +/* EncryptedPrivateKeyInfo DER of the PKCS#8 `plain` (+1 malloc'd, + * *out_len), as OpenSSL 3 writes it; NULL on failure. */ +static unsigned char *sp_encrypt_pkcs8(const unsigned char *plain, size_t plain_len, const SpKeyCipher *ci, + const unsigned char *pass, size_t pass_len, size_t *out_len) { + unsigned char salt[16], iv[16], key[32]; + sp_random(NULL, salt, sizeof salt); + sp_random(NULL, iv, ci->iv_len); + if (mbedtls_pkcs5_pbkdf2_hmac_ext(MBEDTLS_MD_SHA256, pass, pass_len, salt, sizeof salt, 2048, + (uint32_t)ci->key_len, key) != 0) + return NULL; + size_t enc_len; + unsigned char *enc = sp_key_cipher_run(ci, true, key, iv, plain, plain_len, &enc_len); + mbedtls_platform_zeroize(key, sizeof key); + if (!enc) return NULL; + size_t size = enc_len + 256; + unsigned char *buf = malloc(size); + if (!buf) scr_trap("scriptc: out of memory\n"); + unsigned char *c = buf + size; + int r, len = 0, w; +#define SP_W(call) do { if ((w = (call)) < 0) { r = w; goto fail; } } while (0) + SP_W(mbedtls_asn1_write_octet_string(&c, buf, enc, enc_len)); + len += w; + /* encryptionScheme: the cipher with its IV (empty for ECB) */ + int scheme = 0; + SP_W(mbedtls_asn1_write_octet_string(&c, buf, iv, ci->iv_len)); + scheme += w; + SP_W(mbedtls_asn1_write_oid(&c, buf, ci->oid, ci->oid_len)); + scheme += w; + SP_W(mbedtls_asn1_write_len(&c, buf, (size_t)scheme)); + scheme += w; + SP_W(mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); + scheme += w; + /* keyDerivationFunc: PBKDF2 { salt, 2048, hmacWithSHA256 } */ + int kdf = 0, prf = 0; + SP_W(mbedtls_asn1_write_null(&c, buf)); + prf += w; + SP_W(mbedtls_asn1_write_oid(&c, buf, MBEDTLS_OID_HMAC_SHA256, MBEDTLS_OID_SIZE(MBEDTLS_OID_HMAC_SHA256))); + prf += w; + SP_W(mbedtls_asn1_write_len(&c, buf, (size_t)prf)); + prf += w; + SP_W(mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); + prf += w; + kdf += prf; + SP_W(mbedtls_asn1_write_int(&c, buf, 2048)); + kdf += w; + SP_W(mbedtls_asn1_write_octet_string(&c, buf, salt, sizeof salt)); + kdf += w; + SP_W(mbedtls_asn1_write_len(&c, buf, (size_t)kdf)); + kdf += w; + SP_W(mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); + kdf += w; + SP_W(mbedtls_asn1_write_oid(&c, buf, MBEDTLS_OID_PKCS5_PBKDF2, MBEDTLS_OID_SIZE(MBEDTLS_OID_PKCS5_PBKDF2))); + kdf += w; + SP_W(mbedtls_asn1_write_len(&c, buf, (size_t)kdf)); + kdf += w; + SP_W(mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); + kdf += w; + int params = kdf + scheme; + SP_W(mbedtls_asn1_write_len(&c, buf, (size_t)params)); + params += w; + SP_W(mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); + params += w; + SP_W(mbedtls_asn1_write_oid(&c, buf, MBEDTLS_OID_PKCS5_PBES2, MBEDTLS_OID_SIZE(MBEDTLS_OID_PKCS5_PBES2))); + params += w; + SP_W(mbedtls_asn1_write_len(&c, buf, (size_t)params)); + params += w; + SP_W(mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); + params += w; + len += params; + SP_W(mbedtls_asn1_write_len(&c, buf, (size_t)len)); + len += w; + SP_W(mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); + len += w; +#undef SP_W + memmove(buf, c, (size_t)len); + mbedtls_platform_zeroize(enc, enc_len); + free(enc); + *out_len = (size_t)len; + return buf; +fail: + (void)r; + mbedtls_platform_zeroize(enc, enc_len); + free(enc); + free(buf); + return NULL; +} + +/* OpenSSL's EVP_BytesToKey with MD5 and one round over the first 8 IV + * bytes, as traditional PEM encryption derives its key. */ +static void sp_bytes_to_key(const unsigned char *pass, size_t pass_len, const unsigned char *salt, unsigned char *key, + size_t key_len) { + const mbedtls_md_info_t *md5 = mbedtls_md_info_from_type(MBEDTLS_MD_MD5); + unsigned char digest[16]; + size_t done = 0; + mbedtls_md_context_t ctx; + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, md5, 0); + for (bool first = true; done < key_len; first = false) { + mbedtls_md_starts(&ctx); + if (!first) mbedtls_md_update(&ctx, digest, sizeof digest); + mbedtls_md_update(&ctx, pass, pass_len); + mbedtls_md_update(&ctx, salt, 8); + mbedtls_md_finish(&ctx, digest); + size_t n = key_len - done < sizeof digest ? key_len - done : sizeof digest; + memcpy(key + done, digest, n); + done += n; + } + mbedtls_md_free(&ctx); + mbedtls_platform_zeroize(digest, sizeof digest); +} + +/* The traditional encrypted PEM of a PKCS#1 or SEC1 key (+1), or NULL. */ +static ScrStr *sp_encrypted_pem(const char *label, const unsigned char *der, size_t len, const SpKeyCipher *ci, + const unsigned char *pass, size_t pass_len) { + unsigned char iv[16], key[32]; + sp_random(NULL, iv, ci->iv_len); + sp_bytes_to_key(pass, pass_len, iv, key, ci->key_len); + size_t enc_len; + unsigned char *enc = sp_key_cipher_run(ci, true, key, iv, der, len, &enc_len); + mbedtls_platform_zeroize(key, sizeof key); + if (!enc) return NULL; + ScrStr *body = sp_pem(label, enc, enc_len); + mbedtls_platform_zeroize(enc, enc_len); + free(enc); + /* The headers follow the BEGIN line, then a blank line. */ + const char *nl = memchr(body->data, '\n', body->len); + size_t head = (size_t)(nl - body->data) + 1; + ScrJsonBuf out; + scr_jb_init(&out); + for (size_t i = 0; i < head; i++) scr_jb_putc(&out, body->data[i]); + scr_jb_puts(&out, "Proc-Type: 4,ENCRYPTED\nDEK-Info: "); + scr_jb_puts(&out, ci->pem); + scr_jb_putc(&out, ','); + static const char hex[] = "0123456789ABCDEF"; + for (size_t i = 0; i < ci->iv_len; i++) { + scr_jb_putc(&out, hex[iv[i] >> 4]); + scr_jb_putc(&out, hex[iv[i] & 15]); + } + scr_jb_puts(&out, "\n\n"); + for (size_t i = head; i < body->len; i++) scr_jb_putc(&out, body->data[i]); + scr_str_release(body); + return scr_jb_finish(&out); +} + +/* OpenSSL's errors for an encrypted private key: the message, Node's + * code, and the error stack OpenSSL leaves for the input's form. */ +static void sp_throw_key_decrypt(const char *message, const char *library, const char *reason, const char *code, + const char *const *stack, size_t stack_len) { + ScrStr *text = scr_str_new(message, strlen(message)); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + ScrDyn *list = scr_dyn_new_arr(); + for (size_t i = 0; i < stack_len; i++) { + ScrStr *entry = scr_str_new(stack[i], strlen(stack[i])); + scr_dyn_arr_push(list, scr_dyn_new_str(entry)); + scr_str_release(entry); + } + scr_error_set_prop(e, "opensslErrorStack", 17, list); + const char *keys[] = {"library", "reason", "code"}; + const char *values[] = {library, reason, code}; + for (size_t i = 0; i < 3; i++) { + if (i == 2) scr_error_set_code(e, code); + ScrStr *value = scr_str_new(values[i], strlen(values[i])); + scr_error_set_prop(e, keys[i], strlen(keys[i]), scr_dyn_new_str(value)); + scr_str_release(value); + } + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + +static const char sp_err_interrupted[] = "error:07880109:common libcrypto routines::interrupted or cancelled"; +static const char sp_err_bad_decrypt[] = "error:1C800064:Provider routines::bad decrypt"; + +/* No passphrase for an encrypted PEM key (pkcs8: whether it is PKCS#8). */ +static void sp_throw_passphrase_missing(bool pkcs8) { + static const char *const pkcs8_stack[] = {"error:04800068:PEM routines::bad password read", sp_err_interrupted, + "error:1C80009F:Provider routines::unable to get passphrase", + sp_err_interrupted, sp_err_interrupted}; + static const char *const pem_stack[] = {"error:04800068:PEM routines::bad password read", sp_err_interrupted, + sp_err_interrupted}; + sp_throw_key_decrypt(sp_err_interrupted, "common libcrypto routines", "interrupted or cancelled", + "ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED", pkcs8 ? pkcs8_stack : pem_stack, pkcs8 ? 5 : 3); +} + +/* A passphrase that does not decrypt the key: PKCS#8 in PEM, PKCS#8 in + * DER, or traditional PEM. */ +static void sp_throw_bad_decrypt(int form) { + static const char cipherfinal[] = "error:11800074:PKCS12 routines::pkcs12 cipherfinal error"; + static const char *const stacks[][4] = { + {cipherfinal, sp_err_bad_decrypt, cipherfinal, sp_err_bad_decrypt}, + {cipherfinal, sp_err_bad_decrypt}, + {"error:04800065:PEM routines::bad decrypt", sp_err_bad_decrypt}, + }; + sp_throw_key_decrypt(sp_err_bad_decrypt, "Provider routines", "bad decrypt", "ERR_OSSL_BAD_DECRYPT", stacks[form], + form == 0 ? 4 : 2); +} + +/* The cipher name of an encrypted traditional PEM key's DEK-Info header + * into `name` (NUL-terminated), or false for other input. */ +static bool sp_pem_dek_info(const unsigned char *data, size_t len, char *name, size_t cap) { + const char *text = (const char *)data; + const char *begin = NULL; + for (size_t i = 0; i + 11 <= len && !begin; i++) + if (memcmp(text + i, "-----BEGIN ", 11) == 0) begin = text + i; + if (!begin) return false; + const char *end = text + len, *line = memchr(begin, '\n', (size_t)(end - begin)); + bool encrypted = false; + while (line && ++line < end) { + const char *eol = memchr(line, '\n', (size_t)(end - line)); + size_t n = (size_t)((eol ? eol : end) - line); + if (n && line[n - 1] == '\r') n--; + if (n >= 10 && memcmp(line, "Proc-Type:", 10) == 0) { + for (size_t i = 10; i + 9 <= n; i++) + if (memcmp(line + i, "ENCRYPTED", 9) == 0) encrypted = true; + } else if (n >= 9 && memcmp(line, "DEK-Info:", 9) == 0) { + if (!encrypted) return false; + size_t i = 9; + while (i < n && line[i] == ' ') i++; + size_t o = 0; + while (i < n && line[i] != ',' && o + 1 < cap) name[o++] = line[i++]; + name[o] = 0; + return true; + } else if (n == 0 || memchr(line, ':', n) == NULL) { + return false; + } + line = eol; + } + return false; +} + +/* Before mbedTLS reads an encrypted traditional PEM key: false with + * OpenSSL's error pending for a DEK-Info cipher it does not know, a + * missing passphrase, or a legacy-provider cipher, or the SC2020 refusal + * for a cipher mbedTLS does not decrypt; true for other input. */ +static bool sp_pem_dek_check(const unsigned char *data, size_t len, const ScrBytes *pass) { + char name[64]; + if (!sp_pem_dek_info(data, len, name, sizeof name)) return true; + size_t n = strlen(name); + static const char *const known[] = {SCR_CIPHER_KNOWN_NAMES}; + static const struct { + const char *name; + unsigned iv_len; + } legacy[] = {SCR_CIPHER_LEGACY_ENTRIES}; + bool is_known = false, is_legacy = false; + for (size_t i = 0; i < sizeof known / sizeof known[0]; i++) + if (sp_ascii_eq_ci(name, n, known[i])) is_known = true; + for (size_t i = 0; i < sizeof legacy / sizeof legacy[0]; i++) + if (sp_ascii_eq_ci(name, n, legacy[i].name)) is_legacy = true; + if (!is_known && !is_legacy) { + static const char *const stack[] = {"error:04800072:PEM routines::unsupported encryption"}; + sp_throw_key_decrypt(stack[0], "PEM routines", "unsupported encryption", "ERR_OSSL_PEM_UNSUPPORTED_ENCRYPTION", + stack, 1); + return false; + } + if (!pass) { + sp_throw_passphrase_missing(false); + return false; + } + if (is_legacy) { + static const char unsupported[] = "error:0308010C:digital envelope routines::unsupported"; + static const char *const stack[] = {"error:04800065:PEM routines::bad decrypt", unsupported}; + sp_throw_key_decrypt(unsupported, "digital envelope routines", "unsupported", "ERR_OSSL_EVP_UNSUPPORTED", stack, + 2); + return false; + } + /* mbedTLS decrypts these names, as OpenSSL writes them. */ + static const char *const read[] = {"AES-128-CBC", "AES-192-CBC", "AES-256-CBC", "DES-EDE3-CBC"}; + for (size_t i = 0; i < sizeof read / sizeof read[0]; i++) + if (strcmp(name, read[i]) == 0) return true; + sp_not_supported("private keys encrypted with this cipher"); + return false; +} + +/* The PKCS#8 inside a PBES2 EncryptedPrivateKeyInfo `der` (+1 malloc'd, + * *out_len): 1 decrypted, 0 another scheme or cipher, -1 a passphrase + * that does not decrypt it. */ +static int sp_decrypt_pkcs8(const unsigned char *der, size_t len, const unsigned char *pass, size_t pass_len, + unsigned char **out, size_t *out_len) { + unsigned char *p = (unsigned char *)der; + const unsigned char *end = der + len; + size_t n; + mbedtls_asn1_buf alg, params; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return 0; + end = p + n; + if (mbedtls_asn1_get_alg(&p, end, &alg, ¶ms) != 0 || alg.len != MBEDTLS_OID_SIZE(MBEDTLS_OID_PKCS5_PBES2) || + memcmp(alg.p, MBEDTLS_OID_PKCS5_PBES2, alg.len) != 0 || + params.tag != (MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) + return 0; + unsigned char *q = params.p; + const unsigned char *params_end = params.p + params.len; + mbedtls_asn1_buf kdf, kdf_params, scheme, scheme_params; + if (mbedtls_asn1_get_alg(&q, params_end, &kdf, &kdf_params) != 0 || + kdf.len != MBEDTLS_OID_SIZE(MBEDTLS_OID_PKCS5_PBKDF2) || memcmp(kdf.p, MBEDTLS_OID_PKCS5_PBKDF2, kdf.len) != 0 || + mbedtls_asn1_get_alg(&q, params_end, &scheme, &scheme_params) != 0) + return 0; + const SpKeyCipher *ci = NULL; + for (size_t i = 0; i < sizeof sp_key_ciphers / sizeof sp_key_ciphers[0]; i++) + if (scheme.len == sp_key_ciphers[i].oid_len && memcmp(scheme.p, sp_key_ciphers[i].oid, scheme.len) == 0) + ci = &sp_key_ciphers[i]; + if (!ci || scheme_params.tag != MBEDTLS_ASN1_OCTET_STRING || scheme_params.len != ci->iv_len) return 0; + /* PBKDF2-params ::= SEQUENCE { salt OCTET STRING, iterationCount + * INTEGER, keyLength INTEGER OPTIONAL, prf AlgorithmIdentifier DEFAULT + * hmacWithSHA1 } */ + unsigned char *k = kdf_params.p; + const unsigned char *k_end = kdf_params.p + kdf_params.len; + size_t salt_len; + int iterations, key_len = (int)ci->key_len; + if (kdf_params.tag != (MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) || + mbedtls_asn1_get_tag(&k, k_end, &salt_len, MBEDTLS_ASN1_OCTET_STRING) != 0) + return 0; + const unsigned char *salt = k; + k += salt_len; + if (mbedtls_asn1_get_int(&k, k_end, &iterations) != 0 || iterations < 1) return 0; + if (k < k_end && *k == MBEDTLS_ASN1_INTEGER && (mbedtls_asn1_get_int(&k, k_end, &key_len) != 0 || + key_len != (int)ci->key_len)) + return 0; + mbedtls_md_type_t prf = MBEDTLS_MD_SHA1; + if (k < k_end) { + mbedtls_asn1_buf prf_oid, prf_params; + if (mbedtls_asn1_get_alg(&k, k_end, &prf_oid, &prf_params) != 0 || mbedtls_oid_get_md_hmac(&prf_oid, &prf) != 0) + return 0; + } + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_OCTET_STRING) != 0) return 0; + unsigned char key[32]; + if (mbedtls_pkcs5_pbkdf2_hmac_ext(prf, pass, pass_len, salt, salt_len, (unsigned)iterations, + (uint32_t)ci->key_len, key) != 0) + return 0; + *out = sp_key_cipher_run(ci, false, key, scheme_params.p, p, n, out_len); + mbedtls_platform_zeroize(key, sizeof key); + return *out ? 1 : -1; +} + 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; @@ -761,12 +1507,13 @@ static SpKey *sp_public_of(SpKey *k) { return pub; } unsigned char der[SP_DER_MAX]; - int n = sp_write_spki_der(k, der, sizeof der); + int n = k->rsa_pss ? mbedtls_pk_write_pubkey_der(&k->pk, der, sizeof der) : 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; } + sp_pss_copy(pub, k); return pub; } @@ -888,7 +1635,158 @@ static SpKey *sp_ed_from_der(const unsigned char *der, size_t len, bool private_ return k; } +/* RSASSA-PSS-params at *p (RFC 4055): true with the restrictions set on + * `k`, false when they are malformed or unsupported, which leaves the key + * without restrictions, as OpenSSL reads a negative or oversized salt. */ +static bool sp_pss_params_read(unsigned char **p, const unsigned char *end, SpKey *k) { + size_t n; + if (mbedtls_asn1_get_tag(p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return false; + const unsigned char *params_end = *p + n; + mbedtls_md_type_t md = MBEDTLS_MD_SHA1, mgf1 = MBEDTLS_MD_SHA1; + int salt = 20; + if (*p < params_end && **p == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0)) { + mbedtls_asn1_buf oid, params; + if (mbedtls_asn1_get_tag(p, params_end, &n, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0) != 0 || + mbedtls_asn1_get_alg(p, *p + n, &oid, ¶ms) != 0 || mbedtls_oid_get_md_alg(&oid, &md) != 0 || + !sp_pss_md_name(md)) + return false; + } + if (*p < params_end && **p == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 1)) { + mbedtls_asn1_buf oid, params; + if (mbedtls_asn1_get_tag(p, params_end, &n, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 1) != 0 || + mbedtls_asn1_get_alg(p, *p + n, &oid, ¶ms) != 0 || oid.len != MBEDTLS_OID_SIZE(MBEDTLS_OID_MGF1) || + memcmp(oid.p, MBEDTLS_OID_MGF1, oid.len) != 0) + return false; + /* MGF1's parameters are the hash's AlgorithmIdentifier, whose + * content starts with the hash's OID. */ + if (params.tag != (MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) return false; + unsigned char *alg = params.p; + mbedtls_asn1_buf hash_oid = {.tag = MBEDTLS_ASN1_OID}; + if (mbedtls_asn1_get_tag(&alg, params.p + params.len, &hash_oid.len, MBEDTLS_ASN1_OID) != 0) return false; + hash_oid.p = alg; + if (mbedtls_oid_get_md_alg(&hash_oid, &mgf1) != 0 || !sp_pss_md_name(mgf1)) return false; + } + if (*p < params_end && **p == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 2)) { + if (mbedtls_asn1_get_tag(p, params_end, &n, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 2) != 0 || + mbedtls_asn1_get_int(p, *p + n, &salt) != 0 || salt < 0) + return false; + } + *p = (unsigned char *)params_end; + k->pss_params = true; + k->pss_md = md; + k->pss_mgf1 = mgf1; + k->pss_salt = salt; + return true; +} + +/* An RSA-PSS key from PKCS#8 or SubjectPublicKeyInfo DER whose algorithm + * is id-RSASSA-PSS; NULL when the DER is neither, or with *failed set and + * Node's decoder error pending. A private key wanted as public answers its + * public key. */ +static SpKey *sp_public_of(SpKey *k); + +static SpKey *sp_pss_from_der(const unsigned char *der, size_t len, bool private_key, bool *failed) { + *failed = false; + 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 NULL; + end = p + n; + bool pkcs8 = p < end && *p == MBEDTLS_ASN1_INTEGER; + if (pkcs8) { + int version; + if (mbedtls_asn1_get_int(&p, end, &version) != 0 || version != 0) return NULL; + } + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return NULL; + const unsigned char *alg_end = p + n; + size_t oid_len; + if (mbedtls_asn1_get_tag(&p, alg_end, &oid_len, MBEDTLS_ASN1_OID) != 0 || + oid_len != MBEDTLS_OID_SIZE(MBEDTLS_OID_RSASSA_PSS) || memcmp(p, MBEDTLS_OID_RSASSA_PSS, oid_len) != 0) + return NULL; + p += oid_len; + SpKey *k = sp_key_alloc(pkcs8); + k->rsa_pss = true; + if (p < alg_end && !sp_pss_params_read(&p, alg_end, k)) k->pss_params = false; + p = (unsigned char *)alg_end; + int r; + if (pkcs8) { + r = mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_OCTET_STRING); + if (r == 0) r = mbedtls_pk_parse_key(&k->pk, p, n, NULL, 0, sp_random, NULL); + } else { + r = private_key ? -1 : mbedtls_asn1_get_bitstring_null(&p, end, &n); + if (r == 0) r = mbedtls_pk_parse_public_key(&k->pk, p, n); + } + if (r != 0 || !sp_is_rsa(k)) { + sp_key_release(k); + *failed = true; + sp_unsupported_input(); + return NULL; + } + if (pkcs8 && !private_key) { + SpKey *pub = sp_public_of(k); + sp_key_release(k); + return pub; + } + return k; +} + +/* Whether `der` is a PKCS#8 EncryptedPrivateKeyInfo: a SEQUENCE of a PKCS#5 + * or PKCS#12 password-based AlgorithmIdentifier and an OCTET STRING. */ +static bool sp_is_encrypted_pkcs8(const unsigned char *der, size_t len) { + unsigned char *p = (unsigned char *)der; + const unsigned char *end = der + len; + size_t n; + mbedtls_asn1_buf alg, params; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return false; + end = p + n; + if (mbedtls_asn1_get_alg(&p, end, &alg, ¶ms) != 0) return false; + static const char pkcs5[] = MBEDTLS_OID_PKCS5, pkcs12[] = MBEDTLS_OID_PKCS12_PBE; + bool password = (alg.len > sizeof pkcs5 - 1 && memcmp(alg.p, pkcs5, sizeof pkcs5 - 1) == 0) || + (alg.len > sizeof pkcs12 - 1 && memcmp(alg.p, pkcs12, sizeof pkcs12 - 1) == 0); + return password && mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_OCTET_STRING) == 0 && p + n == end; +} + +static SpKey *sp_parse_with(const unsigned char *data, size_t len, bool private_key, const ScrBytes *pass); + static SpKey *sp_parse(const unsigned char *data, size_t len, bool private_key) { + return sp_parse_with(data, len, private_key, NULL); +} + +/* sp_parse with a passphrase for an encrypted private key (NULL: none). */ +static SpKey *sp_parse_with(const unsigned char *data, size_t len, bool private_key, const ScrBytes *pass) { + { + unsigned char *decoded; + const unsigned char *der; + size_t der_len; + if (sp_der_of(data, len, &decoded, &der, &der_len) && sp_is_encrypted_pkcs8(der, der_len)) { + bool from_pem = decoded != NULL; + SpKey *k = NULL; + unsigned char *plain = NULL; + size_t plain_len = 0; + int r = 0; + if (!pass) { + if (from_pem) sp_throw_passphrase_missing(true); + else sp_throw(SCR_ERR_TYPE, "ERR_MISSING_PASSPHRASE", "Passphrase required for encrypted key"); + } else { + r = sp_decrypt_pkcs8(der, der_len, pass->data, (size_t)scr_bytes_byte_len(pass), &plain, &plain_len); + if (r < 0) sp_throw_bad_decrypt(from_pem ? 0 : 1); + } + if (r > 0) { + k = sp_parse_with(plain, plain_len, private_key, NULL); + mbedtls_platform_zeroize(plain, plain_len); + free(plain); + } + if (decoded) { + mbedtls_platform_zeroize(decoded, der_len); + free(decoded); + } + if (r == 0 && pass) sp_not_supported("private keys encrypted with this scheme or cipher"); + return k; + } else if (decoded) { + mbedtls_platform_zeroize(decoded, der_len); + free(decoded); + } + } { unsigned char *decoded; const unsigned char *der; @@ -896,6 +1794,7 @@ static SpKey *sp_parse(const unsigned char *data, size_t len, bool private_key) 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 (!ed && !failed) ed = sp_pss_from_der(der, der_len, private_key, &failed); if (decoded) { mbedtls_platform_zeroize(decoded, der_len); free(decoded); @@ -912,6 +1811,7 @@ static SpKey *sp_parse(const unsigned char *data, size_t len, bool private_key) break; } } + if (pem && !sp_pem_dek_check(data, len, pass)) return NULL; unsigned char *buf = malloc(len + 1); if (!buf) scr_trap("scriptc: out of memory\n"); memcpy(buf, data, len); @@ -919,15 +1819,19 @@ static SpKey *sp_parse(const unsigned char *data, size_t len, bool private_key) size_t n = pem ? len + 1 : len; SpKey *k = sp_key_alloc(private_key); bool ok; + const unsigned char *pwd = pass ? pass->data : NULL; + size_t pwd_len = pass ? (size_t)scr_bytes_byte_len(pass) : 0; + int private_rc = 0; if (private_key) { - ok = mbedtls_pk_parse_key(&k->pk, buf, n, NULL, 0, sp_random, NULL) == 0; + private_rc = mbedtls_pk_parse_key(&k->pk, buf, n, pwd, pwd_len, sp_random, NULL); + ok = private_rc == 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) { + if ((private_rc = mbedtls_pk_parse_key(&priv->pk, buf, n, pwd, pwd_len, sp_random, NULL)) == 0) { sp_key_release(k); k = sp_public_of(priv); ok = k != NULL; @@ -949,7 +1853,10 @@ static SpKey *sp_parse(const unsigned char *data, size_t len, bool private_key) free(buf); if (!ok) { sp_key_release(k); - sp_unsupported_input(); + /* An encrypted traditional PEM key mbedTLS read. */ + if (private_rc == MBEDTLS_ERR_PK_PASSWORD_REQUIRED) sp_throw_passphrase_missing(false); + else if (private_rc == MBEDTLS_ERR_PK_PASSWORD_MISMATCH) sp_throw_bad_decrypt(2); + else sp_unsupported_input(); return NULL; } if (!sp_key_supported(k)) { @@ -1011,13 +1918,16 @@ static ScrDyn *sp_create_key(ScrDyn *input, bool private_key) { const ScrDyn *format = scr_dyn_undefined(); const ScrDyn *type = scr_dyn_undefined(); const ScrDyn *encoding = NULL; + const ScrDyn *passphrase = 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"); + passphrase = sp_opt(input, "passphrase"); + if (!sp_absent(passphrase) && passphrase->kind != SCR_DYN_STR && passphrase->kind != SCR_DYN_BYTES && + !scr_buffer_storage_is(passphrase)) { + sp_property_invalid("key.passphrase", passphrase); return NULL; } SpKey *inner = sp_key_of(data); @@ -1055,7 +1965,16 @@ static ScrDyn *sp_create_key(ScrDyn *input, bool private_key) { 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); + ScrBytes *pass = NULL; + if (passphrase && !sp_absent(passphrase)) { + pass = sp_source_bytes(passphrase, NULL); + if (!pass) { + scr_bytes_release(bytes); + return NULL; + } + } + SpKey *k = sp_parse_with(bytes->data, (size_t)scr_bytes_byte_len(bytes), private_key, pass); + if (pass) scr_bytes_release(pass); scr_bytes_release(bytes); return k ? sp_key_box(k) : NULL; } @@ -1296,7 +2215,13 @@ static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { scr_dyn_arg_type_fail("options", "of type object", options ? options : scr_dyn_undefined()); return NULL; } - if (sp_is_str(format, "jwk")) return sp_export_jwk(k); + if (sp_is_str(format, "jwk")) { + if (k->rsa_pss) { + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE", "Unsupported JWK Key Type."); + return NULL; + } + 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); @@ -1311,11 +2236,7 @@ static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { 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)) { + if (pkcs1 && (!sp_is_rsa(k) || k->rsa_pss)) { sp_incompatible("pkcs1", "can only be used for RSA keys"); return NULL; } @@ -1323,6 +2244,30 @@ static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { sp_incompatible("sec1", "can only be used for EC keys"); return NULL; } + /* parseKeyEncoding's cipher and passphrase checks, then the cipher. */ + const ScrDyn *cipher = sp_opt(options, "cipher"), *passphrase = sp_opt(options, "passphrase"); + const SpKeyCipher *ci = NULL; + if (k->private_key) { + bool encrypted = !sp_absent(cipher) && cipher->kind != SCR_DYN_NULL; + if (encrypted) { + if (cipher->kind != SCR_DYN_STR) { + sp_property_invalid("options.cipher", cipher); + return NULL; + } + if (!pem && (pkcs1 || sec1)) { + sp_incompatible(pkcs1 ? "pkcs1" : "sec1", "does not support encryption"); + return NULL; + } + if (passphrase->kind != SCR_DYN_STR && passphrase->kind != SCR_DYN_BYTES && !scr_buffer_storage_is(passphrase)) { + sp_property_invalid("options.passphrase", passphrase); + return NULL; + } + if (!sp_key_cipher(cipher->v.str, &ci)) return NULL; + } else if (!sp_absent(passphrase)) { + sp_property_invalid_because("options.cipher", cipher, "is required when a passphrase is specified"); + return NULL; + } + } unsigned char *der = malloc(SP_DER_MAX); if (!der) scr_trap("scriptc: out of memory\n"); int n; @@ -1343,6 +2288,26 @@ static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { ScrDyn *result = NULL; if (n < 0) { sp_unsupported_input(); + } else if (ci && !sp_key_cipher_ready(ci, pkcs8, (size_t)n)) { + /* OpenSSL's error, raised once the key is encoded. */ + } else if (ci) { + const unsigned char *start = der + SP_DER_MAX - n; + ScrBytes *pass = sp_source_bytes(passphrase, NULL); + const unsigned char *pass_data = pass ? pass->data : (const unsigned char *)""; + size_t pass_len = pass ? (size_t)scr_bytes_byte_len(pass) : 0; + if (pkcs8) { + size_t enc_len; + unsigned char *enc = sp_encrypt_pkcs8(start, (size_t)n, ci, pass_data, pass_len, &enc_len); + if (enc) { + result = pem ? sp_dyn_str(sp_pem("ENCRYPTED PRIVATE KEY", enc, enc_len)) : sp_dyn_buffer(enc, enc_len); + free(enc); + } + } else { + ScrStr *text = sp_encrypted_pem(label, start, (size_t)n, ci, pass_data, pass_len); + if (text) result = sp_dyn_str(text); + } + if (pass) scr_bytes_release(pass); + if (!result) 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); @@ -1372,6 +2337,12 @@ static ScrDyn *sp_key_details(SpKey *k) { ScrBigInt *big = scr_bigint_from_u64(exponent); sp_set(details, "publicExponent", scr_dyn_new_bigint(big)); scr_bigint_release(big); + if (k->pss_params) { + const char *md = sp_pss_md_name(k->pss_md), *mgf1 = sp_pss_md_name(k->pss_mgf1); + sp_set(details, "hashAlgorithm", sp_dyn_str(scr_str_new(md, strlen(md)))); + sp_set(details, "mgf1HashAlgorithm", sp_dyn_str(scr_str_new(mgf1, strlen(mgf1)))); + sp_set(details, "saltLength", scr_dyn_new_num(k->pss_salt)); + } } else { const SpCurve *curve = sp_curve_of(k); sp_set(details, "namedCurve", sp_dyn_str(scr_str_new(curve->name, strlen(curve->name)))); @@ -1429,6 +2400,7 @@ static ScrDyn *sp_key_get(void *h, const char *name, size_t len) { if (len == 17 && memcmp(name, "asymmetricKeyType", 17) == 0) return sp_is_ed(k) ? sp_dyn_str(scr_str_new("ed25519", 7)) : sp_is_x(k) ? sp_dyn_str(scr_str_new("x25519", 6)) + : k->rsa_pss ? sp_dyn_str(scr_str_new("rsa-pss", 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); @@ -1640,7 +2612,7 @@ static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const S return false; } } - if (!*refusal && encrypted) *refusal = "encrypted private key export"; + (void)encrypted; return true; } @@ -1648,10 +2620,122 @@ static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const S typedef struct { enum { SP_GEN_RSA, SP_GEN_EC, SP_GEN_ED25519, SP_GEN_X25519 } kind; double bits, exponent; /* RSA */ + bool pss; /* an RSA-PSS key, restricted when pss_params */ + bool pss_params, md_set, mgf1_set, salt_set; + mbedtls_md_type_t md, mgf1; + int salt; const SpCurve *curve; /* EC */ } SpKeyGen; +static bool sp_digest_of(const ScrStr *name, mbedtls_md_type_t *md); + +/* A digest name of rsa-pss's options: true with *md set, or false with + * Node's ERR_CRYPTO_INVALID_DIGEST pending ("Invalid digest: x" or "Invalid + * MGF1 digest: x"), or the SC2020 refusal for a digest OpenSSL has and + * RSASSA-PSS-params here cannot name. */ +static bool sp_pss_digest(const ScrDyn *name, bool mgf1, mbedtls_md_type_t *md) { + if (sp_digest_of(name->v.str, md) && sp_pss_md_name(*md)) return true; + if (scr_digest_known(name->v.str->data, name->v.str->len)) { + sp_not_supported("generateKeyPair rsa-pss with this digest"); + return false; + } + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_DIGEST", "Invalid %sdigest: %.*s", mgf1 ? "MGF1 " : "", + (int)name->v.str->len, name->v.str->data); + return false; +} + +/* rsa-pss's options after the modulus and exponent, in Node's order + * (lib/internal/crypto/keygen.js): saltLength, hashAlgorithm, + * mgf1HashAlgorithm, and the deprecated hash and mgf1Hash with their + * DEP0154 warnings, then the digest lookups of RsaKeyGenTraits. */ +static bool sp_pss_keygen_options(const ScrDyn *options, SpKeyGen *gen) { + const ScrDyn *salt = sp_opt(options, "saltLength"); + const ScrDyn *hash_alg = sp_opt(options, "hashAlgorithm"), *mgf1_alg = sp_opt(options, "mgf1HashAlgorithm"); + const ScrDyn *hash = sp_opt(options, "hash"), *mgf1_hash = sp_opt(options, "mgf1Hash"); + if (!sp_absent(salt)) { + if (salt->kind != SCR_DYN_NUM) { + scr_dyn_prop_type_fail("options.saltLength", "of type number", salt); + return false; + } + double x = salt->v.num; + bool integer = isfinite(x) && trunc(x) == x; + if (!integer || x < 0 || x > 2147483647.0) { + ScrStr *received = sp_received(salt, true); + scr_pki_throw_fmt(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", + "The value of \"options.saltLength\" is out of range. It must be %s. Received %.*s", + integer ? ">= 0 && <= 2147483647" : "an integer", (int)received->len, received->data); + scr_str_release(received); + return false; + } + gen->salt_set = true; + gen->salt = (int)x; + } + if (!sp_absent(hash_alg) && hash_alg->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("options.hashAlgorithm", "of type string", hash_alg); + return false; + } + if (!sp_absent(mgf1_alg) && mgf1_alg->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("options.mgf1HashAlgorithm", "of type string", mgf1_alg); + return false; + } + const struct { + const ScrDyn *value, *preferred; + const char *name, *preferred_name; + } deprecated[] = { + {hash, hash_alg, "options.hash", "options.hashAlgorithm"}, + {mgf1_hash, mgf1_alg, "options.mgf1Hash", "options.mgf1HashAlgorithm"}, + }; + for (size_t i = 0; i < 2; i++) { + if (sp_absent(deprecated[i].value)) continue; + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_puts(&text, "\""); + scr_jb_puts(&text, deprecated[i].name); + scr_jb_puts(&text, "\" is deprecated, use \""); + scr_jb_puts(&text, deprecated[i].preferred_name); + scr_jb_puts(&text, "\" instead."); + /* process.emitWarning: reported on the next tick, dropped under + * process.noDeprecation, and thrown from its own tick under + * process.throwDeprecation. */ + ScrStr *message = scr_jb_finish(&text); + ScrStr *code = scr_str_new("DEP0154", 7); + ScrDyn *warning = scr_warning_new(message, "DeprecationWarning", code); + scr_str_release(code); + scr_str_release(message); + scr_warning_emit_deferred(warning); + scr_dyn_release(warning); + if (deprecated[i].value->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail(deprecated[i].name, "of type string", deprecated[i].value); + return false; + } + const ScrDyn *preferred = deprecated[i].preferred; + if (preferred->kind == SCR_DYN_STR && preferred->v.str->len > 0 && + !scr_str_eq(preferred->v.str, deprecated[i].value->v.str)) { + sp_property_invalid(deprecated[i].name, deprecated[i].value); + return false; + } + } + /* `hashAlgorithm || hash`: an empty string reads as absent. */ + const ScrDyn *md = hash_alg->kind == SCR_DYN_STR && hash_alg->v.str->len > 0 ? hash_alg : hash; + const ScrDyn *mgf1 = mgf1_alg->kind == SCR_DYN_STR && mgf1_alg->v.str->len > 0 ? mgf1_alg : mgf1_hash; + if (md->kind == SCR_DYN_STR) { + if (!sp_pss_digest(md, false, &gen->md)) return false; + gen->md_set = true; + } + if (mgf1->kind == SCR_DYN_STR) { + if (!sp_pss_digest(mgf1, true, &gen->mgf1)) return false; + gen->mgf1_set = true; + } + /* OpenSSL's defaults: MGF1 over the digest and a salt of the digest's + * length once a digest is given, SHA-1 and 20 otherwise. */ + gen->pss_params = gen->md_set || gen->mgf1_set || gen->salt_set; + if (!gen->md_set) gen->md = MBEDTLS_MD_SHA1; + if (!gen->mgf1_set) gen->mgf1 = gen->md; + if (!gen->salt_set) gen->salt = gen->md_set ? (int)mbedtls_md_get_size(mbedtls_md_info_from_type(gen->md)) : 20; + return true; +} + /* createJob's checks for generateKeyPair(Sync)(type, options) in Node's * order: the type, the options object, the public and private key * encodings, then the options of the type. False with the error pending. @@ -1677,7 +2761,8 @@ static bool sp_keygen_check(const ScrDyn *type, const ScrDyn *options, SpKeyGen if (!sp_absent(exponent) && exponent->kind != SCR_DYN_NULL && !sp_check_uint32(exponent, "options.publicExponent", &gen->exponent)) return false; - if (pss && !refusal) refusal = "generateKeyPairSync for this key type"; + gen->pss = pss; + if (pss && !sp_pss_keygen_options(options, gen)) return false; } else if (sp_is_str(type, "dsa") || sp_is_str(type, "dh")) { if (!sp_check_object(options, "options")) return false; if (!refusal) refusal = "generateKeyPairSync for this key type"; @@ -1723,6 +2808,23 @@ static bool sp_keygen_check(const ScrDyn *type, const ScrDyn *options, SpKeyGen } if (!refusal) refusal = "generateKeyPairSync for this key type"; } + /* The job's private key cipher, which Node looks up once the options + * of the type are read. */ + { + const ScrDyn *enc = sp_opt(options, "privateKeyEncoding"); + const ScrDyn *cipher = sp_is_object(enc) ? sp_opt(enc, "cipher") : scr_dyn_undefined(); + const SpKeyCipher *ci; + if (cipher->kind == SCR_DYN_STR) { + if (!sp_key_cipher(cipher->v.str, &ci)) return false; + /* OpenSSL's errors for the cipher come from the export once the + * key is generated; a cipher it encrypts with and keys here do not + * is refused first. */ + bool pkcs8 = sp_is_str(sp_opt(enc, "type"), "pkcs8"); + if (ci->type == MBEDTLS_CIPHER_NONE && (pkcs8 ? ci->pkcs8_error : ci->traditional_error) == SP_KC_OK && + !refusal) + refusal = "private keys encrypted with this cipher"; + } + } if (refusal) { sp_not_supported(refusal); return false; @@ -1730,6 +2832,54 @@ static bool sp_keygen_check(const ScrDyn *type, const ScrDyn *options, SpKeyGen return gen->kind != SP_GEN_ED25519 || sp_ed() != NULL; } +/* An RSA key of `bits` bits with public exponent `exponent`, within + * OpenSSL's limits rather than mbedtls_rsa_gen_key's (even lengths from + * 1024 bits, exponents up to INT_MAX): any length from 512 bits and odd + * exponents up to 2^32 - 1. From 2048 bits OpenSSL generates two primes + * of equal length (SP 800-56B), so an odd length yields one bit less. + * The primes have (bits + 1) / 2 and bits / 2 bits, each with its top + * bits set (mbedtls_mpi_gen_prime's FIPS 186-4 bound), so their product + * has exactly `bits` bits; each prime minus one is coprime to the + * exponent. 0 on success. */ +static int sp_rsa_generate(mbedtls_rsa_context *rsa, unsigned bits, uint64_t exponent) { + if (bits >= 2048) bits &= ~1u; + mbedtls_mpi P, Q, E, N, T, G; + mbedtls_mpi_init(&P); + mbedtls_mpi_init(&Q); + mbedtls_mpi_init(&E); + mbedtls_mpi_init(&N); + mbedtls_mpi_init(&T); + mbedtls_mpi_init(&G); + unsigned char e_bytes[8]; + for (int i = 0; i < 8; i++) e_bytes[i] = (unsigned char)(exponent >> (56 - 8 * i)); + int r = mbedtls_mpi_read_binary(&E, e_bytes, sizeof e_bytes); + const int flags = MBEDTLS_MPI_GEN_PRIME_FLAG_LOW_ERR; + while (r == 0) { + r = mbedtls_mpi_gen_prime(&P, (bits + 1) / 2, flags, sp_random, NULL); + if (r == 0) r = mbedtls_mpi_sub_int(&T, &P, 1); + if (r == 0) r = mbedtls_mpi_gcd(&G, &E, &T); + if (r != 0 || mbedtls_mpi_cmp_int(&G, 1) != 0) continue; + r = mbedtls_mpi_gen_prime(&Q, bits / 2, flags, sp_random, NULL); + if (r == 0) r = mbedtls_mpi_sub_int(&T, &Q, 1); + if (r == 0) r = mbedtls_mpi_gcd(&G, &E, &T); + if (r != 0 || mbedtls_mpi_cmp_int(&G, 1) != 0 || mbedtls_mpi_cmp_mpi(&P, &Q) == 0) continue; + r = mbedtls_mpi_mul_mpi(&N, &P, &Q); + if (r == 0 && mbedtls_mpi_bitlen(&N) == bits) break; + } + /* The larger prime is p, as OpenSSL orders them. */ + if (r == 0 && mbedtls_mpi_cmp_mpi(&P, &Q) < 0) mbedtls_mpi_swap(&P, &Q); + if (r == 0) r = mbedtls_rsa_import(rsa, &N, &P, &Q, NULL, &E); + if (r == 0) r = mbedtls_rsa_complete(rsa); + if (r == 0) r = mbedtls_rsa_check_privkey(rsa); + mbedtls_mpi_free(&P); + mbedtls_mpi_free(&Q); + mbedtls_mpi_free(&E); + mbedtls_mpi_free(&N); + mbedtls_mpi_free(&T); + mbedtls_mpi_free(&G); + return r; +} + /* OpenSSL's error for an RSA modulus length or public exponent it does not * generate, as Node reports it, or NULL. */ static const char *sp_rsa_keygen_failure(double bits, double exponent) { @@ -1773,16 +2923,22 @@ static SpKey *sp_keygen_run(const SpKeyGen *gen, bool *job_error) { scr_pki_throw_fmt(SCR_ERR_ERROR, NULL, "%s", failure); return NULL; } - /* mbedTLS generates even modulus lengths from 1024 to 8192 bits and, - * past the checks above, odd exponents up to INT_MAX. */ + /* Moduli past mbedTLS's 8192-bit numbers stay unsupported (OpenSSL + * generates up to 16384 bits). */ double b = gen->bits, e = gen->exponent; - if (b < MBEDTLS_RSA_GEN_KEY_MIN_BITS || b > MBEDTLS_MPI_MAX_BITS || fmod(b, 2) != 0 || e > 2147483647.0 || - mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0 || - mbedtls_rsa_gen_key(mbedtls_pk_rsa(k->pk), sp_random, NULL, (unsigned)b, (int)e) != 0) { + if (b > MBEDTLS_MPI_MAX_BITS || mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0 || + sp_rsa_generate(mbedtls_pk_rsa(k->pk), (unsigned)b, (uint64_t)e) != 0) { sp_key_release(k); sp_not_supported("generateKeyPairSync with this RSA modulus length or exponent"); return NULL; } + if (gen->pss) { + k->rsa_pss = true; + k->pss_params = gen->pss_params; + k->pss_md = gen->md; + k->pss_mgf1 = gen->mgf1; + k->pss_salt = gen->salt; + } return k; } if (mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) != 0 || @@ -1799,7 +2955,7 @@ static SpKey *sp_keygen_run(const SpKeyGen *gen, bool *job_error) { * pending, and *job_error as sp_keygen_run sets it. */ static ScrDyn *sp_key_pair(const ScrDyn *type, const ScrDyn *options, bool *job_error) { *job_error = false; - SpKeyGen gen; + SpKeyGen gen = {0}; if (!sp_keygen_check(type, options, &gen)) return NULL; SpKey *k = sp_keygen_run(&gen, job_error); if (!k) return NULL; @@ -1910,8 +3066,11 @@ static bool sp_algorithm(const ScrDyn *alg, mbedtls_md_type_t *md) { 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) */ + int salt; /* -1 digest length, -2 maximum (sign) or auto (verify), + * -4 the digest length or the maximum, if smaller */ bool p1363; /* dsaEncoding: "ieee-p1363" */ + bool padding_given, salt_given; + mbedtls_md_type_t mgf1; /* PSS's MGF1 digest; NONE for the message's */ } SpSigKey; /* The key argument of a signing operation: a KeyObject, key material, or @@ -1932,6 +3091,9 @@ static bool sp_sig_key_plain(const ScrDyn *value, bool sign, SpSigKey *out) { out->padding = 1; out->salt = sign ? -2 : -2; out->p1363 = false; + out->padding_given = false; + out->salt_given = false; + out->mgf1 = MBEDTLS_MD_NONE; const ScrDyn *options = NULL; if (sp_is_object(value) && !sp_key_of(value) && !scr_buffer_storage_is(value)) { options = value; @@ -1948,6 +3110,7 @@ static bool sp_sig_key_plain(const ScrDyn *value, bool sign, SpSigKey *out) { return false; } out->padding = (int)padding->v.num; + out->padding_given = true; } const ScrDyn *salt = sp_opt(value, "saltLength"); if (!sp_absent(salt)) { @@ -1956,6 +3119,7 @@ static bool sp_sig_key_plain(const ScrDyn *value, bool sign, SpSigKey *out) { return false; } out->salt = (int)salt->v.num; + out->salt_given = true; } } SpKey *direct = sp_key_of(options ? sp_opt(options, "key") : value); @@ -2022,6 +3186,40 @@ static int sp_p1363_to_der(const unsigned char *sig, size_t len, unsigned char * static size_t sp_ec_size(SpKey *k) { return (mbedtls_pk_ec(k->pk)->MBEDTLS_PRIVATE(grp).nbits + 7) / 8; } +/* An RSA-PSS key's restrictions on a signature over a `md` digest, as + * OpenSSL's provider applies them: PSS padding only (the default), and + * for a key with RSASSA-PSS-params its digest, its MGF1 digest and salts + * of at least its own (the default). Without parameters a signature + * salts with the digest's length, or the most the key holds when that is + * less (RSA_PSS_SALTLEN_AUTO_DIGEST_MAX). False with OpenSSL's error + * pending. */ +static bool sp_pss_key_rules(SpSigKey *key, mbedtls_md_type_t md, size_t hash_len, bool sign) { + const SpKey *k = key->key; + if (!k->rsa_pss) return true; + if (key->padding_given && key->padding != 6) { + sp_throw_openssl("1C8000A5", "Provider routines", "illegal or unsupported padding mode", + "ERR_OSSL_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE", true); + return false; + } + key->padding = 6; + if (!k->pss_params) { + if (!key->salt_given) key->salt = sign ? -4 : -2; + return true; + } + if (md != k->pss_md) { + sp_throw_openssl("1C8000AE", "Provider routines", "digest not allowed", "ERR_OSSL_DIGEST_NOT_ALLOWED", true); + return false; + } + key->mgf1 = k->pss_mgf1; + if (!key->salt_given) { + key->salt = k->pss_salt; + } else if (key->salt != -2 && (key->salt == -1 ? (int)hash_len : key->salt) < k->pss_salt) { + sp_throw_openssl("1C8000AC", "Provider routines", "pss saltlen too small", "ERR_OSSL_PSS_SALTLEN_TOO_SMALL", true); + return false; + } + return true; +} + /* 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; @@ -2030,11 +3228,119 @@ static bool sp_rsa_padding(SpSigKey *key, mbedtls_md_type_t md) { return mbedtls_rsa_set_padding(rsa, MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_NONE) == 0; } +/* MGF1 over `mgf1` (RFC 8017 B.2.1), XORed into mask[0, len). */ +static int sp_mgf1_xor(mbedtls_md_type_t mgf1, const unsigned char *seed, size_t seed_len, unsigned char *mask, + size_t len) { + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(mgf1); + size_t h = mbedtls_md_get_size(info); + unsigned char block[MBEDTLS_MD_MAX_SIZE]; + mbedtls_md_context_t ctx; + mbedtls_md_init(&ctx); + int r = mbedtls_md_setup(&ctx, info, 0); + for (uint32_t counter = 0; r == 0 && (size_t)counter * h < len; counter++) { + unsigned char c[4] = {(unsigned char)(counter >> 24), (unsigned char)(counter >> 16), + (unsigned char)(counter >> 8), (unsigned char)counter}; + if ((r = mbedtls_md_starts(&ctx)) == 0 && (r = mbedtls_md_update(&ctx, seed, seed_len)) == 0 && + (r = mbedtls_md_update(&ctx, c, 4)) == 0 && (r = mbedtls_md_finish(&ctx, block)) == 0) { + for (size_t i = 0; i < h && (size_t)counter * h + i < len; i++) mask[(size_t)counter * h + i] ^= block[i]; + } + } + mbedtls_md_free(&ctx); + mbedtls_platform_zeroize(block, sizeof block); + return r; +} + +/* An RSASSA-PSS signature whose MGF1 digest differs from the message's + * (RFC 8017 9.1.1 and 8.1.1), which mbedtls_rsa_rsassa_pss_sign_ext + * cannot make: the encoding here, then the private-key operation. */ +static int sp_pss_sign_mgf1(mbedtls_rsa_context *rsa, mbedtls_md_type_t md, mbedtls_md_type_t mgf1, + const unsigned char *hash, size_t hash_len, int salt_len, unsigned char *sig) { + size_t bytes = mbedtls_rsa_get_len(rsa); + size_t em_bits = mbedtls_rsa_get_bitlen(rsa) - 1; + size_t em_len = (em_bits + 7) / 8; + size_t salt = (size_t)salt_len; + if (em_len < hash_len + salt + 2) return MBEDTLS_ERR_RSA_BAD_INPUT_DATA; + unsigned char em[MBEDTLS_MPI_MAX_SIZE] = {0}; + unsigned char *db = em, *h = em + em_len - hash_len - 1; + size_t db_len = em_len - hash_len - 1; + unsigned char salt_bytes[MBEDTLS_MPI_MAX_SIZE]; + int r = sp_random(NULL, salt_bytes, salt); + /* H = Hash(0x00 * 8 || mHash || salt) */ + mbedtls_md_context_t ctx; + mbedtls_md_init(&ctx); + static const unsigned char zeros[8] = {0}; + if (r == 0 && (r = mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md), 0)) == 0 && + (r = mbedtls_md_starts(&ctx)) == 0 && (r = mbedtls_md_update(&ctx, zeros, 8)) == 0 && + (r = mbedtls_md_update(&ctx, hash, hash_len)) == 0 && (r = mbedtls_md_update(&ctx, salt_bytes, salt)) == 0) + r = mbedtls_md_finish(&ctx, h); + mbedtls_md_free(&ctx); + if (r == 0) { + /* DB = PS || 0x01 || salt, masked with MGF1(H). */ + db[db_len - salt - 1] = 0x01; + memcpy(db + db_len - salt, salt_bytes, salt); + r = sp_mgf1_xor(mgf1, h, hash_len, db, db_len); + } + if (r == 0) { + db[0] &= (unsigned char)(0xff >> (8 * em_len - em_bits)); + em[em_len - 1] = 0xbc; + unsigned char block[MBEDTLS_MPI_MAX_SIZE] = {0}; + memcpy(block + bytes - em_len, em, em_len); + r = mbedtls_rsa_private(rsa, sp_random, NULL, block, sig); + mbedtls_platform_zeroize(block, sizeof block); + } + mbedtls_platform_zeroize(em, sizeof em); + mbedtls_platform_zeroize(salt_bytes, sizeof salt_bytes); + return r; +} + +/* Verifies an RSASSA-PSS signature whose MGF1 digest differs from the + * message's (RFC 8017 9.1.2), which mbedtls_rsa_rsassa_pss_verify_ext + * cannot: it hashes M' with the MGF1 digest too. `salt_len` -2 accepts any + * salt length. */ +static bool sp_pss_verify_mgf1(mbedtls_rsa_context *rsa, mbedtls_md_type_t md, mbedtls_md_type_t mgf1, + const unsigned char *hash, size_t hash_len, int salt_len, + const unsigned char *sig, size_t sig_len) { + size_t bytes = mbedtls_rsa_get_len(rsa); + if (sig_len != bytes) return false; + unsigned char decoded[MBEDTLS_MPI_MAX_SIZE]; + if (mbedtls_rsa_public(rsa, sig, decoded) != 0) return false; + size_t em_bits = mbedtls_rsa_get_bitlen(rsa) - 1; + size_t em_len = (em_bits + 7) / 8; + if (bytes > em_len && decoded[0] != 0) return false; + unsigned char *em = decoded + (bytes - em_len); + if (em_len < hash_len + 2 || em[em_len - 1] != 0xbc) return false; + size_t db_len = em_len - hash_len - 1; + unsigned char *h = em + db_len; + unsigned char top = (unsigned char)(0xff >> (8 * em_len - em_bits)); + if (em[0] & (unsigned char)~top) return false; + unsigned char db[MBEDTLS_MPI_MAX_SIZE]; + memcpy(db, em, db_len); + if (sp_mgf1_xor(mgf1, h, hash_len, db, db_len) != 0) return false; + db[0] &= top; + size_t i = 0; + while (i < db_len && db[i] == 0) i++; + if (i == db_len || db[i] != 0x01) return false; + size_t salt = db_len - i - 1; + if (salt_len >= 0 && salt != (size_t)salt_len) return false; + static const unsigned char zeros[8] = {0}; + unsigned char check[MBEDTLS_MD_MAX_SIZE]; + mbedtls_md_context_t ctx; + mbedtls_md_init(&ctx); + int r; + if ((r = mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md), 0)) == 0 && (r = mbedtls_md_starts(&ctx)) == 0 && + (r = mbedtls_md_update(&ctx, zeros, 8)) == 0 && (r = mbedtls_md_update(&ctx, hash, hash_len)) == 0 && + (r = mbedtls_md_update(&ctx, db + i + 1, salt)) == 0) + r = mbedtls_md_finish(&ctx, check); + mbedtls_md_free(&ctx); + return r == 0 && memcmp(check, h, hash_len) == 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_pss_key_rules(key, md, hash_len, true)) return NULL; if (!sp_rsa_padding(key, md)) { sp_unsupported_input(); return NULL; @@ -2047,12 +3353,15 @@ static ScrDyn *sp_sign_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned 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; + else if (salt == -4) salt = (int)(hash_len < max ? hash_len : 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); + r = key->mgf1 != MBEDTLS_MD_NONE && key->mgf1 != md + ? sp_pss_sign_mgf1(rsa, md, key->mgf1, hash, hash_len, salt, sig) + : 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); @@ -2077,6 +3386,7 @@ static ScrDyn *sp_sign_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned * 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_pss_key_rules(key, md, hash_len, false)) return false; 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); @@ -2084,6 +3394,9 @@ static bool sp_verify_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned c int salt = key->salt; if (salt == -1) salt = (int)hash_len; else if (salt == -2) salt = MBEDTLS_RSA_SALT_LEN_ANY; + if (key->mgf1 != MBEDTLS_MD_NONE && key->mgf1 != md) + return sp_pss_verify_mgf1(rsa, md, key->mgf1, hash, hash_len, salt == MBEDTLS_RSA_SALT_LEN_ANY ? -2 : salt, + sig, sig_len); 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) { @@ -2127,6 +3440,9 @@ ScrDyn *scr_crypto_sign_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key) { SpSigKey k; if (!sp_sig_key(key, true, &k)) return NULL; if (sp_reject_x25519(&k)) return NULL; + /* A null algorithm signs with an RSA-PSS key's own digest. */ + if ((algorithm->kind == SCR_DYN_UNDEF || algorithm->kind == SCR_DYN_NULL) && k.key->pss_params) + md = k.key->pss_md; ScrBytes *bytes = scr_bytes_buffer_source(data); if (sp_is_ed(k.key)) { ScrDyn *result = NULL; @@ -2165,6 +3481,8 @@ ScrDyn *scr_crypto_verify_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key, return NULL; } if (sp_reject_x25519(&k)) return NULL; + if ((algorithm->kind == SCR_DYN_UNDEF || algorithm->kind == SCR_DYN_NULL) && k.key->pss_params) + md = k.key->pss_md; ScrBytes *bytes = scr_bytes_buffer_source(data); ScrBytes *sig = scr_bytes_buffer_source(signature); if (sp_is_ed(k.key)) { @@ -2186,7 +3504,7 @@ ScrDyn *scr_crypto_verify_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key, scr_bytes_release(bytes); scr_bytes_release(sig); sp_key_release(k.key); - return scr_dyn_new_bool(ok); + return scr_exc_pending() ? NULL : scr_dyn_new_bool(ok); } /* ── Sign and Verify objects ──────────────────────────────────────────── */ @@ -2305,7 +3623,7 @@ static ScrDyn *sp_signer_invoke(void *h, ScrDyn *self, const char *method, ScrDy sig->data, (size_t)scr_bytes_byte_len(sig)); scr_bytes_release(sig); sp_key_release(k.key); - return scr_dyn_new_bool(ok); + return scr_exc_pending() ? NULL : scr_dyn_new_bool(ok); } scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "%s is not a function", what); return NULL; @@ -2466,7 +3784,11 @@ static const char sp_dh_public_types[] = /* Node's message names: a KeyObject's asymmetricKeyType. */ static const char *sp_type_name(const SpKey *k) { - return sp_is_ed(k) ? "ed25519" : sp_is_x(k) ? "x25519" : sp_is_rsa(k) ? "rsa" : "ec"; + return sp_is_ed(k) ? "ed25519" + : sp_is_x(k) ? "x25519" + : k->rsa_pss ? "rsa-pss" + : sp_is_rsa(k) ? "rsa" + : "ec"; } /* A key input of diffieHellman's options: a primitive other than a string @@ -2683,8 +4005,9 @@ 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) { + if (bits < 512 || bits > MBEDTLS_MPI_MAX_BITS || exponent < 3 || exponent % 2 == 0 || + exponent > 4294967295u || mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0 || + sp_rsa_generate(mbedtls_pk_rsa(k->pk), bits, exponent) != 0) { sp_key_release(k); return NULL; } diff --git a/packages/runtime/src/scr_webcrypto_pki.c b/packages/runtime/src/scr_webcrypto_pki.c index 7439a12db..458cfeae8 100644 --- a/packages/runtime/src/scr_webcrypto_pki.c +++ b/packages/runtime/src/scr_webcrypto_pki.c @@ -380,7 +380,8 @@ static ScrPromise *sw_generate_key(const ScrDyn *args) { scr_throw_domex("OperationError", "algorithm.publicExponent must be odd"); goto done; } - if (bits > 16384 || value < 3) { + /* The pki unit's numbers hold moduli up to 8192 bits. */ + if (bits > 8192 || value < 3) { scr_wc_unsupported("generateKey", "this RSA modulus length or public exponent"); goto done; } diff --git a/tests/corpus/8165-rsa-keygen-lengths.cjs b/tests/corpus/8165-rsa-keygen-lengths.cjs new file mode 100644 index 000000000..35e2cb28f --- /dev/null +++ b/tests/corpus/8165-rsa-keygen-lengths.cjs @@ -0,0 +1,40 @@ +// RSA key generation within OpenSSL's limits: any modulus length from +// 512 bits (an odd length from 2048 bits yields one bit less, as +// OpenSSL's SP 800-56B generation does) and odd exponents up to 2^32 - 1. +"use strict"; +const crypto = require("crypto"); + +const cases = [ + [512, 65537], + [513, 65537], + [1023, 3], + [1024, 0x10001], + [2049, 65537], + [768, 4294967295], + [520, 17], +]; +for (const [modulusLength, publicExponent] of cases) { + const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { modulusLength, publicExponent }); + const details = privateKey.asymmetricKeyDetails; + const data = Buffer.from("rsa key lengths"); + const signature = crypto.sign("sha256", data, privateKey); + console.log( + modulusLength, + details.modulusLength, + details.publicExponent, + publicKey.asymmetricKeyDetails.modulusLength, + signature.length, + crypto.verify("sha256", data, publicKey, signature), + ); +} +for (const [modulusLength, publicExponent] of [[511, 65537], [1024, 1], [1024, 4], [1024, 0]]) { + try { + crypto.generateKeyPairSync("rsa", { modulusLength, publicExponent }); + console.log(modulusLength, publicExponent, "generated"); + } catch (error) { + console.log(modulusLength, publicExponent, error.message); + } +} +crypto.generateKeyPair("rsa", { modulusLength: 515, publicExponent: 3 }, (error, publicKey) => { + console.log("async", error, publicKey.asymmetricKeyDetails.modulusLength); +}); diff --git a/tests/corpus/8166-rsa-pss-keys.cjs b/tests/corpus/8166-rsa-pss-keys.cjs new file mode 100644 index 000000000..aad90f6aa --- /dev/null +++ b/tests/corpus/8166-rsa-pss-keys.cjs @@ -0,0 +1,104 @@ +// RSA-PSS keys (id-RSASSA-PSS): generation with and without +// RSASSA-PSS-params, their details, SubjectPublicKeyInfo and PKCS#8 in +// OpenSSL's encoding, import of both, and the restrictions on signatures: +// PSS padding only, the key's digest and MGF1 digest, and at least its +// salt length. The deprecated hash and mgf1Hash options run in a child +// process, whose DEP0154 reports are printed with process ids normalized. +"use strict"; +const { spawnSync } = require("child_process"); +const c = require("crypto"); +const fmt = (v) => JSON.stringify(v, (k, x) => (typeof x === "bigint" ? `${x}n` : x)); +const tryIt = (label, fn) => { + try { + const r = fn(); + console.log(label, typeof r === "string" ? r.split("\n")[0] : Buffer.isBuffer(r) ? `buffer ${r.length > 0}` : fmt(r)); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +}; +const algOf = (spki) => { + // The AlgorithmIdentifier: the SEQUENCE after the outer header. + const start = spki[1] & 0x80 ? 2 + (spki[1] & 0x7f) : 2; + const len = spki[start + 1]; + return spki.subarray(start, start + 2 + len).toString("hex"); +}; +const report = (label, options) => tryIt(label, () => c.generateKeyPairSync("rsa-pss", { modulusLength: 512, ...options }).publicKey.asymmetricKeyDetails); + +function main() { + const cases = { + none: {}, + sha256: { hashAlgorithm: "sha256" }, + full: { hashAlgorithm: "sha256", mgf1HashAlgorithm: "sha384", saltLength: 16 }, + sha1: { hashAlgorithm: "sha1" }, + salt: { saltLength: 10 }, + s512: { hashAlgorithm: "sha512", saltLength: 0 }, + mgf: { mgf1HashAlgorithm: "sha256" }, + }; + for (const [name, options] of Object.entries(cases)) { + const { publicKey, privateKey } = c.generateKeyPairSync("rsa-pss", { modulusLength: ["full", "sha1", "salt", "mgf"].includes(name) ? 512 : 1024, ...options }); + const spki = publicKey.export({ format: "der", type: "spki" }); + const pkcs8 = privateKey.export({ format: "der", type: "pkcs8" }); + console.log(name, publicKey.asymmetricKeyType, fmt(publicKey.asymmetricKeyDetails), fmt(privateKey.asymmetricKeyDetails)); + console.log(name, "spki alg", algOf(spki), "pkcs8 alg", pkcs8.indexOf(Buffer.from(algOf(spki), "hex")) > 0); + tryIt(`${name} pkcs1`, () => privateKey.export({ format: "pem", type: "pkcs1" })); + tryIt(`${name} jwk`, () => publicKey.export({ format: "jwk" })); + tryIt(`${name} pem`, () => privateKey.export({ format: "pem", type: "pkcs8" })); + const reimported = c.createPublicKey(publicKey.export({ format: "pem", type: "spki" })); + const fromPrivate = c.createPublicKey(privateKey.export({ format: "pem", type: "pkcs8" })); + const privateAgain = c.createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" }); + console.log(name, reimported.equals(publicKey), fromPrivate.equals(publicKey), privateAgain.asymmetricKeyType); + const data = Buffer.from("rsa-pss"); + const signature = c.sign(null, data, privateAgain); + console.log(name, c.verify(null, data, publicKey, signature), c.verify(null, data, reimported, signature)); + } + + const data = Buffer.from("restricted"); + const { publicKey, privateKey } = c.generateKeyPairSync("rsa-pss", { + modulusLength: 512, + hashAlgorithm: "sha256", + saltLength: 16, + }); + tryIt("sign sha1", () => c.sign("sha1", data, privateKey)); + tryIt("sign pkcs1", () => c.sign("sha256", data, { key: privateKey, padding: c.constants.RSA_PKCS1_PADDING })); + tryIt("sign salt 8", () => c.sign("sha256", data, { key: privateKey, saltLength: 8 })); + tryIt("verify salt 8", () => c.verify("sha256", data, { key: publicKey, saltLength: 8 }, Buffer.alloc(64))); + for (const saltLength of [16, 20, 30]) { + const signature = c.sign("sha256", data, { key: privateKey, saltLength }); + console.log( + saltLength, + c.verify("sha256", data, publicKey, signature), + c.verify("sha256", data, { key: publicKey, saltLength: c.constants.RSA_PSS_SALTLEN_AUTO }, signature), + ); + } + const signer = c.createSign("sha256").update(data); + console.log("createVerify", c.createVerify("sha256").update(data).verify(publicKey, signer.sign(privateKey))); + const free = c.generateKeyPairSync("rsa-pss", { modulusLength: 1024 }); + const freeSignature = c.sign("sha512", data, free.privateKey); + console.log("free sha512", c.verify("sha512", data, free.publicKey, freeSignature)); + + report("bad digest", { hashAlgorithm: "nope" }); + report("bad mgf1", { mgf1HashAlgorithm: "nope" }); + report("negative salt", { saltLength: -1 }); + report("upper", { hashAlgorithm: "SHA256" }); + report("alias", { hashAlgorithm: "RSA-SHA256" }); +} + +if (process.argv[2] === "deprecated") { + report("hash", { hash: "sha256" }); + report("conflict", { hashAlgorithm: "sha256", hash: "sha1" }); + report("mgf1Hash", { hashAlgorithm: "sha256", mgf1Hash: "sha384" }); + report("hash type", { hash: 5 }); + // Node drops deprecations while noDeprecation is set when it reports + // them, so the flag is set once the reports above are out. + setTimeout(() => { + process.noDeprecation = true; + report("quiet", { hash: "sha512" }); + console.log("end of turn"); + }); +} else { + main(); + const child = spawnSync(process.execPath, [process.argv[1], "deprecated"], { encoding: "utf8" }); + console.log(`status: ${child.status}`); + console.log(child.stdout); + console.log(child.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/8167-encrypted-private-keys.cjs b/tests/corpus/8167-encrypted-private-keys.cjs new file mode 100644 index 000000000..561a0aec3 --- /dev/null +++ b/tests/corpus/8167-encrypted-private-keys.cjs @@ -0,0 +1,114 @@ +// Encrypted private keys: PKCS#8 as PBES2 and PKCS#1 and SEC1 as +// traditional PEM with the CBC and ECB ciphers, import with a passphrase, +// generateKeyPair with a cipher, and OpenSSL's errors for missing and +// wrong passphrases, unknown ciphers, and ciphers a form refuses. +"use strict"; +const c = require("crypto"); +const show = (e) => + `${e.name} ${e.code} ${e.message}${e.library ? ` [${e.library}: ${e.reason}]` : ""}${ + e.opensslErrorStack ? ` ${JSON.stringify(e.opensslErrorStack)}` : "" + }`; +const tryIt = (label, fn) => { + try { + console.log(label, fn()); + } catch (e) { + console.log(label, show(e)); + } +}; +// The PEM label and headers, without the random IV. +const head = (pem) => + pem + .split("\n") + .filter((l) => l.startsWith("-----BEGIN") || l.includes(":")) + .map((l) => l.replace(/,[0-9A-F]+$/, "")) + .join(" | "); + +const ec = c.generateKeyPairSync("ec", { namedCurve: "P-256" }); +const rsa = c.generateKeyPairSync("rsa", { modulusLength: 1024 }); +const same = (key, pair) => c.createPublicKey(key).equals(pair.publicKey); + +for (const cipher of ["aes-128-cbc", "aes-192-cbc", "aes-256-cbc", "aes-128-ecb", "aes-256-ecb", "des-ede3-cbc", "aes256", "AES-128-CBC", "2.16.840.1.101.3.4.1.42"]) { + for (const format of ["pem", "der"]) { + tryIt(`pkcs8 ${format} ${cipher}`, () => { + const out = ec.privateKey.export({ type: "pkcs8", format, cipher, passphrase: "top secret" }); + const key = c.createPrivateKey({ key: out, format, type: format === "der" ? "pkcs8" : undefined, passphrase: "top secret" }); + return `${format === "pem" ? head(out) : Buffer.isBuffer(out)} ${key.asymmetricKeyType} ${same(key, ec)}`; + }); + } +} +for (const cipher of ["aes-128-cbc", "aes-256-cbc", "des-ede3-cbc", "des3"]) { + tryIt(`sec1 ${cipher}`, () => { + const out = ec.privateKey.export({ type: "sec1", format: "pem", cipher, passphrase: Buffer.from("pw") }); + return `${head(out)} ${same(c.createPrivateKey({ key: out, passphrase: "pw" }), ec)}`; + }); + tryIt(`pkcs1 ${cipher}`, () => { + const out = rsa.privateKey.export({ type: "pkcs1", format: "pem", cipher, passphrase: "pw" }); + return `${head(out)} ${same(c.createPrivateKey({ key: out, passphrase: Buffer.from("pw") }), rsa)}`; + }); +} +tryIt("empty passphrase", () => { + const out = rsa.privateKey.export({ type: "pkcs8", format: "pem", cipher: "aes-128-cbc", passphrase: "" }); + return same(c.createPrivateKey({ key: out, passphrase: "" }), rsa); +}); + +// Reading encrypted keys. +const p8pem = ec.privateKey.export({ type: "pkcs8", format: "pem", cipher: "aes-256-cbc", passphrase: "pw" }); +const p8der = ec.privateKey.export({ type: "pkcs8", format: "der", cipher: "aes-256-cbc", passphrase: "pw" }); +const sec1 = String(ec.privateKey.export({ type: "sec1", format: "pem", cipher: "aes-128-cbc", passphrase: "pw" })); +tryIt("public from encrypted", () => c.createPublicKey({ key: p8pem, passphrase: "pw" }).equals(ec.publicKey)); +tryIt("public from traditional", () => c.createPublicKey({ key: sec1, passphrase: "pw" }).type); +tryIt("pkcs8 pem no passphrase", () => c.createPrivateKey(p8pem)); +tryIt("pkcs8 der no passphrase", () => c.createPrivateKey({ key: p8der, format: "der", type: "pkcs8" })); +tryIt("sec1 no passphrase", () => c.createPrivateKey(sec1)); +tryIt("public no passphrase", () => c.createPublicKey(p8pem)); +tryIt("pkcs8 pem wrong", () => c.createPrivateKey({ key: p8pem, passphrase: "nope" })); +tryIt("pkcs8 der wrong", () => c.createPrivateKey({ key: p8der, format: "der", type: "pkcs8", passphrase: "nope" })); +tryIt("sec1 wrong", () => c.createPrivateKey({ key: sec1, passphrase: "nope" })); +tryIt("passphrase type", () => c.createPrivateKey({ key: p8pem, passphrase: 5 })); +tryIt("dek unknown", () => c.createPrivateKey({ key: sec1.replace(/DEK-Info: AES-128-CBC/, "DEK-Info: NOPE-CBC"), passphrase: "pw" })); +tryIt("dek legacy", () => + c.createPrivateKey({ key: sec1.replace(/DEK-Info: AES-128-CBC,([0-9A-F]{16})[0-9A-F]+/, "DEK-Info: DES-CBC,$1"), passphrase: "pw" }), +); +tryIt("dek legacy no passphrase", () => + c.createPrivateKey(sec1.replace(/DEK-Info: AES-128-CBC,([0-9A-F]{16})[0-9A-F]+/, "DEK-Info: DES-CBC,$1")), +); + +// Export options. +const opts = (extra) => ({ type: "pkcs8", format: "pem", ...extra }); +tryIt("unknown cipher", () => ec.privateKey.export(opts({ cipher: "nope", passphrase: "x" }))); +tryIt("cipher without passphrase", () => ec.privateKey.export(opts({ cipher: "aes-128-cbc" }))); +tryIt("passphrase without cipher", () => ec.privateKey.export(opts({ passphrase: "x" }))); +tryIt("cipher type", () => ec.privateKey.export(opts({ cipher: 5, passphrase: "x" }))); +tryIt("sec1 der", () => ec.privateKey.export({ type: "sec1", format: "der", cipher: "aes-128-cbc", passphrase: "x" })); +tryIt("pkcs1 der", () => rsa.privateKey.export({ type: "pkcs1", format: "der", cipher: "aes-128-cbc", passphrase: "x" })); +// OpenSSL refuses these ciphers for PKCS#8, and the last ones for +// traditional PEM too (the P-256 key's PKCS#8 and SEC1 lengths are not +// multiples of 8, which key wrap needs). +for (const cipher of ["aes-128-gcm", "aes-256-ccm", "aes-128-ctr", "chacha20-poly1305", "aes-128-xts", "des-ede3-cfb", "2.16.840.1.101.3.4.1.6"]) + tryIt(`refused pkcs8 ${cipher}`, () => ec.privateKey.export(opts({ cipher, passphrase: "x" })).length > 0); +for (const cipher of ["aes-128-wrap", "id-aes256-wrap", "des-cbc", "rc4"]) { + tryIt(`refused pkcs8 ${cipher}`, () => ec.privateKey.export(opts({ cipher, passphrase: "x" })).length > 0); + tryIt(`refused sec1 ${cipher}`, () => ec.privateKey.export({ type: "sec1", format: "pem", cipher, passphrase: "x" })); +} +for (const cipher of ["aes-128-ecb", "aes-256-ecb", "des-ede3"]) + tryIt(`refused sec1 ${cipher}`, () => ec.privateKey.export({ type: "sec1", format: "pem", cipher, passphrase: "x" })); + +// generateKeyPair with a cipher. +const encoding = (cipher, type = "pkcs8") => ({ + namedCurve: "P-256", + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type, format: "pem", cipher, passphrase: "gen" }, +}); +tryIt("keygen sync", () => { + const { publicKey, privateKey } = c.generateKeyPairSync("ec", encoding("aes-256-cbc")); + return `${head(privateKey)} ${c.createPublicKey({ key: privateKey, passphrase: "gen" }).equals(c.createPublicKey(publicKey))}`; +}); +tryIt("keygen unknown", () => c.generateKeyPairSync("ec", encoding("nope"))); +tryIt("keygen gcm", () => c.generateKeyPairSync("ec", encoding("aes-128-gcm"))); +tryIt("keygen sec1 ecb", () => c.generateKeyPairSync("ec", encoding("aes-128-ecb", "sec1"))); +// One job at a time: Node's jobs finish in either order. +c.generateKeyPair("ec", encoding("des-ede3-cbc", "sec1"), (err, publicKey, privateKey) => { + console.log("keygen async", err, head(privateKey)); + console.log("keygen async key", c.createPublicKey({ key: privateKey, passphrase: "gen" }).equals(c.createPublicKey(publicKey))); + c.generateKeyPair("ec", encoding("aes-128-ccm"), (err) => console.log("keygen async ccm", show(err))); +}); diff --git a/tests/harness/corpus-costs/8165-rsa-keygen-lengths.cjs.json b/tests/harness/corpus-costs/8165-rsa-keygen-lengths.cjs.json new file mode 100644 index 000000000..bbf64dca5 --- /dev/null +++ b/tests/harness/corpus-costs/8165-rsa-keygen-lengths.cjs.json @@ -0,0 +1 @@ +{ "plain": 11640, "san": 77506 } diff --git a/tests/harness/corpus-costs/8166-rsa-pss-keys.cjs.json b/tests/harness/corpus-costs/8166-rsa-pss-keys.cjs.json new file mode 100644 index 000000000..436e9ed51 --- /dev/null +++ b/tests/harness/corpus-costs/8166-rsa-pss-keys.cjs.json @@ -0,0 +1 @@ +{ "plain": 11459, "san": 74658 } diff --git a/tests/harness/corpus-costs/8167-encrypted-private-keys.cjs.json b/tests/harness/corpus-costs/8167-encrypted-private-keys.cjs.json new file mode 100644 index 000000000..0f258ddbb --- /dev/null +++ b/tests/harness/corpus-costs/8167-encrypted-private-keys.cjs.json @@ -0,0 +1 @@ +{ "plain": 3144, "san": 8504 } From f2e710d65f4233424be8a0c01db56520b440828b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 09:05:16 -0500 Subject: [PATCH 057/273] Answer stream, StringDecoder and handle-object members in untyped code as Node does --- packages/compiler/src/backend/llvm/dyn.ts | 45 +++--- .../compiler/src/backend/llvm/expr-dynamic.ts | 2 +- .../src/backend/llvm/expr-stream-bridges.ts | 5 +- .../frontend/lowering/expressions/typeof.ts | 7 +- .../src/frontend/lowering/f64-handles.ts | 15 ++ packages/compiler/src/ir/ir.ts | 26 +++- packages/compiler/src/ir/runtime-features.ts | 7 +- .../8232-native-streams-untyped.cjs.json | 6 + .../8233-string-decoder-values.cjs.json | 6 + .../tests/corpus/8234-handle-typeof.ts.json | 6 + packages/runtime/src/scr_bytes.h | 6 + packages/runtime/src/scr_dyn.h | 1 + packages/runtime/src/scr_stream.c | 96 ++++++++++++- packages/runtime/src/scr_text_codec_dyn.c | 128 +++++++++++++++++- tests/corpus/8232-native-streams-untyped.cjs | 56 ++++++++ tests/corpus/8233-string-decoder-values.cjs | 33 +++++ tests/corpus/8234-handle-typeof.ts | 24 ++++ .../8232-native-streams-untyped.cjs.json | 1 + .../8233-string-decoder-values.cjs.json | 1 + .../corpus-costs/8234-handle-typeof.ts.json | 1 + 20 files changed, 437 insertions(+), 35 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/f64-handles.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8232-native-streams-untyped.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8233-string-decoder-values.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8234-handle-typeof.ts.json create mode 100644 tests/corpus/8232-native-streams-untyped.cjs create mode 100644 tests/corpus/8233-string-decoder-values.cjs create mode 100644 tests/corpus/8234-handle-typeof.ts create mode 100644 tests/harness/corpus-costs/8232-native-streams-untyped.cjs.json create mode 100644 tests/harness/corpus-costs/8233-string-decoder-values.cjs.json create mode 100644 tests/harness/corpus-costs/8234-handle-typeof.ts.json diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index da31ffdbb..c7b80b71d 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -43,7 +43,7 @@ import { isDynTypedRefType, isRefCounted, isUnitType, - recordTextCodecClass, + recordHandleCodecClass, textCodecRecordClass, typeKey, } from "../../ir/ir.js"; @@ -98,14 +98,16 @@ export const DYN_HANDLE_TAG_NUM: Record = { /** The handle tags of TextEncoder and TextDecoder records boxed as dyn * (scr_dyn.h; scr_text_codec_dyn.c boxes and unboxes them). */ -export const TEXT_CODEC_TAG_NUM: Record<"TextEncoder" | "TextDecoder", number> = { +export const TEXT_CODEC_TAG_NUM: Record<"TextEncoder" | "TextDecoder" | "StringDecoder", number> = { TextDecoder: 76, TextEncoder: 77, + StringDecoder: 78, }; /** The decoder record fields scr_text_decoder_to_dyn reads, in its * parameter order. */ const TEXT_DECODER_FIELDS = ["%TextDecoder", "%fatal", "%ignoreBOM", "%state"] as const; +const STRING_DECODER_FIELDS = ["%enc", "%pending"] as const; /** Handle kinds whose runtime unit boxes them through its own function, * which installs the tag's ops on first use (scr_crypto.h). Programs that @@ -1825,11 +1827,18 @@ export class LlDyn { const codec = textCodecRecordClass(t, (id) => host.recordsById.get(id)); if (codec !== null) { // The record the codec's handle box holds (+1), or the path TypeError. - host.declare(`declare ptr @scr_text_codec_from_dyn(ptr, i1 zeroext, ptr, ptr)`); const checked = B.tmp(); - B.line( - `${checked} = call ptr @scr_text_codec_from_dyn(ptr %d, i1 ${codec === "TextDecoder"}, ptr %path, ptr ${want})`, - ); + if (codec === "StringDecoder") { + host.declare(`declare ptr @scr_string_decoder_from_dyn(ptr, ptr, ptr)`); + B.line( + `${checked} = call ptr @scr_string_decoder_from_dyn(ptr %d, ptr %path, ptr ${want})`, + ); + } else { + host.declare(`declare ptr @scr_text_codec_from_dyn(ptr, i1 zeroext, ptr, ptr)`); + B.line( + `${checked} = call ptr @scr_text_codec_from_dyn(ptr %d, i1 ${codec === "TextDecoder"}, ptr %path, ptr ${want})`, + ); + } B.terminate(`ret ptr ${checked}`); this.defs.push( `define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} {`, @@ -2917,14 +2926,14 @@ export class LlDyn { return matched; } - /** A TextEncoder or TextDecoder record boxes as its handle (+1), which - * holds the record by reference. A decoder passes the byte offsets of the - * fields its boxed decode reads. */ + /** A TextEncoder, TextDecoder or StringDecoder record boxes as its + * handle (+1), which holds the record by reference. The decoders pass + * the byte offsets of the fields their boxed methods read. */ private textCodecToDyn( B: BlockBuilder, t: IrType & { kind: "record" }, shape: IrRecordShape, - codec: "TextEncoder" | "TextDecoder", + codec: "TextEncoder" | "TextDecoder" | "StringDecoder", ): void { const host = this.host; const rc = vAdapters(host, t); @@ -2939,10 +2948,12 @@ export class LlDyn { const base = host.nullableUnions.fieldBase(t.shapeId); const start = B.tmp(); B.line(`${start} = ptrtoint ptr %v to ${this.S}`); - const offsets = TEXT_DECODER_FIELDS.map((field) => { + const fields: readonly string[] = + codec === "StringDecoder" ? STRING_DECODER_FIELDS : TEXT_DECODER_FIELDS; + const offsets = fields.map((field) => { const index = shape.fields.findIndex((f) => f.name === field); if (index < 0) - throw new InternalCompilerError(`llvm emitter bug: TextDecoder record lacks ${field}`); + throw new InternalCompilerError(`llvm emitter bug: ${codec} record lacks ${field}`); const p = B.tmp(); const at = B.tmp(); const offset = B.tmp(); @@ -2953,11 +2964,11 @@ export class LlDyn { B.line(`${offset} = sub ${this.S} ${at}, ${start}`); return `${this.S} ${offset}`; }); - host.declare( - `declare ptr @scr_text_decoder_to_dyn(ptr, ptr, ptr, ${this.S}, ${this.S}, ${this.S}, ${this.S})`, - ); + const fn = + codec === "StringDecoder" ? "scr_string_decoder_to_dyn" : "scr_text_decoder_to_dyn"; + host.declare(`declare ptr @${fn}(ptr, ptr, ptr, ${fields.map(() => this.S).join(", ")})`); B.line( - `${r} = call ptr @scr_text_decoder_to_dyn(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ${offsets.join(", ")})`, + `${r} = call ptr @${fn}(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ${offsets.join(", ")})`, ); } B.terminate(`ret ptr ${r}`); @@ -3137,7 +3148,7 @@ export class LlDyn { const shape = host.recordsById.get(t.shapeId); if (!shape) throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`); - const codec = recordTextCodecClass(shape); + const codec = recordHandleCodecClass(shape); if (codec !== null) { this.textCodecToDyn(B, t, shape, codec); break; diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index b7a5c7736..3d9aec3cc 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -89,7 +89,7 @@ export function emitDynamicExpr( // Bytes already box their shared mutable storage directly. A second // capsule would split identity between checked and native views. const errorRef = v.type.kind === "object" && host.shapeHost.isErrorClass(v.type.className); - // A TextEncoder or TextDecoder record boxes as its own handle, which + // A TextEncoder, TextDecoder or StringDecoder record boxes as its own handle, which // keeps the record's identity (toDynHelper). const codecRef = textCodecRecordClass(v.type, (id) => host.recordsById.get(id)) !== null; if (!errorRef && !codecRef && ((e.liveRef && v.type.kind !== "bytes") || identityRef)) { diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 675932d47..28f5ae5bc 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -1,7 +1,7 @@ import { typedRefConstructor } from "./shapes.js"; /* Focused LLVM expression emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; -import { streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js"; +import { streamRooted, streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js"; import { type IrRecordShape, type IrType, @@ -843,8 +843,7 @@ export function streamTypedRefMaterializeAdapter( host.declare(`declare ptr @scr_error_dyn_fields(ptr)`); B.line(`${out} = call ptr @scr_error_dyn_fields(ptr %p)`); } else if ( - meta.def.runtime && - RUNTIME_STREAM_CLASSES.has(t.className) && + ((meta.def.runtime && RUNTIME_STREAM_CLASSES.has(t.className)) || streamRooted(meta)) && !meta.def.instancePrototypeHelper && !meta.def.prototypeDataHelper ) { diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts index fbbc068b9..ba1aba68e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts +++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts @@ -35,6 +35,7 @@ import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors import { InternalCompilerError } from "../../../errors.js"; import { TEMPORAL_GLOBAL } from "../../builtin-modules.js"; import { isTemporalGlobalRoot } from "../lower-namespaces.js"; +import { isF64HandleTyped } from "../f64-handles.js"; export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc: SrcLoc): IrExpr { const webAssembly = lowerWebAssemblyTypeof(lowerer, expr.expression); @@ -287,7 +288,11 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc undefinedT: "undefined", nullT: "object", }; - const folded = FOLD[operand.type.kind]; + // An f64 handle (a readline Interface, a Timeout) is an object. + const folded = + operand.type.kind === "f64" && isF64HandleTyped(lowerer, expr.expression) + ? "object" + : FOLD[operand.type.kind]; if (folded) { const result: IrExpr = { kind: "strLit", value: folded, type: STRING, loc }; return isSafeToDiscard(operand) diff --git a/packages/compiler/src/frontend/lowering/f64-handles.ts b/packages/compiler/src/frontend/lowering/f64-handles.ts new file mode 100644 index 000000000..0d7da0ff9 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/f64-handles.ts @@ -0,0 +1,15 @@ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; + +/** Whether `node`'s value is an f64 handle: an object whose class + * type-mapper.ts represents as an f64 id into a runtime registry (a + * readline Interface, a Timeout or Immediate, an AsyncLocalStorage, a + * diagnostics_channel Channel). Its checker type is an object type that + * maps to f64; the Number wrapper interface maps to f64 for primitives. */ +export function isF64HandleTyped(lowerer: Lowerer, node: ts.Node): boolean { + const type = lowerer.checker.getTypeAtLocation(node); + if ((type.flags & ts.TypeFlags.Object) === 0) return false; + const symbol = type.getSymbol(); + if (symbol !== undefined && ts.symbolName(symbol) === "Number") return false; + return lowerer.mapTypeOf(type)?.kind === "f64"; +} diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 3818433f8..c6dcb6e08 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1781,16 +1781,32 @@ export function recordTextCodecClass(shape: IrRecordShape): "TextEncoder" | "Tex return null; } -/** The codec class of a TextEncoder or TextDecoder record type, else null. - * Such a record crosses an `unknown` slot as a native handle that holds the - * record itself (scr_text_codec_dyn.c), never as a copy or a field view. */ +/** The class of a record that crosses checked-dynamic slots as a native + * handle: the codecs, and a StringDecoder (type-mapper.ts: its canonical + * encoding and the packing of its pending bytes). */ +export function recordHandleCodecClass( + shape: IrRecordShape, +): "TextEncoder" | "TextDecoder" | "StringDecoder" | null { + const codec = recordTextCodecClass(shape); + if (codec !== null) return codec; + return shape.fields.length === 2 && + shape.fields[0]!.name === "%enc" && + shape.fields[1]!.name === "%pending" + ? "StringDecoder" + : null; +} + +/** The codec class of a TextEncoder, TextDecoder or StringDecoder record + * type, else null. Such a record crosses an `unknown` slot as a native + * handle that holds the record itself (scr_text_codec_dyn.c), never as a + * copy or a field view. */ export function textCodecRecordClass( t: IrType, getRecord: (shapeId: string) => IrRecordShape | undefined, -): "TextEncoder" | "TextDecoder" | null { +): "TextEncoder" | "TextDecoder" | "StringDecoder" | null { if (t.kind !== "record") return null; const shape = getRecord(t.shapeId); - return shape ? recordTextCodecClass(shape) : null; + return shape ? recordHandleCodecClass(shape) : null; } /** Object-literal ACCESSOR properties (`{ get x() {...}, set x(v) {...} }`) diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 85b21af7b..917406d6c 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -2,6 +2,7 @@ import type { IrClassDef, IrExpr, IrModule, IrRecordShape, IrType, IrUnionDef } import type { IrLibFn } from "./builtin-signatures.js"; import { isDirentShape, + recordHandleCodecClass, recordTextCodecClass, RUNTIME_EMITTER_CLASS, RUNTIME_EVENT_CLASS, @@ -586,7 +587,7 @@ function mentionsDgramSocket(t: IrType, tables: TypeTables): boolean { const textCodecLeaf: CarrierLeaf = (t, tables) => { if (t.kind !== "record") return false; const shape = tables.records.get(t.shapeId); - return shape !== undefined && recordTextCodecClass(shape) !== null; + return shape !== undefined && recordHandleCodecClass(shape) !== null; }; const textDecoderLeaf: CarrierLeaf = (t, tables) => { @@ -596,7 +597,7 @@ const textDecoderLeaf: CarrierLeaf = (t, tables) => { }; /** Whether converting a value of type `t` to or from dyn converts a - * TextEncoder or TextDecoder record, which boxes it as a handle of + * TextEncoder, TextDecoder or StringDecoder record, which boxes it as a handle of * scr_text_codec_dyn.c (the same carrier walk as dgram sockets). */ function mentionsTextCodec(t: IrType, tables: TypeTables): boolean { tables.textCodecCarriers ??= leafCarriers(tables, textCodecLeaf); @@ -858,7 +859,7 @@ const RUNTIME_FEATURE_RULES = { (node.kind === "dynFrom" && mentionsDgramSocket(node.value.type, tables())) || (node.kind === "dynCheck" && mentionsDgramSocket(node.type, tables())), }), - /** A TextEncoder or TextDecoder record converts to or from an untyped + /** A TextEncoder, TextDecoder or StringDecoder record converts to or from an untyped * value (its handle box, or a boxed function, record or collection that * carries one): scr_text_codec_dyn.c. */ textCodecDyn: rule({ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8232-native-streams-untyped.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8232-native-streams-untyped.cjs.json new file mode 100644 index 000000000..2b07cde52 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8232-native-streams-untyped.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8232-native-streams-untyped.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8233-string-decoder-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8233-string-decoder-values.cjs.json new file mode 100644 index 000000000..a734fb48c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8233-string-decoder-values.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8233-string-decoder-values.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8234-handle-typeof.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8234-handle-typeof.ts.json new file mode 100644 index 000000000..7f37dbc5d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8234-handle-typeof.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8234-handle-typeof.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 7c0a935cc..c7ea68988 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -369,6 +369,12 @@ ScrDyn *scr_text_decoder_to_dyn(void *rec, void *(*retain)(void *), void (*relea void *scr_text_codec_from_dyn(const ScrDyn *d, bool decoder, const struct ScrDynPath *path, const char *want); bool scr_text_codec_is(const ScrDyn *d, bool decoder); +/* StringDecoder records as checked-dynamic handles (the same unit): the + * byte offsets of the encoding (string) and pending (f64) fields. */ +ScrDyn *scr_string_decoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *), + size_t encoding, size_t pending); +void *scr_string_decoder_from_dyn(const ScrDyn *d, const struct ScrDynPath *path, + const char *want); bool scr_buffer_is_ascii(const ScrDyn *input); bool scr_buffer_is_utf8(const ScrDyn *input); ScrBytes *scr_buffer_transcode(const ScrDyn *source, const ScrDyn *from, const ScrDyn *to); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 82fe1a6d9..1c7a9a0e0 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -200,6 +200,7 @@ typedef enum { X(SHARED_ARRAY_BUFFER, 37) \ X(SIGN, 67) /* node:crypto Sign (scr_pki.c) */ \ X(STDIO, 27) /* stable process stdin/stdout/stderr values */ \ + X(STRING_DECODER, 78) /* compiled StringDecoder record (scr_text_codec_dyn.c) */ \ X(TEXT_DECODER, 76) /* compiled TextDecoder record (scr_text_codec_dyn.c) */ \ X(TEXT_ENCODER, 77) /* compiled TextEncoder record (scr_text_codec_dyn.c) */ \ X(URL, 28) /* native WHATWG URL */ \ diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index ec061e10c..33382363b 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -6634,6 +6634,12 @@ ScrStream *scr_stream_on_dyn(ScrStream *s, ScrStr *name, ScrDyn *cb, bool once, return (ScrStream *)scr_emitter_on_dyn((ScrEmitter *)s, name, cb, adapter, error ? scr_ee_inv_fixed1 : scr_ee_inv_fixed0, once, prepend); } + /* The stream emits 'pipe' and 'unpipe' with its source stream; other + * names reach the listener only through emit calls, with every argument + * they pass. */ + bool piping = (name->len == 4 && memcmp(name->data, "pipe", 4) == 0) || + (name->len == 6 && memcmp(name->data, "unpipe", 6) == 0); + if (!piping) return (ScrStream *)scr_emitter_on_flex((ScrEmitter *)s, name, cb, once, prepend); static const char message[] = "this stream event has no checked-dynamic listener adapter"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return NULL; @@ -8058,10 +8064,68 @@ static ScrStream *scr_stream_view_receiver(const ScrDyn *value) { /* 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", + "off", "removeListener", "removeAllListeners", "listenerCount", "eventNames", "pipe", + "unpipe", "read", "push", "unshift", "pause", "resume", "isPaused", "setEncoding", "write", + "end", "destroy", "cork", "uncork", "setDefaultEncoding", }; +/* An event name argument: a string, +1; other values throw the "not + * supported yet" Error. */ +static ScrStr *scr_stream_dyn_event_name(const ScrDyn *value) { + if (value->kind == SCR_DYN_STR) return scr_str_retain(value->v.str); + static const char message[] = "event names other than strings on a native stream are not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; +} + +/* push(chunk, encoding): a string with an encoding pushes the string's + * bytes in that encoding, undefined pushes an empty chunk outside object + * mode, and other chunks push as push(chunk) does. */ +static ScrDyn *scr_stream_dyn_push(ScrStream *stream, ScrDyn *const *args, size_t argc) { + ScrDyn *chunk = argc ? args[0] : scr_dyn_undefined(); + ScrDyn *encoding = argc > 1 ? args[1] : scr_dyn_undefined(); + if (chunk->kind == SCR_DYN_STR && encoding->kind == SCR_DYN_STR) { + ScrStr *canonical = scr_bytes_normalize_encoding(encoding->v.str); + if (!canonical) return NULL; + bool more = scr_stream_push_str_enc(stream, chunk->v.str, canonical); + scr_str_release(canonical); + return scr_dyn_new_bool(more); + } + if (chunk->kind == SCR_DYN_UNDEF && !(stream->st->has_r && stream->st->r.object_entries)) { + ScrStr *empty = scr_str_new("", 0); + bool more = scr_stream_push_str(stream, empty); + scr_str_release(empty); + return scr_dyn_new_bool(more); + } + bool more = scr_stream_push_dyn(stream, chunk); + return scr_exc_pending() ? NULL : scr_dyn_new_bool(more); +} + +/* unshift(chunk): whether the stream takes more data afterwards, Node's + * canPushMore. */ +static ScrDyn *scr_stream_dyn_unshift(ScrStream *stream, const ScrDyn *chunk) { + scr_stream_unshift_dyn(stream, chunk); + if (scr_exc_pending()) return NULL; + ScrStreamState *st = stream->st; + bool more = chunk->kind != SCR_DYN_NULL && st->has_r && !st->destroyed && !st->errored && + !st->r.ended && (st->r.length < st->r.hwm || st->r.length == 0); + return scr_dyn_new_bool(more); +} + +/* eventNames(): the stream's event names as a string array, +1. */ +static ScrDyn *scr_stream_dyn_event_names(ScrStream *stream) { + ScrArr *names = scr_emitter_event_names((ScrEmitter *)stream); + ScrDyn *result = scr_dyn_new_arr(); + double length = scr_arr_len(names); + for (double i = 0; i < length; i++) { + ScrStr *name = scr_arr_get_ref(names, i); + scr_dyn_arr_push(result, scr_dyn_new_str(name)); + scr_str_release(name); + } + scr_arr_release(names); + return result; +} + /* 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. */ @@ -8086,6 +8150,29 @@ static ScrDyn *scr_stream_dyn_call(ScrStream *stream, ScrDyn *self, const char * argc > 1 ? args[1] : scr_dyn_undefined()); scr_emitter_release(registered); } + } else if (!strcmp(method, "removeAllListeners")) { + bool all = first->kind == SCR_DYN_UNDEF; + ScrStr *name = all ? scr_str_new("", 0) : scr_stream_dyn_event_name(first); + if (name) { + scr_emitter_release(scr_emitter_remove_all((ScrEmitter *)stream, name, all)); + scr_str_release(name); + } + } else if (!strcmp(method, "listenerCount")) { + ScrStr *name = scr_stream_dyn_event_name(first); + if (name) { + ScrDyn *only = argc > 1 ? args[1] : scr_dyn_undefined(); + double count = only->kind == SCR_DYN_UNDEF || only->kind == SCR_DYN_NULL + ? scr_emitter_listener_count((ScrEmitter *)stream, name) + : only->kind == SCR_DYN_FUNC ? scr_emitter_listener_count_dyn((ScrEmitter *)stream, name, only) : 0; + scr_str_release(name); + if (!scr_exc_pending()) result = scr_dyn_new_num(count); + } + } else if (!strcmp(method, "eventNames")) result = scr_stream_dyn_event_names(stream); + else if (!strcmp(method, "push")) result = scr_stream_dyn_push(stream, args, argc); + else if (!strcmp(method, "unshift")) result = scr_stream_dyn_unshift(stream, first); + else if (!strcmp(method, "setDefaultEncoding")) { + ScrStream *returned = scr_stream_set_default_encoding(stream, first); + if (returned) scr_stream_release(returned); } else if (!strcmp(method, "pipe") || !strcmp(method, "unpipe")) { ScrStream *destination = first->kind == SCR_DYN_UNDEF && !strcmp(method, "unpipe") ? NULL : scr_stream_view_receiver(first); @@ -8220,6 +8307,11 @@ ScrDyn *scr_stream_dyn_view(ScrStream *stream) { 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->has_r) { + int flowing = stream->st->r.flowing; + scr_dyn_obj_set(view, "readableFlowing", 15, + flowing < 0 ? scr_dyn_new_null() : scr_dyn_new_bool(flowing == 1)); + } if (stream->st->native && stream->st->native_ops && stream->st->native_ops->view) { 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); diff --git a/packages/runtime/src/scr_text_codec_dyn.c b/packages/runtime/src/scr_text_codec_dyn.c index 5102bc558..37fdfb27b 100644 --- a/packages/runtime/src/scr_text_codec_dyn.c +++ b/packages/runtime/src/scr_text_codec_dyn.c @@ -1,9 +1,10 @@ -/* WHATWG TextEncoder and TextDecoder instances as checked-dynamic values - * (SCR_DYNH_TEXT_ENCODER, SCR_DYNH_TEXT_DECODER). A codec is a compiled +/* WHATWG TextEncoder and TextDecoder instances, and node:string_decoder + * StringDecoder instances, as checked-dynamic values (SCR_DYNH_TEXT_ENCODER, + * SCR_DYNH_TEXT_DECODER, SCR_DYNH_STRING_DECODER). A codec is a compiled * record (type-mapper.ts): the handle holds that record by reference, so * a codec stored in an untyped slot and read back keeps its identity and * a decoder keeps its streaming state. The compiler passes the record's - * reference-count entry points and, for decoders, the byte offsets of the + * reference-count entry points and, for a TextDecoder, the byte offsets of the * encoding, fatal, ignoreBOM and state fields when it boxes one. A unit * of its own so programs that never box a codec link none of it * (native-toolchain gating: the textCodecDyn runtime feature). */ @@ -213,3 +214,124 @@ bool scr_text_codec_is(const ScrDyn *d, bool decoder) { return d->kind == SCR_DYN_HANDLE && d->v.handle.tag == (decoder ? SCR_DYNH_TEXT_DECODER : SCR_DYNH_TEXT_ENCODER); } + +/* node:string_decoder StringDecoder instances (SCR_DYNH_STRING_DECODER). + * The decoder is a compiled record of its canonical encoding (a string) + * and its pending partial sequence (an f64 packing, scr_bytes.c's + * scr_strdec_* helpers); the compiler passes both fields' byte offsets + * when it boxes one. */ +typedef struct { + void *(*retain)(void *); + void (*release)(void *); + size_t encoding, pending; +} ScrStringDecoderLayout; + +static SCR_TL ScrStringDecoderLayout scr_sdd_layout; + +static void *scr_sdd_retain(void *rec) { return scr_sdd_layout.retain(rec); } +static void scr_sdd_release(void *rec) { scr_sdd_layout.release(rec); } + +static ScrStr *scr_sdd_encoding(const void *rec) { + ScrStr *encoding; + memcpy(&encoding, (const char *)rec + scr_sdd_layout.encoding, sizeof encoding); + return encoding; +} + +static double scr_sdd_pending(const void *rec) { + double pending; + memcpy(&pending, (const char *)rec + scr_sdd_layout.pending, sizeof pending); + return pending; +} + +static void scr_sdd_set_pending(void *rec, double pending) { + memcpy((char *)rec + scr_sdd_layout.pending, &pending, sizeof pending); +} + +/* write(buf): a string answers itself; an ArrayBuffer view decodes with + * the pending bytes before it; other values throw Node's + * ERR_INVALID_ARG_TYPE. +1 or NULL. */ +static ScrStr *scr_sdd_write(void *rec, const ScrDyn *buf) { + if (buf->kind == SCR_DYN_STR) return scr_str_retain(buf->v.str); + if (buf->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("buf", "an instance of Buffer, TypedArray, or DataView", buf); + return NULL; + } + ScrBytes *bytes = scr_bytes_raw_view(buf->v.bytes); + ScrStr *encoding = scr_sdd_encoding(rec); + double pending = scr_sdd_pending(rec); + ScrStr *text = scr_strdec_write(encoding, pending, bytes); + scr_sdd_set_pending(rec, scr_strdec_next(encoding, pending, bytes)); + scr_bytes_release(bytes); + return text; +} + +static ScrDyn *scr_sdd_invoke(void *rec, ScrDyn *self, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + (void)self; + ScrDyn *buf = scr_tcd_codec_arg(args, argc, 0); + if (strcmp(method, "write") == 0) { + ScrStr *text = scr_sdd_write(rec, buf); + return text ? scr_tcd_str(text) : NULL; + } + if (strcmp(method, "end") == 0) { + /* end(buf): write(buf) when buf is not undefined, then the pending + * bytes' replacement text. */ + ScrStr *head = buf->kind == SCR_DYN_UNDEF ? scr_str_new("", 0) : scr_sdd_write(rec, buf); + if (!head) return NULL; + ScrStr *tail = scr_strdec_end(scr_sdd_encoding(rec), scr_sdd_pending(rec)); + scr_sdd_set_pending(rec, 0); + ScrStr *text = scr_str_concat(head, tail); + scr_str_release(head); + scr_str_release(tail); + return scr_tcd_str(text); + } + if (strcmp(method, "text") == 0) { + static const char message[] = "StringDecoder.prototype.text is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; + } + scr_tcd_codec_not_a_function(what); + return NULL; +} + +static ScrDyn *scr_sdd_get(void *rec, const char *key, size_t len) { + if (len == 8 && memcmp(key, "encoding", 8) == 0) + return scr_dyn_new_str(scr_sdd_encoding(rec)); + if ((len == 8 && memcmp(key, "lastChar", 8) == 0) || + (len == 8 && memcmp(key, "lastNeed", 8) == 0) || + (len == 9 && memcmp(key, "lastTotal", 9) == 0)) { + static const char message[] = "StringDecoder lastChar, lastNeed and lastTotal are not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + } + return NULL; +} + +static const ScrDynHandleMethod scr_sdd_methods[] = { + {.name = "write", .length = 1}, + {.name = "end", .length = 1}, + {.name = "text", .length = 2}, + {.name = NULL}, +}; + +static const ScrDynHandleOps scr_sdd_ops = { + .cls = "StringDecoder", + .retain = scr_sdd_retain, + .release = scr_sdd_release, + .invoke = scr_sdd_invoke, + .get = scr_sdd_get, + .set = scr_tcd_codec_set, + .methods = scr_sdd_methods, +}; + +ScrDyn *scr_string_decoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *), + size_t encoding, size_t pending) { + if (!scr_sdd_layout.retain) { + scr_sdd_layout = (ScrStringDecoderLayout){retain, release, encoding, pending}; + scr_dyn_handle_install(SCR_DYNH_STRING_DECODER, &scr_sdd_ops); + } + return scr_dyn_new_handle(rec, SCR_DYNH_STRING_DECODER); +} + +void *scr_string_decoder_from_dyn(const ScrDyn *d, const ScrDynPath *path, const char *want) { + return scr_dyn_handle_unbox(d, SCR_DYNH_STRING_DECODER, path, want); +} diff --git a/tests/corpus/8232-native-streams-untyped.cjs b/tests/corpus/8232-native-streams-untyped.cjs new file mode 100644 index 000000000..b70017b5a --- /dev/null +++ b/tests/corpus/8232-native-streams-untyped.cjs @@ -0,0 +1,56 @@ +// Native streams passed to untyped helpers: push, unshift, the listener +// members, setDefaultEncoding and readableFlowing answer through the +// untyped value as on the typed one, also for a program subclass and in +// object mode. +const { Readable, Writable } = require("stream"); + +function feed(s) { + console.log("push", s.push("a"), s.push("b", "utf8"), s.unshift("z")); + console.log("counts", s.listenerCount("data"), s.eventNames().join(",")); + s.on("custom", (x, y) => { + console.log("custom", x, y); + }); + s.emit("custom", 1, 2); + s.removeAllListeners("custom"); + console.log("after removeAll", s.listenerCount("custom"), s.readableFlowing); + s.push(null); +} + +const r = new Readable({ read() {} }); +const chunks = []; +r.on("data", (c) => chunks.push(String(c))); +r.on("end", () => console.log("end", chunks.join("|"))); +feed(r); + +class Source extends Readable { + _read() {} +} +const sub = new Source(); +sub.on("data", (c) => console.log("source data", String(c))); +console.log("source push", sub instanceof Readable); +(function (s) { + console.log(s.push("from untyped"), s.readableLength); + s.push(null); +})(sub); + +const objects = new Readable({ objectMode: true, read() {} }); +console.log("flowing before", objects.readableFlowing); +objects.on("data", (c) => console.log("object data", c)); +(function (s) { + console.log(s.push({ n: 1 }), s.push(undefined), s.push(2), s.readableFlowing); + s.push(null); +})(objects); + +function drain(w) { + w.setDefaultEncoding("latin1"); + w.write("é"); + w.end(); +} +const w = new Writable({ + write(c, e, cb) { + console.log("wrote", c.length, e); + cb(); + }, +}); +w.on("finish", () => console.log("finish")); +drain(w); diff --git a/tests/corpus/8233-string-decoder-values.cjs b/tests/corpus/8233-string-decoder-values.cjs new file mode 100644 index 000000000..475a31d57 --- /dev/null +++ b/tests/corpus/8233-string-decoder-values.cjs @@ -0,0 +1,33 @@ +// A StringDecoder passed to untyped code keeps its identity and its +// pending bytes, and answers write, end and encoding as in Node. +const { StringDecoder } = require("string_decoder"); + +function feed(decoder, chunks) { + let out = ""; + for (const chunk of chunks) out += decoder.write(chunk); + return out + decoder.end(); +} + +const euro = Buffer.from("€ and 😀"); +const utf8 = new StringDecoder("utf-8"); +const parts = []; +for (let i = 0; i < euro.length; i++) parts.push(euro.subarray(i, i + 1)); +console.log(feed(utf8, parts), utf8.encoding); + +const hex = new StringDecoder("hex"); +console.log(feed(hex, [Buffer.from([1, 2]), Buffer.from([255])]), hex.encoding); + +const utf16 = new StringDecoder("ucs2"); +const pending = [Buffer.from([0x61]), Buffer.from([0x00, 0x62]), Buffer.from([0x00])]; +console.log(JSON.stringify(feed(utf16, pending)), utf16.encoding); + +function passThrough(d) { + return d; +} +const kept = passThrough(utf8); +console.log(kept === utf8, typeof kept, kept.write("text"), kept.end(Buffer.from([0xf0, 0x9f]))); +try { + kept.write(null); +} catch (e) { + console.log(e.code, e.message); +} diff --git a/tests/corpus/8234-handle-typeof.ts b/tests/corpus/8234-handle-typeof.ts new file mode 100644 index 000000000..f18240130 --- /dev/null +++ b/tests/corpus/8234-handle-typeof.ts @@ -0,0 +1,24 @@ +// typeof of the objects the runtime keeps as numeric handles answers +// "object", as Node's does: readline interfaces, timers, async-local +// stores, diagnostics channels and event-loop histograms. +import * as readline from "node:readline"; +import { PassThrough } from "node:stream"; +import { AsyncLocalStorage } from "node:async_hooks"; +import * as dc from "node:diagnostics_channel"; +import { monitorEventLoopDelay } from "node:perf_hooks"; + +const rl = readline.createInterface({ input: new PassThrough(), terminal: false }); +const timeout = setTimeout(() => {}, 1); +const interval = setInterval(() => {}, 1000); +const immediate = setImmediate(() => {}); +const store = new AsyncLocalStorage(); +const channel = dc.channel("handles"); +const tracing = dc.tracingChannel("handles"); +const histogram = monitorEventLoopDelay(); +const wrapped: Number = 5; +console.log(typeof rl, typeof timeout, typeof interval, typeof immediate); +console.log(typeof store, typeof channel, typeof tracing, typeof histogram, typeof wrapped); +clearTimeout(timeout); +clearInterval(interval); +clearImmediate(immediate); +rl.close(); diff --git a/tests/harness/corpus-costs/8232-native-streams-untyped.cjs.json b/tests/harness/corpus-costs/8232-native-streams-untyped.cjs.json new file mode 100644 index 000000000..bec4e42a8 --- /dev/null +++ b/tests/harness/corpus-costs/8232-native-streams-untyped.cjs.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 5000 } diff --git a/tests/harness/corpus-costs/8233-string-decoder-values.cjs.json b/tests/harness/corpus-costs/8233-string-decoder-values.cjs.json new file mode 100644 index 000000000..bb380e241 --- /dev/null +++ b/tests/harness/corpus-costs/8233-string-decoder-values.cjs.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3000 } diff --git a/tests/harness/corpus-costs/8234-handle-typeof.ts.json b/tests/harness/corpus-costs/8234-handle-typeof.ts.json new file mode 100644 index 000000000..4c7ad07f1 --- /dev/null +++ b/tests/harness/corpus-costs/8234-handle-typeof.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 5500 } From 48cbf3a89eef85d456b06a24df7d83ee8099d98a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 09:20:05 -0500 Subject: [PATCH 058/273] Verify runtime pack artifacts once per file identity, and store pack selections Every native exe build that the executable cache does not restore hashed each selected runtime artifact before staging it, and every exe build parsed and validated the whole runtime-pack.json manifest to select its artifacts. - Staging still copies each artifact. It skips the SHA-256 of an artifact when the file's identity (device, inode, size, mtime, ctime) is the one recorded the last time that path matched the same size and digest, and the identity does not change while the file is read. Other artifacts are hashed as before, and their identities are recorded after they match. - A pack's selection for a target, compiler version, feature set, flavor and mode is stored, keyed by the file identities of package.json and runtime-pack.json. A stored selection skips parsing and validating the manifest; the manifest is parsed only when a caller needs it, through runtimePackManifest. - The native compiler keeps both in its cache, in the runtime-pack family, which prune covers. --- .../src/backend/runtime-pack-native.test.ts | 155 ++++++++++- .../src/backend/runtime-pack-native.ts | 255 ++++++++++++++++-- packages/compiler/src/native/cache.ts | 1 + packages/compiler/src/native/compiler.ts | 32 ++- .../src/native/executable-cache-open.test.ts | 2 +- .../compiler/src/native/executable-cache.ts | 4 +- .../compiler/src/native/sanitizer.test.ts | 1 + packages/compiler/src/native/sanitizer.ts | 11 +- 8 files changed, 432 insertions(+), 29 deletions(-) diff --git a/packages/compiler/src/backend/runtime-pack-native.test.ts b/packages/compiler/src/backend/runtime-pack-native.test.ts index 20214818f..723deba39 100644 --- a/packages/compiler/src/backend/runtime-pack-native.test.ts +++ b/packages/compiler/src/backend/runtime-pack-native.test.ts @@ -1,12 +1,26 @@ +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync, renameSync, statSync, utimesSync, writeFileSync } from "node:fs"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { afterEach, expect, test } from "vitest"; +import { afterEach, expect, test, vi } from "vitest"; import { nativeCodegenDiag } from "../diagnostics/diagnostic.js"; +import { compilerReleaseVersion } from "../library/sidecar.js"; import type { NativeLinkFeatures } from "./native-link-info.js"; -import { RuntimePackError } from "./runtime-pack-core.js"; -import { selectNativeRuntimePack } from "./runtime-pack-native.js"; -import { WASM32_WASI_TARGET } from "./targets.js"; +import { RuntimePackError, type RuntimePackManifest } from "./runtime-pack-core.js"; +import { + runtimePackManifest, + selectNativeRuntimePack, + stageNativeRuntimeSelection, + type NativeRuntimeSelection, + type RuntimePackStore, +} from "./runtime-pack-native.js"; +import { MACOS_ARM64_TARGET, WASM32_WASI_TARGET } from "./targets.js"; + +vi.mock("node:crypto", async (original) => { + const crypto = await original(); + return { ...crypto, createHash: vi.fn(crypto.createHash) }; +}); const dirs: string[] = []; afterEach(async () => @@ -45,3 +59,136 @@ test("a missing native helper keeps the reinstall hint", () => { "reinstall scriptc with optional dependencies enabled for this host", ); }); + +/** A runtime pack with one runtime object and a license, and a store that + * keeps entries in memory. */ +async function packFixture() { + const root = await mkdtemp(join(tmpdir(), "scriptc-runtime-pack-store-")); + dirs.push(root); + const target = MACOS_ARM64_TARGET; + const version = compilerReleaseVersion(); + writeFileSync( + join(root, "package.json"), + JSON.stringify({ name: target.runtimePackPackage, version }), + ); + mkdirSync(join(root, "artifacts")); + const object = join(root, "artifacts", "base.o"); + writeFileSync(object, "base object"); + writeFileSync(join(root, "license.txt"), "license"); + const unit = { + source: "scr_base.c", + predicate: true, + variants: [ + { + id: "default", + when: {}, + defines: [], + path: "artifacts/base.o", + sha256: createHash("sha256").update("base object").digest("hex"), + size: Buffer.byteLength("base object"), + }, + ], + }; + const manifest: RuntimePackManifest = { + schema: "scriptc.runtime-pack.v1", + format: 1, + package: target.runtimePackPackage, + version, + target: { + name: target.name, + llvm_triple: target.llvmTriple, + architecture: target.architecture, + object_format: target.objectFormat, + minimum_os: target.minimumOs, + }, + runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + compiler: { command: "clang", identity: "fixture clang", target: target.llvmTriple }, + macros: { + executable: [], + excluded: ["SCR_LIB", "SCR_THREAD_INSTANCES", "SCR_RC_AUDIT"], + sanitizer: "external-toolchain-required", + }, + flavors: { + release: { optimization: "-O2", runtime_units: [unit] }, + dev: { optimization: "-O0", runtime_units: [unit] }, + }, + archives: [], + system_libraries: [{ name: "System", predicate: true }], + licenses: [{ path: "license.txt", license: "fixture" }], + }; + const manifestText = JSON.stringify(manifest); + writeFileSync(join(root, "runtime-pack.json"), manifestText); + const entries = new Map(); + const store: RuntimePackStore = { + read: (key) => entries.get(key) ?? null, + write: (key, text) => void entries.set(key, text), + }; + const select = () => + selectNativeRuntimePack( + root, + target, + version, + {} as NativeLinkFeatures, + "release", + "executable", + store, + ); + let stages = 0; + const stage = (selection: NativeRuntimeSelection) => { + const directory = join(root, `stage-${stages++}`); + mkdirSync(directory); + return stageNativeRuntimeSelection(selection, directory, store); + }; + return { root, object, manifestText, entries, select, stage }; +} + +test("a stored selection answers while the pack's files keep their identities", async () => { + const f = await packFixture(); + const first = f.select(); + expect(first.selected.systemLibraries).toEqual(["System"]); + expect(f.entries.size).toBe(1); + // The next selection reads the stored answer instead of the manifest. + const [key, text] = [...f.entries][0]!; + const stored = JSON.parse(text); + stored.selected.systemLibraries = ["Stored"]; + f.entries.set(key, JSON.stringify(stored)); + const second = f.select(); + expect(second.selected.systemLibraries).toEqual(["Stored"]); + expect(second.licenses).toEqual(first.licenses); + expect(second.manifest).toBeUndefined(); + expect(runtimePackManifest(second).flavors.release).toEqual(first.manifest!.flavors.release); + // Writing the manifest again, with the same text, gives it a new identity. + writeFileSync(join(f.root, "runtime-pack.json"), f.manifestText); + expect(f.select().selected.systemLibraries).toEqual(["System"]); +}); + +test.each(["replaced", "rewritten in place"])( + "staging hashes an artifact again when it is %s, even at the same size and mtime", + async (change) => { + const f = await packFixture(); + const selection = f.select(); + f.stage(selection); + const before = statSync(f.object); + const changed = change === "replaced" ? f.object + ".new" : f.object; + writeFileSync(changed, "x".repeat(before.size)); + utimesSync(changed, before.atime, before.mtime); + if (changed !== f.object) renameSync(changed, f.object); + expect(() => f.stage(selection)).toThrow( + "runtime pack artifact hash mismatch: artifacts/base.o", + ); + }, +); + +test("staging does not hash an unchanged artifact again", async () => { + const f = await packFixture(); + const selection = f.select(); + const hashes = () => vi.mocked(createHash).mock.calls.length; + let start = hashes(); + f.stage(selection); + const firstStage = hashes() - start; + start = hashes(); + const staged = f.stage(selection); + // Only the store key is hashed; the staged copy has the verified bytes. + expect(hashes() - start).toBe(firstStage - 1); + expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object"); +}); diff --git a/packages/compiler/src/backend/runtime-pack-native.ts b/packages/compiler/src/backend/runtime-pack-native.ts index bd38e2ef0..f51b24221 100644 --- a/packages/compiler/src/backend/runtime-pack-native.ts +++ b/packages/compiler/src/backend/runtime-pack-native.ts @@ -1,6 +1,8 @@ /** Synchronous runtime-pack host for the statically compiled driver. Each - * selected artifact is hashed and copied from the same read into private - * storage. This host has no persistent cache or mutable installed link inputs. */ + * selected artifact is verified and copied from the same read into private + * storage. With a store, a pack's selections and the identities of the + * artifacts it verified are kept between builds: an artifact whose file + * identity is unchanged since it was hashed is not hashed again. */ import type { NativeOptimization } from "./optimization.js"; import { createHash } from "node:crypto"; import { @@ -10,6 +12,7 @@ import { mkdirSync, openSync, readFileSync, + statSync, writeSync, } from "node:fs"; import { dirname, join } from "node:path"; @@ -34,13 +37,63 @@ export interface NativeRuntimePack { export interface NativeRuntimeSelection { root: string; - manifest: RuntimePackManifest; + /** The parsed manifest; a selection read from a store has none until + * runtimePackManifest parses it. */ + manifest?: RuntimePackManifest; + licenses: RuntimePackManifest["licenses"]; packageText: string; manifestText: string; selected: RuntimePackArtifacts; flavor: NativeOptimization; } +/** Text entries kept between builds (the native compiler's cache). */ +export interface RuntimePackStore { + read(key: string): string | null; + write(key: string, text: string): void; +} + +interface FileIdentity { + dev: number; + ino: number; + size: number; + mtime: number; + ctime: number; +} + +function fileIdentity(path: string): FileIdentity { + const info = statSync(path); + return { + dev: info.dev, + ino: info.ino, + size: info.size, + mtime: info.mtimeMs, + ctime: info.ctimeMs, + }; +} + +function sameIdentity(a: FileIdentity, b: FileIdentity): boolean { + return ( + a.dev === b.dev && + a.ino === b.ino && + a.size === b.size && + a.mtime === b.mtime && + a.ctime === b.ctime + ); +} + +function storeKey(text: string): string { + return createHash("sha256").update(text).digest("hex"); +} + +/** A stored selection: what selecting from the pack's files, with these + * identities, returned. */ +interface StoredSelection { + schema: 1; + licenses: RuntimePackManifest["licenses"]; + selected: RuntimePackArtifacts; +} + export function selectNativeRuntimePack( root: string, target: NativeTargetSpec, @@ -48,38 +101,197 @@ export function selectNativeRuntimePack( features: NativeLinkFeatures, flavor: NativeOptimization, mode: RuntimePackMode = "executable", + store?: RuntimePackStore, ): NativeRuntimeSelection { - if (!existsSync(join(root, "package.json"))) { + const packagePath = join(root, "package.json"); + const manifestPath = join(root, "runtime-pack.json"); + if (!existsSync(packagePath)) { throw new RuntimePackError( `runtime pack for ${target.name} is not installed; install ${target.runtimePackPackage}@${compilerVersion} in your project or set SCRIPTC_RUNTIME_PACK to its directory`, "missing", "scriptc includes the runtime pack for its own platform only; other targets need their runtime pack package in the project", ); } - const packageText = readFileSync(join(root, "package.json"), "utf8"); - const manifestText = readFileSync(join(root, "runtime-pack.json"), "utf8"); + // The pack's files by identity, with everything else selection reads. + let key: string | null = null; + let identities: FileIdentity[] = []; + if (store !== undefined) { + try { + identities = [fileIdentity(packagePath), fileIdentity(manifestPath)]; + key = storeKey( + JSON.stringify({ + schema: 1, + root, + identities, + target: target.name, + compilerVersion, + features, + flavor, + mode, + }), + ); + const stored = readStoredSelection(store.read(key)); + if (stored !== null) { + const packageText = readFileSync(packagePath, "utf8"); + const manifestText = readFileSync(manifestPath, "utf8"); + // The texts read are the ones with the stored identities. + if ( + sameIdentity(fileIdentity(packagePath), identities[0]!) && + sameIdentity(fileIdentity(manifestPath), identities[1]!) + ) { + return { + root, + licenses: stored.licenses, + packageText, + manifestText, + selected: stored.selected, + flavor, + }; + } + } + } catch { + key = null; + } + } + const packageText = readFileSync(packagePath, "utf8"); + const manifestText = readFileSync(manifestPath, "utf8"); const identity = JSON.parse(packageText) as { name?: string; version?: string }; const manifest = parseRuntimePackManifest(JSON.parse(manifestText)); validateRuntimePackIdentity(manifest, identity.name, identity.version, target, compilerVersion); const selected = selectRuntimePackArtifacts(manifest, features, flavor, process.env, mode); - return { root, manifest, packageText, manifestText, selected, flavor }; + if (store !== undefined && key !== null) { + try { + // Stored only when the files selected from still have the identities + // in the key. + if ( + sameIdentity(fileIdentity(packagePath), identities[0]!) && + sameIdentity(fileIdentity(manifestPath), identities[1]!) + ) { + const entry: StoredSelection = { schema: 1, licenses: manifest.licenses, selected }; + store.write(key, JSON.stringify(entry)); + } + } catch { + /* A failed write is a later miss. */ + } + } + return { + root, + manifest, + licenses: manifest.licenses, + packageText, + manifestText, + selected, + flavor, + }; } -function stageArtifact(root: string, stage: string, artifact: RuntimePackArtifact): string { +/** The selection's parsed manifest. */ +export function runtimePackManifest(selection: NativeRuntimeSelection): RuntimePackManifest { + return (selection.manifest ??= parseRuntimePackManifest(JSON.parse(selection.manifestText))); +} + +function readStoredSelection(text: string | null): StoredSelection | null { + if (text === null) return null; + const value: unknown = JSON.parse(text); + if (value === null || typeof value !== "object") return null; + const entry = value as Partial; + if ( + entry.schema !== 1 || + !Array.isArray(entry.licenses) || + entry.selected === undefined || + entry.selected === null || + typeof entry.selected !== "object" || + !Array.isArray(entry.selected.runtime) || + !Array.isArray(entry.selected.archives) || + !Array.isArray(entry.selected.systemLibraries) + ) + return null; + return entry as StoredSelection; +} + +/** An artifact path's identity when its bytes last matched the manifest's + * size and digest. */ +interface VerifiedArtifact { + path: string; + sha256: string; + dev: number; + ino: number; + size: number; + mtime: number; + ctime: number; +} + +interface StoredVerification { + schema: number; + artifacts: VerifiedArtifact[]; +} + +function readVerified( + store: RuntimePackStore | undefined, + key: string, +): Map { + const verified = new Map(); + try { + const text = store?.read(key) ?? null; + if (text === null) return verified; + const stored = JSON.parse(text) as StoredVerification; + if (stored.schema === 1) + for (const artifact of stored.artifacts) verified.set(artifact.path, artifact); + } catch { + /* A malformed entry verifies everything again. */ + } + return verified; +} + +function stageArtifact( + root: string, + stage: string, + artifact: RuntimePackArtifact, + verified: Map, + newlyVerified: VerifiedArtifact[], +): string { const source = join(root, artifact.path); const destination = join(stage, artifact.path); let bytes: Buffer; + let before: FileIdentity; + let after: FileIdentity; try { + before = fileIdentity(source); bytes = readFileSync(source); + after = fileIdentity(source); } catch { throw new RuntimePackError(`runtime pack artifact is missing: ${artifact.path}`, "invalid"); } + // The bytes are those last hashed when the file kept the identity it had + // then for the whole read. + const known = verified.get(artifact.path); + const unchanged = sameIdentity(before, after); + const trusted = + known !== undefined && + known.sha256 === artifact.sha256 && + known.size === artifact.size && + unchanged && + known.dev === before.dev && + known.ino === before.ino && + known.size === before.size && + known.mtime === before.mtime && + known.ctime === before.ctime; if ( bytes.length !== artifact.size || - createHash("sha256").update(bytes).digest("hex") !== artifact.sha256 + (!trusted && createHash("sha256").update(bytes).digest("hex") !== artifact.sha256) ) { throw new RuntimePackError(`runtime pack artifact hash mismatch: ${artifact.path}`, "invalid"); } + if (!trusted && unchanged) + newlyVerified.push({ + path: artifact.path, + sha256: artifact.sha256, + dev: before.dev, + ino: before.ino, + size: before.size, + mtime: before.mtime, + ctime: before.ctime, + }); mkdirSync(dirname(destination), { recursive: true }); const fd = openSync( destination, @@ -123,15 +335,19 @@ export function stageNativeRuntimePack( export function stageNativeRuntimeSelection( selection: NativeRuntimeSelection, stageRoot: string, + store?: RuntimePackStore, ): NativeRuntimePack { - const { root, manifest, selected, packageText, manifestText } = selection; + const { root, licenses, selected, packageText, manifestText } = selection; const packagePath = join(root, "package.json"); const manifestPath = join(root, "runtime-pack.json"); - const runtimeObjects = selected.runtime.map((artifact) => - stageArtifact(root, stageRoot, artifact), - ); - const archives = selected.archives.map((artifact) => stageArtifact(root, stageRoot, artifact)); - for (const license of manifest.licenses) { + const key = storeKey(JSON.stringify({ schema: 1, verified: root })); + const verified = readVerified(store, key); + const newlyVerified: VerifiedArtifact[] = []; + const stage = (artifact: RuntimePackArtifact): string => + stageArtifact(root, stageRoot, artifact, verified, newlyVerified); + const runtimeObjects = selected.runtime.map(stage); + const archives = selected.archives.map(stage); + for (const license of licenses) { try { readFileSync(join(root, license.path)); } catch { @@ -144,5 +360,14 @@ export function stageNativeRuntimeSelection( ) { throw new RuntimePackError("runtime pack changed while staging verified artifacts", "invalid"); } + if (store !== undefined && newlyVerified.length > 0) { + for (const artifact of newlyVerified) verified.set(artifact.path, artifact); + const entry: StoredVerification = { schema: 1, artifacts: [...verified.values()] }; + try { + store.write(key, JSON.stringify(entry)); + } catch { + /* A failed write only means hashing again next time. */ + } + } return { runtimeObjects, archives, systemLibraries: selected.systemLibraries }; } diff --git a/packages/compiler/src/native/cache.ts b/packages/compiler/src/native/cache.ts index 937b4b109..f48410d1d 100644 --- a/packages/compiler/src/native/cache.ts +++ b/packages/compiler/src/native/cache.ts @@ -179,6 +179,7 @@ export class NativeCache { "sanitizer", "executable", "linked", + "runtime-pack", "binary", "dsym", "coverage", diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts index 63769fba6..bcd5c1935 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -31,7 +31,9 @@ import { } from "../backend/native-tools.js"; import { selectNativeRuntimePack, + runtimePackManifest, stageNativeRuntimeSelection, + type RuntimePackStore, } from "../backend/runtime-pack-native.js"; import { RuntimePackError } from "../backend/runtime-pack-core.js"; import { @@ -160,6 +162,17 @@ export class NativeCompiler { return result; } + /** Runtime pack selections and verified artifact identities, kept in the + * cache. */ + private runtimePackStore(): RuntimePackStore | undefined { + const cache = this.cache; + if (cache === null) return undefined; + return { + read: (key) => cache.read("runtime-pack", key)?.toString("utf8") ?? null, + write: (key, text) => cache.write("runtime-pack", key, text), + }; + } + private emitObject( input: string, output: string, @@ -394,6 +407,8 @@ export class NativeCompiler { toolchain.compilerVersion, features, optimization, + "executable", + this.runtimePackStore(), ) : null; const executableCache = @@ -449,16 +464,24 @@ export class NativeCompiler { toolchain.compilerVersion, features, optimization, + "executable", + this.runtimePackStore(), ) : null; - if (pack !== null) stageNativeRuntimeSelection(pack, join(stage, "runtime")); + if (pack !== null) + stageNativeRuntimeSelection(pack, join(stage, "runtime"), this.runtimePackStore()); const info = pack === null ? undefined : formatNativeLinkInfo( { programObject: output, target, ffi }, toolchain.compilerVersion, - pack, + { + root: pack.root, + manifest: runtimePackManifest(pack), + selected: pack.selected, + flavor: pack.flavor, + }, ); renameSync(stagedOutput, output); return { @@ -473,7 +496,7 @@ export class NativeCompiler { const pack = executablePack!; const runtime = options.sanitize ? buildSanitizedRuntime(toolchain, pack, join(stage, "runtime"), "executable", this.cache) - : stageNativeRuntimeSelection(pack, join(stage, "runtime")); + : stageNativeRuntimeSelection(pack, join(stage, "runtime"), this.runtimePackStore()); timing("runtime-stage"); const plan = executableLinkInputs({ target, @@ -617,6 +640,7 @@ export class NativeCompiler { features, profile.optimization, profile.instancePerThread ? "library-thread" : "library", + this.runtimePackStore(), ); const runtime = options.sanitize ? buildSanitizedRuntime( @@ -626,7 +650,7 @@ export class NativeCompiler { profile.instancePerThread ? "library-thread" : "library", this.cache, ) - : stageNativeRuntimeSelection(pack, join(stage, "runtime")); + : stageNativeRuntimeSelection(pack, join(stage, "runtime"), this.runtimePackStore()); timing("runtime-stage"); const stagedArchive = join(stage, wasm ? "output.wasm" : "output.a"); const localizeSymbols = libraryLocalizeSymbols(profile); diff --git a/packages/compiler/src/native/executable-cache-open.test.ts b/packages/compiler/src/native/executable-cache-open.test.ts index 05e57148d..09a8b9cc9 100644 --- a/packages/compiler/src/native/executable-cache-open.test.ts +++ b/packages/compiler/src/native/executable-cache-open.test.ts @@ -52,7 +52,7 @@ function fixture(platform: string, driver: string) { } as unknown as NativeToolchain; const pack = { root: runtime, - manifest: { licenses: [] }, + licenses: [], packageText: "{}", manifestText: "{}", selected: { runtime: [], archives: [] }, diff --git a/packages/compiler/src/native/executable-cache.ts b/packages/compiler/src/native/executable-cache.ts index 1dfbf0b00..74c935d88 100644 --- a/packages/compiler/src/native/executable-cache.ts +++ b/packages/compiler/src/native/executable-cache.ts @@ -331,8 +331,8 @@ export function openNativeExecutableCache( join(toolchain.helperPackageRoot, "package.json"), join(toolchain.runtimePackRoot, "package.json"), join(toolchain.runtimePackRoot, "runtime-pack.json"), - ...[...pack.selected.runtime, ...pack.selected.archives, ...pack.manifest.licenses].map( - (artifact) => join(pack.root, artifact.path), + ...[...pack.selected.runtime, ...pack.selected.archives, ...pack.licenses].map((artifact) => + join(pack.root, artifact.path), ), ]; if (platform === "linux") paths.push(join(pack.root, "runtime-pack.json")); diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 1415f2971..31a8c3355 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -64,6 +64,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( { root: directory, manifest, + licenses: manifest.licenses, packageText: "", manifestText: "", flavor: "dev", diff --git a/packages/compiler/src/native/sanitizer.ts b/packages/compiler/src/native/sanitizer.ts index cb3728ec0..5d228a615 100644 --- a/packages/compiler/src/native/sanitizer.ts +++ b/packages/compiler/src/native/sanitizer.ts @@ -4,7 +4,11 @@ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; import { basename, join } from "node:path"; import { NativeCodegenError } from "../backend/native-codegen-core.js"; import { requireNativeArtifact, runNativeTool } from "../backend/native-tools.js"; -import type { NativeRuntimePack, NativeRuntimeSelection } from "../backend/runtime-pack-native.js"; +import { + runtimePackManifest, + type NativeRuntimePack, + type NativeRuntimeSelection, +} from "../backend/runtime-pack-native.js"; import { runtimePackFlavorKey, type RuntimePackMode } from "../backend/runtime-pack-core.js"; import { LRE_SOURCES, ZLIB_SOURCES } from "../backend/vendor-inputs.js"; import { contentDigest, type NativeCache } from "./cache.js"; @@ -119,8 +123,9 @@ export function buildSanitizedRuntime( } return output; }; - const key = runtimePackFlavorKey(selection.manifest, selection.flavor, mode); - const flavor = selection.manifest.flavors[key]; + const manifest = runtimePackManifest(selection); + const key = runtimePackFlavorKey(manifest, selection.flavor, mode); + const flavor = manifest.flavors[key]; if (flavor === undefined) throw new NativeCodegenError("SC3003", `runtime pack lacks ${key}`); const wanted = new Set(selection.selected.runtime.map((artifact) => artifact.path)); const common = [ From 537dfbeb687f1022e68978c68a524e65cf468043 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 09:37:24 -0500 Subject: [PATCH 059/273] Evaluate assert.fail's value messages after declaring them, with Node's defaults assert.fail with a message that is not a string read the message's hidden locals before they were declared: the call stored the message and its generated flag in temporaries ahead of the message's declarations, so `assert.fail(new RangeError('re'))` crashed. Arguments computed from the message now read it after the declarations. assert.fail also follows Node's defaults for such values: null and undefined report "Failed" with generatedMessage true, an Error is thrown as is, and any other value, falsy ones included, becomes the message as its string, with generatedMessage set to !value. --- .../src/frontend/lowering/lower-assert.ts | 76 ++++++++++++------- tests/corpus/8062-assert-message-values.cjs | 7 ++ 2 files changed, 57 insertions(+), 26 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index 39ec9c959..02faa7ade 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -57,6 +57,7 @@ import { typeKey, } from "../../ir/ir.js"; import { boolLit, countedFor, dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js"; +import { everyExpr } from "../../ir/traverse.js"; import { checkedClassInstanceOf } from "./class-construction.js"; import { isJsSourceFile } from "../program.js"; @@ -231,8 +232,9 @@ function requireStatementPosition(lowerer: Lowerer, call: ts.CallExpression, wha const VALUE_MESSAGE = "\u0001scriptc:assert-message-value"; /** A message argument that is not a string, met while one assertion - * lowers (withValueMessage). */ -let valueMessage: { value: IrExpr; has: string; text: string } | null = null; + * lowers (withValueMessage). `fail` marks assert.fail's message, which + * Node uses for every value but null and undefined. */ +let valueMessage: { value: IrExpr; has: string; text: string; fail: boolean } | null = null; /** The optional trailing message argument: Node accepts a string or an * Error (throwing the Error itself) — the STRING form lowers; a literal @@ -246,6 +248,7 @@ function lowerMessageArg( lowerer: Lowerer, node: ts.Expression | undefined, loc: SrcLoc, + fail = false, ): { msg: IrExpr; hasMsg: IrExpr } { if (!node || (ts.isIdentifier(node) && node.text === "undefined")) { return { msg: { kind: "strLit", value: "", type: STRING, loc }, hasMsg: boolLit(false, loc) }; @@ -265,7 +268,7 @@ function lowerMessageArg( } const has = lowerer.declareHiddenLocal("assert.hasMessage", BOOL); const text = lowerer.declareHiddenLocal("assert.messageText", STRING); - valueMessage = { value, has: has.id, text: text.id }; + valueMessage = { value, has: has.id, text: text.id, fail }; return { msg: varRef(text.id, STRING, loc), hasMsg: varRef(has.id, BOOL, loc) }; } @@ -285,7 +288,12 @@ function withValueMessage( valueMessage = null; try { const lowered = lower(); - const message = valueMessage as { value: IrExpr; has: string; text: string } | null; + const message = valueMessage as { + value: IrExpr; + has: string; + text: string; + fail: boolean; + } | null; if (lowered === null || message === null) return lowered; // assert.ok's statements evaluate the value first; its call is last. const before = lowered.kind === "seqExpr" ? lowered.stmts : []; @@ -298,13 +306,25 @@ function withValueMessage( ); } const stmts: IrStmt[] = [...before]; + // An argument computed from the message (assert.fail's generated flag) + // reads it after the declarations below; the others are evaluated first. + const readsMessage = (arg: IrExpr): boolean => + !everyExpr(arg, { + stmt: () => true, + expr: (e) => + !(e.kind === "varRef" && (e.localId === message.has || e.localId === message.text)), + }); const args = call.args.map((arg, index) => - arg.kind === "varRef" && (arg.localId === message.has || arg.localId === message.text) - ? arg - : stableValue(lowerer, stmts, arg, `assert.arg${index}`, loc), + readsMessage(arg) ? arg : stableValue(lowerer, stmts, arg, `assert.arg${index}`, loc), ); const local = lowerer.declareHiddenLocal("assert.message", DYN); const value = varRef(local.id, DYN, loc); + // The runtime reports a registered value: a truthy one for the other + // methods, any value but null and undefined for assert.fail (Node's + // `actual == null` test), whose other values read "Failed". + const registered: IrExpr = message.fail + ? varRef(message.has, BOOL, loc) + : { kind: "dynTest", test: "truthy", value, type: BOOL, loc }; stmts.push( { kind: "varDecl", localId: local.id, init: message.value, loc }, { @@ -318,9 +338,9 @@ function withValueMessage( localId: message.text, init: { kind: "ternary", - cond: { kind: "dynTest", test: "truthy", value, type: BOOL, loc }, + cond: registered, then: strLit(VALUE_MESSAGE, loc), - else_: strLit("", loc), + else_: strLit(message.fail ? "Failed" : "", loc), type: STRING, loc, }, @@ -328,7 +348,7 @@ function withValueMessage( }, { kind: "if", - cond: { kind: "dynTest", test: "truthy", value, type: BOOL, loc }, + cond: registered, then: [ { kind: "exprStmt", @@ -1148,22 +1168,26 @@ function lowerAssertFail(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc) generated = boolLit(true, loc); } else { // Node's AssertionError sets generatedMessage to !message, so an - // empty custom message counts as generated. - msg = stableValue( - lowerer, - stmts, - lowerMessageArg(lowerer, msgNode, loc).msg, - "%assertFailMessage", - loc, - ); - generated = { - kind: "strEq", - negated: false, - left: msg, - right: strLit("", loc), - type: BOOL, - loc, - }; + // empty custom message counts as generated. A message that is not a + // string is read from the hidden local that withValueMessage declares + // after these statements, so it is used as is. + const { msg: lowered, hasMsg } = lowerMessageArg(lowerer, msgNode, loc, true); + if (lowered.kind === "varRef" && hasMsg.kind === "varRef") { + // A value message: generated when Node reads "Failed" for null or + // undefined; the runtime sets the flag for a registered value. + msg = lowered; + generated = { kind: "unary", op: "!", operand: hasMsg, type: BOOL, loc }; + } else { + msg = stableValue(lowerer, stmts, lowered, "%assertFailMessage", loc); + generated = { + kind: "strEq", + negated: false, + left: msg, + right: strLit("", loc), + type: BOOL, + loc, + }; + } } const falseLit: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; stmts.push(recordOnFailure(lowerer, boolLit(true, loc), null, null, "fail", loc)); diff --git a/tests/corpus/8062-assert-message-values.cjs b/tests/corpus/8062-assert-message-values.cjs index 7f84ccd37..fc37a71e4 100644 --- a/tests/corpus/8062-assert-message-values.cjs +++ b/tests/corpus/8062-assert-message-values.cjs @@ -39,3 +39,10 @@ const row = [['a', 'b', true], ['c', 'd', 'x']]; for (const [, , expected] of row) { attempt('union operand', () => assert.strictEqual(JSON.parse(JSON.stringify(expected)), expected)); } + +// assert.fail with a message that is not a string: an Error is thrown as +// is; other values become the message, generated when falsy. +attempt('fail error', () => assert.fail(new RangeError('re'))); +attempt('fail value', () => assert.fail(JSON.parse('{"a":1}'))); +attempt('fail zero', () => assert.fail(zero)); +attempt('fail null', () => assert.fail(JSON.parse('null'))); From e792cf47713ad7c0e23b2ab10e4f089be894bc69 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 09:49:01 -0500 Subject: [PATCH 060/273] Test RSA-PSS verification with an automatic salt length on restricted keys Node recovers the salt length under RSA_PSS_SALTLEN_AUTO without enforcing the minimum in an RSA-PSS key's parameters, while an explicit saltLength below it throws and the default saltLength requires the key's. The corpus program pins that behavior for MGF1 digests equal to and different from the message digest, through crypto.verify and createVerify. --- .../8178-rsa-pss-auto-min-salt.cjs.json | 6 ++ tests/corpus/8178-rsa-pss-auto-min-salt.cjs | 57 +++++++++++++++++++ .../8178-rsa-pss-auto-min-salt.cjs.json | 1 + 3 files changed, 64 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8178-rsa-pss-auto-min-salt.cjs.json create mode 100644 tests/corpus/8178-rsa-pss-auto-min-salt.cjs create mode 100644 tests/harness/corpus-costs/8178-rsa-pss-auto-min-salt.cjs.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8178-rsa-pss-auto-min-salt.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8178-rsa-pss-auto-min-salt.cjs.json new file mode 100644 index 000000000..7b3e5eef7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8178-rsa-pss-auto-min-salt.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8178-rsa-pss-auto-min-salt.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/8178-rsa-pss-auto-min-salt.cjs b/tests/corpus/8178-rsa-pss-auto-min-salt.cjs new file mode 100644 index 000000000..97b9d1c05 --- /dev/null +++ b/tests/corpus/8178-rsa-pss-auto-min-salt.cjs @@ -0,0 +1,57 @@ +// Verifying with RSA_PSS_SALTLEN_AUTO against an RSA-PSS key whose +// parameters set a minimum salt length still enforces that minimum, as in +// Node: a valid signature with a shorter salt does not verify. The keys use +// an MGF1 digest equal to the message digest and a different one. The +// stricter public key is the generated one with its minimum salt length +// raised from 16 to 32 in its DER. +const c = require("node:crypto"); + +const data = Buffer.from("restricted salt"); + +function raiseMinimumSalt(publicKey) { + const der = publicKey.export({ format: "der", type: "spki" }); + const from = Buffer.from([0xa2, 0x03, 0x02, 0x01, 0x10]); + const at = der.indexOf(from); + if (at < 0) throw new Error("no saltLength field"); + der[at + 4] = 0x20; + return c.createPublicKey({ key: der, format: "der", type: "spki" }); +} + +function attempt(label, fn) { + try { + console.log(label, fn()); + } catch (e) { + console.log(label, "throws", e.code, e.message); + } +} + +for (const mgf1HashAlgorithm of ["sha256", "sha384"]) { + const { privateKey, publicKey } = c.generateKeyPairSync("rsa-pss", { + modulusLength: 1024, + hashAlgorithm: "sha256", + mgf1HashAlgorithm, + saltLength: 16, + }); + const strict = raiseMinimumSalt(publicKey); + const { mgf1HashAlgorithm: mgf1, saltLength: minimum } = strict.asymmetricKeyDetails; + console.log(mgf1HashAlgorithm, "strict key", mgf1, minimum); + for (const saltLength of [16, 20, 32, 40]) { + const signature = c.sign("sha256", data, { key: privateKey, saltLength }); + const label = `${mgf1HashAlgorithm} salt ${saltLength}`; + attempt(`${label} auto, key minimum 16:`, () => + c.verify("sha256", data, { key: publicKey, saltLength: c.constants.RSA_PSS_SALTLEN_AUTO }, signature), + ); + attempt(`${label} auto, key minimum 32:`, () => + c.verify("sha256", data, { key: strict, saltLength: c.constants.RSA_PSS_SALTLEN_AUTO }, signature), + ); + attempt(`${label} default, key minimum 32:`, () => c.verify("sha256", data, strict, signature)); + attempt(`${label} digest length, key minimum 32:`, () => + c.verify("sha256", data, { key: strict, saltLength: c.constants.RSA_PSS_SALTLEN_DIGEST }, signature), + ); + const v = c.createVerify("sha256"); + v.update(data); + attempt(`${label} createVerify auto, key minimum 32:`, () => + v.verify({ key: strict, saltLength: c.constants.RSA_PSS_SALTLEN_AUTO }, signature), + ); + } +} diff --git a/tests/harness/corpus-costs/8178-rsa-pss-auto-min-salt.cjs.json b/tests/harness/corpus-costs/8178-rsa-pss-auto-min-salt.cjs.json new file mode 100644 index 000000000..b1711dea1 --- /dev/null +++ b/tests/harness/corpus-costs/8178-rsa-pss-auto-min-salt.cjs.json @@ -0,0 +1 @@ +{ "plain": 5740, "san": 60808 } From e35621323dfdceb4b56a01bcbe8c9523591e495a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 10:06:35 -0500 Subject: [PATCH 061/273] Trust no certificate for an empty ca in TLS clients tls.connect, https.request and http2.connect verified the server against the default CAs when ca was an empty Buffer, because the runtime read a zero-length ca as none. The compiled calls now pass a null ca for none (an omitted ca, or an empty string, which Node reads as no ca) and a non-null one for any Buffer, and runtime option records read ca the same way, so an empty Buffer or array trusts no certificate, as in Node. The connection pool keys tell an empty ca from none. An empty array, which threw, is an empty trust store too. A runtime-valued empty string ca in http2.connect is refused instead of being read as an empty store. --- docs/content/limitations/tls-certificates.mdx | 3 + packages/compiler/src/backend/llvm/lib-abi.ts | 20 ++++ .../compiler/src/backend/llvm/lib-dispatch.ts | 7 +- .../compiler/src/backend/llvm/lib-http2.ts | 3 +- packages/compiler/src/backend/llvm/lib-tls.ts | 5 +- .../corpus/8176-tls-empty-context-ca.ts.json | 6 + .../corpus/8177-tls-empty-client-ca.ts.json | 6 + packages/runtime/src/scr_http2.c | 4 +- packages/runtime/src/scr_http2.h | 6 +- packages/runtime/src/scr_tls.c | 99 ++++++++++++----- packages/runtime/src/scr_tls.h | 2 +- tests/corpus/8176-tls-empty-context-ca.ts | 73 ++++++++++++ tests/corpus/8177-tls-empty-client-ca.ts | 104 ++++++++++++++++++ .../8176-tls-empty-context-ca.ts.json | 1 + .../8177-tls-empty-client-ca.ts.json | 1 + 15 files changed, 300 insertions(+), 40 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8176-tls-empty-context-ca.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8177-tls-empty-client-ca.ts.json create mode 100644 tests/corpus/8176-tls-empty-context-ca.ts create mode 100644 tests/corpus/8177-tls-empty-client-ca.ts create mode 100644 tests/harness/corpus-costs/8176-tls-empty-context-ca.ts.json create mode 100644 tests/harness/corpus-costs/8177-tls-empty-client-ca.ts.json diff --git a/docs/content/limitations/tls-certificates.mdx b/docs/content/limitations/tls-certificates.mdx index bbe4ceaf9..639dd3d83 100644 --- a/docs/content/limitations/tls-certificates.mdx +++ b/docs/content/limitations/tls-certificates.mdx @@ -2,6 +2,9 @@ On a TLS socket held in an `unknown` or `any` value, or in an untyped JavaScript parameter, they throw an error that names the call as not supported. Client certificates follow Node: `tls.createServer`, `https.createServer`, and `new tls.TLSSocket` take `requestCert`, `rejectUnauthorized`, and `ca`, and `tls.connect` and `https.request` take `cert` and `key`. A server's `authorized` and `authorizationError` report the client certificate's verification as Node does, and a server, client, or `tls.createSecureContext` given a `ca` also sends the chain it completes, as OpenSSL's automatic chain does. +A server verifies client certificates against the `ca` of the context it serves, including contexts from `addContext()` and `setSecureContext()`: an empty `ca` (an empty Buffer or array) trusts no certificate, and an omitted `ca` trusts the default CAs. +The same holds for a client's `ca` in `tls.connect`, `https.request`, and `http2.connect`, where an empty string is no `ca`, as in Node. +In `http2.connect`, a `ca` held in an `unknown` or `any` value that is an empty string is refused. `requestCert` beside a TLSSocket's `secureContext` or an `SNICallback`, client certificates on http2 secure servers, and a `cert` without a `key` (or the reverse) are refused. `server.addContext()` and `server.setSecureContext()` follow Node on tls and https servers, including wildcard names, which match one label without regard to case. A name with regular-expression groups or alternation, or one outside ASCII, is refused, as are both methods on a server that is not a tls or https server. diff --git a/packages/compiler/src/backend/llvm/lib-abi.ts b/packages/compiler/src/backend/llvm/lib-abi.ts index 6917d8256..ea5ab82a2 100644 --- a/packages/compiler/src/backend/llvm/lib-abi.ts +++ b/packages/compiler/src/backend/llvm/lib-abi.ts @@ -73,6 +73,26 @@ export function rawBytes(host: LlvmEmitterContext, value: LlValue): AbiArgument[ return [ptr(data), { type: host.sizeType, name: length }]; } +/** A TLS client's `ca` as rawBytes, with a null pointer for none: an empty + * string, which Node reads as no ca (and which an omitted ca lowers to). + * A Buffer's pointer is never null, so an empty one is an empty trust + * store. */ +export function caBytes(host: LlvmEmitterContext, value: LlValue): AbiArgument[] { + const B = host.B; + const [data, length] = rawBytes(host, value); + const test = B.tmp(), + pointer = B.tmp(); + if (value.type.kind === "string") { + B.line(`${test} = icmp eq ${host.sizeType} ${length!.name}, 0`); + B.line(`${pointer} = select i1 ${test}, ptr null, ptr ${data!.name}`); + } else { + // An empty Buffer's data pointer may be null; its object stands in. + B.line(`${test} = icmp eq ptr ${data!.name}, null`); + B.line(`${pointer} = select i1 ${test}, ptr ${value.name}, ptr ${data!.name}`); + } + return [ptr(pointer), length!]; +} + export function callbackType(value: LlValue): IrType & { kind: "func" } { if (value.type.kind !== "func") throw new InternalCompilerError("runtime callback must be a function"); diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 9cd11feb1..e371b10a4 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -22,7 +22,7 @@ import { } from "./lib-shared.js"; import { isDynUndefined } from "../../ir/runtime-features.js"; import { DYN_KIND } from "./dyn.js"; -import { rawBytes } from "./lib-abi.js"; +import { caBytes } from "./lib-abi.js"; import { emitInlineAtomicsOp } from "./expr-bytes.js"; import { regexLiteralCaptures } from "./regex-matchers.js"; @@ -595,7 +595,8 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll // The https URL row is the http one with the TLS entry point — same // three arguments, same response-callback adapter. The https options // row is wider: rejectUnauthorized stays an i1, while its ScrStr or - // ScrBytes CA value expands to the runtime's raw pointer + length. + // ScrBytes CA value expands to the runtime's raw pointer + length + // (caBytes: a null pointer for none). const isTls = e.fn.startsWith("https."); const isUrl = e.fn.includes("requestUrl"); const isTlsOptions = isTls && !isUrl; @@ -629,7 +630,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll if (isTlsOptions) { callArgs = [ ...callArgs.slice(0, 8), - ...rawBytes(host, args[8]!).map((arg) => `${arg.type} ${arg.name}`), + ...caBytes(host, args[8]!).map((arg) => `${arg.type} ${arg.name}`), ]; host.declare( `declare ptr @scr_https_request(ptr, double, ptr, ptr, double, ptr, i1 zeroext, i1 zeroext, ptr, ${host.sizeType}, ptr, ptr)`, diff --git a/packages/compiler/src/backend/llvm/lib-http2.ts b/packages/compiler/src/backend/llvm/lib-http2.ts index 321f1d005..d750fb7c5 100644 --- a/packages/compiler/src/backend/llvm/lib-http2.ts +++ b/packages/compiler/src/backend/llvm/lib-http2.ts @@ -9,6 +9,7 @@ import { callbackType, finishRuntimeCall, ptr, + caBytes, rawBytes, requestHandler, } from "./lib-abi.js"; @@ -204,7 +205,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa ); return finishRuntimeCall(host, e, "scr_http2_connect", [ ...a.slice(0, 2), - ...rawBytes(host, args[2]!), + ...caBytes(host, args[2]!), ...handler, ]); } diff --git a/packages/compiler/src/backend/llvm/lib-tls.ts b/packages/compiler/src/backend/llvm/lib-tls.ts index 307aeb3dd..dbe492180 100644 --- a/packages/compiler/src/backend/llvm/lib-tls.ts +++ b/packages/compiler/src/backend/llvm/lib-tls.ts @@ -8,6 +8,7 @@ import { finishRuntimeCall, nullableReference, ptr, + caBytes, rawBytes, requestHandler, } from "./lib-abi.js"; @@ -184,7 +185,7 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu ); return finishRuntimeCall(host, e, "scr_https_request_agent", [ ...a.slice(0, 8), - ...rawBytes(host, args[8]!), + ...caBytes(host, args[8]!), a[9]!, ...handler, ]); @@ -204,7 +205,7 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu ["ptr", "ptr"], ); // Evaluate arguments once before selecting which transport consumes the callback. - const pem = rawBytes(host, args[9]!); + const pem = caBytes(host, args[9]!); const B = host.B, tls = B.newLabel("request.tls"), plain = B.newLabel("request.plain"), diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8176-tls-empty-context-ca.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8176-tls-empty-context-ca.ts.json new file mode 100644 index 000000000..536027066 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8176-tls-empty-context-ca.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8176-tls-empty-context-ca.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8177-tls-empty-client-ca.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8177-tls-empty-client-ca.ts.json new file mode 100644 index 000000000..b28587594 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8177-tls-empty-client-ca.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8177-tls-empty-client-ca.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_http2.c b/packages/runtime/src/scr_http2.c index 55d880c01..489946a12 100644 --- a/packages/runtime/src/scr_http2.c +++ b/packages/runtime/src/scr_http2.c @@ -2617,11 +2617,11 @@ static void scr_h2_on_established(void *ctx) { * TLS client transport with ALPN ["h2"] (scr_tls.c — linked whenever * this unit is): the same preface/SETTINGS buffer through the handshake * and everything after establishment is protocol-identical. `ca` - * replaces the client trust anchors (len 0 = none) and + * replaces the client trust anchors (NULL = none) and * reject_unauthorized false disables the verify gate — the local-CA * self-connect shapes. Throws catchably on unparsable input. */ ScrH2Session *scr_http2_connect(ScrStr *url /*borrowed*/, bool reject_unauthorized, - const char *ca /*borrowed, len 0 = none*/, size_t ca_len, + const char *ca /*borrowed, NULL = none*/, size_t ca_len, ScrClosure *cb /*moves, nullable*/, ScrH2ConnectFn fn) { const char *p = url->data; const char *sep = strstr(p, "://"); diff --git a/packages/runtime/src/scr_http2.h b/packages/runtime/src/scr_http2.h index 25e236e6f..c4ce850d0 100644 --- a/packages/runtime/src/scr_http2.h +++ b/packages/runtime/src/scr_http2.h @@ -69,7 +69,7 @@ void scr_http2_server_on_session(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrH * reject_unauthorized false disables the verify gate — the local-CA * self-connect shapes); throws catchably on bad input */ ScrH2Session *scr_http2_connect(ScrStr *url /*borrowed*/, bool reject_unauthorized, - const char *ca /*borrowed, len 0 = none*/, size_t ca_len, + const char *ca /*borrowed, NULL = none*/, size_t ca_len, ScrClosure *cb /*moves, nullable*/, ScrH2ConnectFn fn); /* +1 */ /* end_stream: -1 = the method's payload-meaningless default, 0/1 explicit */ @@ -173,7 +173,7 @@ ScrHttpClientReq *scr_https_request(ScrStr *host /*borrowed*/, double port, ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/, double timeout_ms, ScrArr *header_pairs /*borrowed*/, bool auto_end, bool reject_unauthorized, - const char *ca /*borrowed, len 0 = none*/, size_t ca_len, + const char *ca /*borrowed, NULL = none*/, size_t ca_len, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */ /* The URL-string form (https.get('https://host/path')): scr_http.c's * shared parse, then the options form's dial with Node's no-options @@ -187,7 +187,7 @@ ScrHttpClientReq *scr_https_request_agent(ScrStr *host /*borrowed*/, double port ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/, double timeout_ms, ScrArr *header_pairs /*borrowed*/, bool auto_end, bool reject_unauthorized, - const char *ca /*borrowed, len 0 = none*/, size_t ca_len, + const char *ca /*borrowed, NULL = none*/, size_t ca_len, const struct ScrDyn *agent /*borrowed*/, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */ /* The fetch unit's https leg (defined in scr_tls.c): a client transport diff --git a/packages/runtime/src/scr_tls.c b/packages/runtime/src/scr_tls.c index 2f448a417..e764926b2 100644 --- a/packages/runtime/src/scr_tls.c +++ b/packages/runtime/src/scr_tls.c @@ -253,7 +253,10 @@ struct ScrSecureCtx { mbedtls_x509_crt cert; mbedtls_pk_context pk; /* the ca option: a socket created from this context (a TLSSocket's - * secureContext) sends the chain it completes */ + * secureContext) sends the chain it completes, and a server that serves + * the context verifies client certificates against it. has_ca tells an + * empty ca (an empty trust store) from none (the default store). */ + bool has_ca; mbedtls_x509_crt ca; }; @@ -573,6 +576,9 @@ static void scr_tls_srv_client_auth(ScrTlsSrv *s, const ScrTlsClientAuth *auth) typedef struct ScrTlsCli { mbedtls_ssl_config conf; + /* the ca option, which replaces the default store; has_ca tells an + * empty ca (an empty trust store) from none */ + bool has_ca; mbedtls_x509_crt ca; char *hostname; /* the SNI name; NULL sends none (an IP literal, servername: "") */ char *verify_name; /* the name the leaf is checked against; NULL fails the check */ @@ -696,6 +702,7 @@ static ScrTlsCli *scr_tls_cli_new2(ScrStr *host, bool reject_unauthorized, const mbedtls_ssl_conf_rng(&c->conf, mbedtls_ctr_drbg_random, &scr_tls_drbg); c->verify_recorded = reject_unauthorized || verify_optional; c->reject = reject_unauthorized; + c->has_ca = ca != NULL; if (ca_len > 0) { int r = scr_tls_parse_crt(&c->ca, ca, ca_len); if (r != 0) scr_tls_bad_material("ca certificate", r); @@ -705,7 +712,7 @@ static ScrTlsCli *scr_tls_cli_new2(ScrStr *host, bool reject_unauthorized, const } else { mbedtls_ssl_conf_authmode(&c->conf, reject_unauthorized ? MBEDTLS_SSL_VERIFY_REQUIRED : MBEDTLS_SSL_VERIFY_OPTIONAL); - mbedtls_ssl_conf_ca_chain(&c->conf, ca_len > 0 ? &c->ca : scr_tls_system_ca(), NULL); + mbedtls_ssl_conf_ca_chain(&c->conf, c->has_ca ? &c->ca : scr_tls_system_ca(), NULL); } /* The host is the verified name; it is also the SNI name unless it is * an IP literal. */ @@ -799,6 +806,9 @@ 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) */ + /* scr_tls_f_cert verified client certificates against sni_ctx's ca + * (or the default store without one), not the server's */ + bool ctx_verifies; /* 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) */ @@ -1889,8 +1899,8 @@ static ScrStr *scr_tls_pool_key(ScrStr *host, bool reject_unauthorized, const ch uint64_t h = 1469598103934665603ull; for (size_t i = 0; i < ca_len; i++) h = (h ^ (unsigned char)ca[i]) * 1099511628211ull; char buf[64]; - int n = snprintf(buf, sizeof buf, "%d:%zu:%016llx:", reject_unauthorized ? 1 : 0, ca_len, - (unsigned long long)h); + int n = snprintf(buf, sizeof buf, "%d:%s%zu:%016llx:", reject_unauthorized ? 1 : 0, + ca != NULL ? "" : "-", ca_len, (unsigned long long)h); ScrJsonBuf b; scr_jb_init(&b); scr_jb_write(&b, buf, (size_t)n); @@ -2148,7 +2158,7 @@ ScrHttpClientReq *scr_https_request(ScrStr *host /*borrowed*/, double port, ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/, double timeout_ms, ScrArr *header_pairs /*borrowed*/, bool auto_end, bool reject_unauthorized, - const char *ca /*borrowed, len 0 = none*/, size_t ca_len, + const char *ca /*borrowed, NULL = none*/, size_t ca_len, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) { ScrStr *servername = scr_https_take_servername(); ScrTlsCli *cli = scr_tls_cli_for(host, reject_unauthorized, ca, ca_len, servername); @@ -2189,7 +2199,7 @@ ScrHttpClientReq *scr_https_request_agent(ScrStr *host /*borrowed*/, double port ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/, double timeout_ms, ScrArr *header_pairs /*borrowed*/, bool auto_end, bool reject_unauthorized, - const char *ca /*borrowed, len 0 = none*/, size_t ca_len, + const char *ca /*borrowed, NULL = none*/, size_t ca_len, const ScrDyn *agent /*borrowed*/, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) { ScrStr *servername = scr_https_take_servername(); @@ -2242,6 +2252,14 @@ static bool scr_tls_dyn_truthy(const ScrDyn *v) { * the exception pending. `what` names the fence ("a tls.createServer * 'cert' option"). */ static ScrBytes *scr_tls_pem_dyn(const ScrDyn *v, const char *what, bool concat) { + if (v->kind == SCR_DYN_TYPED_REF) { + /* a typed array (an array literal in a typed record): its entries */ + ScrDyn *plain = scr_dyn_typed_ref_materialize(v); + if (plain == NULL) return NULL; + ScrBytes *b = scr_tls_pem_dyn(plain, what, concat); + scr_dyn_release(plain); + return b; + } if (v->kind == SCR_DYN_STR) { ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)v->v.str->len); memcpy(b->data, v->v.str->data, v->v.str->len); @@ -2292,6 +2310,33 @@ static ScrBytes *scr_tls_pem_dyn(const ScrDyn *v, const char *what, bool concat) return NULL; } +/* A `ca` option from a runtime value: NULL for none (an empty string, + * which Node reads as no ca), empty bytes for an empty Buffer or array + * (an empty trust store), else the PEM bundle (+1). False with the + * exception pending. */ +static bool scr_tls_ca_dyn(const ScrDyn *v, const char *what, ScrBytes **out) { + *out = NULL; + ScrDyn *plain = v->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(v) : NULL; + if (v->kind == SCR_DYN_TYPED_REF && plain == NULL) return false; + const ScrDyn *ca = plain != NULL ? plain : v; + bool ok = true; + if (ca->kind == SCR_DYN_ARR && ca->v.arr.len == 0) { + *out = scr_bytes_new(SCR_BYTES_U8, 0); + } else if (ca->kind != SCR_DYN_STR || ca->v.str->len > 0) { + *out = scr_tls_pem_dyn(ca, what, true); + ok = *out != NULL; + } + scr_dyn_release(plain); + return ok; +} + +/* A client's ca for scr_tls_cli_new2: NULL for none, and never NULL for a + * given ca, even an empty one. */ +static const char *scr_tls_ca_data(const ScrBytes *ca) { + if (ca == NULL) return NULL; + return ca->data != NULL ? (const char *)ca->data : ""; +} + /* https.request/get with a runtime options record: the http walk, then * the TLS options the literal https path lowers (rejectUnauthorized, ca, * servername) and a client certificate (cert and key). Other tls.connect @@ -2330,8 +2375,7 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au if (r != NULL && r->kind == SCR_DYN_BOOL && !r->v.b) reject = false; const ScrDyn *c = ok ? scr_dyn_obj_get(rec, "ca", 2) : NULL; if (c != NULL && c->kind != SCR_DYN_UNDEF && c->kind != SCR_DYN_NULL) { - ca = scr_tls_pem_dyn(c, "an https.request 'ca' option", true); - if (ca == NULL) ok = false; + ok = scr_tls_ca_dyn(c, "an https.request 'ca' option", &ca); } const ScrDyn *oc = ok ? scr_dyn_obj_get(rec, "cert", 4) : NULL; if (oc != NULL && oc->kind != SCR_DYN_UNDEF && oc->kind != SCR_DYN_NULL) { @@ -2366,7 +2410,7 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au if (cb) scr_closure_release(cb); return NULL; } - const char *ca_data = ca ? (const char *)ca->data : NULL; + const char *ca_data = scr_tls_ca_data(ca); size_t ca_len = ca ? ca->len : 0; ScrTlsCli *cli = scr_tls_cli_for(o.host, reject, ca_data, ca_len, servername); ScrStr *key = scr_tls_pool_key(o.host, reject, ca_data, ca_len, servername); @@ -2397,13 +2441,17 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au /* The literal path's runtime-valued cert/key extraction (the compiler's * tls.pemDyn libCall): `what` arrives precomposed. undefined throws — the - * literal walk required the member's presence. */ + * literal walk required the member's presence — and so does an empty + * string, which Node reads as no material while empty bytes here are an + * empty ca. */ ScrBytes *scr_tls_pem_from_dyn(const ScrDyn *v, const char *what) { - if (v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL) { + bool empty = v->kind == SCR_DYN_STR && v->v.str->len == 0; + if (v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL || empty) { ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, what); - scr_jb_puts(&b, " holding undefined has no static lowering — pass PEM material (a string or a Buffer)"); + scr_jb_puts(&b, empty ? " holding an empty string" : " holding undefined"); + scr_jb_puts(&b, " has no static lowering — pass PEM material (a string or a Buffer)"); scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b)); return NULL; } @@ -2565,14 +2613,7 @@ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrByt char what[96]; snprintf(what, sizeof what, "a %s 'ca' option", api); scr_bytes_release(auth->ca); - /* An empty string is no ca, as Node reads it; an empty Buffer is - * an empty trust store. */ - if (v->kind == SCR_DYN_STR && v->v.str->len == 0) { - auth->ca = NULL; - } else { - auth->ca = scr_tls_pem_dyn(v, what, true); - if (auth->ca == NULL) ok = false; - } + if (!scr_tls_ca_dyn(v, what, &auth->ca)) ok = false; } continue; } @@ -2658,6 +2699,7 @@ ScrSecureCtx *scr_tls_create_secure_context_dyn(const ScrDyn *opts /*borrowed*/) int r = scr_tls_parse_crt(&c->ca, (const char *)auth.ca->data, auth.ca->len); if (r != 0) scr_tls_bad_material("ca certificate", r); } + c->has_ca = true; scr_bytes_release(auth.ca); } scr_bytes_release(cert); @@ -2810,8 +2852,10 @@ static int scr_tls_f_cert(mbedtls_ssl_context *ssl) { scr_tls_chained_copy(&t->served_chain, &pick->cert, store); own = &t->served_chain; } - if (s->request_cert) - mbedtls_ssl_set_hs_ca_chain(ssl, pick->ca.raw.len > 0 ? &pick->ca : scr_tls_system_ca(), NULL); + if (s->request_cert) { + t->ctx_verifies = true; + mbedtls_ssl_set_hs_ca_chain(ssl, pick->has_ca ? &pick->ca : scr_tls_system_ca(), NULL); + } return mbedtls_ssl_set_hs_own_cert(ssl, own, &pick->pk); } @@ -3261,8 +3305,7 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, } if (strcmp(e->key, "ca") == 0) { scr_bytes_release(ca); - ca = scr_tls_pem_dyn(v, "a tls.connect 'ca' option", true); - if (ca == NULL) ok = false; + if (!scr_tls_ca_dyn(v, "a tls.connect 'ca' option", &ca)) ok = false; continue; } if (strcmp(e->key, "cert") == 0 || strcmp(e->key, "key") == 0) { @@ -3386,8 +3429,7 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, 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); + scr_tls_ca_data(ca), ca != NULL ? ca->len : 0, !reject); if (servername != NULL) scr_tls_cli_set_servername(cli, servername); if (own_cert != NULL) scr_tls_cli_set_own(cli, own_cert, own_key); scr_bytes_release(own_cert); @@ -3441,7 +3483,7 @@ void scr_tls_server_wrap_h2(ScrNetServer *s, const char *cert, size_t cert_len, /* The h2-over-TLS CLIENT wrap (http2.connect with an https authority): * a client transport offering ALPN ["h2"] — SNI/verification against - * the authority host; `ca` replaces the trust anchors ("" = none) and + * the authority host; `ca` replaces the trust anchors (NULL = none) and * reject_unauthorized false records like tls.connect's. The session's * preface/SETTINGS ride the socket's write buffer until establishment. */ void scr_tls_h2_client_wrap(ScrNetSocket *sock, ScrStr *host /*borrowed*/, @@ -3595,7 +3637,8 @@ void scr_tls_sock_each_trusted(ScrNetSocket *s, bool (*visit)(void *ctx, const u void *ctx) { ScrTlsSock *t = scr_tls_sock_of(s); if (t == NULL || (t->cli == NULL && t->srv == NULL)) return; - const mbedtls_x509_crt *c = t->cli != NULL ? (t->cli->ca.raw.len > 0 ? &t->cli->ca : scr_tls_system_ca()) + const mbedtls_x509_crt *c = t->cli != NULL ? (t->cli->has_ca ? &t->cli->ca : scr_tls_system_ca()) + : t->ctx_verifies ? (t->sni_ctx->has_ca ? &t->sni_ctx->ca : scr_tls_system_ca()) : t->srv->has_ca ? &t->srv->ca : scr_tls_system_ca(); for (; c != NULL && c->raw.len > 0; c = c->next) diff --git a/packages/runtime/src/scr_tls.h b/packages/runtime/src/scr_tls.h index 94bf41d33..8e044f12d 100644 --- a/packages/runtime/src/scr_tls.h +++ b/packages/runtime/src/scr_tls.h @@ -85,7 +85,7 @@ void scr_tls_sock_on_secure(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once * every http2-using binary links the TLS unit (the moduleUsesTls * switch), so no registration indirection is needed. */ void scr_tls_server_wrap_h2(ScrNetServer *s, const char *cert, size_t cert_len, const char *key, size_t key_len, ScrNetNativeConnFn h2_conn, void *h2_ctx /*moves*/, void (*h2_ctx_free)(void *), ScrClosure *sni_cb /*moves, nullable*/, void *sni_answer_fn); -void scr_tls_h2_client_wrap(ScrNetSocket *sock, ScrStr *host /*borrowed*/, bool reject_unauthorized, const char *ca /*borrowed, len 0 = none*/, size_t ca_len); +void scr_tls_h2_client_wrap(ScrNetSocket *sock, ScrStr *host /*borrowed*/, bool reject_unauthorized, const char *ca /*borrowed, NULL = none*/, size_t ca_len); /* ── tls.SecureContext + the SNI callback (scr_tls.c) ───────────────── * A SecureContext is an opaque refcounted parsed cert/key pair diff --git a/tests/corpus/8176-tls-empty-context-ca.ts b/tests/corpus/8176-tls-empty-context-ca.ts new file mode 100644 index 000000000..510c9881a --- /dev/null +++ b/tests/corpus/8176-tls-empty-context-ca.ts @@ -0,0 +1,73 @@ +// An explicitly empty ca is an empty trust store for client certificates, +// in the server's options, in setSecureContext and in addContext, even when +// the client certificate's issuer is a default CA +// (tls.setDefaultCACertificates); an omitted ca trusts the default CAs. +// Each server has requestCert, with rejectUnauthorized false (the server's +// verdict) and true (the client is dropped). +import { readFileSync } from "node:fs"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f: string): Buffer => readFileSync(`${D}/${f}`); +const ca = r("ca.pem"), cert = r("localhost.pem"), key = r("localhost-key.pem"); +const alt = r("alt.pem"), altKey = r("alt-key.pem"); +const client = r("client.pem"), clientKey = r("client-key.pem"); + +tls.setDefaultCACertificates([ca.toString()]); + +function listen(server: tls.Server): Promise { + return new Promise((res) => server.listen(0, () => res((server.address() as AddressInfo).port))); +} + +type Handler = (s: tls.TLSSocket) => void; + +async function run(label: string, make: (h: Handler) => tls.Server, servername: string): Promise { + const events: string[] = []; + const server = make((s) => { + events.push(`server authorized=${s.authorized} error=${s.authorizationError}`); + s.end("ok"); + }); + const port = await listen(server); + await new Promise((done) => { + const c = tls.connect({ port, servername, ca, cert: client, key: clientKey }); + c.on("secureConnect", () => events.push("client secureConnect")); + c.on("data", (d: Buffer) => events.push(`client data ${d}`)); + c.on("error", (e: NodeJS.ErrnoException) => events.push(`client error ${e.code}`)); + c.on("close", () => setTimeout(done, 50)); + }); + console.log(label, "|", events.join(" ; ")); + await new Promise((res) => server.close(() => res())); +} + +for (const reject of [false, true]) { + const opts = { cert, key, requestCert: true, rejectUnauthorized: reject }; + const tag = reject ? "reject" : "request"; + await run(`${tag} omitted`, (h) => tls.createServer(opts, h), "localhost"); + await run(`${tag} empty array`, (h) => tls.createServer({ ...opts, ca: [] }, h), "localhost"); + await run(`${tag} setSecureContext empty`, (h) => { + const server = tls.createServer(opts, h); + server.setSecureContext({ cert, key, ca: [] }); + return server; + }, "localhost"); + await run(`${tag} setSecureContext omitted`, (h) => { + const server = tls.createServer({ ...opts, ca: [] }, h); + server.setSecureContext({ cert, key }); + return server; + }, "localhost"); + await run(`${tag} addContext empty array`, (h) => { + const server = tls.createServer(opts, h); + server.addContext("alt.localhost", { cert: alt, key: altKey, ca: [] }); + return server; + }, "alt.localhost"); + await run(`${tag} addContext empty Buffer`, (h) => { + const server = tls.createServer(opts, h); + server.addContext("alt.localhost", tls.createSecureContext({ cert: alt, key: altKey, ca: Buffer.alloc(0) })); + return server; + }, "alt.localhost"); + await run(`${tag} addContext omitted`, (h) => { + const server = tls.createServer({ ...opts, ca: [] }, h); + server.addContext("alt.localhost", { cert: alt, key: altKey }); + return server; + }, "alt.localhost"); +} diff --git a/tests/corpus/8177-tls-empty-client-ca.ts b/tests/corpus/8177-tls-empty-client-ca.ts new file mode 100644 index 000000000..0fb596ddb --- /dev/null +++ b/tests/corpus/8177-tls-empty-client-ca.ts @@ -0,0 +1,104 @@ +// An explicitly empty ca is an empty trust store for a client's view of +// the server certificate, in tls.connect, https.request and http2.connect, +// even when the server certificate's issuer is a default CA +// (tls.setDefaultCACertificates); an omitted ca, or an empty string, trusts +// the default CAs. +import { readFileSync } from "node:fs"; +import * as http2 from "node:http2"; +import * as https from "node:https"; +import type { AddressInfo } from "node:net"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f: string): Buffer => readFileSync(`${D}/${f}`); +const ca = r("ca.pem"), key = r("localhost-key.pem"); +// The servers send their issuer too, so each client sees the same chain. +const cert = Buffer.concat([r("localhost.pem"), ca]); + +tls.setDefaultCACertificates([ca.toString()]); + +function listen(server: tls.Server | https.Server | http2.Http2SecureServer): Promise { + return new Promise((res) => server.listen(0, () => res((server.address() as AddressInfo).port))); +} + +async function watch(label: string, c: tls.TLSSocket): Promise { + const events: string[] = []; + await new Promise((done) => { + c.on("secureConnect", () => { + events.push(`secureConnect authorized=${c.authorized} error=${c.authorizationError}`); + c.end(); + }); + c.on("error", (e: NodeJS.ErrnoException) => events.push(`error ${e.code}`)); + c.on("close", () => done()); + }); + console.log("tls.connect", label, "|", events.join(" ; ")); +} + +{ + const server = tls.createServer({ cert, key }, (s) => s.end()); + const port = await listen(server); + const host = "localhost"; + await watch("omitted", tls.connect({ port, host })); + await watch("empty string", tls.connect({ port, host, ca: "" })); + await watch("empty Buffer", tls.connect({ port, host, ca: Buffer.alloc(0) })); + await watch("empty array", tls.connect({ port, host, ca: [] })); + await watch( + "empty Buffer, rejectUnauthorized false", + tls.connect({ port, host, ca: Buffer.alloc(0), rejectUnauthorized: false }), + ); + await watch("empty array, rejectUnauthorized false", tls.connect({ port, host, ca: [], rejectUnauthorized: false })); + server.close(); +} + +function report(label: string, req: ReturnType, done: () => void): void { + req.on("response", (res) => { + console.log("https.request", label, "| status", res.statusCode); + res.resume(); + res.on("end", done); + }); + req.on("error", (e: NodeJS.ErrnoException) => { + console.log("https.request", label, "| error", e.code); + done(); + }); + req.end(); +} + +{ + const server = https.createServer({ cert, key }, (_req, res) => res.end("ok")); + const port = await listen(server); + await new Promise((done) => + report("omitted", https.request({ host: "localhost", port, path: "/", agent: false }), done), + ); + await new Promise((done) => + report("empty string", https.request({ host: "localhost", port, path: "/", agent: false, ca: "" }), done), + ); + await new Promise((done) => + report( + "empty Buffer", + https.request({ host: "localhost", port, path: "/", agent: false, ca: Buffer.alloc(0) }), + done, + ), + ); + const options: unknown = { host: "localhost", port, path: "/", agent: false, ca: [] }; + await new Promise((done) => + report("empty array in a runtime record", https.request(options as https.RequestOptions), done), + ); + server.close(); +} + +{ + const server = http2.createSecureServer({ cert, key }, (_req, res) => res.end("ok")); + const port = await listen(server); + for (const [label, ca] of [["omitted", undefined], ["empty Buffer", Buffer.alloc(0)]] as const) { + await new Promise((done) => { + const session = ca === undefined ? http2.connect(`https://localhost:${port}`) : http2.connect(`https://localhost:${port}`, { ca }); + session.on("connect", () => { + console.log("http2.connect", label, "| connect"); + session.close(); + }); + session.on("error", (e: NodeJS.ErrnoException) => console.log("http2.connect", label, "| error", e.code)); + session.on("close", () => done()); + }); + } + server.close(); +} diff --git a/tests/harness/corpus-costs/8176-tls-empty-context-ca.ts.json b/tests/harness/corpus-costs/8176-tls-empty-context-ca.ts.json new file mode 100644 index 000000000..2f8507d0f --- /dev/null +++ b/tests/harness/corpus-costs/8176-tls-empty-context-ca.ts.json @@ -0,0 +1 @@ +{ "plain": 7322, "san": 94064 } diff --git a/tests/harness/corpus-costs/8177-tls-empty-client-ca.ts.json b/tests/harness/corpus-costs/8177-tls-empty-client-ca.ts.json new file mode 100644 index 000000000..d7df308c8 --- /dev/null +++ b/tests/harness/corpus-costs/8177-tls-empty-client-ca.ts.json @@ -0,0 +1 @@ +{ "plain": 4335, "san": 61880 } From 53baad4ebee91f746f0e4c310c889cfa1cff4dde Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 10:12:06 -0500 Subject: [PATCH 062/273] Pump untyped iterable pipeline sources A pipeline whose first stage is a checked-dynamic value now checks it at run time, as Node's stage ladder does: a stream is piped as before, and a string or a value with a Symbol.asyncIterator or Symbol.iterator method is pumped into the next stage through an async generator over its iterator. A typed Set, Map, generator or async generator object as the first stage takes the same path. This covers the callback and stream/promises forms, with or without function stages and pipeline options. A write whose callback fails synchronously no longer sets needDrain, so no 'drain' follows the error and a pumped source stops at the failed write, as in Node. RUNTIME_ABI_VERSION goes from 37 to 42 for scr_stream_dyn_iterable_source. --- .../builtin-apis/streams-and-readline.mdx | 5 +- packages/cli/test/native-link-info.test.ts | 4 +- .../compiler/src/backend/llvm/lib-shared.ts | 1 + packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../src/frontend/lowering/lower-stream.ts | 626 ++++++++++++++---- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 4 + packages/compiler/src/ir/runtime-features.ts | 1 + .../test/native-codegen-integration.test.ts | 4 +- .../7503-pipeline-untyped-sources.mjs.json | 6 + ...04-pipeline-untyped-source-errors.mjs.json | 6 + .../7505-pipeline-untyped-sources.cjs.json | 6 + .../corpus/7506-pipeline-any-source.ts.json | 6 + .../7507-stream-write-error-drain.mjs.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_stream.c | 38 +- packages/runtime/src/scr_stream.h | 4 + .../corpus/7503-pipeline-untyped-sources.mjs | 102 +++ .../7504-pipeline-untyped-source-errors.mjs | 90 +++ .../corpus/7505-pipeline-untyped-sources.cjs | 40 ++ tests/corpus/7506-pipeline-any-source.ts | 48 ++ .../corpus/7507-stream-write-error-drain.mjs | 37 ++ .../7503-pipeline-untyped-sources.mjs.json | 1 + ...04-pipeline-untyped-source-errors.mjs.json | 1 + .../7505-pipeline-untyped-sources.cjs.json | 1 + .../7506-pipeline-any-source.ts.json | 1 + .../7507-stream-write-error-drain.mjs.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- 33 files changed, 911 insertions(+), 147 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7503-pipeline-untyped-sources.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7504-pipeline-untyped-source-errors.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7505-pipeline-untyped-sources.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7506-pipeline-any-source.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7507-stream-write-error-drain.mjs.json create mode 100644 tests/corpus/7503-pipeline-untyped-sources.mjs create mode 100644 tests/corpus/7504-pipeline-untyped-source-errors.mjs create mode 100644 tests/corpus/7505-pipeline-untyped-sources.cjs create mode 100644 tests/corpus/7506-pipeline-any-source.ts create mode 100644 tests/corpus/7507-stream-write-error-drain.mjs create mode 100644 tests/harness/corpus-costs/7503-pipeline-untyped-sources.mjs.json create mode 100644 tests/harness/corpus-costs/7504-pipeline-untyped-source-errors.mjs.json create mode 100644 tests/harness/corpus-costs/7505-pipeline-untyped-sources.cjs.json create mode 100644 tests/harness/corpus-costs/7506-pipeline-any-source.ts.json create mode 100644 tests/harness/corpus-costs/7507-stream-write-error-drain.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 f9d69e3db..561840268 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -3,7 +3,8 @@ `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. + The first `pipeline` stage may arrive as an untyped value that is a native stream, a `Cipheriv` or `Decipheriv` value, or an iterable: a string, an array, a generator object, or any value with a `Symbol.asyncIterator` or `Symbol.iterator` method, which the pipeline pumps into the next stage as Node does. + Later `pipeline` stages and `for await` sources 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 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. @@ -14,7 +15,7 @@ `node:stream/consumers` and readline interfaces read object-mode streams of strings and Buffers; other values throw a not-supported error. `pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), an async generator function as the destination, and an async function destination whose value resolves the promise form and reaches the callback's second argument. A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns. - 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. + The source may also be an array, a string, a `Set`, a `Map`, a generator or async 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. 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`). diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 8ac579449..5e862e126 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: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 42, marker: "scr_runtime_abi_v42" }, 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_v37"); + expect(error?.stderr).toContain("scr_runtime_abi_v42"); }); }); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 7a16c37e8..9c38c0983 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -795,6 +795,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.dynIterableSource": "scr_stream_dyn_iterable_source", "stream.handleStream": "scr_stream_handle", "stream.pipeDyn": "scr_stream_pipe_dyn", "stream.unpipeDyn": "scr_stream_unpipe_dyn", diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 11bde6c2b..0eda18a5d 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 = 37 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v37" as const; +export const RUNTIME_ABI_VERSION = 42 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v42" 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 9846e8d89..6f39c6d08 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: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 42, marker: "scr_runtime_abi_v42" }, 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 e2b24f8ca..4561fad54 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -128,7 +128,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 42, marker: "scr_runtime_abi_v42" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 04c2fa043..72bea5dd7 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -2837,6 +2837,107 @@ export function streamClassAliasDecl( const PIPELINE_STAGE_EXPECTED = "of type function or an instance of Blob, ReadableStream, WritableStream, Stream, Iterable, AsyncIterable, or Promise or { readable, writable } pair"; +/** A checked-dynamic pipeline stage (an untyped source or destination): + * a boxed native stream unwraps at run time; other values (functions, + * pairs) throw the not-supported Error there. */ +function unboxDynStage(lowerer: Lowerer, v: IrExpr, node: ts.Expression): IrExpr { + noteUntypedStreamFlow(lowerer, null); + return { + kind: "libCall", + fn: "stream.unboxDyn", + args: [ + v, + strLit( + lowerer.dynNodeStreams + ? "pipeline stages other than native streams (JavaScript stream objects, iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet" + : "pipeline stages other than native streams (iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet", + locOf(node), + ), + ], + type: { kind: "object", className: "%Duplex" }, + loc: locOf(node), + }; +} + +/** A pipeline whose first stage is checked-dynamic, lowered with that + * stage already evaluated: `expr` is the stream it unboxes to, or, when + * `iterable` is set, the value to pump as an iterable source. */ +interface DynPipelineSource { + expr: IrExpr; + iterable: boolean; +} + +/** pipeline() whose first stage is a stream argument (not a function + * stage, an iterable of a static type, or a cipher value), lowered here. + * When the value is checked-dynamic, Node keeps a stream there and pipes + * it, and keeps an iterable (a string, an array, a generator, any value + * with an iterator method) and pumps it into the next stage. The value is + * evaluated once, and stream.dynIterableSource picks the lowering at run + * time. Null when the first stage is not such an argument. */ +function lowerDynSourcePipeline( + lowerer: Lowerer, + node: ts.Expression, + loc: SrcLoc, + build: (source: DynPipelineSource) => IrExpr, +): IrExpr | null { + if (pipelineStageFunction(lowerer, node) !== null) return null; + if (iterableSourceType(lowerer, node) !== null || isValueStream(lowerer, node)) return null; + let value = lowerer.lowerExpr(node); + // A Set, a Map or a generator object is iterated through its + // checked-dynamic value. + if (BOXED_SOURCE_KINDS.has(value.type.kind) && lowerer.dynConvertible(value.type)) { + value = { kind: "dynFrom", value, type: DYN, loc: value.loc }; + } + if (value.type.kind !== "dyn") { + let stream: IrExpr; + try { + stream = streamArgOf(lowerer, node, value, "pipeline"); + } catch (e) { + if (e instanceof StreamArgTypeThrow) return e.expr; + throw e; + } + return build({ expr: stream, iterable: false }); + } + const local = lowerer.declareHiddenLocal("%pipelineSource", DYN); + const ref = varRef(local.id, DYN, loc); + const stream = build({ expr: unboxDynStage(lowerer, ref, node), iterable: false }); + const iterable = build({ expr: ref, iterable: true }); + const type = stream.type; + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }], + result: { + kind: "ternary", + cond: { kind: "libCall", fn: "stream.dynIterableSource", args: [ref], type: BOOL, loc }, + then: lowerer.coerceToExpected(iterable, type), + else_: stream, + type, + loc, + }, + type, + loc, + }; +} + +/** The static types of a pipeline source that lowerDynSourcePipeline boxes + * as checked-dynamic values. An array or a string reaches it when the + * checker types the argument as any while its value keeps a static type. */ +const BOXED_SOURCE_KINDS: ReadonlySet = new Set([ + "set", + "map", + "generator", + "array", + "string", +]); + +/** lowerPipelineStages' source arguments for a checked-dynamic first stage. */ +function stagesSource( + src: DynPipelineSource | null, +): [ReadonlyMap | undefined, IrExpr | undefined] { + if (src === null) return [undefined, undefined]; + return src.iterable ? [undefined, src.expr] : [new Map([[0, src.expr]]), undefined]; +} + /** A stream-rooted argument, lowered — or the pointed fence (pipeline's * iterable/generator/function stages have no lowering yet). `what` picks * the validation ladder: finished() rejects EVERY non-stream through @@ -2848,7 +2949,16 @@ function lowerStreamArg( node: ts.Expression, what: "finished" | "pipeline" | "addAbortSignal" | "compose", ): IrExpr { - const lowered = lowerer.lowerExpr(node); + return streamArgOf(lowerer, node, lowerer.lowerExpr(node), what); +} + +/** lowerStreamArg over the argument's lowered value. */ +function streamArgOf( + lowerer: Lowerer, + node: ts.Expression, + lowered: IrExpr, + what: "finished" | "pipeline" | "addAbortSignal" | "compose", +): IrExpr { // 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) @@ -2857,24 +2967,7 @@ function lowerStreamArg( // A checked-dynamic pipeline stage (an untyped source or destination): // a boxed native stream unwraps at run time; other values (iterables, // functions) throw the not-supported Error there. - if (v.type.kind === "dyn" && what === "pipeline") { - noteUntypedStreamFlow(lowerer, null); - return { - kind: "libCall", - fn: "stream.unboxDyn", - args: [ - v, - strLit( - lowerer.dynNodeStreams - ? "pipeline stages other than native streams (JavaScript stream objects, iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet" - : "pipeline stages other than native streams (iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet", - locOf(node), - ), - ], - type: { kind: "object", className: "%Duplex" }, - loc: locOf(node), - }; - } + if (v.type.kind === "dyn" && what === "pipeline") return unboxDynStage(lowerer, v, node); const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined; if (!streamSidesOf(lowerer, info)) { // A provably-non-stream value in a JS source (the invalid-input @@ -3210,57 +3303,82 @@ export function lowerStreamModuleCall( ? pipelineOptions(lowerer, lastArg) : null; const stageArgs = opts ? args.slice(0, -1) : args; - if (hasPipelineFunctionStage(lowerer, stageArgs)) { - const staged = lowerPipelineStages(lowerer, stageArgs, loc); - const type = staged.destPromise ?? { kind: "promise", inner: VOID }; - // A promise destination's value settles the result (same type). - 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 { - streams = stageArgs.map((a) => lowerStreamArg(lowerer, a, "pipeline")); - } catch (e) { - if (e instanceof StreamArgTypeThrow) return e.expr; - throw e; - } - if (opts) { - const head = [numLit(streams.length, loc), ...streams, boolLit(opts.end, loc)]; - return { - kind: "libCall", - fn: opts.signal ? "sp.pipelineOptsSignal" : "sp.pipelineOpts", - args: opts.signal ? [...head, opts.signal] : head, - type: { kind: "promise", inner: VOID }, - loc, - }; - } - return { - kind: "libCall", - fn: "sp.pipeline", - args: [numLit(streams.length, loc), ...streams], - type: { kind: "promise", inner: VOID }, - loc, - }; + const build = (src: DynPipelineSource | null): IrExpr => + lowerPromisePipeline(lowerer, stageArgs, opts, src, loc); + return lowerDynSourcePipeline(lowerer, stageArgs[0]!, loc, build) ?? build(null); } return null; } + return lowerStreamModuleCallbackCall(lowerer, call, bi, loc); +} + +/** stream/promises pipeline(...stages[, options]), with the first stage + * pre-lowered when it is checked-dynamic (lowerDynSourcePipeline). */ +function lowerPromisePipeline( + lowerer: Lowerer, + stageArgs: readonly ts.Expression[], + opts: ReturnType | null, + src: DynPipelineSource | null, + loc: SrcLoc, +): IrExpr { + if (src?.iterable === true || hasPipelineFunctionStage(lowerer, stageArgs)) { + const staged = lowerPipelineStages(lowerer, stageArgs, loc, ...stagesSource(src)); + const type = staged.destPromise ?? { kind: "promise", inner: VOID }; + // A promise destination's value settles the result (same type). + 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 { + streams = stageArgs.map((a, i) => + i === 0 && src !== null ? src.expr : lowerStreamArg(lowerer, a, "pipeline"), + ); + } catch (e) { + if (e instanceof StreamArgTypeThrow) return e.expr; + throw e; + } + if (opts) { + const head = [numLit(streams.length, loc), ...streams, boolLit(opts.end, loc)]; + return { + kind: "libCall", + fn: opts.signal ? "sp.pipelineOptsSignal" : "sp.pipelineOpts", + args: opts.signal ? [...head, opts.signal] : head, + type: { kind: "promise", inner: VOID }, + loc, + }; + } + return { + kind: "libCall", + fn: "sp.pipeline", + args: [numLit(streams.length, loc), ...streams], + type: { kind: "promise", inner: VOID }, + loc, + }; +} + +/** The node:stream and stream/consumers members of lowerStreamModuleCall. */ +function lowerStreamModuleCallbackCall( + lowerer: Lowerer, + call: ts.CallExpression, + bi: { module: string; member: string }, + loc: SrcLoc, +): IrExpr | null { if (bi.module === "stream/consumers") { const args = call.arguments.nodes; // text/json/buffer/arrayBuffer(stream) → a pending promise the @@ -3411,67 +3529,9 @@ export function lowerStreamModuleCall( "the supported form is pipeline(source, ...streams, callback)", ); } - if (hasPipelineFunctionStage(lowerer, args.slice(0, -1))) { - const staged = lowerPipelineStages(lowerer, args.slice(0, -1), loc); - 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: a typed callback whose error parameter cannot - // hold undefined takes null for no error; others take undefined. - const stagedArgs = [...staged.args]; - const flagsArg = stagedArgs[3]!; - if (flagsArg.kind !== "numLit") - throw new InternalCompilerError("pipeline flags not a literal"); - if ( - !isJsSourceFile(ts.getSourceFileOfNode(call)) && - !callbackErrorAdmitsUndefined(lowerer, cb) - ) - stagedArgs[3] = numLit(flagsArg.value | 128, loc); - return staged.wrap({ - kind: "libCall", - fn: "stream.pipelineStagesDyn", - args: [...stagedArgs, cb], - type: staged.last, - loc, - }); - } - const { cb, dyn } = lowerEosCallback( - lowerer, - "pipeline", - args[args.length - 1]!, - staged.last, - ); - return staged.wrap({ - kind: "libCall", - fn: dyn ? "stream.pipelineStagesDyn" : "stream.pipelineStages", - args: [...staged.args, cb], - type: staged.last, - loc, - }); - } - let streams: IrExpr[]; - try { - streams = args.slice(0, -1).map((a) => lowerStreamArg(lowerer, a, "pipeline")); - } catch (e) { - if (e instanceof StreamArgTypeThrow) return e.expr; - throw e; - } - const last = streams[streams.length - 1]!; - const { cb, dyn } = lowerEosCallback(lowerer, "pipeline", args[args.length - 1]!, last.type); - return { - kind: "libCall", - fn: dyn ? "stream.pipelineDyn" : "stream.pipeline", - args: [numLit(streams.length, loc), ...streams, cb], - type: last.type, - loc, - }; + const build = (src: DynPipelineSource | null): IrExpr => + lowerCallbackPipeline(lowerer, call, args, src, loc); + return lowerDynSourcePipeline(lowerer, args[0]!, loc, build) ?? build(null); } if (bi.member === "compose") { @@ -3484,6 +3544,75 @@ export function lowerStreamModuleCall( return null; } +/** pipeline(...stages, callback), with the first stage pre-lowered when it + * is checked-dynamic (lowerDynSourcePipeline). */ +function lowerCallbackPipeline( + lowerer: Lowerer, + call: ts.CallExpression, + args: readonly ts.Expression[], + src: DynPipelineSource | null, + loc: SrcLoc, +): IrExpr { + if (src?.iterable === true || hasPipelineFunctionStage(lowerer, args.slice(0, -1))) { + const staged = lowerPipelineStages(lowerer, args.slice(0, -1), loc, ...stagesSource(src)); + 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: a typed callback whose error parameter cannot + // hold undefined takes null for no error; others take undefined. + const stagedArgs = [...staged.args]; + const flagsArg = stagedArgs[3]!; + if (flagsArg.kind !== "numLit") + throw new InternalCompilerError("pipeline flags not a literal"); + if ( + !isJsSourceFile(ts.getSourceFileOfNode(call)) && + !callbackErrorAdmitsUndefined(lowerer, cb) + ) + stagedArgs[3] = numLit(flagsArg.value | 128, loc); + return staged.wrap({ + kind: "libCall", + fn: "stream.pipelineStagesDyn", + args: [...stagedArgs, cb], + type: staged.last, + loc, + }); + } + const { cb, dyn } = lowerEosCallback(lowerer, "pipeline", args[args.length - 1]!, staged.last); + return staged.wrap({ + kind: "libCall", + fn: dyn ? "stream.pipelineStagesDyn" : "stream.pipelineStages", + args: [...staged.args, cb], + type: staged.last, + loc, + }); + } + let streams: IrExpr[]; + try { + streams = args + .slice(0, -1) + .map((a, i) => (i === 0 && src !== null ? src.expr : lowerStreamArg(lowerer, a, "pipeline"))); + } catch (e) { + if (e instanceof StreamArgTypeThrow) return e.expr; + throw e; + } + const last = streams[streams.length - 1]!; + const { cb, dyn } = lowerEosCallback(lowerer, "pipeline", args[args.length - 1]!, last.type); + return { + kind: "libCall", + fn: dyn ? "stream.pipelineDyn" : "stream.pipeline", + args: [numLit(streams.length, loc), ...streams, cb], + type: last.type, + loc, + }; +} + /** 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. */ @@ -3788,6 +3917,237 @@ function lowerWebStreamSource( }; } +/** A checked-dynamic pipeline source that is an iterable at run time + * (stream.dynIterableSource), as an async generator closure that iterates + * it as Node's pipeline does with `for await`: the value's async iterator, + * or its sync iterator stepped like the typed iterable sources. Each value + * is yielded; leaving early closes the iterator through its return(). */ +function lowerDynIterableSource( + lowerer: Lowerer, + value: IrExpr, + loc: SrcLoc, +): { closure: IrExpr; out: SyncGenType } | null { + const out: SyncGenType = { + kind: "generator", + async: true, + yieldT: DYN, + retT: VOID, + nextT: UNDEFINED_T, + }; + const genResultT = genResultRecord(DYN, VOID, lowerer.shapes, lowerer.unions); + if (!genResultT) return null; + const key = "dynIterableSource"; + let maker = lowerer.valueHelpers.get(key); + if (!maker) { + maker = `%stream.dynIterableSource.${lowerer.valueHelpers.size}`; + const genName = `${maker}.gen`; + const frame = new LiftedFrame(loc); + const srcId = frame.local("src", DYN, false); + const itId = frame.local("it", DYN, false); + const syncId = frame.local("sync", BOOL, false); + const nextId = frame.local("next", DYN, false); + const stepId = frame.local("step", DYN, true); + const activeId = frame.local("active", BOOL, true); + const closeId = frame.local("close", DYN, false); + const it = (): IrExpr => varRef(itId, DYN, loc); + const sync = (): IrExpr => varRef(syncId, BOOL, loc); + const step = (): IrExpr => varRef(stepId, DYN, loc); + const get = (obj: IrExpr, name: string): IrExpr => ({ + kind: "dynKeyGet", + value: obj, + key: strLit(name, loc), + type: DYN, + loc, + }); + const awaitDyn = (v: IrExpr): IrExpr => ({ + kind: "libCall", + fn: "async.awaitDyn", + args: [v], + type: DYN, + loc, + }); + const call = (callee: IrExpr, calleeName: string): IrExpr => ({ + kind: "dynCall", + callee, + receiver: it(), + calleeName, + args: [], + type: DYN, + loc, + }); + const setActive = (on: boolean): IrStmt => ({ + kind: "assign", + localId: activeId, + value: boolLit(on, loc), + loc, + }); + const nextCall = (): IrExpr => call(varRef(nextId, DYN, loc), "iterator method"); + const body: IrStmt[] = [ + { + kind: "varDecl", + localId: itId, + init: { + kind: "libCall", + fn: "dyn.iteratorResult", + args: [ + { + kind: "libCall", + fn: "dyn.asyncIteratorOpen", + args: [varRef(srcId, DYN, loc), strLit("", loc)], + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: syncId, + init: { kind: "libCall", fn: "dyn.asyncIteratorWasSync", args: [], type: BOOL, loc }, + loc, + }, + { kind: "varDecl", localId: nextId, init: get(it(), "next"), loc }, + { kind: "varDecl", localId: stepId, init: dynUndefinedExpr(loc), loc }, + { kind: "varDecl", localId: activeId, init: boolLit(false, loc), loc }, + { + kind: "tryCatch", + tryBody: [ + { + kind: "while", + cond: boolLit(true, loc), + body: [ + setActive(false), + { + kind: "assign", + localId: stepId, + value: { + kind: "libCall", + fn: "dyn.iteratorResult", + args: [ + { + kind: "ternary", + cond: sync(), + then: nextCall(), + else_: awaitDyn(nextCall()), + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "dynTest", + test: "truthy", + value: get(step(), "done"), + type: BOOL, + loc, + }, + then: [{ kind: "break", loc }], + else_: null, + loc, + }, + setActive(true), + { + kind: "exprStmt", + expr: { kind: "yieldExpr", value: get(step(), "value"), type: VOID, loc }, + loc, + }, + ], + loc, + }, + ], + catchBody: null, + catchLocalId: null, + finallyBody: [ + { + kind: "if", + cond: varRef(activeId, BOOL, loc), + then: [ + { kind: "varDecl", localId: closeId, init: get(it(), "return"), loc }, + { + kind: "if", + cond: { + kind: "unary", + op: "!", + operand: { + kind: "dynTest", + test: "nullish", + value: varRef(closeId, DYN, loc), + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + then: [ + { + kind: "exprStmt", + expr: awaitDyn(call(varRef(closeId, DYN, loc), "iterator return")), + loc, + }, + ], + else_: null, + loc, + }, + ], + else_: null, + loc, + }, + ], + loc, + }, + { kind: "return", value: null, loc }, + ]; + const srcLocal = frame.locals.find((l) => l.id === srcId)!; + srcLocal.boxed = true; + lowerer.valueHelpers.set(key, maker); + lowerer.liftedFns.push({ + name: genName, + params: [], + returnType: VOID, + async: true, + captures: [{ localId: srcId, name: "src", type: DYN }], + locals: frame.locals, + body, + generator: { yieldT: DYN, nextT: UNDEFINED_T, resultType: genResultT }, + loc, + }); + const makerSrc: IrLocal = { id: "src.0", name: "src", type: DYN, mutable: false, boxed: true }; + lowerer.liftedFns.push({ + name: maker, + params: [{ localId: makerSrc.id, name: makerSrc.name, type: DYN }], + returnType: funcOf([], out), + locals: [makerSrc], + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: genName, + captures: [makerSrc.id], + type: funcOf([], out), + loc, + }, + loc, + }, + ], + loc, + }); + } + return { + closure: { kind: "call", callee: maker, args: [value], type: funcOf([], out), loc }, + out, + }; +} + function lowerIterableSource( lowerer: Lowerer, node: ts.Expression, @@ -3951,6 +4311,7 @@ function lowerPipelineStages( stageArgs: readonly ts.Expression[], loc: SrcLoc, preLowered?: ReadonlyMap, + dynSource?: IrExpr, ): PipelineStagesLowering { if (stageArgs.length > 52) { lowerer.noLowering( @@ -3975,6 +4336,15 @@ function lowerPipelineStages( const argOrder: { list: IrExpr[]; index: number }[] = []; for (let j = 0; j < stageArgs.length; j++) { const arg = stageArgs[j]!; + if (j === 0 && dynSource !== undefined) { + const source = lowerDynIterableSource(lowerer, dynSource, 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; + } // compose's head and tail functions arrive as Duplex.from streams const fn = preLowered?.has(j) ? null : pipelineStageFunction(lowerer, arg); const syncSourceFn = diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 390c7fbc3..1821b6a6b 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -91,6 +91,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "procStream.write", "child.stdioStream", "stream.unboxDyn", + "stream.dynIterableSource", "stream.handleStream", "stream.pipeDyn", "stream.unpipeDyn", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 216e46a4d..2cc5c708e 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2786,6 +2786,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ argTypes: [DYN, STRING], result: { kind: "object", className: "%Duplex" }, }, + /** Whether pipeline() pumps a checked-dynamic first stage as an + * iterable (a string, or a value with an iterator method that is not a + * stream with pipe() and on()) instead of piping it as a stream. */ + "stream.dynIterableSource": { argTypes: [DYN], result: BOOL }, /** The Transform a Cipheriv or Decipheriv reads and writes as a stream * (+1, created on first use and owned by the cipher). */ "stream.handleStream": { diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 85b21af7b..f66697567 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -263,6 +263,7 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "stream.prop", "stream.stdioWritable", "stream.unboxDyn", + "stream.dynIterableSource", "stream.handleStream", "stream.unpipeDyn", "stream.workerStdio", diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index cecebb49c..1be1b7a3f 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_v37"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v42"); 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_v37"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v42"); }); 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/7503-pipeline-untyped-sources.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7503-pipeline-untyped-sources.mjs.json new file mode 100644 index 000000000..4d93af132 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7503-pipeline-untyped-sources.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7503-pipeline-untyped-sources.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7504-pipeline-untyped-source-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7504-pipeline-untyped-source-errors.mjs.json new file mode 100644 index 000000000..19402586b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7504-pipeline-untyped-source-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7504-pipeline-untyped-source-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7505-pipeline-untyped-sources.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7505-pipeline-untyped-sources.cjs.json new file mode 100644 index 000000000..29d85351d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7505-pipeline-untyped-sources.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7505-pipeline-untyped-sources.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7506-pipeline-any-source.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7506-pipeline-any-source.ts.json new file mode 100644 index 000000000..942a2b3e8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7506-pipeline-any-source.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7506-pipeline-any-source.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7507-stream-write-error-drain.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7507-stream-write-error-drain.mjs.json new file mode 100644 index 000000000..6fa55d09d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7507-stream-write-error-drain.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7507-stream-write-error-drain.mjs" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index b6b855c05..56f5ca559 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -366,7 +366,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 42, marker: "scr_runtime_abi_v42" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 479d8aad4..c571a25c5 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -469,7 +469,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 42, marker: "scr_runtime_abi_v42" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 086340014..6c15f932c 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_v37(void) {} +void scr_runtime_abi_v42(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 3707b8d50..5415ccdc6 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_v37(void); +void scr_runtime_abi_v42(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 ec061e10c..be6d3635f 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -6866,10 +6866,12 @@ static bool scr_stream_write_chunk_enc(ScrStream *s, ScrBytes *chunk /*borrowed* 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. */ - bool ret = st->w.length < st->w.hwm && !st->destroyed && !st->errored; - if (!ret) st->w.need_drain = true; - return ret; + * a sync completion has already drained its bytes from length. Only + * the length sets needDrain; an errored or destroyed stream answers + * false without waiting for 'drain'. */ + bool below = st->w.length < st->w.hwm; + if (!below) st->w.need_drain = true; + return below && !st->destroyed && !st->errored; } static bool scr_stream_write_chunk(ScrStream *s, ScrBytes *chunk /*borrowed*/, ScrClosure *cb /*moves*/) { @@ -8407,3 +8409,31 @@ ScrStream *scr_stream_unbox_dyn(const ScrDyn *value, const ScrStr *message) { scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); return NULL; } + +/* pipeline.js's first stage: a value with pipe() and on() methods is a + * readable node stream, which the stage ladder tests before isIterable; + * a string, or a value with a Symbol.asyncIterator or Symbol.iterator + * method, is an iterable the pipeline pumps into the next stage. */ +bool scr_stream_dyn_iterable_source(const ScrDyn *value) { + ScrStream *stream = scr_stream_of_handle(value); + if (!stream) stream = scr_stream_boxed(value); + if (stream) { + scr_stream_release(stream); + return false; + } + if (value->kind == SCR_DYN_STR) return true; + if (value->kind == SCR_DYN_OBJ) { + ScrDyn *pipe = scr_dyn_obj_read(value, "pipe", 4); + if (!pipe) return false; + bool streamish = pipe->kind == SCR_DYN_FUNC; + scr_dyn_release(pipe); + if (streamish) { + ScrDyn *on = scr_dyn_obj_read(value, "on", 2); + if (!on) return false; + streamish = on->kind == SCR_DYN_FUNC; + scr_dyn_release(on); + if (streamish) return false; + } + } + return scr_dyn_iterable_kind(value) > 0; +} diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index a4798c629..7acad0eeb 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -145,6 +145,10 @@ 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); +/* Whether pipeline() keeps a checked-dynamic first stage as an iterable + * to pump rather than as a stream (false with an exception pending when + * reading the value's members throws). */ +bool scr_stream_dyn_iterable_source(const struct ScrDyn *value); /* 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. diff --git a/tests/corpus/7503-pipeline-untyped-sources.mjs b/tests/corpus/7503-pipeline-untyped-sources.mjs new file mode 100644 index 000000000..3dbc90eee --- /dev/null +++ b/tests/corpus/7503-pipeline-untyped-sources.mjs @@ -0,0 +1,102 @@ +// pipeline() over untyped iterable sources: arrays, strings, Sets, generator +// objects, async generator objects, class iterables and object values, +// into Writables, a Transform chain, a function stage and the promise form. +// Each source is pumped as Node pumps an iterable, on the same ticks. +import { pipeline, Writable, Transform } from "node:stream"; +import { pipeline as pipelineP } from "node:stream/promises"; + +const log = []; +function show(chunk) { + return typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString() : JSON.stringify(chunk); +} +function objectSink(label) { + return new Writable({ + objectMode: true, + write(chunk, _enc, cb) { + log.push(`${label} write ${show(chunk)}`); + cb(); + }, + final(cb) { log.push(`${label} final`); cb(); }, + }); +} +function sink(label, objectMode = false) { + if (objectMode) return objectSink(label); + return new Writable({ + write(chunk, _enc, cb) { + log.push(`${label} write ${typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString() : JSON.stringify(chunk)}`); + cb(); + }, + final(cb) { log.push(`${label} final`); cb(); }, + }); +} +function* gen() { + log.push("gen start"); + yield "g1"; + log.push("gen mid"); + yield "g2"; + log.push("gen end"); +} +async function* agen() { + yield "a1"; + await null; + yield "a2"; +} +const sources = { + array: () => ["x", "y"], + string: () => "hey", + set: () => new Set(["s1", "s2"]), + gen: () => gen(), + agen: () => agen(), + iterable: () => ({ *[Symbol.iterator]() { yield "i1"; yield "i2"; } }), + objects: () => [{ n: 1 }, { n: 2 }], +}; +const names = Object.keys(sources); +async function run(name) { + const src = sources[name](); + const objectMode = name === "objects"; + await new Promise((resolve) => { + queueMicrotask(() => log.push(`${name} microtask`)); + pipeline(src, sink(name, objectMode), (err) => { + log.push(`${name} done ${err ? err.code ?? err.message : err}`); + resolve(); + }); + log.push(`${name} after call`); + }); +} +for (const n of names) await run(n); + +// a transform chain and a function stage after an untyped source +const src2 = ["p", "q"]; +await new Promise((resolve) => + pipeline( + src2, + new Transform({ transform(c, _e, cb) { cb(null, c.toString().toUpperCase()); } }), + sink("transform"), + (err) => { log.push(`transform done ${err}`); resolve(); }, + ), +); +const src3 = new Set(["f1", "f2"]); +await new Promise((resolve) => + pipeline( + src3, + async function* (source) { for await (const c of source) yield c + "!"; }, + sink("fnstage"), + (err) => { log.push(`fnstage done ${err}`); resolve(); }, + ), +); +// promises form +const src4 = ["m", "n"]; +await pipelineP(src4, sink("promises")); +log.push("promises resolved"); +// an error thrown by the source +function* bad() { yield "ok"; throw new Error("source failed"); } +const src5 = bad(); +await new Promise((resolve) => + pipeline(src5, sink("bad"), (err) => { log.push(`bad done ${err && err.message}`); resolve(); }), +); +// a source of non-string values into a byte-mode writable +const src6 = [1, 2]; +await new Promise((resolve) => + pipeline(src6, sink("numbers"), (err) => { log.push(`numbers done ${err && err.code}`); resolve(); }), +); +console.log(log.join("\n")); diff --git a/tests/corpus/7504-pipeline-untyped-source-errors.mjs b/tests/corpus/7504-pipeline-untyped-source-errors.mjs new file mode 100644 index 000000000..fcbfa9d60 --- /dev/null +++ b/tests/corpus/7504-pipeline-untyped-source-errors.mjs @@ -0,0 +1,90 @@ +// pipeline() over untyped iterable sources: a destination that fails closes +// the source, backpressure waits for drain, an abort signal stops the pump, +// and async generator and Map sources reach object-mode destinations. +import { pipeline, Writable, PassThrough } from "node:stream"; +import { pipeline as pipelineP } from "node:stream/promises"; + +function* counted(label, n) { + try { + for (let i = 0; i < n; i++) { + console.log(label, "yield", i); + yield `${label}${i}`; + } + } finally { + console.log(label, "finally"); + } +} +function pick(v) { + return v; +} + +// the destination fails on its second write: the source's finally runs +await new Promise((resolve) => { + let n = 0; + const dst = new Writable({ + write(chunk, _e, cb) { + n++; + console.log("fail write", String(chunk)); + cb(n === 2 ? new Error("write failed") : null); + }, + }); + pipeline(pick(counted("f", 5)), dst, (err) => { + console.log("fail done", err && err.message); + resolve(); + }); +}); + +// backpressure: a one-byte highWaterMark and asynchronous writes +await new Promise((resolve) => { + const dst = new Writable({ + highWaterMark: 1, + write(chunk, _e, cb) { + console.log("slow write", String(chunk)); + setTimeout(cb, 1); + }, + }); + pipeline(pick(counted("b", 3)), dst, (err) => { + console.log("slow done", err); + resolve(); + }); +}); + +// an abort signal +const ac = new AbortController(); +try { + await pipelineP( + pick(counted("s", 10)), + new Writable({ + write(chunk, _e, cb) { + console.log("signal write", String(chunk)); + if (String(chunk) === "s1") ac.abort(); + cb(); + }, + }), + { signal: ac.signal }, + ); +} catch (err) { + console.log("signal rejected", err.name); +} + +// an untyped async generator into a PassThrough read with for await +const out = new PassThrough({ objectMode: true }); +const both = pipelineP( + pick((async function* () { + yield "u1"; + yield "u2"; + })()), + out, +); +for await (const c of out) console.log("passthrough", c); +await both; +console.log("passthrough resolved"); + +// a Map source of entries into an object-mode sink +await new Promise((resolve) => { + pipeline( + pick(new Map([["k", 1]])), + new Writable({ objectMode: true, write(c, _e, cb) { console.log("map write", JSON.stringify(c)); cb(); } }), + (err) => { console.log("map done", err); resolve(); }, + ); +}); diff --git a/tests/corpus/7505-pipeline-untyped-sources.cjs b/tests/corpus/7505-pipeline-untyped-sources.cjs new file mode 100644 index 000000000..6dd67b4ff --- /dev/null +++ b/tests/corpus/7505-pipeline-untyped-sources.cjs @@ -0,0 +1,40 @@ +// pipeline() over untyped sources in CommonJS: a source that a function +// answers, which may be an array, a generator, a stream, a string, or a +// Buffer, whose bytes are numbers that a byte-mode Writable refuses. +const { pipeline, Readable, Writable } = require("node:stream"); + +function sink(label) { + const parts = []; + return new Writable({ + write(chunk, _enc, cb) { + parts.push(String(chunk)); + cb(); + }, + final(cb) { + console.log(label, parts.join("|")); + cb(); + }, + }); +} + +function* letters() { + yield "a"; + yield "b"; +} + +const sources = [ + () => ["one", "two"], + () => letters(), + () => Readable.from(["r1", "r2"]), + () => "xyz", + () => Buffer.from("buf"), +]; + +function run(i) { + if (i === sources.length) return; + pipeline(sources[i](), sink(`source ${i}`), (err) => { + console.log(`source ${i} done`, err === undefined ? "ok" : err.code); + run(i + 1); + }); +} +run(0); diff --git a/tests/corpus/7506-pipeline-any-source.ts b/tests/corpus/7506-pipeline-any-source.ts new file mode 100644 index 000000000..84eec0cf4 --- /dev/null +++ b/tests/corpus/7506-pipeline-any-source.ts @@ -0,0 +1,48 @@ +// pipeline() in TypeScript over sources typed any or unknown, and over a +// Set, a Map and an async generator object, which are pumped as iterables. +import { pipeline, Writable } from "node:stream"; +import { pipeline as pipelineP } from "node:stream/promises"; + +function sink(label: string): Writable { + const parts: string[] = []; + return new Writable({ + objectMode: true, + write(chunk: unknown, _enc, cb) { + parts.push(JSON.stringify(chunk)); + cb(); + }, + final(cb) { + console.log(label, parts.join(" ")); + cb(); + }, + }); +} + +async function* ticks(): AsyncGenerator { + yield "t1"; + await new Promise((r) => setTimeout(r, 1)); + yield "t2"; +} + +async function main(): Promise { + const anySource: any = ["any1", "any2"]; + await pipelineP(anySource, sink("any")); + const unknownSource: unknown = new Set([1, 2, 3]); + await pipelineP(unknownSource as any, sink("unknown")); + await pipelineP(new Set(["s"]), sink("set")); + await pipelineP( + new Map([ + ["a", 1], + ["b", 2], + ]), + sink("map"), + ); + await pipelineP(ticks(), sink("async generator")); + await new Promise((resolve) => { + pipeline(anySource, sink("callback"), (err?: Error | null) => { + console.log("callback done", err); + resolve(); + }); + }); +} +void main(); diff --git a/tests/corpus/7507-stream-write-error-drain.mjs b/tests/corpus/7507-stream-write-error-drain.mjs new file mode 100644 index 000000000..8298bcb42 --- /dev/null +++ b/tests/corpus/7507-stream-write-error-drain.mjs @@ -0,0 +1,37 @@ +// A write whose callback fails synchronously: write() answers false and +// writableNeedDrain stays false, so no 'drain' follows the error, and a +// pipeline pumping a generator stops at the failed write. +import { pipeline, Writable } from "node:stream"; + +const w = new Writable({ + write(_chunk, _enc, cb) { + cb(new Error("bad write")); + }, +}); +w.on("error", (e) => console.log("error event", e.message)); +w.on("drain", () => console.log("drain")); +const r = w.write("a"); +console.log("returned", r, "needDrain", w.writableNeedDrain, "errored", w.errored.message); + +function* source() { + try { + for (let i = 0; i < 4; i++) { + console.log("yield", i); + yield `c${i}`; + } + } finally { + console.log("source finally"); + } +} +let writes = 0; +pipeline( + source(), + new Writable({ + write(chunk, _enc, cb) { + writes++; + console.log("write", String(chunk)); + cb(writes === 2 ? new Error("second write failed") : null); + }, + }), + (err) => console.log("pipeline done", err.message), +); diff --git a/tests/harness/corpus-costs/7503-pipeline-untyped-sources.mjs.json b/tests/harness/corpus-costs/7503-pipeline-untyped-sources.mjs.json new file mode 100644 index 000000000..d2d431b0f --- /dev/null +++ b/tests/harness/corpus-costs/7503-pipeline-untyped-sources.mjs.json @@ -0,0 +1 @@ +{ "plain": 5499, "san": 7259 } diff --git a/tests/harness/corpus-costs/7504-pipeline-untyped-source-errors.mjs.json b/tests/harness/corpus-costs/7504-pipeline-untyped-source-errors.mjs.json new file mode 100644 index 000000000..4fd20681f --- /dev/null +++ b/tests/harness/corpus-costs/7504-pipeline-untyped-source-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 3431, "san": 4529 } diff --git a/tests/harness/corpus-costs/7505-pipeline-untyped-sources.cjs.json b/tests/harness/corpus-costs/7505-pipeline-untyped-sources.cjs.json new file mode 100644 index 000000000..3bb5df076 --- /dev/null +++ b/tests/harness/corpus-costs/7505-pipeline-untyped-sources.cjs.json @@ -0,0 +1 @@ +{ "plain": 3197, "san": 4220 } diff --git a/tests/harness/corpus-costs/7506-pipeline-any-source.ts.json b/tests/harness/corpus-costs/7506-pipeline-any-source.ts.json new file mode 100644 index 000000000..454dcb2c0 --- /dev/null +++ b/tests/harness/corpus-costs/7506-pipeline-any-source.ts.json @@ -0,0 +1 @@ +{ "plain": 3250, "san": 4741 } diff --git a/tests/harness/corpus-costs/7507-stream-write-error-drain.mjs.json b/tests/harness/corpus-costs/7507-stream-write-error-drain.mjs.json new file mode 100644 index 000000000..35f298bef --- /dev/null +++ b/tests/harness/corpus-costs/7507-stream-write-error-drain.mjs.json @@ -0,0 +1 @@ +{ "plain": 2735, "san": 4076 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c5476328e..f4121bf88 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -109,7 +109,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: 42, marker: "scr_runtime_abi_v42" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From 56b9158de21b5d25612c535f37b588ff1d17fcde Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 10:42:05 -0500 Subject: [PATCH 063/273] Keep integer index arithmetic in integers across nested counted loops --- .../src/backend/llvm/counted-loops.ts | 10 +- .../backend/llvm/dense-array-loops.test.ts | 1 + packages/compiler/src/backend/llvm/emitter.ts | 17 +- .../src/backend/llvm/wrapping-sums.ts | 50 ++++-- .../src/ir/expression-inlining.test.ts | 145 ++++++++++++++++ .../compiler/src/ir/expression-inlining.ts | 157 ++++++++++++++++++ .../compiler/src/ir/integer-loops.test.ts | 68 ++++++++ packages/compiler/src/ir/integer-loops.ts | 105 +++++++++++- .../corpus/8193-counted-loop-products.ts.json | 6 + tests/corpus/8193-counted-loop-products.ts | 59 +++++++ 10 files changed, 588 insertions(+), 30 deletions(-) create mode 100644 packages/compiler/src/ir/expression-inlining.test.ts create mode 100644 packages/compiler/src/ir/expression-inlining.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8193-counted-loop-products.ts.json create mode 100644 tests/corpus/8193-counted-loop-products.ts diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts index 1ff4c6580..bb402d5b8 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -50,8 +50,8 @@ export function emitCountedLoopLimit( } /** The run-time test of a guarded counted loop: an unproven limit is within - * the integer form's finite range and an unproven start is an exact safe - * integer. Returns `true` when nothing needs testing. */ + * the integer form's finite range and an unproven start is an exact integer + * within the loop's start bound. Returns `true` when nothing needs testing. */ export function emitCountedLoopSafety( host: LlvmEmitterContext, loop: IntegerCountedForLoop, @@ -65,7 +65,11 @@ 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)); + if (loop.startGuarded) { + const start = host.emitExpr(loop.start); + const bound = loop.startBound; + safe = andSafe(host, safe, emitExactIntegerTest(host, start.name, -bound, bound).ok); + } 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)); 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 a88c38575..9ec2d5d39 100644 --- a/packages/compiler/src/backend/llvm/dense-array-loops.test.ts +++ b/packages/compiler/src/backend/llvm/dense-array-loops.test.ts @@ -122,6 +122,7 @@ const counted = ( guarded: false, limitGuarded: false, startGuarded: false, + startBound: Number.MAX_SAFE_INTEGER, guardLimit: 0, followers: [], ...fields, diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 6729da197..27389e4ff 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -80,6 +80,7 @@ import { } from "./emit-journal.js"; import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js"; import { versionLoopBoundParameters } from "../../ir/parameter-versions.js"; +import { inlineExpressionFunctions } from "../../ir/expression-inlining.js"; import { allocatedLiteral, fillAllocationLength, @@ -562,13 +563,15 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter { scalarizeNumericRecords( specializeCallbackCalls( specializeNumericCalls( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads( - freezeInitOnlyGlobals( - holdCharLocalsAsUnits( - dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)), + inlineExpressionFunctions( + foldConstantGlobals( + copyStableCaptures( + resolveSelfCaptures( + hoistFinalFieldReads( + freezeInitOnlyGlobals( + holdCharLocalsAsUnits( + dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)), + ), ), ), ), diff --git a/packages/compiler/src/backend/llvm/wrapping-sums.ts b/packages/compiler/src/backend/llvm/wrapping-sums.ts index c736fcf72..e93d66909 100644 --- a/packages/compiler/src/backend/llvm/wrapping-sums.ts +++ b/packages/compiler/src/backend/llvm/wrapping-sums.ts @@ -4,13 +4,15 @@ import { integerArithmeticRange, type IntegerRange } from "../../ir/integer-rang import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import type { LlInteger } from "./integer-values.js"; -/** Marks the `+` and `-` operands whose number only reaches ToInt32/ToUint32 - * consumers (`| 0`, the other bitwise operators, `Math.imul`, `Math.clz32`), - * directly or through further sums, in the emitter's `wrappingSums`. Called - * by the consumer before its operands emit. */ +/** Marks the `+`, `-` and `*` operands whose number only reaches + * ToInt32/ToUint32 consumers (`| 0`, the other bitwise operators, + * `Math.imul`, `Math.clz32`), directly or through further sums and + * products, in the emitter's `wrappingSums`. Called by the consumer before + * its operands emit. */ export function markWrappingOperands(host: LlvmEmitterContext, ...operands: IrExpr[]): void { for (const operand of operands) { - if (operand.kind !== "bin" || (operand.op !== "+" && operand.op !== "-")) continue; + if (operand.kind !== "bin" || (operand.op !== "+" && operand.op !== "-" && operand.op !== "*")) + continue; if (operand.type.kind !== "f64" || host.wrappingSums.has(operand)) continue; host.wrappingSums.add(operand); markWrappingOperands(host, operand.left, operand.right); @@ -128,12 +130,15 @@ function wrappingOperand( return { low, valid }; } -/** ToUint32 of `left op right` (`+` or `-`) in 32-bit arithmetic when the - * expression only reaches ToInt32/ToUint32 consumers and both operands are - * exact integers or NaN with an exact result. Both conversions are - * arithmetic mod 2^32, so the wrapping i32 sum of the operands' low halves - * is the converted exact sum; a NaN operand makes the sum NaN, which - * converts to 0. Returns undefined, emitting nothing, for any other sum. */ +/** ToUint32 of `left op right` (`+`, `-` or `*`) in 32-bit arithmetic when + * the expression only reaches ToInt32/ToUint32 consumers and both operands + * are exact integers or NaN with an exact result. Both conversions are + * arithmetic mod 2^32, so the wrapping i32 sum or product of the operands' + * low halves is the converted exact result; a NaN operand makes the result + * NaN, which converts to 0. A product is exact when the operands' magnitude + * bounds multiply to at most 2^53; it may be -0 (`0 * -1`), which converts + * to 0 like the integer 0. Returns undefined, emitting nothing, for any + * other expression. */ export function emitWrappingSum( host: LlvmEmitterContext, e: IrExpr & { kind: "bin" }, @@ -142,15 +147,23 @@ export function emitWrappingSum( li: LlInteger | null, ri: LlInteger | null, ): string | undefined { - if ((e.op !== "+" && e.op !== "-") || !host.wrappingSums.has(e)) return undefined; + if ((e.op !== "+" && e.op !== "-" && e.op !== "*") || !host.wrappingSums.has(e)) return undefined; const leftRange = li ? li.range : integerOrNaNRange(host, e.left); const rightRange = leftRange && (ri ? ri.range : integerOrNaNRange(host, e.right)); - if (!rightRange || !integerArithmeticRange(e.op, leftRange!, rightRange)) return undefined; + if (!rightRange) return undefined; + if ( + e.op === "*" + ? !exactProduct(leftRange!, rightRange) + : !integerArithmeticRange(e.op, leftRange!, rightRange) + ) + return undefined; const B = host.B; const left = wrappingOperand(host, l, li, leftRange!); const right = wrappingOperand(host, r, ri, rightRange); const sum = B.tmp(); - B.line(`${sum} = ${e.op === "+" ? "add" : "sub"} i32 ${left.low}, ${right.low}`); + B.line( + `${sum} = ${e.op === "+" ? "add" : e.op === "-" ? "sub" : "mul"} i32 ${left.low}, ${right.low}`, + ); let valid = left.valid ?? right.valid; if (valid === null) return sum; if (left.valid && right.valid) { @@ -162,6 +175,15 @@ export function emitWrappingSum( return out; } +/** Whether every product of a value in `left` and one in `right` is an + * integer of magnitude at most 2^53, which the double product holds + * exactly. */ +function exactProduct(left: IntegerRange, right: IntegerRange): boolean { + const magnitude = (range: IntegerRange): number => + Math.max(Math.abs(range.min), Math.abs(range.max)); + return magnitude(left) * magnitude(right) <= 2 ** 53; +} + /** The number locals of `fn` that only hold exact integers (never -0) or NaN, with the range of * those integers: locals declared once with an initializer, neither parameters nor captured or * boxed, whose initializer and every later write `integerOrNaNRange` covers. A local joins once diff --git a/packages/compiler/src/ir/expression-inlining.test.ts b/packages/compiler/src/ir/expression-inlining.test.ts new file mode 100644 index 000000000..ae68c44ea --- /dev/null +++ b/packages/compiler/src/ir/expression-inlining.test.ts @@ -0,0 +1,145 @@ +import { expect, test } from "vitest"; +import { inlineExpressionFunctions } from "./expression-inlining.js"; +import { BOOL, F64, VOID, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; + +const loc = { file: "expression-inlining.ts", start: 0, end: 0 }; +const num = (value: number): IrExpr => ({ loc, kind: "numLit", value, type: F64 }); +const ref = (localId: string): IrExpr => ({ loc, kind: "varRef", localId, type: F64 }); +const bin = (op: "+" | "*" | ">>>", left: IrExpr, right: IrExpr): IrExpr => ({ + loc, + kind: "bin", + op, + left, + right, + type: F64, +}); +const call = (callee: string, args: IrExpr[]): IrExpr => ({ + loc, + kind: "call", + callee, + args, + type: F64, +}); +const local = (id: string, extra: object = {}) => ({ + id, + name: id, + type: F64, + mutable: true, + ...extra, +}); + +// a(i, j) = ((i + j) * (i + j + 1)) >>> 1 +const a: IrFunction = { + loc, + name: "a", + params: [ + { localId: "i", name: "i", type: F64 }, + { localId: "j", name: "j", type: F64 }, + ], + locals: [local("i"), local("j")], + returnType: F64, + body: [ + { + loc, + kind: "return", + value: bin( + ">>>", + bin("*", bin("+", ref("i"), ref("j")), bin("+", bin("+", ref("i"), ref("j")), num(1))), + num(1), + ), + }, + ], +}; + +/** `caller` running `body` in a loop. */ +function caller(body: IrStmt[], extra: object = {}): IrFunction { + return { + loc, + name: "caller", + params: [], + locals: [local("x", extra), local("y"), local("out")], + returnType: VOID, + body: [{ loc, kind: "while", cond: { loc, kind: "boolLit", value: true, type: BOOL }, body }], + }; +} + +const loopBody = (fn: IrFunction): IrStmt[] => (fn.body[0] as IrStmt & { kind: "while" }).body; + +const mod = (fn: IrFunction): IrModule => ({ + irVersion: 17, + sourceFile: "expression-inlining.ts", + entry: "caller", + functions: [a, fn], +}); + +test("calls passing literals and locals take the callee's expression over them", () => { + const result = inlineExpressionFunctions( + mod(caller([{ loc, kind: "assign", localId: "out", value: call("a", [ref("x"), num(3)]) }])), + ); + const assign = loopBody(result.functions[1]!)[0] as IrStmt & { kind: "assign" }; + expect(assign.value).toMatchObject({ + kind: "bin", + op: ">>>", + left: { + kind: "bin", + op: "*", + left: { + kind: "bin", + op: "+", + left: { kind: "varRef", localId: "x" }, + right: { kind: "numLit", value: 3 }, + }, + }, + }); + // Every substituted read is a fresh node. + const left = (assign.value as IrExpr & { kind: "bin" }).left as IrExpr & { kind: "bin" }; + const factors = [left.left, (left.right as IrExpr & { kind: "bin" }).left] as (IrExpr & { + kind: "bin"; + })[]; + expect(factors[0]!.left).not.toBe(factors[1]!.left); + expect(result.functions[0]).toBe(a); +}); + +test("calls with other arguments, boxed locals or callees with other bodies keep their call", () => { + const nested = call("a", [call("a", [ref("x"), ref("y")]), num(1)]); + const kept = inlineExpressionFunctions( + mod(caller([{ loc, kind: "assign", localId: "out", value: nested }])), + ); + const outer = (loopBody(kept.functions[1]!)[0] as IrStmt & { kind: "assign" }).value; + // The inner call inlines; the outer one passes a computed argument. + expect(outer).toMatchObject({ + kind: "call", + callee: "a", + args: [{ kind: "bin", op: ">>>" }, { kind: "numLit" }], + }); + const boxed = caller( + [{ loc, kind: "assign", localId: "out", value: call("a", [ref("x"), ref("y")]) }], + { + boxed: true, + }, + ); + expect(inlineExpressionFunctions(mod(boxed)).functions[1]).toBe(boxed); + const twoStatements: IrFunction = { + ...a, + body: [{ loc, kind: "exprStmt", expr: ref("i") }, ...a.body], + }; + const unchanged: IrModule = { + ...mod( + caller([{ loc, kind: "assign", localId: "out", value: call("a", [ref("x"), ref("y")]) }]), + ), + functions: [ + twoStatements, + caller([{ loc, kind: "assign", localId: "out", value: call("a", [ref("x"), ref("y")]) }]), + ], + }; + expect(inlineExpressionFunctions(unchanged)).toBe(unchanged); +}); + +test("calls outside loops keep their call", () => { + const outside: IrFunction = { + ...caller([]), + body: [{ loc, kind: "assign", localId: "out", value: call("a", [ref("x"), num(3)]) }], + }; + const m = mod(outside); + expect(inlineExpressionFunctions(m)).toBe(m); +}); diff --git a/packages/compiler/src/ir/expression-inlining.ts b/packages/compiler/src/ir/expression-inlining.ts new file mode 100644 index 000000000..377cfaf3f --- /dev/null +++ b/packages/compiler/src/ir/expression-inlining.ts @@ -0,0 +1,157 @@ +import type { IrExpr, IrFunction, IrModule, IrStmt } from "./ir.js"; +import { everyExprChild, everyStmtChild, mapExprChildren, transformStmtList } from "./traverse.js"; + +/** Callee bodies above this many nodes keep their call. */ +const MAX_EXPRESSION_NODES = 32; + +/** Number operators that run no other code and cannot throw. */ +const PURE_BINARY = new Set([ + "+", + "-", + "*", + "/", + "%", + "**", + "&", + "|", + "^", + "<<", + ">>", + ">>>", +]); + +/** The returned expression of a function whose body is `return ` over + * its number parameters: literals, parameter reads, and the number + * operators of PURE_BINARY, unary `-` and `~`. Null for any other function. */ +function inlinableExpression(fn: IrFunction): IrExpr | null { + if (fn.async || fn.generator || fn.captures !== undefined || fn.classCaptures?.length) + return null; + if (fn.tier === "dynamic" || fn.returnType.kind !== "f64") return null; + if (fn.body.length !== 1) return null; + const only = fn.body[0]!; + if (only.kind !== "return" || only.value === null) return null; + const params = new Set(); + for (const param of fn.params) { + if (param.type.kind !== "f64") return null; + params.add(param.localId); + } + const locals = new Map(fn.locals.map((local) => [local.id, local])); + for (const id of params) { + const local = locals.get(id); + if (local === undefined || local.boxed || local.tdz) return null; + } + let nodes = 0; + const pure = (e: IrExpr): boolean => { + if (++nodes > MAX_EXPRESSION_NODES || e.type.kind !== "f64") return false; + switch (e.kind) { + case "numLit": + return true; + case "varRef": + return params.has(e.localId); + case "bin": + return PURE_BINARY.has(e.op) && pure(e.left) && pure(e.right); + case "unary": + return (e.op === "-" || e.op === "~") && pure(e.operand); + default: + return false; + } + }; + return pure(only.value) ? only.value : null; +} + +/** A call argument that reads the same value wherever the inlined body + * reads it: a number literal or a read of one of the caller's own unboxed + * locals outside the temporal dead zone. The body writes nothing, so every + * read sees the value the call would have passed. */ +function duplicableArgument(e: IrExpr, caller: ReadonlyMap) { + if (e.type.kind !== "f64") return false; + // A literal whose source spelling does not round-trip keeps its call. + if (e.kind === "numLit") return e.spelling === undefined; + if (e.kind !== "varRef") return false; + const local = caller.get(e.localId); + return local !== undefined && !local.boxed && !local.tdz; +} + +/** A fresh copy of `e`: facts downstream are keyed by node identity. */ +function copy(e: IrExpr, substitute: (ref: IrExpr & { kind: "varRef" }) => IrExpr): IrExpr { + if (e.kind === "varRef") return substitute(e); + return mapExprChildren( + { ...e }, + (child) => copy(child, substitute), + (stmt) => stmt, + ); +} + +/** The direct calls of `body` that run inside a loop body or condition. */ +function callsInLoops(body: IrStmt[]): Set { + const found = new Set(); + let depth = 0; + const expr = (e: IrExpr): boolean => { + if (depth > 0 && e.kind === "call") found.add(e); + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + const loop = + s.kind === "for" || s.kind === "while" || s.kind === "doWhile" || s.kind === "forOf"; + if (loop) depth++; + everyStmtChild(s, expr, stmt); + if (loop) depth--; + return true; + }; + body.forEach(stmt); + return found; +} + +/** Inlines functions whose body is one number expression over their + * parameters (`function a(i, j) { return (((i + j) * (i + j + 1)) >>> 1) + + * i + 1; }`) at direct call sites that pass literals or reads of the + * caller's locals. + * + * The callee runs no other code, allocates nothing and cannot throw, so + * the call's only effect is its result. Its arguments are reads that + * nothing between them and the callee's reads can change, so substituting + * them gives the same value. The caller's loop and integer analyses then + * see the arithmetic on its own counters (`a(i, j)` inside a counted loop + * over `i` and `j`). Only calls inside loops are inlined: elsewhere the + * integer analyses gain nothing. The function itself stays for its other + * uses. */ +export function inlineExpressionFunctions(mod: IrModule): IrModule { + const members = new Set((mod.classes ?? []).flatMap((cls) => cls.methods ?? [])); + const expressions = new Map(); + for (const fn of mod.functions) { + if (fn.name === mod.entry || members.has(fn.name)) continue; + const value = inlinableExpression(fn); + if (value !== null) expressions.set(fn.name, { fn, value }); + } + if (expressions.size === 0) return mod; + let changed = false; + const functions = mod.functions.map((fn) => { + const locals = new Map(fn.locals.map((local) => [local.id, local])); + const inLoops = callsInLoops(fn.body); + if (inLoops.size === 0) return fn; + let inlined = false; + const body = transformStmtList(fn.body, { + stmt: (s) => s, + expr: (e) => { + if (e.kind !== "call" || e.callee === fn.name || !inLoops.has(e)) return e; + const target = expressions.get(e.callee); + if (target === undefined || target.fn.params.length !== e.args.length) return e; + if (!e.args.every((arg) => duplicableArgument(arg, locals))) return e; + const args = new Map(target.fn.params.map((param, i) => [param.localId, e.args[i]!])); + inlined = true; + return copy(target.value, (ref) => { + const arg = args.get(ref.localId); + if (arg?.kind === "numLit") + return { kind: "numLit", value: arg.value, type: arg.type, loc: ref.loc }; + if (arg?.kind === "varRef") + return { kind: "varRef", localId: arg.localId, type: arg.type, loc: ref.loc }; + return ref; + }); + }, + }); + if (!inlined) return fn; + changed = true; + return { ...fn, body }; + }); + return changed ? { ...mod, functions } : mod; +} diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index 2cca43a00..48be67fbf 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -552,3 +552,71 @@ test("counters stepped beside the tested one run as integers", () => { { startGuarded: true, tripGuarded: true }, ]); }); + +/** `counted(limit)` with the body `x = ((i + k) * (i + k + 1)) >>> 1` + * inside a nested `for (let j = 0; j < 4; j++)` loop. */ +function productLoop(limit: IrExpr, start: IrExpr = number(0)): IrStmt & { kind: "for" } { + const s = counted(limit); + if (s.init?.kind === "varDecl") s.init.init = start; + const sum = (extra: number): IrExpr => { + const base: IrExpr = { kind: "bin", op: "+", left: ref("i"), right: ref("k"), type: F64, loc }; + return extra === 0 + ? base + : { kind: "bin", op: "+", left: base, right: number(extra), type: F64, loc }; + }; + const product: IrExpr = { kind: "bin", op: "*", left: sum(0), right: sum(1), type: F64, loc }; + const inner = counted(number(4)); + if (inner.init?.kind === "varDecl") inner.init.localId = "j"; + if (inner.cond?.kind === "bin") inner.cond.left = ref("j"); + if (inner.update?.kind === "assign") { + inner.update.localId = "j"; + if (inner.update.value.kind === "bin") inner.update.value.left = ref("j"); + } + inner.body = [ + { + kind: "assign", + localId: "x", + value: { kind: "bin", op: ">>>", left: product, right: number(1), type: F64, loc }, + loc, + }, + ]; + s.body = [inner]; + return s; +} + +test("guarded counted loops whose products the counter bound proves exact may hold nested loops", () => { + const scope = new Map(locals); + for (const id of ["limit", "start", "k", "x", "j"]) + scope.set(id, { id, name: id, type: F64, mutable: true }); + const tight = matchIntegerCountedForLoop(productLoop(ref("limit"), ref("start")), scope); + expect(tight).toMatchObject({ + guarded: true, + limitGuarded: true, + startGuarded: true, + startBound: 2 ** 25, + guardLimit: 2 ** 25, + range: { min: -(2 ** 25), max: 2 ** 25 - 1 }, + }); + // A literal trip count past the bound keeps the safe-integer bound, and + // a guarded loop under that bound holds no nested loop. + expect(matchIntegerCountedForLoop(productLoop(number(1e9), ref("start")), scope)).toBeNull(); + const plain = counted(ref("limit")); + plain.body = [productLoop(number(4)).body[0]!]; + const inner = plain.body[0] as IrStmt & { kind: "for" }; + inner.body = [ + { + kind: "assign", + localId: "x", + value: { kind: "bin", op: "|", left: ref("i"), right: number(0), type: F64, loc }, + loc, + }, + ]; + expect(matchIntegerCountedForLoop(plain, scope)).toBeNull(); + // Without a nested loop the product bound still applies to the limit. + const flat = productLoop(ref("limit"), ref("start")); + flat.body = (flat.body[0] as IrStmt & { kind: "for" }).body; + expect(matchIntegerCountedForLoop(flat, scope)).toMatchObject({ + startBound: 2 ** 25, + guardLimit: 2 ** 25, + }); +}); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index 2f7d2bea6..7cc49b7a0 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -24,8 +24,12 @@ export interface IntegerCountedForLoop { /** The limit needs the run-time `guardLimit` test (part of `guarded`). */ limitGuarded: boolean; /** The start has no proven integer range: the versioned loop also tests - * that it is an exact integer (not -0) of at most 2^53 in magnitude. */ + * that it is an exact integer (not -0) of at most `startBound` in + * magnitude. */ startGuarded: boolean; + /** The magnitude the guarded start is tested against: 2^53 - 1, or + * PRODUCT_COUNTER_BOUND when the body multiplies sums of the counter. */ + startBound: number; guardLimit: number; range: IntegerRange; /** Counters declared and stepped beside this one that the condition does @@ -50,6 +54,64 @@ export interface IntegerLoopFollower { range: IntegerRange; } +/** The counter magnitude below which a product of two sums of counters and + * small literals stays exact in a double: `(i + j) * (i + j + 1)` with both + * counters within 2^25 is below 2^52. A guarded loop whose body multiplies + * such sums for a bitwise operator tests its counter against this bound, so + * the product's integer form is proven exact. */ +export const PRODUCT_COUNTER_BOUND = 2 ** 25; + +/** Whether `e` is a sum or difference of locals and safe integer literals + * of magnitude at most 2^20, and whether it reads `counter`. */ +function localSum(e: IrExpr, counter: string): { sum: boolean; reads: boolean } { + let reads = false; + const sum = (x: IrExpr): boolean => { + if (x.kind === "varRef") { + if (x.localId === counter) reads = true; + return true; + } + if (x.kind === "numLit") return Number.isSafeInteger(x.value) && Math.abs(x.value) <= 2 ** 20; + return x.kind === "bin" && (x.op === "+" || x.op === "-") && sum(x.left) && sum(x.right); + }; + return { sum: sum(e), reads }; +} + +/** Whether a bitwise operand `e` multiplies two sums of locals and + * literals, one of which reads `counter` (`((i + j) * (i + j + 1)) >>> 1`). */ +function multipliesCounterSums(e: IrExpr, counter: string): boolean { + if (e.kind !== "bin") return false; + if (e.op === "*") { + const left = localSum(e.left, counter), + right = localSum(e.right, counter); + return left.sum && right.sum && (left.reads || right.reads); + } + return ( + (e.op === "+" || e.op === "-") && + (multipliesCounterSums(e.left, counter) || multipliesCounterSums(e.right, counter)) + ); +} + +const BITWISE = new Set(["&", "|", "^", "<<", ">>", ">>>"]); + +/** Whether `body`, nested loops included, takes a bitwise operator of a + * product that multipliesCounterSums matches. */ +function bodyMultipliesCounter(body: IrStmt[], counter: string): boolean { + let found = false; + everyStmtList(body, { + stmt: () => !found, + expr: (e) => { + if ( + e.kind === "bin" && + BITWISE.has(e.op) && + (multipliesCounterSums(e.left, counter) || multipliesCounterSums(e.right, counter)) + ) + found = true; + return !found; + }, + }); + return found; +} + /** 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 @@ -171,6 +233,23 @@ export function matchIntegerCountedForLoop( // The condition tests the driving counter; every other declaration // follows it. const driver = declared(cond.left) ?? declared(cond.right); + // A body that multiplies sums of the driver tests it against the bound + // under which those products are exact. A limit proven past that bound + // (a literal trip count) keeps the safe-integer bound instead. + const limitSide = driver !== null && declared(cond.left) === driver ? cond.right : cond.left; + const knownLimit = + limitSide.kind === "numLit" + ? { min: limitSide.value, max: limitSide.value } + : ranges.get(limitSide); + const driverBound = + driver !== null && + !( + knownLimit && + Math.max(Math.abs(knownLimit.min), Math.abs(knownLimit.max)) > PRODUCT_COUNTER_BOUND + ) && + bodyMultipliesCounter(stmt.body, driver) + ? PRODUCT_COUNTER_BOUND + : Number.MAX_SAFE_INTEGER; 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); @@ -191,7 +270,8 @@ export function matchIntegerCountedForLoop( ? { 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 bound = id === driver ? driverBound : Number.MAX_SAFE_INTEGER; + const range = guarded ? { min: -bound, max: bound } : known; const step = stepOf(id); if ( !range || @@ -248,7 +328,9 @@ export function matchIntegerCountedForLoop( ); if (!stableLoopValue(limit, locals, stmt.body, true, local.id, stableLength)) return null; const inclusive = op === "<=" || op === ">="; - const guardLimit = step > 0 ? 2 ** 53 - (inclusive ? 1 : 0) : -(2 ** 53) + (inclusive ? 1 : 0); + const limitBound = driverBound === Number.MAX_SAFE_INTEGER ? 2 ** 53 : driverBound; + const guardLimit = + step > 0 ? limitBound - (inclusive ? 1 : 0) : -limitBound + (inclusive ? 1 : 0); const boundRange = limit.kind === "numLit" ? { min: limit.value, max: limit.value } : ranges.get(limit); const limitGuarded = @@ -264,7 +346,9 @@ export function matchIntegerCountedForLoop( Number.MAX_SAFE_INTEGER, boundRange ? Math.ceil(boundRange.max) - (inclusive ? 0 : 1) - : Number.MAX_SAFE_INTEGER, + : limitGuarded + ? Math.ceil(guardLimit) - (inclusive ? 0 : 1) + : Number.MAX_SAFE_INTEGER, ), ), } @@ -275,7 +359,9 @@ export function matchIntegerCountedForLoop( -Number.MAX_SAFE_INTEGER, boundRange ? Math.floor(boundRange.min) + (inclusive ? 0 : 1) - : -Number.MAX_SAFE_INTEGER, + : limitGuarded + ? Math.floor(guardLimit) + (inclusive ? 0 : 1) + : -Number.MAX_SAFE_INTEGER, ), ), max: startRange.max, @@ -366,9 +452,15 @@ export function matchIntegerCountedForLoop( const bodyStmt = (s: IrStmt): boolean => { if (s.kind === "bytesSet" && counter(s.index)) useful = true; if (!guarded && s.kind === "arraySet" && counter(s.index)) useful = true; + // A guarded loop's copy duplicates nested loops too, so only a body + // whose counter products the bound proves exact may hold a nested + // `for` loop; the node budget counts its body. if ( guarded && - (s.kind === "for" || s.kind === "forOf" || s.kind === "while" || s.kind === "doWhile") + (s.kind === "forOf" || + s.kind === "while" || + s.kind === "doWhile" || + (s.kind === "for" && driverBound === Number.MAX_SAFE_INTEGER)) ) return false; return (!guarded || ++cost <= 80) && everyStmtChild(s, expr, bodyStmt); @@ -383,6 +475,7 @@ export function matchIntegerCountedForLoop( guarded, limitGuarded, startGuarded, + startBound: driverBound, guardLimit, range, followers, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8193-counted-loop-products.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8193-counted-loop-products.ts.json new file mode 100644 index 000000000..7a3b97b6c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8193-counted-loop-products.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8193-counted-loop-products.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8193-counted-loop-products.ts b/tests/corpus/8193-counted-loop-products.ts new file mode 100644 index 000000000..d347aee76 --- /dev/null +++ b/tests/corpus/8193-counted-loop-products.ts @@ -0,0 +1,59 @@ +// Nested counted loops whose bounds arrive at run time, with the counters' +// products reaching bitwise operators. Starts and limits take the integer +// copy or the double copy: negative, fractional, NaN, -0, and starts or +// limits past 2^25, past which the guarded copy no longer proves products +// exact. +function a(i: number, j: number): number { + return (((i + j) * (i + j + 1)) >>> 1) + i + 1; +} + +function rows(start: number, end: number, n: number): number[] { + const out: number[] = []; + for (let i = start; i < end; i++) { + let t = 0; + for (let j = 0; j < n; j++) t += 1 / a(i, j); + out.push(t); + } + return out; +} + +function mixed(start: number, end: number, n: number): number { + let h = 0; + for (let i = start; i < end; i++) { + for (let j = start; j < n; j++) h = (h + ((i - j) * (j - i + 3) | 0)) | 0; + } + return h; +} + +function wide(start: number, count: number): number[] { + const out: number[] = []; + for (let i = start; i < start + count; i++) out.push(((i * (i + 1)) >>> 0) ^ (i * i) | 0); + return out; +} + +const args = process.argv.slice(2).map(Number); +const big = 2 ** 25; +for (const [start, end, n] of [ + [0, 4, 6], + [-3, 2, 5], + [1.5, 4, 3], + [-0, 2, 2], + [NaN, 2, 2], + [big - 2, big + 2, 3], + [0, 3, args.length > 0 ? big + 1 : 4], +]) { + console.log(start, end, n, rows(start!, end!, Math.min(n!, 8)).map((x) => x.toFixed(12)).join(" ")); + console.log("mixed", mixed(start!, end!, Math.min(n!, 8))); +} +for (const start of [0, -5, 2 ** 26, 2 ** 31 - 2, 2 ** 52, -(2 ** 40), 0.5]) console.log(start, wide(start, 4).join(" ")); +console.log(Object.is(((-0 * 5) >>> 0), 0), a(-1, 0), a(0, -1), a(2 ** 30, 2 ** 30)); + +// Single-expression functions inlined at calls that pass literals and +// locals, also read as values and called with other arguments. +function mix(x: number, y: number): number { + return ((x * 31 + y) ^ (y << 3)) % 1000; +} +const table = [mix, (x: number, y: number) => x - y]; +let acc = 0; +for (let k = 0; k < 5; k++) acc += mix(k, 7) + mix(3, k) + table[k % 2]!(k, 2) + mix(k * 2, args.length); +console.log(acc, mix(2 ** 40, -0), mix(NaN, 1), mix(Infinity, 2), [1, 2, 3].map((v) => mix(v, v)).join()); From 2967a1b8c064094958b6b1b8fc23af76f5141bf6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 11:03:34 -0500 Subject: [PATCH 064/273] Propagate exceptions by unwinding from directly called functions on Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Emitted functions report an exception by returning with the thread's pending flag set, and every caller tests the flag after each call that may throw. On Linux executables (glibc and musl), functions reached only by direct calls now propagate exceptions by unwinding the native stack instead: - unwindAbiFunctions selects the may-throw functions that are not async or generators, constructors, virtually called or overridden methods, closure targets, the entry, library exports, module initializers or callees of coroutine bodies. - A native unwind costs about 2 µs a raise and 0.1 µs a frame, so functions that exceptions are likely to cross keep the flag: those with a `throw` of their own, those up to three calls above one, and recursive functions, counting calls through methods, closures and callbacks. - Their propagating exits call scr_unwind_raise (scr_unwind.c), a forced unwind through _Unwind_ForcedUnwind with the exception still in the active cell. - Direct calls to them are `invoke`s without a pending check. The landing pad runs the frame's usual unwind path: it releases the frame's values and then enters the enclosing try handler or resumes the unwind. - Remaining plain calls to them (wrappers, adapters, runtime-facing entry points) become invokes whose landing pad continues on the pending flag, or resumes when the wrapper is itself invoked. Every other call is marked `nounwind`, so inlining a raising function at an invoke keeps its calls plain. - When emitted text breaks these rules (a raising function's address is taken, or an invoked function returns with the flag pending), the module is emitted again with those functions on the pending flag. Emitted code does not declare scr_unwind_raise `noreturn`, so sanitized builds do not run the sanitizer's no-return hook, which needs stack bounds that fiber stacks do not report. Instead, scr_unwind_raise clears the shadow of the frames it unwinds. Executables link a static unwinder: libgcc_eh with the host toolchain and the glibc runtime packs, and Zig's libunwind with Zig and the musl packs. macOS, Windows, Android, WASI, libraries and debug builds keep the pending flag everywhere and emit the same code as before. The throw-chains runtime workload measures exceptions caught 2, 5 and 20 recursive calls above the throw, through direct calls and a callback, and a parser that backtracks by throwing. --- benchmarks/runtime/README.md | 1 + .../runtime/workloads/throw-chains.json | 6 + benchmarks/runtime/workloads/throw-chains.ts | 196 +++++++ packages/compiler/src/backend/llvm/blocks.ts | 14 +- .../src/backend/llvm/emit-function.ts | 10 +- packages/compiler/src/backend/llvm/emitter.ts | 135 ++++- .../compiler/src/backend/llvm/expr-calls.ts | 31 +- .../src/backend/llvm/unwind-abi.test.ts | 285 ++++++++++ .../compiler/src/backend/llvm/unwind-abi.ts | 491 ++++++++++++++++++ .../compiler/src/backend/native/executable.ts | 12 + .../src/backend/native/runtime-inputs.ts | 1 + packages/compiler/src/index.ts | 1 + packages/compiler/src/native/prepare.ts | 1 + .../runtime-linux-arm64-gnu/scripts/build.mjs | 6 +- .../scripts/build.mjs | 6 +- .../runtime-linux-x64-gnu/scripts/build.mjs | 6 +- .../runtime-linux-x64-musl/scripts/build.mjs | 6 +- .../runtime-pack-matrix.mjs | 1 + packages/runtime/src/scr_exception.h | 5 + packages/runtime/src/scr_unwind.c | 76 +++ scripts/zero-cost-compare.mjs | 5 +- 21 files changed, 1269 insertions(+), 26 deletions(-) create mode 100644 benchmarks/runtime/workloads/throw-chains.json create mode 100644 benchmarks/runtime/workloads/throw-chains.ts create mode 100644 packages/compiler/src/backend/llvm/unwind-abi.test.ts create mode 100644 packages/compiler/src/backend/llvm/unwind-abi.ts create mode 100644 packages/runtime/src/scr_unwind.c diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 03238871d..726a9951b 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -30,6 +30,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` | | `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` | +| `throw-chains` | cpu | exceptions caught 2, 5 and 20 recursive calls above the throw, through direct calls and through a callback, and a backtracking parser that throws on a failed alternative | | `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, a row-major `Float32Array` matrix-vector product, 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 | diff --git a/benchmarks/runtime/workloads/throw-chains.json b/benchmarks/runtime/workloads/throw-chains.json new file mode 100644 index 000000000..79cf5ea87 --- /dev/null +++ b/benchmarks/runtime/workloads/throw-chains.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/throw-chains.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/throw-chains.ts b/benchmarks/runtime/workloads/throw-chains.ts new file mode 100644 index 000000000..c96816aed --- /dev/null +++ b/benchmarks/runtime/workloads/throw-chains.ts @@ -0,0 +1,196 @@ +// Exceptions that cross many frames: throws from helpers below a recursion, +// caught 2, 5 and 20 recursive calls up (direct calls, then calls through a +// callback), with each frame holding an array across its call, and a +// backtracking parser that uses exceptions for control flow. +class ParseError extends Error { + readonly at: number; + constructor(at: number, message: string) { + super(message); + this.at = at; + } +} + +/** A thrown value that is not an Error (no stack to capture). */ +class Stop { + readonly code: number; + constructor(code: number) { + this.code = code; + } +} + +const held: number[][] = []; +function fail(n: number, plain: boolean): number { + if (plain) throw new Stop(n); + throw new RangeError("chain " + n); +} +function check(n: number, plain: boolean): number { + return (n & 3) === 0 ? fail(n, plain) : n; +} +function validate(n: number, plain: boolean): number { + return check(n, plain) + (n & 1); +} +function bottom(n: number, plain: boolean): number { + const box = [n, -1]; + const r = validate(n, plain); + held[0] = box; + return r + box.length; +} +function walk(n: number, depth: number, plain: boolean): number { + const box = [n, depth]; + const r = depth === 0 ? bottom(n, plain) : walk(n, depth - 1, plain); + held[depth] = box; + return r + box.length; +} + +function chains(rounds: number): string { + const out: string[] = []; + for (const depth of [2, 5, 20]) + for (const plain of [false, true]) { + let caught = 0; + let sum = 0; + for (let i = 0; i < rounds; i++) { + try { + sum += walk(i, depth - 2, plain); + } catch (error) { + caught++; + sum += error instanceof Stop ? error.code & 7 : 1; + } + } + out.push(`depth ${depth} ${plain ? "object" : "error"}: caught ${caught} sum ${sum}`); + } + return out.join("\n"); +} + +// The same chains through a callback: each level visits the next with a +// closure, as tree visitors do. +function visit(n: number, depth: number, each: (n: number, depth: number) => number): number { + const box = [n, depth]; + const r = depth === 0 ? bottom(n, false) : each(n, depth - 1); + held[depth] = box; + return r + box.length; +} +function visits(rounds: number): string { + const out: string[] = []; + const each = (n: number, depth: number): number => visit(n, depth, each); + for (const depth of [2, 5, 20]) { + let caught = 0; + let sum = 0; + for (let i = 0; i < rounds; i++) { + try { + sum += visit(i, depth - 2, each); + } catch (error) { + caught++; + sum += error instanceof RangeError ? 1 : 0; + } + } + out.push(`visit depth ${depth}: caught ${caught} sum ${sum}`); + } + return out.join("\n"); +} + +// expr := term (("+" | "-") term)*; term := atom ("*" atom)*; +// atom := number | "(" expr ")" | name "(" expr ("," expr)* ")" | name. +// A call and a name start alike: the parser tries the call first and +// backtracks when it fails. +let text = ""; +let pos = 0; +function peek(): string { + return pos < text.length ? text[pos]! : ""; +} +function expect(c: string): void { + if (peek() !== c) throw new ParseError(pos, `expected ${c}`); + pos++; +} +function name(): string { + const start = pos; + while (pos < text.length && text.charCodeAt(pos) >= 97 && text.charCodeAt(pos) <= 122) pos++; + if (pos === start) throw new ParseError(pos, "expected a name"); + return text.slice(start, pos); +} +function call(): number { + const callee = name(); + expect("("); + let value = expr() + callee.length; + while (peek() === ",") { + pos++; + value += expr(); + } + expect(")"); + return value; +} +function atom(): number { + const c = peek(); + if (c >= "0" && c <= "9") { + let value = 0; + while (peek() >= "0" && peek() <= "9") value = value * 10 + (text.charCodeAt(pos++) - 48); + return value; + } + if (c === "(") { + pos++; + const value = expr(); + expect(")"); + return value; + } + const start = pos; + try { + return call(); + } catch (error) { + if (!(error instanceof ParseError)) throw error; + pos = start; + return name().length; + } +} +function term(): number { + let value = atom(); + while (peek() === "*") { + pos++; + value = (value * atom()) % 1000003; + } + return value; +} +function expr(): number { + let value = term(); + while (peek() === "+" || peek() === "-") { + const op = text[pos++]; + value = op === "+" ? value + term() : value - term(); + } + return value; +} + +let seed = 4242; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} +function source(depth: number): string { + const r = random(); + if (depth > 6 || r < 0.25) return String(Math.floor(random() * 100)); + if (r < 0.45) return "abcdefgh".slice(0, 1 + Math.floor(random() * 8)); + if (r < 0.6) return `(${source(depth + 1)})`; + if (r < 0.8) return `${source(depth + 1)}${random() < 0.5 ? "+" : "*"}${source(depth + 1)}`; + return `f${"xyz".slice(0, Math.floor(random() * 3))}(${source(depth + 1)},${source(depth + 1)})`; +} + +function parse(count: number): string { + let total = 0; + let failed = 0; + for (let i = 0; i < count; i++) { + // Every tenth input lacks its last character, which fails deep inside it. + const full = source(0); + text = i % 10 === 0 ? full.slice(0, -1) : full; + pos = 0; + try { + total = (total + expr()) % 1000003; + if (pos !== text.length) throw new ParseError(pos, "trailing input"); + } catch (error) { + if (!(error instanceof ParseError)) throw error; + failed++; + } + } + return `parsed ${count}: total ${total} failed ${failed}`; +} + +const scale = Number(process.argv[2] ?? "1"); +console.log(chains(Math.floor(60000 * scale))); +console.log(visits(Math.floor(60000 * scale))); +console.log(parse(Math.floor(40000 * scale))); diff --git a/packages/compiler/src/backend/llvm/blocks.ts b/packages/compiler/src/backend/llvm/blocks.ts index 354e631c6..eee4d00b9 100644 --- a/packages/compiler/src/backend/llvm/blocks.ts +++ b/packages/compiler/src/backend/llvm/blocks.ts @@ -9,6 +9,7 @@ * code after it); here the lines are DROPPED instead, and emitStmts skips * whole statements once the current block is terminated, so no dropped * definition can leak into a later block. */ +import { UNWIND_RESUME, UNWIND_SLOT } from "./unwind-abi.js"; export class BlockBuilder { private readonly blocks: { label: string; lines: string[]; term: string | null }[] = []; @@ -86,7 +87,18 @@ export class BlockBuilder { terminate(s: string): void { if (this.cur.term === null) { - if (this.returnEpilogue && s.startsWith("ret ")) this.line(this.returnEpilogue); + if ( + this.returnEpilogue && + (s.startsWith("ret ") || + s.startsWith("call void @scr_unwind_raise()") || + s === UNWIND_RESUME) + ) + this.line(this.returnEpilogue); + if (s === UNWIND_RESUME) { + const exception = this.tmp(); + this.line(`${exception} = load { ptr, i32 }, ptr ${UNWIND_SLOT}`); + s = `resume { ptr, i32 } ${exception}`; + } this.cur.term = ` ${this.withDebugLocation(s)}`; } } diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index e015c2d72..749c91858 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -56,11 +56,14 @@ import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js"; import { beginFunctionLoops, LOOPS } from "./loop-state.js"; import { runningExtremumUpdates } from "./running-extrema.js"; import { findIntegerOrNaNLocals } from "./wrapping-sums.js"; +import { UNWIND_PERSONALITY, UNWIND_SLOT } from "./unwind-abi.js"; export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const B = new BlockBuilder(); emitter.B = B; emitter.currentFunctionName = fn.name; + emitter.currentUnwindAbi = emitter.unwindAbi.has(fn.name); + emitter.usesLandingPads = false; emitter.findInt32Allocations(fn); emitter.currentTier = fn.tier; // Fiber switches restore the active exception cell before control @@ -596,6 +599,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { B.terminate(cleanup.terminator); } + if (emitter.usesLandingPads && emitter.currentUnwindAbi) + B.entryAllocas.push(`${UNWIND_SLOT} = alloca { ptr, i32 }`); if (emitter.logArgSlots > 0) { B.entryAllocas.push(`%logargs = alloca [${emitter.logArgSlots} x %ScrLogArg]`); } @@ -636,7 +641,10 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const debug = emitter.debugScope === null ? "" : ` !dbg ${emitter.debugScope}`; const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name); emitter.privateGlobalSlots.clear(); - let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`; + // Landing pads (emitDirectCall) need the personality. + const personality = emitter.usesLandingPads ? ` ${UNWIND_PERSONALITY}` : ""; + if (emitter.usesLandingPads) emitter.declare(`declare i32 @__gcc_personality_v0(...)`); + let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${personality}${debug} { ; ${fn.name}\n${B.render()}\n}`; if (int32Abi) body += "\n" + emitter.emitInt32AbiWrapper(fn, symbol, int32Abi); if (emitter.integerClones.members.has(fn.name)) { body += "\n" + emitIntegerClone(fn, emitter, emitter.stackChecks?.has(fn.name) === true); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 6729da197..814c1a23e 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -128,6 +128,16 @@ import { type FfiCallbackAdapter, } from "../ffi-callbacks.js"; import { computeMayThrow } from "../may-throw.js"; +import { + UNWIND_PERSONALITY, + UNWIND_RAISE, + UNWIND_RAISE_DECLARATION, + UNWIND_RESUME, + UNWIND_SLOT, + UnwindAbiViolation, + bridgePlainUnwindCalls, + unwindAbiFunctions, +} from "./unwind-abi.js"; import { mangleAsyncInlineAwaitCall, mangleAsyncSpawn, @@ -439,6 +449,12 @@ export interface ForkPrimitives { export interface LlvmTargetOptions { /** C ABI target; omitted direct emission uses the host ABI. */ targetTriple?: string; + /** Linux (glibc and musl) executables: functions reached only by direct + * calls propagate exceptions by unwinding the native stack (unwind-abi.ts). */ + unwindAbi?: boolean; + /** Functions kept on the pending-flag ABI under `unwindAbi`: the ones an + * earlier emission found breaking its rules (UnwindAbiViolation). */ + unwindExcluded?: ReadonlySet; /** 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. Debug builds (`debugSources`) @@ -590,16 +606,36 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter { return new LlEmitter(emitted, options, null, prepared?.mod === emitted ? prepared : null); } +/** Emits the module's parts, emitting it again with the functions an + * UnwindAbiViolation names on the pending-flag ABI. */ +function emitModuleText(input: IrModule, options: LlvmTargetOptions): string[] { + for (let excluded = options.unwindExcluded ?? new Set(); ;) { + const emitter = moduleEmitter(input, { ...options, unwindExcluded: excluded }); + try { + return emitter.emitParts(); + } catch (error) { + if (!(error instanceof UnwindAbiViolation)) throw error; + const names = new Set(excluded); + for (const name of emitter.unwindAbi) + for (const symbol of [mangleFunction(name), mangleBorrowedFunction(name)]) + if (error.symbols.has(symbol) || error.symbols.has(symbol + INT32_ABI_SUFFIX)) + names.add(name); + if (names.size === excluded.size) throw error; + excluded = names; + } + } +} + export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string { - return moduleEmitter(input, options).emit(); + return emitModuleText(input, options).join("\n"); } /** emitLlvmModule's text as parts that concatenate to it, without the copy * that joining them makes: a large program's text is 150 MB. */ export function emitLlvmModuleParts(input: IrModule, options: LlvmTargetOptions = {}): string[] { - return moduleEmitter(input, options) - .emitParts() - .flatMap((part, index) => (index === 0 ? [part] : ["\n", part])); + return emitModuleText(input, options).flatMap((part, index) => + index === 0 ? [part] : ["\n", part], + ); } /** Keep large translation units below the host's single-string limit. */ @@ -609,7 +645,7 @@ export function emitLlvmModuleSource( ): string | readonly string[] { // IR validation, contracts and the frontend's disposal run after lowering without checks. relieveHeapPressure(true); - const parts = moduleEmitter(input, options).emitParts(); + const parts = emitModuleText(input, options); const length = parts.reduce((total, part) => total + part.length, Math.max(0, parts.length - 1)); if (length <= 256 * 1024 * 1024) return parts.join("\n"); return parts.flatMap((part, index) => (index === 0 ? [part] : ["\n", part])); @@ -1253,6 +1289,10 @@ export class LlEmitter { /** May-throw analysis (computeMayThrow): pending * checks are emitted only after calls that can actually raise. */ readonly mayThrow: Set; + /** The functions that propagate exceptions by unwinding (unwind-abi.ts). */ + readonly unwindAbi: Set; + /** Whether the function being emitted is one of unwindAbi. */ + currentUnwindAbi = false; readonly indirectMayThrow: boolean; /** Worker programs: the functions that poll for termination on entry * (workerTerminationPolls); null outside worker programs. */ @@ -1711,6 +1751,16 @@ export class LlEmitter { nodes: this.nodeIndex, }); this.mayThrow = mt.fns; + this.unwindAbi = + options.unwindAbi === true && + !this.wasi && + mod.lib === undefined && + options.debugSources === undefined && + (options.targetTriple + ? /-linux-(?:gnu|musl)/.test(options.targetTriple) + : process.platform === "linux") + ? unwindAbiFunctions(mod, this.mayThrow, options.unwindExcluded) + : new Set(); this.workerEntryPolls = mt.workerEntryPolls ?? null; this.stackChecks = mt.stackChecks ?? null; // A stackless call's eager prefix borrows its caller's stack; the @@ -1846,6 +1896,9 @@ export class LlEmitter { : undefined, ) : NO_INTEGER_CLONES; + // Integer clones and their dispatchers call each other through their + // own lowering, which keeps plain calls and flag tests. + for (const name of this.integerClones.members) this.unwindAbi.delete(name); this.stackCallbacks = new StackCallbacks(this.fnByName); this.receiverReaders = new AmbientReceiverReaders(this.fnByName, this.nodeIndex); this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks); @@ -2125,7 +2178,8 @@ export class LlEmitter { return this.emitParts().join("\n"); } emitParts(): string[] { - return emitParts(this); + const parts = emitParts(this); + return this.unwindAbi.size > 0 ? bridgePlainUnwindCalls(parts) : parts; } /** The dispose-observer gates the emitted frees and record writers test @@ -3235,6 +3289,13 @@ export class LlEmitter { terminator = `br label %${target.label}`; } else if (this.currentCoro !== null) { terminator = `br label %${this.currentCoro.finalLabel}`; + } else if (this.currentUnwindAbi && this.inLandingPad) { + // The unwind that entered the landing pad goes on to the caller's. + terminator = UNWIND_RESUME; + } else if (this.currentUnwindAbi) { + // The caller's invoke takes the exception at its landing pad. + this.declare(UNWIND_RAISE_DECLARATION); + terminator = UNWIND_RAISE; } else { const t = this.llType(this.currentReturnType); terminator = @@ -3265,6 +3326,7 @@ export class LlEmitter { passThrough !== undefined && !target && this.currentCoro === null && + !this.currentUnwindAbi && !this.currentInt32Result ) { const t = this.llType(this.currentReturnType); @@ -3451,6 +3513,46 @@ export class LlEmitter { B.startBlock(B.newLabel("exc.dead")); } + /** Emits `line`, a `call` of `callee`, and reports whether the call + * needs no pending check after it: a call of an unwinding-ABI function + * becomes an `invoke` whose landing pad runs this frame's unwind (the + * same releases and handler as emitPendingCheck's unwind path), and the + * normal path continues in a fresh block. Coroutine bodies keep plain + * calls (unwindAbiFunctions excludes their callees). */ + emitDirectCall(line: string, callee: string): boolean { + const B = this.B; + if (!this.unwindAbi.has(callee) || this.currentCoro !== null || B.isTerminated()) { + B.line(line); + return false; + } + const ok = B.newLabel("inv.ok"); + const pad = B.newLabel("inv.lp"); + this.usesLandingPads = true; + // A trailing comment goes after the invoke's labels. + const comment = line.indexOf(" ; "); + const text = comment < 0 ? line : line.slice(0, comment); + B.terminate( + `${text.replace(/^((?:%[\w.]+ = )?)call /, "$1invoke ")} to label %${ok} unwind label %${pad}`, + ); + B.startBlock(pad); + const exception = B.tmp(); + B.line(`${exception} = landingpad { ptr, i32 } cleanup`); + if (this.currentUnwindAbi) B.line(`store { ptr, i32 } ${exception}, ptr ${UNWIND_SLOT}`); + this.inLandingPad = true; + this.emitUnwind(); + this.inLandingPad = false; + B.startBlock(ok); + return true; + } + + /** Whether emitUnwind runs for a landing pad, whose unwind goes on with + * `resume` instead of raising again. */ + private inLandingPad = false; + + /** Whether the function being emitted has a landing pad (its definition + * needs the personality). */ + usesLandingPads = false; + /** A recursive function's entry check: below the running stack's guard, * throw Node's RangeError and unwind like any other pending exception. */ emitStackCheck(): void { @@ -5214,7 +5316,22 @@ export class LlEmitter { if (fn.captures !== undefined) params.unshift("ptr %sc_env"); const ret = this.llType(fn.returnType); const call = `call ${ret} @${mangleBorrowedFunction(fn.name)}(${params.join(", ")})`; - B.line(ret === "void" ? call : `%result = ${call}`); + // The owned entry of an unwinding-ABI body releases the borrowed + // parameters on the unwind too, then resumes it to its caller. + const unwinding = this.unwindAbi.has(fn.name) && kept.size === 0; + if (unwinding) { + const ok = B.newLabel("adapt.ok"); + const pad = B.newLabel("adapt.lp"); + B.terminate( + `${ret === "void" ? "" : "%result = "}invoke ${call.slice(5)} to label %${ok} unwind label %${pad}`, + ); + B.startBlock(pad); + const exception = B.tmp(); + B.line(`${exception} = landingpad { ptr, i32 } cleanup`); + for (const index of borrowed) this.releaseFrame(`%p${index}`, fn.params[index]!.type); + B.terminate(`resume { ptr, i32 } ${exception}`); + B.startBlock(ok); + } else B.line(ret === "void" ? call : `%result = ${call}`); // A borrowed result is reachable from the parameters: own it first. A // throwing body returns a null dummy, which needs no owner (and string // or array retains do not accept NULL). @@ -5240,7 +5357,9 @@ export class LlEmitter { B.terminate(ret === "void" ? "ret void" : `ret ${ret} ${result}`); const symbol = kept.size > 0 ? `${mangleBorrowedFunction(fn.name)}.virtual` : mangleFunction(fn.name); - return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS} {\n${B.render()}\n}`; + if (unwinding) this.declare(`declare i32 @__gcc_personality_v0(...)`); + const personality = unwinding ? ` ${UNWIND_PERSONALITY}` : ""; + return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS}${personality} {\n${B.render()}\n}`; } canBorrowCallArgument(value: IrExpr): boolean { return canBorrowCallArgument(this, value); diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index 6ff52619e..d411d9172 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -244,38 +244,46 @@ export function emitCallExpr( ? INTEGER_DISPATCH_SUFFIX : ""; const target = `@${borrowed ? mangleBorrowedFunction(e.callee) : host.callTarget(e.callee)}${abi ? INT32_ABI_SUFFIX : ""}${dispatch}`; + // A call of an unwinding-ABI callee is an invoke (emitDirectCall): + // its landing pad unwinds, so no pending check follows it. if (e.type.kind === "void") { - B.line(`call void ${target}(${argList})`); - if (host.mayThrow.has(e.callee)) host.emitPendingCheck(); + const invoked = host.emitDirectCall(`call void ${target}(${argList})`, e.callee); + if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(); if (borrowed) host.releaseTemps(host.frames.pop()!); return { name: "", type: e.type }; } const t = B.tmp(); if (abi?.union) { - B.line(`${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`); - if (host.mayThrow.has(e.callee)) host.emitPendingCheck(); + const invoked = host.emitDirectCall( + `${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`, + e.callee, + ); + if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(); if (borrowed) host.releaseTemps(host.frames.pop()!); if (stackResult) return host.stackScalarUnion(t, e.type); return host.own({ name: host.boxScalarUnion(t, e.type), type: e.type }); } if (abi?.result) { - B.line(`${t} = call i32 ${target}(${argList})`); + const invoked = host.emitDirectCall(`${t} = call i32 ${target}(${argList})`, e.callee); const out = host.int32Result(t, e.type); - if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out); + if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(out); if (borrowed) host.releaseTemps(host.frames.pop()!); return out; } - B.line(`${t} = call ${host.llType(e.type)} ${target}(${argList})`); + const invoked = host.emitDirectCall( + `${t} = call ${host.llType(e.type)} ${target}(${argList})`, + e.callee, + ); let out: LlValue; if (borrowed && host.borrowedReturns.has(e.callee)) { // A borrowed-return body hands back +0 (a null dummy when it threw): // test the exception first, then own the result before the // argument snapshots that keep it reachable are released. - if (host.mayThrow.has(e.callee)) host.emitPendingCheck(); + if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(); out = host.own({ name: host.retainFrame(t, e.type), type: e.type }); } else { out = host.own({ name: t, type: e.type }); - if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out); + if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(out); } if (borrowed) { host.toFrame(out); @@ -1366,10 +1374,11 @@ export function emitBorrowedResultCall(host: LlvmEmitterContext, e: ExprOf<"call ) .join(", "); const t = host.B.tmp(); - host.B.line( + const invoked = host.emitDirectCall( `${t} = call ${host.llType(e.type)} @${mangleBorrowedFunction(e.callee)}${abi ? INT32_ABI_SUFFIX : ""}(${argList}) ; borrowed result`, + e.callee, ); - if (host.mayThrow.has(e.callee)) host.emitPendingCheck(); + if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(); return { name: t, type: e.type }; } diff --git a/packages/compiler/src/backend/llvm/unwind-abi.test.ts b/packages/compiler/src/backend/llvm/unwind-abi.test.ts new file mode 100644 index 000000000..5f7f5895b --- /dev/null +++ b/packages/compiler/src/backend/llvm/unwind-abi.test.ts @@ -0,0 +1,285 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../../index.js"; +import { validateModule } from "../../ir/validate.js"; +import { emitLlvmModule } from "./emitter.js"; +import { UnwindAbiViolation, bridgePlainUnwindCalls } from "./unwind-abi.js"; + +async function moduleFor(source: string) { + const dir = await mkdtemp(join(tmpdir(), "scriptc-unwind-abi-")); + try { + const entry = join(dir, "main.ts"), + output = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n")); + const module = deserializeModule(await readFile(output, "utf8")); + expect(validateModule(module)).toEqual([]); + return module; + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** The definition of the source function `name` (its i32 or borrowing entry when it has one). */ +function body(ir: string, name: string): string { + return new RegExp( + `^define internal [^\\n]*@sc_b?f_[^\\n(]*${name}(?:\\.i32)?\\([^]*?^}`, + "m", + ).exec(ir)![0]; +} + +const SOURCE = ` +function leaf(n: number): number { + if (n === 3) throw new RangeError("three"); + return n * 2; +} +function first(n: number): number { + return leaf(n) + 1; +} +function second(n: number): number { + return first(n) * 3; +} +function third(n: number): number { + return second(n) - 2; +} +function mid(n: number): number { + const parts: string[] = ["a" + n]; + const r = third(n); + parts.push("b"); + return r + parts.length; +} +class Counter { + total = 0; + add(n: number): void { this.total += mid(n); } +} +function outer(n: number): string { + try { return "ok " + mid(n); } catch (e) { return "caught " + (e as Error).message; } +} +const counter = new Counter(); +for (let i = 0; i < 5; i++) { console.log(outer(i)); try { counter.add(i); } catch { console.log("add", i); } } +const f = (x: number) => mid(x); +console.log(f(1), counter.total); +`; + +const LINUX = { targetTriple: "x86_64-unknown-linux-gnu", unwindAbi: true } as const; + +test("directly called functions raise, and their callers invoke them without a pending check", async () => { + const ir = emitLlvmModule(await moduleFor(SOURCE), LINUX); + // leaf throws: the exception starts on the pending flag, and the three + // calls above it test the flag too; mid raises it on. + for (const name of ["leaf", "first", "second", "third"]) + expect(body(ir, name)).not.toContain("@scr_unwind_raise"); + const mid = body(ir, "mid"); + expect(mid).toMatch(/= call double @sc_b?f_[^(]*third\(/); + expect(mid).toContain("call void @scr_unwind_raise()"); + // outer invokes mid; the landing pad takes the exception to its handler. + const outer = body(ir, "outer"); + expect(outer).toMatch( + /= invoke double @sc_b?f_[^(]*mid[^\n]* to label %inv\.ok\d+ unwind label %inv\.lp\d+/, + ); + expect(outer).toMatch(/inv\.lp\d+:\n\s+%t\d+ = landingpad \{ ptr, i32 \} cleanup/); + expect(outer).toContain("personality ptr @__gcc_personality_v0"); + expect(ir).toContain("declare i32 @__gcc_personality_v0(...)"); + expect(ir).toContain("declare void @scr_unwind_raise() cold"); +}); + +test("a method of a class outside any hierarchy is on the unwinding ABI; closures keep the flag", async () => { + const ir = emitLlvmModule(await moduleFor(SOURCE), LINUX); + // Counter.add invokes mid and resumes the unwind from its landing pad; the + // closure over mid is a pointer target and returns with the flag. + const add = body(ir, "Counter_add"); + expect(add).toMatch(/invoke double @sc_b?f_[^(]*mid/); + expect(add).toMatch(/resume \{ ptr, i32 \} %[\w.]+/); + expect(add).not.toContain("@scr_unwind_raise"); + const closures = ir + .split(/\n(?=define )/) + .filter((def) => /^define internal [^\n]*@sc_b?f_[^\n(]*fn_/.test(def)); + expect(closures.length).toBeGreaterThan(0); + for (const def of closures) expect(def).not.toContain("call void @scr_unwind_raise()"); +}); + +test("recursive functions keep the pending flag, also when they recurse through a callback", async () => { + const source = ` +function leaf(n: number): number { + if (n < 0) throw new RangeError("negative"); + return n; +} +function one(n: number): number { return leaf(n) + 1; } +function two(n: number): number { return one(n) + 1; } +function three(n: number): number { return two(n) + 1; } +function four(n: number): number { + const parts: string[] = ["a" + n]; + const r = three(n); + return r + parts.length; +} +function walk(n: number, depth: number): number { + const parts: string[] = ["w" + n]; + const r = depth === 0 ? four(n) : walk(n, depth - 1); + return r + parts.length; +} +function visit(n: number, depth: number, each: (n: number, depth: number) => number): number { + const parts: string[] = ["v" + n]; + const r = depth === 0 ? four(n) : each(n, depth - 1); + return r + parts.length; +} +const each = (n: number, depth: number): number => visit(n, depth, each); +for (let i = -1; i < 2; i++) { + try { console.log(walk(i, 3), visit(i, 3, each)); } catch (e) { console.log((e as Error).message); } +} +`; + const ir = emitLlvmModule(await moduleFor(source), LINUX); + expect(body(ir, "four")).toContain("call void @scr_unwind_raise()"); + for (const name of ["walk", "visit"]) { + expect(body(ir, name)).not.toContain("@scr_unwind_raise"); + expect(body(ir, name)).toMatch(/invoke double @sc_b?f_[^(]*four/); + } +}); + +test("without the option, and off Linux, emission keeps the pending-flag ABI", async () => { + const mod = await moduleFor(SOURCE); + for (const options of [{}, { targetTriple: "arm64-apple-macosx14.0.0", unwindAbi: true }]) { + const ir = emitLlvmModule(mod, options); + expect(ir).not.toContain("invoke "); + expect(ir).not.toContain("@scr_unwind_raise"); + expect(ir).not.toContain("personality"); + } +}); + +test("bridgePlainUnwindCalls keeps the pending-flag contract at plain calls of raising functions", () => { + const raising = [ + "define internal ptr @sc_bf_raises(ptr %p) #0 { ; raises", + "entry:", + " call void @scr_unwind_raise()", + " unreachable", + "}", + ].join("\n"); + const caller = [ + "define internal ptr @sc_f_adapter(ptr %p0) #0 {", + "entry:", + " %result = call ptr @sc_bf_raises(ptr %p0) ; borrowed result", + " br label %next", + "next:", + " %v = phi ptr [ %result, %entry ]", + " ret ptr %v", + "}", + ].join("\n"); + const [out] = bridgePlainUnwindCalls([`${raising}\n${caller}`]); + expect(out).toContain( + "%result.ub = invoke ptr @sc_bf_raises(ptr %p0) to label %ub.n.0 unwind label %ub.lp.0", + ); + expect(out).toContain("%result = phi ptr [ %result.ub, %ub.n.0 ], [ null, %ub.lp.0 ]"); + // The split block's successor now comes from its last part. + expect(out).toContain("%v = phi ptr [ %result, %ub.ok.0 ]"); + expect(out).toMatch( + /define internal ptr @sc_f_adapter\(ptr %p0\) #0 personality ptr @__gcc_personality_v0 \{/, + ); + expect(out).toContain("declare i32 @__gcc_personality_v0(...)"); +}); + +test("an invoked wrapper of a raising function resumes the unwind; its address must not escape", () => { + const raising = [ + "define internal double @sc_bf_body.i32(i32 %a) #0 {", + "entry:", + " call void @scr_unwind_raise()", + " unreachable", + "}", + ].join("\n"); + const wrapper = [ + "define internal double @sc_bf_body(double %a) #0 {", + "entry:", + " %i = fptosi double %a to i32", + " %r = call double @sc_bf_body.i32(i32 %i)", + " ret double %r", + "}", + ].join("\n"); + const caller = [ + "define internal double @sc_bf_caller(double %a) #0 personality ptr @__gcc_personality_v0 {", + "entry:", + " %r = invoke double @sc_bf_body(double %a) to label %ok unwind label %lp", + "lp:", + " %x = landingpad { ptr, i32 } cleanup", + " call void @scr_unwind_raise()", + " unreachable", + "ok:", + " ret double %r", + "}", + ].join("\n"); + const [out] = bridgePlainUnwindCalls([[raising, wrapper, caller].join("\n")]); + expect(out).toContain( + "%r = invoke double @sc_bf_body.i32(i32 %i) to label %ub.ok.0 unwind label %ub.lp.0", + ); + expect(out).toContain("resume { ptr, i32 } %ub.v.0"); + expect(() => + bridgePlainUnwindCalls([ + `${raising}\n@table = internal constant [1 x ptr] [ptr @sc_bf_body.i32]`, + ]), + ).toThrow(UnwindAbiViolation); + try { + bridgePlainUnwindCalls([ + `${raising}\n@table = internal constant [1 x ptr] [ptr @sc_bf_body.i32]`, + ]); + } catch (error) { + expect([...(error as UnwindAbiViolation).symbols]).toEqual(["sc_bf_body.i32"]); + } +}); + +test("an invoked function that never tests the pending flag needs no raise", () => { + const raising = [ + "define internal void @sc_bf_raises() #0 {", + "entry:", + " call void @scr_unwind_raise()", + " unreachable", + "}", + ].join("\n"); + const leaf = [ + "define internal double @sc_bf_leaf() #0 {", + "entry:", + " ret double 1.0", + "}", + ].join("\n"); + const caller = (callee: string) => + [ + "define internal double @sc_bf_caller() #0 personality ptr @__gcc_personality_v0 {", + "entry:", + ` %r = invoke double @${callee}() to label %ok unwind label %lp`, + "lp:", + " %x = landingpad { ptr, i32 } cleanup", + " call void @scr_unwind_raise()", + " unreachable", + "ok:", + " ret double %r", + "}", + ].join("\n"); + expect(() => + bridgePlainUnwindCalls([[raising, leaf, caller("sc_bf_leaf")].join("\n")]), + ).not.toThrow(); + const testing = leaf.replace( + " ret double 1.0", + " %f = load i32, ptr @scr_exc_flag\n ret double 1.0", + ); + expect(() => + bridgePlainUnwindCalls([[raising, testing, caller("sc_bf_leaf")].join("\n")]), + ).toThrow(UnwindAbiViolation); +}); + +test("calls that cannot unwind are marked nounwind", () => { + const raising = [ + "define internal void @sc_bf_raises(ptr %b) #0 {", + "entry:", + " %n = call i64 @scr_bytes_length(ptr %b) ; length", + " call void @scr_bytes_release(ptr %b), !dbg !4", + " %r = call ptr %b(ptr %b)", + " call void @scr_unwind_raise()", + " unreachable", + "}", + ].join("\n"); + const [out] = bridgePlainUnwindCalls([raising]); + expect(out).toContain("%n = call i64 @scr_bytes_length(ptr %b) nounwind ; length"); + expect(out).toContain("call void @scr_bytes_release(ptr %b) nounwind, !dbg !4"); + expect(out).toContain("%r = call ptr %b(ptr %b) nounwind"); + expect(out).toContain(" call void @scr_unwind_raise()\n"); +}); diff --git a/packages/compiler/src/backend/llvm/unwind-abi.ts b/packages/compiler/src/backend/llvm/unwind-abi.ts new file mode 100644 index 000000000..4dc611913 --- /dev/null +++ b/packages/compiler/src/backend/llvm/unwind-abi.ts @@ -0,0 +1,491 @@ +/* The functions that propagate exceptions by unwinding instead of the + * pending flag (see unwindAbiFunctions). */ +import type { IrExpr, IrModule } from "../../ir/ir.js"; +import { everyStmtList } from "../../ir/traverse.js"; + +/** How many calls above a `throw` functions keep the pending flag. */ +const THROW_LEVELS = 3; +/** The call graph's node for calls through values. */ +const ANY_FUNCTION = " any function"; + +/** The nodes of `edges` on a cycle (a recursion), by Tarjan's algorithm + * without recursion of its own: a large module's call chains are deeper + * than the host stack. */ +function recursiveFunctions(edges: ReadonlyMap): Set { + const index = new Map(); + const low = new Map(); + const stack: string[] = []; + const onStack = new Set(); + const result = new Set(); + let counter = 0; + const enter = (name: string): { name: string; next: number } => { + index.set(name, counter); + low.set(name, counter++); + stack.push(name); + onStack.add(name); + return { name, next: 0 }; + }; + for (const root of edges.keys()) { + if (index.has(root)) continue; + const work = [enter(root)]; + while (work.length > 0) { + const top = work[work.length - 1]!; + const out = edges.get(top.name) ?? []; + if (top.next < out.length) { + const callee = out[top.next++]!; + if (!index.has(callee)) work.push(enter(callee)); + else if (onStack.has(callee)) + low.set(top.name, Math.min(low.get(top.name)!, index.get(callee)!)); + continue; + } + work.pop(); + const parent = work[work.length - 1]; + if (parent !== undefined) + low.set(parent.name, Math.min(low.get(parent.name)!, low.get(top.name)!)); + if (low.get(top.name) !== index.get(top.name)) continue; + const members: string[] = []; + let member: string; + do { + member = stack.pop()!; + onStack.delete(member); + members.push(member); + } while (member !== top.name); + if (members.length > 1 || out.includes(top.name)) for (const m of members) result.add(m); + } + } + return result; +} + +/** The may-throw functions whose exceptions reach their callers by + * unwinding the native stack (scr_unwind_raise) instead of returning with + * the pending flag set. Their callers call them with `invoke`, so the call + * needs no flag test; the landing pad runs the frame's unwind. + * + * A function qualifies only when every way it can be reached is a direct + * `call` from emitted code that can lower it as an `invoke`: it is not + * async or a generator, not a constructor, not a method a virtual call can + * reach or one a vtable holds (an overridden method), not a closure + * target, not the entry or a library export, not named by any node other + * than a `call`'s callee, and not called from an async or generator body + * (coroutine bodies keep plain calls). + * + * A native unwind costs about 2 µs a raise and 0.1 µs a frame, against a + * few nanoseconds for a flag test, so the functions an exception is likely to + * cross many of keep the flag: those with a `throw` or rethrow of their own + * and those up to THROW_LEVELS calls above one (a handler near the throw + * takes it without a native unwind), and the recursive ones (deep stacks + * are recursions). */ +export function unwindAbiFunctions( + mod: IrModule, + mayThrow: ReadonlySet, + excluded: ReadonlySet = new Set(), +): Set { + const names = new Set(mod.functions.map((fn) => fn.name)); + const blocked = new Set(excluded); + blocked.add(mod.entry); + // The runtime calls the error property readers through the pointers the + // module installs (emit-module.ts). + blocked.add("%error.name.read"); + blocked.add("%error.message.read"); + for (const entry of mod.lib?.exports ?? []) blocked.add(entry.fnName); + // Methods a virtual call can reach, and the overridden methods of class + // hierarchies (whose vtables list them), are called through pointers; the + // others are called directly, like functions. A virtual call reaches its listed + // targets, or else the method in its receiver class's hierarchy; a + // receiver that is no class of the module (an interface) may reach any + // class's method of that name. + const classes = new Map((mod.classes ?? []).map((cls) => [cls.name, cls])); + const anyClass = new Set(); + for (const fn of mod.functions) + everyStmtList(fn.body, { + expr: (e: IrExpr) => { + if (e.kind !== "virtualCall") return true; + if (e.targets !== undefined) for (const target of e.targets) blocked.add(target); + else if (classes.has(e.className)) blocked.add(`%${e.className}.${e.method}`); + else anyClass.add(e.method); + return true; + }, + stmt: () => true, + }); + // A hierarchy's vtables hold the methods declared by more than one of its + // classes (an override and what it overrides). + const rootOf = (name: string): string => { + let cls = classes.get(name); + while (cls?.base !== undefined && classes.has(cls.base)) cls = classes.get(cls.base); + return cls?.name ?? name; + }; + const declarers = new Map(); + for (const cls of mod.classes ?? []) + for (const m of cls.methods ?? []) { + const key = `${rootOf(cls.name)}\0${m}`; + declarers.set(key, (declarers.get(key) ?? 0) + 1); + } + for (const cls of mod.classes ?? []) { + blocked.add(`%${cls.name}.constructor`); + blocked.add(`%${cls.name}.construct`); + const root = rootOf(cls.name); + for (const m of cls.methods ?? []) + if (anyClass.has(m) || declarers.get(`${root}\0${m}`)! > 1) blocked.add(`%${cls.name}.${m}`); + } + // Any function a node names other than as a call's callee may be reached + // through a pointer. + const referenced = new Set(); + const noteNames = (node: object, skip: string | undefined): void => { + for (const key in node) { + if (key === skip) continue; + const value = (node as Record)[key]; + if (typeof value === "string" && names.has(value)) referenced.add(value); + } + }; + // The call graph, for the functions near a throw and the recursive ones. + // Calls through values (closures, class values, dynamic calls, callbacks + // passed to the runtime) reach ANY_FUNCTION, which reaches every function + // a value can hold. + const throws = new Set(); + const callers = new Map(); + const callees = new Map(); + const edge = (from: string, to: string): void => { + let list = callers.get(to); + if (list === undefined) callers.set(to, (list = [])); + list.push(from); + let out = callees.get(from); + if (out === undefined) callees.set(from, (out = [])); + out.push(to); + }; + const methodsNamed = new Map(); + for (const cls of mod.classes ?? []) + for (const m of cls.methods ?? []) { + let list = methodsNamed.get(m); + if (list === undefined) methodsNamed.set(m, (list = [])); + list.push(`%${cls.name}.${m}`); + } + for (const fn of mod.functions) { + const coroutine = fn.async === true || fn.generator !== undefined; + everyStmtList(fn.body, { + expr: (e: IrExpr) => { + const kind = e.kind as string; + if (kind === "throw" || kind === "rethrow") throws.add(fn.name); + if (e.kind === "call") { + edge(fn.name, e.callee); + // The entry's callees (the module initializers) run once: on the + // flag they keep landing pads out of the top-level code inlined + // into the entry. + if (coroutine || fn.name === mod.entry) blocked.add(e.callee); + noteNames(e, "callee"); + return true; + } + noteNames(e, undefined); + if (e.kind === "virtualCall") + for (const target of e.targets ?? methodsNamed.get(e.method) ?? []) edge(fn.name, target); + else if (e.kind === "new") { + edge(fn.name, `%${e.className}.constructor`); + edge(fn.name, `%${e.className}.construct`); + } else if (kind === "callValue" || kind === "dynCall" || kind === "newValue") + edge(fn.name, ANY_FUNCTION); + else if ("args" in e) { + const args: unknown = e.args; + if (Array.isArray(args)) + for (const arg of args as IrExpr[]) + if (arg.type.kind === "func") edge(fn.name, ANY_FUNCTION); + } + return true; + }, + stmt: (s) => { + const kind = s.kind as string; + if (kind === "throw" || kind === "rethrow") throws.add(fn.name); + noteNames(s, undefined); + return true; + }, + }); + } + for (const name of referenced) { + blocked.add(name); + edge(ANY_FUNCTION, name); + } + // A throw starts on the pending flag, and so do the functions up to + // THROW_LEVELS calls above it. The union narrowing helpers' throws (a + // failed narrowing) don't count for their callers. + for (const name of throws) blocked.add(name); + let level = [...throws].filter((name) => !name.startsWith("%union.")); + const near = new Set(level); + for (let i = 0; i < THROW_LEVELS; i++) { + const next: string[] = []; + for (const name of level) + for (const caller of callers.get(name) ?? []) + if (!near.has(caller)) { + near.add(caller); + next.push(caller); + } + level = next; + } + for (const name of near) blocked.add(name); + for (const name of recursiveFunctions(callees)) blocked.add(name); + const result = new Set(); + for (const fn of mod.functions) { + if (!mayThrow.has(fn.name) || blocked.has(fn.name)) continue; + if (fn.async === true || fn.generator !== undefined) continue; + result.add(fn.name); + } + return result; +} + +/** What an unwinding-ABI function's unwind ends with when no handler of + * its own takes the exception. */ +export const UNWIND_RAISE = "call void @scr_unwind_raise()\n unreachable"; +/** What the unwind of a landing pad ends with when no handler of the + * function takes the exception: the unwind goes on (`resume`) with the + * exception the landing pad stored in UNWIND_SLOT. The block builder + * expands it (BlockBuilder.terminate). */ +export const UNWIND_RESUME = "resume { ptr, i32 } "; +/** The function's slot for the exception its landing pads take. */ +export const UNWIND_SLOT = "%unw.exc"; +/** scr_unwind_raise's declaration. It is not `noreturn`: sanitized builds + * would call the sanitizer's no-return hook before it, which fails on fiber + * stacks; scr_unwind_raise clears the unwound frames' shadow itself. */ +export const UNWIND_RAISE_DECLARATION = "declare void @scr_unwind_raise() cold"; + +/** The personality of functions with landing pads: the landing pads are + * cleanups that scr_unwind_raise's forced unwind runs. */ +export const UNWIND_PERSONALITY = "personality ptr @__gcc_personality_v0"; + +/** An unwind leaving the function: a raise, or a landing pad's `resume`. */ +const UNWINDS = /call void @scr_unwind_raise\(\)|\n\s+resume /; +/** A pending-flag test: the thread's exception word, the active cell, or the runtime query. */ +const PENDING_TEST = /@scr_exc_flag\b|%exc\.cell\b|@scr_exc_active\b|@scr_exc_pending\(/; + +/** Functions emission found breaking the unwinding ABI's rules + * (bridgePlainUnwindCalls), by symbol: the caller emits the module again + * with them on the pending-flag ABI. */ +export class UnwindAbiViolation extends Error { + readonly symbols: ReadonlySet; + constructor(symbols: ReadonlySet) { + super(`unwinding ABI: ${[...symbols].slice(0, 3).join(", ")} must keep the pending flag`); + this.symbols = symbols; + } +} +const DEFINE_NAME = /^define [^@\n]*@([\w.$%-]+)\(/; +const PLAIN_CALL = /^(\s*)(%[\w.$-]+) = call (.*?)@([\w.$%-]+)\(|^(\s*)call (.*?)@([\w.$%-]+)\(/; +const RETURN_ATTRIBUTES = + /\b(?:zeroext|signext|noalias|nonnull|noundef|inreg|dereferenceable(?:_or_null)?\(\d+\)|align \d+)\s+/g; + +/** A call instruction's line. */ +const CALL_LINE = /^\s*(?:%[\w.$-]+ = )?(?:(?:tail|musttail|notail) )?call /; +/** A call's callee (a function or a register) and the parenthesis that + * opens its arguments. */ +const CALLEE = /[@%][\w.$%-]+\(/; + +/** The index of the parenthesis that closes the argument list opened at `open`. */ +function argumentsEnd(line: string, open: number): number { + let depth = 0; + let close = open; + for (; close < line.length; close++) { + const c = line.charAt(close); + if (c === "(") depth++; + else if (c === ")" && --depth === 0) break; + } + return close; +} + +/** A call that cannot unwind, marked so: only scr_unwind_raise and the + * raising functions propagate an unwind (runtime code and every function + * reached through a pointer return normally), so every other call is + * `nounwind`, and the inliner keeps it a call where it inlines a raising + * function at an invoke. */ +function markNounwind(line: string): string { + const call = line.indexOf("call "); + const callee = CALLEE.exec(line.slice(call)); + if (callee === null || callee[0] === "@scr_unwind_raise(") return line; + const close = argumentsEnd(line, call + callee.index + callee[0].length - 1); + return close >= line.length + ? line + : `${line.slice(0, close + 1)} nounwind${line.slice(close + 1)}`; +} + +/** The value a bridged call yields on its landing pad: the dummy a + * pending-flag callee returns with an exception pending. */ +function dummyValue(type: string): string { + if (type === "ptr") return "null"; + if (type === "double" || type === "float") return "0.0"; + if (/^i\d+$/.test(type)) return "0"; + return "zeroinitializer"; +} + +/** Rewrites every remaining plain call of an unwinding-ABI function (one + * that raises, scr_unwind_raise, or resumes an unwind) in `parts`, the module's text, into an + * invoke whose landing pad continues after the call with the exception + * pending and the call's dummy result: the pending-flag contract those + * call sites were emitted for, whichever lowering emitted them. Calls of + * these functions that emitDirectCall lowered are invokes already. */ +export function bridgePlainUnwindCalls(parts: string[]): string[] { + const raisers = new Set(); + const split = parts.map((part) => part.split(/\n(?=define )/)); + for (const chunks of split) + for (const chunk of chunks) { + const m = DEFINE_NAME.exec(chunk); + if (m !== null && UNWINDS.test(chunk)) raisers.add(m[1]!); + } + if (raisers.size === 0) return parts; + // An invoke's callee must raise or never test the pending flag: one that + // returned with the flag pending would pass the exception by its invoking + // caller unseen. A wrapper an invoke reaches (an int32 or borrowed-ABI + // entry forwarding to the body) propagates its callee's unwind instead + // (`resume`), so it raises too. A raising function must be reached only by + // calls: a pointer to it (a vtable or table entry, a runtime callback) + // could be called without a landing pad. Functions that break either rule + // go back to the pending-flag ABI (UnwindAbiViolation). + const violations = new Set(); + const invoked = new Set(); + const testsFlag = new Set(); + for (const chunks of split) + for (const chunk of chunks) { + const m = DEFINE_NAME.exec(chunk); + if (m !== null && PENDING_TEST.test(chunk)) testsFlag.add(m[1]!); + for (const c of chunk.matchAll(/ invoke [^@\n]*@([\w.$%-]+)\(/g)) invoked.add(c[1]!); + } + const resumers = new Set(); + for (let grew = true; grew;) { + grew = false; + for (const chunks of split) + for (const chunk of chunks) { + const m = DEFINE_NAME.exec(chunk); + const name = m?.[1]; + if (name === undefined || raisers.has(name) || !invoked.has(name)) continue; + if (![...chunk.matchAll(/ call [^@\n]*@([\w.$%-]+)\(/g)].some((c) => raisers.has(c[1]!))) + continue; + if (testsFlag.has(name)) continue; + raisers.add(name); + resumers.add(name); + grew = true; + } + } + for (const name of invoked) if (!raisers.has(name) && testsFlag.has(name)) violations.add(name); + for (const chunks of split) + for (const chunk of chunks) + for (const line of chunk.split("\n")) { + if (line.startsWith("define ")) continue; + for (const m of line.matchAll(/@([\w.$%-]+)/g)) { + if (!raisers.has(m[1]!)) continue; + const at = m.index!; + if ( + /(?:call|invoke) [^@]*$/.test(line.slice(0, at)) && + line.charAt(at + m[0].length) === "(" + ) + continue; + violations.add(m[1]!); + } + } + if (violations.size > 0) throw new UnwindAbiViolation(violations); + let personality = false; + let counter = 0; + const out = split.map((chunks) => + chunks + .map((chunk) => { + if (!DEFINE_NAME.test(chunk) || !chunk.includes("call ")) return chunk; + const lines = chunk.split("\n"); + const resume = resumers.has(DEFINE_NAME.exec(chunk)![1]!); + const result: string[] = []; + // The label a block's terminator now follows, for phi operands. + const moved = new Map(); + let block = ""; + let changed = false; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]!; + const label = /^([\w.$-]+):/.exec(line); + if (label !== null) block = label[1]!; + const m = PLAIN_CALL.exec(line); + const callee = m === null ? undefined : (m[4] ?? m[7]); + if (m === null || callee === undefined || !raisers.has(callee)) { + const marked = CALL_LINE.test(line) ? markNounwind(line) : line; + changed ||= marked !== line; + result.push(marked); + continue; + } + // The call's text from its callee to the end of the argument list. + const close = argumentsEnd(line, line.indexOf(`@${callee}(`) + callee.length + 1); + const head = line.slice(line.indexOf("call ") + 5, close + 1); + const tail = line.slice(close + 1); + const meta = /,\s*!/.exec(tail); + const attrs = ( + meta === null ? tail.replace(/;.*$/, "") : tail.slice(0, meta.index) + ).trimEnd(); + const dbg = meta === null ? "" : tail.slice(meta.index).replace(/\s;.*$/, ""); + const n = counter++; + if (block === "") { + result.splice(1, 0, "entry.ub:"); + block = "entry.ub"; + } + const indent = m[1] ?? m[5] ?? " "; + const ok = `ub.ok.${n}`; + const pad = `ub.lp.${n}`; + if (resume) { + // The wrapper holds nothing to release: the unwind goes on to + // its caller's landing pad. + const assign = m[2] !== undefined ? `${m[2]} = ` : ""; + result.push( + `${indent}${assign}invoke ${head}${attrs} to label %${ok} unwind label %${pad}${dbg}`, + ); + result.push( + `${pad}:`, + `${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`, + `${indent}resume { ptr, i32 } %ub.v.${n}`, + ); + result.push(`${ok}:`); + } else if (m[2] !== undefined) { + const type = (m[3] ?? "").replace(RETURN_ATTRIBUTES, "").trim(); + const normal = `ub.n.${n}`; + result.push( + `${indent}${m[2]}.ub = invoke ${head}${attrs} to label %${normal} unwind label %${pad}${dbg}`, + ); + result.push(`${normal}:`, `${indent}br label %${ok}`); + result.push( + `${pad}:`, + `${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`, + `${indent}br label %${ok}`, + ); + result.push( + `${ok}:`, + `${indent}${m[2]} = phi ${type} [ ${m[2]}.ub, %${normal} ], [ ${dummyValue(type)}, %${pad} ]`, + ); + } else { + result.push( + `${indent}invoke ${head}${attrs} to label %${ok} unwind label %${pad}${dbg}`, + ); + result.push( + `${pad}:`, + `${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`, + `${indent}br label %${ok}`, + ); + result.push(`${ok}:`); + } + moved.set(block, ok); + changed = true; + } + if (!changed) return chunk; + if (moved.size === 0) return result.join("\n"); + personality = true; + const header = result[0]!; + const brace = header.lastIndexOf(" {"); + if (!header.includes("personality") && brace >= 0) { + // Before the function's metadata attachments (`!dbg`), if any. + const dbg = header.slice(0, brace).search(/\s!dbg !\d+$/); + const at = dbg >= 0 ? dbg : brace; + result[0] = `${header.slice(0, at)} ${UNWIND_PERSONALITY}${header.slice(at)}`; + } + // A split block's successors now come from its last part. + if (moved.size > 0) + for (let i = 0; i < result.length; i++) { + const line = result[i]!; + if (!line.includes(" phi ")) continue; + result[i] = line.replace(/, %([\w.$-]+) \]/g, (all, name: string) => { + const to = moved.get(name); + return to === undefined ? all : `, %${to} ]`; + }); + } + return result.join("\n"); + }) + .join("\n"), + ); + if (personality && !out.some((part) => part.includes("@__gcc_personality_v0(...)"))) + out[0] = `declare i32 @__gcc_personality_v0(...)\n${out[0]}`; + return out; +} diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index ceb162bcc..1364bca4d 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -43,6 +43,7 @@ import { cacheTargetIdentity, isZigDriver, isMuslTarget, + type CcDriver, } from "./driver.js"; import { resolveProgramShardMergeIdentity, @@ -821,6 +822,7 @@ export async function compileCInternal( // generated/native input that references inflate symbols. Cross // and targetless Zig builds use vendored zlib objects in the input section above. ...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []), + ...unwinderLinkArgs(driver), // glibc keeps libm separate from libc. This must trail the generated // program and every native FFI input because GNU ld resolves archives // from left to right. @@ -1068,6 +1070,7 @@ export async function compileCInternal( ...(mbedtls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []), ...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []), ...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []), + ...unwinderLinkArgs(driver), ...driver.linkArgs, ...executableLinkFlags, ]; @@ -1080,6 +1083,7 @@ export async function compileCInternal( ...(mbedtls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []), ...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []), ...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []), + ...unwinderLinkArgs(driver), ...driver.linkArgs, ...executableLinkFlags, ]; @@ -1689,3 +1693,11 @@ export async function compileC(opts: CcOptions): Promise { await rm(`${opts.outPath}.dSYM`, { recursive: true, force: true }); } } + +/** Linux runtimes raise through the native unwinder (scr_unwind_raise): + * Zig's libunwind, or the toolchain's libgcc_eh. Both are static archives, + * so programs load no shared library for them. */ +function unwinderLinkArgs(driver: CcDriver): string[] { + if (targetPlatform(driver) !== "linux") return []; + return isZigDriver(driver) ? ["-lunwind"] : ["-lgcc_eh"]; +} diff --git a/packages/compiler/src/backend/native/runtime-inputs.ts b/packages/compiler/src/backend/native/runtime-inputs.ts index c7c5bc32b..d32867331 100644 --- a/packages/compiler/src/backend/native/runtime-inputs.ts +++ b/packages/compiler/src/backend/native/runtime-inputs.ts @@ -53,6 +53,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [ "scr_object.c", "scr_union.c", "scr_exception.c", + "scr_unwind.c", "scr_error.c", "scr_console.c", "scr_lib.c", diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index e2d1c97cc..4fb64968b 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -875,6 +875,7 @@ async function prepareExecutableInput( pointerBits: buildPlatform === "wasi" ? 32 : 64, wasi: buildPlatform === "wasi", runtimeAbiMarker: useRuntimePack, + unwindAbi: buildPlatform === "linux", objectAudit: opts.sanitize === true, lowRegion: lowRegionTarget(buildPlatform, opts.sanitize === true), ...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}), diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index 67a4b5a9e..e90c1f043 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -402,6 +402,7 @@ export function prepareNativeExecutable( pointerBits: toolchain.target.pointerBits, wasi: toolchain.target.platform === "wasi", runtimeAbiMarker: outputKind === "obj" || outputKind === "exe", + unwindAbi: outputKind === "exe" && toolchain.target.platform === "linux", objectAudit: options.sanitize === true, lowRegion: lowRegionTarget(toolchain.target.platform, options.sanitize === true), functionWorkers: emitWorkers(), diff --git a/packages/runtime-linux-arm64-gnu/scripts/build.mjs b/packages/runtime-linux-arm64-gnu/scripts/build.mjs index 729c3fbb6..d16fb1528 100644 --- a/packages/runtime-linux-arm64-gnu/scripts/build.mjs +++ b/packages/runtime-linux-arm64-gnu/scripts/build.mjs @@ -24,6 +24,10 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ }, ...toolchain, compileFlags: ["-ffunction-sections", "-fdata-sections"], - systemLibraries: [{ name: "m", predicate: true }], + // scr_unwind_raise: the static libgcc_eh, which loads no shared library. + systemLibraries: [ + { name: "m", predicate: true }, + { name: "gcc_eh", predicate: true }, + ], }); await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-linux-arm64-musl/scripts/build.mjs b/packages/runtime-linux-arm64-musl/scripts/build.mjs index 10635d857..6c9b38244 100644 --- a/packages/runtime-linux-arm64-musl/scripts/build.mjs +++ b/packages/runtime-linux-arm64-musl/scripts/build.mjs @@ -26,6 +26,10 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ }, targetArgs: ["-target", "aarch64-linux-musl"], compileFlags: ["-ffunction-sections", "-fdata-sections"], - systemLibraries: [{ name: "m", predicate: true }], + // scr_unwind_raise: Zig's libunwind. + systemLibraries: [ + { name: "m", predicate: true }, + { name: "unwind", predicate: true }, + ], }); await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-linux-x64-gnu/scripts/build.mjs b/packages/runtime-linux-x64-gnu/scripts/build.mjs index 811bb537b..53df5f7e7 100644 --- a/packages/runtime-linux-x64-gnu/scripts/build.mjs +++ b/packages/runtime-linux-x64-gnu/scripts/build.mjs @@ -24,6 +24,10 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ }, ...toolchain, compileFlags: ["-ffunction-sections", "-fdata-sections"], - systemLibraries: [{ name: "m", predicate: true }], + // scr_unwind_raise: the static libgcc_eh, which loads no shared library. + systemLibraries: [ + { name: "m", predicate: true }, + { name: "gcc_eh", predicate: true }, + ], }); await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-linux-x64-musl/scripts/build.mjs b/packages/runtime-linux-x64-musl/scripts/build.mjs index 33fc4c3d1..88538de76 100644 --- a/packages/runtime-linux-x64-musl/scripts/build.mjs +++ b/packages/runtime-linux-x64-musl/scripts/build.mjs @@ -26,6 +26,10 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ }, targetArgs: ["-target", "x86_64-linux-musl"], compileFlags: ["-ffunction-sections", "-fdata-sections"], - systemLibraries: [{ name: "m", predicate: true }], + // scr_unwind_raise: Zig's libunwind. + systemLibraries: [ + { name: "m", predicate: true }, + { name: "unwind", predicate: true }, + ], }); await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 600637ba6..4df534739 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -30,6 +30,7 @@ export const BASE_RUNTIME_SOURCES = [ "scr_object.c", "scr_union.c", "scr_exception.c", + "scr_unwind.c", "scr_error.c", "scr_console.c", "scr_lib.c", diff --git a/packages/runtime/src/scr_exception.h b/packages/runtime/src/scr_exception.h index e99f84243..63dd4a3ca 100644 --- a/packages/runtime/src/scr_exception.h +++ b/packages/runtime/src/scr_exception.h @@ -123,6 +123,11 @@ static inline ScrExcCell *scr_exc_swap_cell_inline(ScrExcCell *cell) { _Static_assert(offsetof(ScrExcCell, kind) == 0 && sizeof(ScrExcKind) == 4 && SCR_EXC_NONE == 0, "the LLVM emitter's inline pending check loads an i32 kind at offset 0"); +/* Unwinds the native stack to the nearest landing pad of emitted code with + * the exception pending in the active cell (the unwinding ABI of emitted + * functions, unwind-abi.ts). Linux (glibc and musl) only; it never + * returns. */ +SCR_COLD _Noreturn void scr_unwind_raise(void); SCR_COLD void scr_throw_f64(double v); SCR_COLD void scr_throw_bool(bool v); SCR_COLD void scr_throw_str(ScrStr *v); /* takes ownership */ diff --git a/packages/runtime/src/scr_unwind.c b/packages/runtime/src/scr_unwind.c new file mode 100644 index 000000000..9bdf2dbd2 --- /dev/null +++ b/packages/runtime/src/scr_unwind.c @@ -0,0 +1,76 @@ +/* scr_unwind_raise: the native unwind of the unwinding exception ABI of + * emitted functions (the compiler's unwind-abi.ts). Linux (glibc and musl) + * only; elsewhere this unit is empty. */ +#include "scr_runtime.h" + +#if defined(__linux__) && !defined(__ANDROID__) && !defined(__wasi__) +#include +#include +#include +#include +#include + +#if defined(__SANITIZE_ADDRESS__) +#define SCR_UNWIND_ASAN 1 +#elif defined(__has_feature) +#if __has_feature(address_sanitizer) +#define SCR_UNWIND_ASAN 1 +#endif +#endif +#ifdef SCR_UNWIND_ASAN +#include +#endif + +#ifdef SCR_UNWIND_ASAN +/* The lowest stack address the unwind has not cleared yet. The unwind + * outlives the frame that started it: landing pads resume it. */ +static SCR_TL uintptr_t scr_unwind_low; +#endif + +/* Emitted landing pads are cleanups: they release the frame's references + * and then either take the exception (a handler of theirs, or a frame on + * the pending-flag ABI that returns with it pending) or resume the unwind. + * So the unwind is forced, a single pass through every landing pad; the + * exception itself stays in the active cell. */ +static _Unwind_Reason_Code scr_unwind_stop(int version, _Unwind_Action actions, + _Unwind_Exception_Class cls, + struct _Unwind_Exception *exc, + struct _Unwind_Context *context, void *arg) { + (void)version; + (void)cls; + (void)exc; +#ifdef SCR_UNWIND_ASAN + /* The frames this unwind passes never return, so it clears their + * redzones' shadow itself. Emitted code calls scr_unwind_raise as an + * ordinary function: the sanitizer's hook before no-return calls needs + * stack bounds, which fiber stacks do not report. */ + uintptr_t cfa = _Unwind_GetCFA(context); + if (cfa > scr_unwind_low) { + __asan_unpoison_memory_region((void *)scr_unwind_low, cfa - scr_unwind_low); + scr_unwind_low = cfa; + } +#else + (void)context; +#endif + (void)arg; + if (actions & _UA_END_OF_STACK) { + fputs("scriptc: an exception unwound past the program's outermost frame\n", stderr); + abort(); + } + return _URC_NO_REASON; +} + +static SCR_TL struct _Unwind_Exception scr_unwind_exception; + +void scr_unwind_raise(void) { + struct _Unwind_Exception *exc = &scr_unwind_exception; + memset(exc, 0, sizeof *exc); + exc->exception_class = 0x5343524950540000ull; /* "SCRIPT\0\0" */ +#ifdef SCR_UNWIND_ASAN + scr_unwind_low = (uintptr_t)__builtin_frame_address(0); +#endif + _Unwind_ForcedUnwind(exc, scr_unwind_stop, NULL); + fputs("scriptc: the native unwinder could not raise an exception\n", stderr); + abort(); +} +#endif diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index a1ab3ca60..b23f61a0a 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -395,7 +395,9 @@ export function dynamicTierSymbols(names) { * bench/channel-messages and bench/worker-messages, added later, link them * through worker messages: * node:worker_threads values are dyn objects. bench/json-unions, added - * later too, decodes JSON.parse trees. bench/dynamic-methods and + * later too, decodes JSON.parse trees. bench/throw-chains, also added + * later, tests caught values with `instanceof`, as bench/validate-errors + * does. bench/dynamic-methods and * bench/dynamic-properties are untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and * tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also * fails when a listed workload stops linking them, so remove a workload here @@ -417,6 +419,7 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/log-summary", "bench/inventory-report", "bench/json-unions", + "bench/throw-chains", ]); /** Names of the functions (text and weak symbols) in `nm` output. Mach-O From 396c6944ecad5bda1d8ab9dd0179181aa0cf97a2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 11:20:47 -0500 Subject: [PATCH 065/273] Report how well the native compiler's profile matches its functions The native compiler is built with a committed optimization profile. A function whose control flow changed since the profile was collected is optimized as without one, so the profile loses its gain as compiler code changes, with nothing to show it. With --profile-use, the helper now counts what the profile annotation reports while it applies the profile: functions that took the profile's counts, functions whose control flow no longer matches (hash mismatch), and functions the profile has no record of. It sums the counts over partitions, keeps each partition's counts beside its cached summary, and prints them as one line of JSON after a successful emit. The compiler reads that line (an older helper prints none) and returns it as the executable artifact's profileMatch. build-native-cli prints the share of profiled functions that match, and warns below 90% that the profile should be refreshed. On main aa1c26845, a profile refreshed on that commit matches 92.7% of the compiler's profiled functions, and the one committed about 200 commits earlier 87.2%; that one kept about half of the fresh profile's gain on a cold tsc-ts build. --- native/llvm-codegen/src/emit.cpp | 157 +++++++++++++++++- .../compiler/src/backend/native-codegen.ts | 10 +- .../src/backend/native-profile.test.ts | 13 ++ .../compiler/src/backend/native-profile.ts | 39 +++++ packages/compiler/src/compile-types.ts | 3 + packages/compiler/src/index.ts | 5 + scripts/build-native-cli.mts | 25 +++ 7 files changed, 242 insertions(+), 10 deletions(-) diff --git a/native/llvm-codegen/src/emit.cpp b/native/llvm-codegen/src/emit.cpp index 9cae164d3..35d7ce4f0 100644 --- a/native/llvm-codegen/src/emit.cpp +++ b/native/llvm-codegen/src/emit.cpp @@ -19,6 +19,7 @@ #include "llvm/CodeGen/MachineFunction.h" #include "llvm/CodeGen/MachineFunctionPass.h" #include "llvm/CodeGen/MachineModuleInfo.h" +#include "llvm/IR/DiagnosticHandler.h" #include "llvm/IR/DiagnosticInfo.h" #include "llvm/IR/DiagnosticPrinter.h" #include "llvm/IR/LegacyPassManager.h" @@ -41,11 +42,13 @@ #include "llvm/Support/SourceMgr.h" #include "llvm/Support/ThreadPool.h" #include "llvm/Support/Threading.h" +#include "llvm/Support/JSON.h" #include "llvm/Support/raw_ostream.h" #include "llvm/Target/TargetMachine.h" #include "llvm/TargetParser/Triple.h" #include "llvm/Transforms/Utils/SplitModule.h" +#include #include #include #include @@ -170,6 +173,108 @@ void limitProfileImports() { (void)Limited; } +// How the --profile-use profile matched the functions it annotated, summed +// over partitions: functions that received its counts, functions whose +// control flow changed since it was collected (hash mismatch), and functions +// it has no record of (new, or never run in training: the merged profile is +// sparse). A partition reused from the cache without its counts is counted +// in Unknown. +struct ProfileMatch { + std::atomic Annotated{0}; + std::atomic Mismatched{0}; + std::atomic Missing{0}; + std::atomic Unknown{0}; + + void add(const ProfileMatch &Other) { + Annotated += Other.Annotated; + Mismatched += Other.Mismatched; + Missing += Other.Missing; + } + + std::string serialize() const { + return formatv("{0} {1} {2}", Annotated.load(), Mismatched.load(), + Missing.load()) + .str(); + } + + bool deserialize(StringRef Text) { + SmallVector Parts; + Text.split(Parts, ' '); + uint64_t Values[3]; + if (Parts.size() != 3) + return false; + for (size_t I = 0; I < 3; ++I) + if (Parts[I].getAsInteger(10, Values[I])) + return false; + Annotated += Values[0]; + Mismatched += Values[1]; + Missing += Values[2]; + return true; + } +}; + +// Counts the profile annotation's hash-mismatch and missing-function +// warnings instead of printing them. Every other diagnostic takes the +// context's default handling. +class ProfileMatchHandler : public DiagnosticHandler { +public: + explicit ProfileMatchHandler(ProfileMatch &Match) : Match(Match) {} + + bool handleDiagnostics(const DiagnosticInfo &Info) override { + if (Info.getKind() != DK_PGOProfile) + return false; + std::string Message; + raw_string_ostream Stream(Message); + DiagnosticPrinterRawOStream Printer(Stream); + Info.print(Printer); + if (StringRef(Message).contains("hash mismatch")) + ++Match.Mismatched; + else if (StringRef(Message).contains_insensitive("no profile data")) + ++Match.Missing; + else + return false; + return true; + } + +private: + ProfileMatch &Match; +}; + +// The annotation warns about functions the profile does not describe only +// when asked; the match counts need those warnings. Process-wide, set once. +void reportMissingProfiles() { + static const bool Reported = [] { + if (cl::Option *Option = + cl::getRegisteredOptions().lookup("pgo-warn-missing-function")) + Option->addOccurrence(0, "pgo-warn-missing-function", "true"); + return true; + }(); + (void)Reported; +} + +// Functions of `M` that the profile annotated with counts. +uint64_t annotatedFunctions(const Module &M) { + uint64_t Count = 0; + for (const Function &F : M) { + if (F.isDeclaration()) + continue; + std::optional Entry = F.getEntryCount(); + if (Entry && !Entry->isSynthetic()) + ++Count; + } + return Count; +} + +// Prints the profile match as the successful emit's one line of JSON. +void reportProfileMatch(const ProfileMatch &Match) { + json::Object Profile{{"annotated", Match.Annotated.load()}, + {"mismatched", Match.Mismatched.load()}, + {"missing", Match.Missing.load()}, + {"unknown_partitions", Match.Unknown.load()}}; + json::Object Response{{"ok", true}, {"profile", std::move(Profile)}}; + outs() << formatv("{0}\n", json::Value(std::move(Response))); +} + // Reads the profile's header once, before any partition needs it: it must // hold IR-level counts. std::optional inspectProfile(const EmitOptions &Options) { @@ -387,9 +492,16 @@ std::optional publish(StringRef TemporaryPath, std::optional emitModule(Module &M, TargetMachine &Machine, OptimizationLevel Level, std::optional Profile, CodeGenFileType Type, - StringRef OutputPath, std::vector> &Temporaries) { + StringRef OutputPath, std::vector> &Temporaries, + ProfileMatch &Match) { + bool Annotating = Profile && Profile->Action == PGOOptions::IRUse; + if (Annotating) + M.getContext().setDiagnosticHandler( + std::make_unique(Match)); Pipeline P(Machine, Level, std::move(Profile)); P.PB.buildPerModuleDefaultPipeline(Level).run(M, P.MAM); + if (Annotating) + Match.Annotated += annotatedFunctions(M); if (std::optional Failure = verify(M, "post_optimization_verification_failed")) return Failure; @@ -555,20 +667,32 @@ std::optional compileOptimizedPartitions(const EmitOptions &Options, const PartitionPlan &Plan, OptimizationLevel Level, StringRef Salt, const ArtifactCache &Cache, - std::vector> &Objects) { + std::vector> &Objects, + ProfileMatch &Match) { size_t Count = Plan.Modules.size(); + bool Annotating = !Options.ProfileUse.empty(); std::vector> Summaries(Count); SharedFailure Failures; forEachPartition(Count, [&](size_t I) { std::string Key = hashParts({Salt, "summary", Plan.Modules[I]}); - if ((Summaries[I] = Cache.lookup(Key, "bc"))) + if ((Summaries[I] = Cache.lookup(Key, "bc"))) { + if (Annotating) { + std::unique_ptr Counts = Cache.lookup(Key, "match"); + if (!Counts || !Match.deserialize(Counts->getBuffer())) + ++Match.Unknown; + } return; + } std::string LookupError; std::unique_ptr Machine = createTargetMachine(Options.Target, Options.OptLevel, LookupError); if (!Machine) return Failures.set({"target_machine_failed", LookupError}); LLVMContext Context; + ProfileMatch PartitionMatch; + if (Annotating) + Context.setDiagnosticHandler( + std::make_unique(PartitionMatch)); Expected> Loaded = loadPartition(Options, Plan.Modules[I], I, Context, *Machine); if (!Loaded) @@ -598,6 +722,12 @@ compileOptimizedPartitions(const EmitOptions &Options, /*EmitModuleHash=*/true)); MPM.run(Mod, P.MAM); } + if (Annotating) { + PartitionMatch.Annotated += annotatedFunctions(Mod); + Match.add(PartitionMatch); + // Stored before the summary, so a reused summary finds its counts. + Cache.store(Key, "match", PartitionMatch.serialize()); + } StringRef Contents(Bitcode.data(), Bitcode.size()); Cache.store(Key, "bc", Contents); Summaries[I] = MemoryBuffer::getMemBufferCopy(Contents); @@ -744,7 +874,8 @@ compileOptimizedPartitions(const EmitOptions &Options, std::optional compilePartitions(const EmitOptions &Options, const PartitionPlan &Plan, - std::vector> &Objects) { + std::vector> &Objects, + ProfileMatch &Match) { // Everything besides a partition's own bytes that determines its // artifacts. The caller's cache directory already identifies the exact // helper build; source content and partition count arrive per module. @@ -778,7 +909,7 @@ compilePartitions(const EmitOptions &Options, const PartitionPlan &Plan, return compileUnoptimizedPartitions(Options, Plan, Salt, Cache, Objects); return compileOptimizedPartitions(Options, Plan, optimizationLevel(Options.OptLevel), Salt, - Cache, Objects); + Cache, Objects, Match); } // Publishes in-memory partition objects through private sibling files. @@ -819,6 +950,8 @@ int emitPartitions(const EmitOptions &Options) { std::vector> Objects; std::optional Failure; bool Compiled = false; + // A failed attempt's counts are discarded with its objects. + auto Match = std::make_unique(); { ErrorOr> Input = MemoryBuffer::getFile(Options.Input, /*IsText=*/false, @@ -830,15 +963,16 @@ int emitPartitions(const EmitOptions &Options) { Input->reset(); PartitionPlan Plan{std::move(Text->Sources), std::move(Text->PublicSymbols)}; - Compiled = !compilePartitions(Options, Plan, Objects); + Compiled = !compilePartitions(Options, Plan, Objects, *Match); } } } if (!Compiled) { + Match = std::make_unique(); PartitionPlan Plan; Failure = splitParsedModule(Options, Plan); if (!Failure) - Failure = compilePartitions(Options, Plan, Objects); + Failure = compilePartitions(Options, Plan, Objects, *Match); } std::vector> Temporaries; if (!Failure) @@ -851,6 +985,8 @@ int emitPartitions(const EmitOptions &Options) { return reportError(Failure->Code, Failure->Message, Options.DiagnosticFormat); } + if (!Options.ProfileUse.empty()) + reportProfileMatch(*Match); return 0; } @@ -895,6 +1031,8 @@ int emit(const EmitOptions &Options) { return reportError("invalid_import", "runtime bitcode import requires an optimized build", Options.DiagnosticFormat); + if (!Options.ProfileUse.empty()) + reportMissingProfiles(); if (std::optional Failure = inspectProfile(Options)) return reportError(Failure->Code, Failure->Message, Options.DiagnosticFormat); @@ -957,9 +1095,10 @@ int emit(const EmitOptions &Options) { ? CodeGenFileType::ObjectFile : CodeGenFileType::AssemblyFile; std::vector> Temporaries; + ProfileMatch Match; std::optional Failure = emitModule(*Mod, *Machine, Level, profileOptions(Options), Type, - Options.Outputs[0], Temporaries); + Options.Outputs[0], Temporaries, Match); if (!Failure) Failure = publish(Temporaries[0], Options.Outputs[0]); if (Failure) { @@ -968,6 +1107,8 @@ int emit(const EmitOptions &Options) { return reportError(Failure->Code, Failure->Message, Options.DiagnosticFormat); } + if (!Options.ProfileUse.empty()) + reportProfileMatch(Match); return 0; } diff --git a/packages/compiler/src/backend/native-codegen.ts b/packages/compiler/src/backend/native-codegen.ts index 5b91c60e7..413fb7e02 100644 --- a/packages/compiler/src/backend/native-codegen.ts +++ b/packages/compiler/src/backend/native-codegen.ts @@ -30,6 +30,8 @@ import { compilerReleaseVersion } from "../library/sidecar.js"; import { nativeProfileHelperArgs, nativeProfileKey, + parseNativeProfileMatch, + type NativeProfileMatch, type NativeProfileOptions, } from "./native-profile.js"; import { writeTextParts } from "../text-parts.js"; @@ -69,6 +71,8 @@ export interface NativeCodegenArtifact { dependencies: NativeArtifactDependency[]; /** Every emitted file, starting with the requested output path. */ outputPaths: string[]; + /** How the build's profile matched, when it used one and the helper ran. */ + profileMatch?: NativeProfileMatch; } const resolvedHelperCache = new Map>(); @@ -458,7 +462,7 @@ export async function emitNativeArtifact( try { if (isLlvmFile(options.llvm)) await copyFile(options.llvm.file, input); else await writeLlvmSource(input, options.llvm, 0o600); - await invoke(helper.binaryPath, [ + const { stdout } = await invoke(helper.binaryPath, [ "emit", "--input", input, @@ -502,7 +506,9 @@ export async function emitNativeArtifact( for (let index = 0; index < stages.length; index++) await rename(stages[index]!, outputs[index]!); await pruneBuildCache(root); - return artifact; + const profileMatch = + options.profile?.use === undefined ? null : parseNativeProfileMatch(stdout); + return profileMatch === null ? artifact : { ...artifact, profileMatch }; } finally { await Promise.all([ ...stages.map((stage) => rm(stage, { force: true }).catch(() => undefined)), diff --git a/packages/compiler/src/backend/native-profile.test.ts b/packages/compiler/src/backend/native-profile.test.ts index 590c39eef..b2cb0c75d 100644 --- a/packages/compiler/src/backend/native-profile.test.ts +++ b/packages/compiler/src/backend/native-profile.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "vitest"; import { executableLinkInputs } from "./link-plan-core.js"; import { + parseNativeProfileMatch, nativeProfileHelperArgs, nativeProfileKey, nativeProfileRefusal, @@ -71,3 +72,15 @@ test("only the instrumented link adds the profile runtime", () => { "-fprofile-generate", ); }); + +test("the helper's profile match is read from its output", () => { + expect( + parseNativeProfileMatch( + '{"ok":true,"profile":{"annotated":120,"mismatched":7,"missing":40,"unknown_partitions":1}}\n', + ), + ).toEqual({ annotated: 120, mismatched: 7, missing: 40, unknownPartitions: 1 }); + // A helper that predates the report prints nothing. + expect(parseNativeProfileMatch("")).toBeNull(); + expect(parseNativeProfileMatch('{"ok":true}\n')).toBeNull(); + expect(parseNativeProfileMatch('{"ok":true,"profile":{"annotated":-1}}\n')).toBeNull(); +}); diff --git a/packages/compiler/src/backend/native-profile.ts b/packages/compiler/src/backend/native-profile.ts index 4cd766319..904994a7d 100644 --- a/packages/compiler/src/backend/native-profile.ts +++ b/packages/compiler/src/backend/native-profile.ts @@ -44,6 +44,45 @@ export function nativeProfileHelperArgs(profile: NativeProfileOptions | undefine return []; } +/** How an optimized build's profile matched the program's functions, as the + * helper reports it: functions that took the profile's counts, functions + * whose control flow changed since the profile was collected, functions the + * profile has no record of (new, or never run while it was collected), and + * partitions reused from the cache without their counts. */ +export interface NativeProfileMatch { + annotated: number; + mismatched: number; + missing: number; + unknownPartitions: number; +} + +/** The profile match in a successful helper emit's output, or null when the + * helper reports none (a helper that predates the report). */ +export function parseNativeProfileMatch(stdout: string): NativeProfileMatch | null { + for (const line of stdout.split("\n")) { + let parsed: unknown; + try { + parsed = JSON.parse(line); + } catch { + continue; + } + const profile = (parsed as { profile?: Record } | null)?.profile; + if (typeof profile !== "object" || profile === null) continue; + const count = (key: string): number | null => { + const value = profile[key]; + return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null; + }; + const annotated = count("annotated"); + const mismatched = count("mismatched"); + const missing = count("missing"); + const unknownPartitions = count("unknown_partitions"); + if (annotated === null || mismatched === null || missing === null || unknownPartitions === null) + continue; + return { annotated, mismatched, missing, unknownPartitions }; + } + return null; +} + /** Cache-key part; empty without a profile so other keys are unchanged. */ export function nativeProfileKey(profile: NativeProfileOptions | undefined): string { if (profile?.generate !== undefined) return `profile-generate:${profile.generate}`; diff --git a/packages/compiler/src/compile-types.ts b/packages/compiler/src/compile-types.ts index c50931c6a..2a5cea5ea 100644 --- a/packages/compiler/src/compile-types.ts +++ b/packages/compiler/src/compile-types.ts @@ -2,6 +2,7 @@ import type { TargetCpu } from "./backend/target-cpu.js"; import type { WindowsSubsystem } from "./backend/targets.js"; import type { NativeLinkInfo } from "./backend/native-link-info.js"; import type { NativeOptimization } from "./backend/optimization.js"; +import type { NativeProfileMatch } from "./backend/native-profile.js"; import type { ScrDiagnostic } from "./diagnostics/diagnostic.js"; import type { CoverageInput } from "./coverage/report.js"; import type { TierMode } from "./frontend/tier-analysis.js"; @@ -100,6 +101,8 @@ export type CompileArtifact = path: string; translationUnitPath: string; backend: "llvm"; + /** How a `profileUse` build's profile matched the program's functions. */ + profileMatch?: NativeProfileMatch; }; export type CompileFailure = { diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index e2d1c97cc..607298427 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -94,6 +94,7 @@ import { createNativeLinkPlan } from "./backend/link-plan.js"; import { nativeProfileKey, nativeProfileRefusal, + type NativeProfileMatch, type NativeProfileOptions, } from "./backend/native-profile.js"; import { @@ -659,6 +660,7 @@ interface NativeProgramObject { partitionPaths: string[]; artifactPath: string; dependencies: NativeArtifactDependency[]; + profileMatch?: NativeProfileMatch; } async function emitNativeProgramObject( @@ -694,6 +696,7 @@ async function emitNativeProgramObject( partitionPaths: artifact.outputPaths.slice(1), artifactPath, dependencies: artifact.dependencies, + ...(artifact.profileMatch === undefined ? {} : { profileMatch: artifact.profileMatch }), }; } catch (error) { await removeNativeProgramObject(linkPath, nativePartitionPaths(linkPath, partitions)); @@ -1851,6 +1854,7 @@ async function compileTracked( } await pruneBuildCache(cacheRoot); timing("complete"); + const profileMatch = nativeProgramObject?.profileMatch; return { ok: true, artifact: { @@ -1858,6 +1862,7 @@ async function compileTracked( path: opts.outPath, translationUnitPath: llvmPath, backend, + ...(profileMatch === undefined ? {} : { profileMatch }), }, binaryPath: opts.outPath, llvmPath, diff --git a/scripts/build-native-cli.mts b/scripts/build-native-cli.mts index e539cea1d..30e0ae39b 100644 --- a/scripts/build-native-cli.mts +++ b/scripts/build-native-cli.mts @@ -20,6 +20,12 @@ const sanitize = process.env["SCRIPTC_SAN"] === "1"; // SCRIPTC_NATIVE_PROFILE_USE names; an empty SCRIPTC_NATIVE_PROFILE_USE // builds without one. const profileGenerate = process.env["SCRIPTC_NATIVE_PROFILE_GENERATE"] || undefined; +// Below this share of matching functions, the build warns that the profile +// has lost most of its gain. A profile refreshed on the same commit matched +// 92.7% of the native compiler's profiled functions, one refreshed 1.5 days +// and about 200 commits earlier 87.2%, and that one kept about half of the +// fresh profile's gain on a cold tsc-ts build. +const PROFILE_MATCH_WARNING = 0.9; const committedProfile = join(root, "packages/compiler/native/cli.profdata.gz"); const requestedProfile = process.env["SCRIPTC_NATIVE_PROFILE_USE"]; const useProfile = @@ -127,6 +133,25 @@ if (!result.ok) ) .join("\n"), ); +// How well the profile still describes the compiler: a function whose +// control flow changed since the profile was collected is optimized as +// without one. The share falls as compiler code changes, and with it the +// profile's gain (refresh it with scripts/refresh-cli-profile.mjs). +const match = useProfile && "artifact" in result ? result.artifact.profileMatch : undefined; +if (match !== undefined) { + const profiled = match.annotated + match.mismatched; + const share = profiled === 0 ? 0 : match.annotated / profiled; + console.log( + `native compiler profile: ${match.annotated} of ${profiled} profiled functions match (${(100 * share).toFixed(1)}%); ${match.missing} functions have no profile` + + (match.unknownPartitions > 0 + ? `; ${match.unknownPartitions} partitions reused without counts` + : ""), + ); + if (match.unknownPartitions === 0 && share < PROFILE_MATCH_WARNING) + console.warn( + `warning: the native compiler profile matches ${(100 * share).toFixed(1)}% of the functions it describes; run scripts/refresh-cli-profile.mjs`, + ); +} const compilerVersion = ( JSON.parse(readFileSync(join(root, "packages/compiler/package.json"), "utf8")) as { version: string; From 4f0975cbbe0d43e5ce2a3c2be936472227ff253c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 12:01:26 -0500 Subject: [PATCH 066/273] Compile more npm and CommonJS JavaScript dependencies statically --- .../builtin-apis/intl-segmenter.mdx | 5 +- .../src/backend/executable-features.ts | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 3 + .../src/backend/native-link-info-core.ts | 3 + .../compiler/src/backend/native/contracts.ts | 3 + .../compiler/src/backend/native/executable.ts | 2 + .../compiler/src/backend/runtime-pack-core.ts | 2 + .../src/executable/executable-cache.ts | 1 + .../src/frontend/lowering/binding-analysis.ts | 17 +- .../frontend/lowering/builtins/reflection.ts | 34 +++- .../lowering/class-dynamic-dispatch.ts | 1 + .../lowering/expressions/identifiers.ts | 32 +++ .../src/frontend/lowering/function-returns.ts | 17 ++ .../src/frontend/lowering/lower-calls.ts | 12 +- .../src/frontend/lowering/lower-classes.ts | 183 ++++++++++++------ .../src/frontend/lowering/lower-namespaces.ts | 50 ++++- .../src/frontend/lowering/lower-stmts.ts | 138 ++++++++++++- .../compiler/src/frontend/lowering/lowerer.ts | 35 +++- .../lowering/module-namespace-elements.ts | 124 +++++++++++- .../compiler/src/frontend/package-shims.ts | 47 +++-- packages/compiler/src/frontend/program.ts | 121 +++++++++--- packages/compiler/src/index.ts | 1 + packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 6 + packages/compiler/src/ir/runtime-features.ts | 4 + .../compiler/src/native/sanitizer.test.ts | 1 + .../main.ts.json | 6 + .../main.ts.json | 6 + .../main.ts.json | 6 + .../8218-cjs-namespace-imports/main.mjs.json | 9 + .../8219-cjs-stable-var-exports/main.js.json | 9 + .../main.js.json | 9 + .../main.ts.json | 6 + .../8222-js-class-static-reflection.js.json | 6 + .../runtime-pack-matrix.mjs | 2 + packages/runtime/src/scr_blob.h | 12 +- packages/runtime/src/scr_dyn.h | 3 + packages/runtime/src/scr_grapheme.c | 55 ++++++ packages/runtime/src/scr_json.c | 39 ++++ packages/runtime/src/scr_module_namespace.c | 121 ++++++++++++ .../8214-unshimmed-package-version/main.ts | 5 + .../node_modules/chalk/package.json | 1 + .../node_modules/chalk/source/index.d.ts | 3 + .../node_modules/chalk/source/index.js | 16 ++ .../node_modules/chalk/source/styles.js | 1 + .../8216-module-namespace-live-values/main.ts | 11 ++ .../node_modules/ns-registry/counter.js | 7 + .../node_modules/ns-registry/index.d.ts | 5 + .../node_modules/ns-registry/index.js | 40 ++++ .../node_modules/ns-registry/package.json | 1 + .../main.ts | 8 + .../node_modules/text-width/index.d.ts | 2 + .../node_modules/text-width/index.js | 11 ++ .../node_modules/text-width/package.json | 1 + .../client/index.mjs | 12 ++ .../client/package.json | 1 + .../client/transport.js | 26 +++ .../8218-cjs-namespace-imports/main.mjs | 13 ++ .../8218-cjs-namespace-imports/table.cjs | 10 + .../8219-cjs-stable-var-exports/lib/core.js | 31 +++ .../8219-cjs-stable-var-exports/lib/index.js | 8 + .../lib/options.js | 12 ++ .../8219-cjs-stable-var-exports/main.js | 21 ++ .../8219-cjs-stable-var-exports/package.json | 3 + .../classes/comparator.js | 22 +++ .../formdata.js | 26 +++ .../main.js | 39 ++++ .../package.json | 3 + .../ranges/outside.js | 13 ++ .../main.ts | 10 + .../node_modules/token-parser/dist/index.d.ts | 4 + .../node_modules/token-parser/dist/index.js | 131 +++++++++++++ .../node_modules/token-parser/package.json | 1 + .../corpus/8222-js-class-static-reflection.js | 94 +++++++++ .../cjs-mutable-exports/main.js | 5 + .../cjs-mutable-exports/package.json | 1 + .../cjs-mutable-exports/reassigned.js | 4 + .../cjs-mutable-exports/redeclared.js | 4 + .../cjs-mutable-exports/stable.js | 3 + .../cjs-mutable-exports/written.js | 4 + tests/diagnostics/intl-segmenter.ts | 4 +- .../node_modules/word-segments/index.d.ts | 1 + .../node_modules/word-segments/index.js | 9 + .../node_modules/word-segments/package.json | 1 + .../fixtures/npm-static/word-segments-cli.ts | 3 + .../__snapshots__/intl-segmenter.ts.txt | 10 +- .../main.ts.json | 1 + .../main.ts.json | 1 + .../main.ts.json | 1 + .../8218-cjs-namespace-imports/main.mjs.json | 1 + .../8219-cjs-stable-var-exports/main.js.json | 1 + .../main.js.json | 1 + .../main.ts.json | 1 + .../8222-js-class-static-reflection.js.json | 1 + tests/harness/coverage.test.ts | 10 + tests/harness/npm-static.test.ts | 22 +++ tests/harness/package-shims.test.ts | 30 ++- 97 files changed, 1725 insertions(+), 119 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8214-unshimmed-package-version/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8216-module-namespace-live-values/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8217-intl-segmenter-word-construct/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8218-cjs-namespace-imports/main.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8219-cjs-stable-var-exports/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8220-js-class-static-destructuring/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8221-js-recursive-generator-returns/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8222-js-class-static-reflection.js.json create mode 100644 packages/runtime/src/scr_module_namespace.c create mode 100644 tests/corpus/8214-unshimmed-package-version/main.ts create mode 100644 tests/corpus/8214-unshimmed-package-version/node_modules/chalk/package.json create mode 100644 tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.d.ts create mode 100644 tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.js create mode 100644 tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/styles.js create mode 100644 tests/corpus/8216-module-namespace-live-values/main.ts create mode 100644 tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/counter.js create mode 100644 tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.d.ts create mode 100644 tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.js create mode 100644 tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/package.json create mode 100644 tests/corpus/8217-intl-segmenter-word-construct/main.ts create mode 100644 tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts create mode 100644 tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js create mode 100644 tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/package.json create mode 100644 tests/corpus/8218-cjs-namespace-imports/client/index.mjs create mode 100644 tests/corpus/8218-cjs-namespace-imports/client/package.json create mode 100644 tests/corpus/8218-cjs-namespace-imports/client/transport.js create mode 100644 tests/corpus/8218-cjs-namespace-imports/main.mjs create mode 100644 tests/corpus/8218-cjs-namespace-imports/table.cjs create mode 100644 tests/corpus/8219-cjs-stable-var-exports/lib/core.js create mode 100644 tests/corpus/8219-cjs-stable-var-exports/lib/index.js create mode 100644 tests/corpus/8219-cjs-stable-var-exports/lib/options.js create mode 100644 tests/corpus/8219-cjs-stable-var-exports/main.js create mode 100644 tests/corpus/8219-cjs-stable-var-exports/package.json create mode 100644 tests/corpus/8220-js-class-static-destructuring/classes/comparator.js create mode 100644 tests/corpus/8220-js-class-static-destructuring/formdata.js create mode 100644 tests/corpus/8220-js-class-static-destructuring/main.js create mode 100644 tests/corpus/8220-js-class-static-destructuring/package.json create mode 100644 tests/corpus/8220-js-class-static-destructuring/ranges/outside.js create mode 100644 tests/corpus/8221-js-recursive-generator-returns/main.ts create mode 100644 tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.d.ts create mode 100644 tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.js create mode 100644 tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/package.json create mode 100644 tests/corpus/8222-js-class-static-reflection.js create mode 100644 tests/coverage-fixtures/cjs-mutable-exports/main.js create mode 100644 tests/coverage-fixtures/cjs-mutable-exports/package.json create mode 100644 tests/coverage-fixtures/cjs-mutable-exports/reassigned.js create mode 100644 tests/coverage-fixtures/cjs-mutable-exports/redeclared.js create mode 100644 tests/coverage-fixtures/cjs-mutable-exports/stable.js create mode 100644 tests/coverage-fixtures/cjs-mutable-exports/written.js create mode 100644 tests/fixtures/npm-static/node_modules/word-segments/index.d.ts create mode 100644 tests/fixtures/npm-static/node_modules/word-segments/index.js create mode 100644 tests/fixtures/npm-static/node_modules/word-segments/package.json create mode 100644 tests/fixtures/npm-static/word-segments-cli.ts create mode 100644 tests/harness/corpus-costs/8214-unshimmed-package-version/main.ts.json create mode 100644 tests/harness/corpus-costs/8216-module-namespace-live-values/main.ts.json create mode 100644 tests/harness/corpus-costs/8217-intl-segmenter-word-construct/main.ts.json create mode 100644 tests/harness/corpus-costs/8218-cjs-namespace-imports/main.mjs.json create mode 100644 tests/harness/corpus-costs/8219-cjs-stable-var-exports/main.js.json create mode 100644 tests/harness/corpus-costs/8220-js-class-static-destructuring/main.js.json create mode 100644 tests/harness/corpus-costs/8221-js-recursive-generator-returns/main.ts.json create mode 100644 tests/harness/corpus-costs/8222-js-class-static-reflection.js.json diff --git a/docs/content/limitations/builtin-apis/intl-segmenter.mdx b/docs/content/limitations/builtin-apis/intl-segmenter.mdx index 41a61711c..2682acaf0 100644 --- a/docs/content/limitations/builtin-apis/intl-segmenter.mdx +++ b/docs/content/limitations/builtin-apis/intl-segmenter.mdx @@ -1,2 +1,3 @@ -- **Intl.Segmenter:** Default Unicode grapheme segmentation, iteration, and `containing()` compile. - Locale negotiation, word and sentence segmentation, `resolvedOptions()`, and detached methods are unsupported. +- **Intl.Segmenter:** Unicode grapheme segmentation, iteration, and `containing()` compile, with the default locale or one language tag of the form language[-script][-region] written as a string literal (grapheme clusters do not depend on the locale). + In JavaScript dependencies, word and sentence segmenters construct, and their `segment()` throws an SC2020 error. + Locale negotiation over several locales, word and sentence segmentation, `resolvedOptions()`, and detached methods are unsupported. diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 2afb887ea..b51f957a3 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -19,6 +19,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { ...(features.wasm ? { wasm: true } : {}), ...(features.warningReplace ? { warningReplace: true } : {}), ...(features.stdioExtended ? { stdioExtended: true } : {}), + ...(features.moduleNamespace ? { moduleNamespace: true } : {}), zlib: features.zlib, ...(features.brotli ? { brotli: true } : {}), assert: features.assert, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 7a16c37e8..de2fbaa2e 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -40,6 +40,7 @@ export const LIB_FN_SYMS: Record = { "insp.jsonDyn": "scr_util_format_j", "http.reqH2StreamOrThrow": "scr_http_req_h2_stream_or_throw", "intl.segmenterNew": "scr_intl_segmenter_new", + "intl.segmenterRefusing": "scr_intl_segmenter_refusing", "weakMap.is": "scr_weak_map_is", "weakSet.is": "scr_weak_set_is", "weakMap.new": "scr_weak_map_new", @@ -254,6 +255,7 @@ export const LIB_FN_SYMS: Record = { "global.native": "scr_global_native", "process.builtinModule": "scr_process_builtin_module", "process.builtinUnsupported": "scr_process_builtin_unsupported", + "module.namespaceValue": "scr_module_namespace_value", "fs.callbackValue": "scr_fs_callback_value", "fs.callbackCall": "scr_fs_callback_call", "process.hrtimeValue": "scr_process_hrtime_value", @@ -987,6 +989,7 @@ export const LIB_FN_SYMS: Record = { "dyn.keySetComputed": "scr_dyn_key_set_computed", "dyn.keyDelete": "scr_dyn_key_delete", "dyn.keyDeleteComputed": "scr_dyn_key_delete_computed", + "dyn.reflectDeleteProperty": "scr_dyn_reflect_delete_property", "dyn.hasKeyComputed": "scr_dyn_has_key_computed", "dyn.hasOwnComputed": "scr_dyn_has_own_computed", "dyn.propertyIsEnumerableComputed": "scr_dyn_property_is_enumerable_computed", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 1e9994422..6af58fb0e 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -34,6 +34,9 @@ export interface NativeLinkFeatures { /** The program uses process.stdin.setEncoding, string 'data' listeners * or stdio 'resize' events: links the SCR_STDIO_EXTENDED events unit. */ stdioExtended?: boolean; + /** Module namespace objects as checked-dynamic values link + * scr_module_namespace.c; absent (false) otherwise. */ + moduleNamespace?: boolean; zlib: boolean; /** node:zlib's Brotli forms: scr_brotli.c and the vendored brotli archive. */ brotli?: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 40bb2352b..ce89cb829 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -147,6 +147,9 @@ export interface CcOptions { warningReplace?: boolean; /** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */ stdioExtended?: boolean; + /** Module namespace objects as checked-dynamic values: compiles + * scr_module_namespace.c. */ + moduleNamespace?: boolean; /** The program uses the process-events surface (signal/exit listeners, * stdin events, for-await over stdin — moduleUsesProcessEvents on the * IR): compiles scr_events.c into the binary. Event-free binaries keep diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index ceb162bcc..1dd88117e 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -194,6 +194,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.dynTier, "scr_js_value.c", "scr_js_object.c"), unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"), unit((u) => u.opts.wasm, "scr_wasm.c"), + // Module namespace objects as checked-dynamic values. + unit((u) => u.opts.moduleNamespace === true, "scr_module_namespace.c"), unit( (u) => u.opts.dynAsync || diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index 49165d8df..22bba85d2 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -94,6 +94,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { warningReplace: boolean; stdioExtended: boolean; brotli: boolean; + moduleNamespace: boolean; nativeFetch: boolean; netEffective: boolean; httpEffective: boolean; @@ -293,6 +294,7 @@ export function effectiveRuntimeFeatures( warningReplace: features.warningReplace === true, stdioExtended: features.stdioExtended === true, brotli: features.brotli === true, + moduleNamespace: features.moduleNamespace === true, nativeFetch, netEffective, httpEffective, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 754c20648..e641b8db0 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -62,6 +62,7 @@ export interface EarlyExecutableNativeFeatures { wasm?: boolean; warningReplace?: boolean; stdioExtended?: boolean; + moduleNamespace?: boolean; zlib: boolean; brotli?: boolean; assert: boolean; diff --git a/packages/compiler/src/frontend/lowering/binding-analysis.ts b/packages/compiler/src/frontend/lowering/binding-analysis.ts index 1987c86ba..c88c06c90 100644 --- a/packages/compiler/src/frontend/lowering/binding-analysis.ts +++ b/packages/compiler/src/frontend/lowering/binding-analysis.ts @@ -19,6 +19,18 @@ export function bindingNeverReassigned( ): boolean { const cached = lowerer.neverReassignedCache.get(sym); if (cached !== undefined) return cached; + const written = fileWritesBinding(sym, ts.getSourceFileOfNode(decl), resolve); + lowerer.neverReassignedCache.set(sym, !written); + return !written; +} + +/** True when something in `file` writes `sym` (the writes + * bindingNeverReassigned counts), resolving identifiers with `resolve`. */ +export function fileWritesBinding( + sym: ts.Symbol, + file: ts.SourceFile, + resolve: (ident: ts.Identifier) => ts.Symbol | null | undefined, +): boolean { let written = false; // Text pre-check keeps the file walk cheap: only same-named // identifiers pay a symbol resolution. @@ -62,9 +74,8 @@ export function bindingNeverReassigned( } ts.forEachChild(n, visit); }; - ts.forEachChild(ts.getSourceFileOfNode(decl), visit); - lowerer.neverReassignedCache.set(sym, !written); - return !written; + ts.forEachChild(file, visit); + return written; } /** Strips the value-preserving wrappers off an expression: parens, diff --git a/packages/compiler/src/frontend/lowering/builtins/reflection.ts b/packages/compiler/src/frontend/lowering/builtins/reflection.ts index 8e1e6fd94..2ce07ea2f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/reflection.ts +++ b/packages/compiler/src/frontend/lowering/builtins/reflection.ts @@ -3,7 +3,8 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; import { builtinModuleFnOf } from "../surfaces.js"; -import { BOOL, DYN, type IrExpr, STRING, arrayOf } from "../../../ir/ir.js"; +import { hasRuntimeStatics } from "../class-runtime-statics.js"; +import { BOOL, DYN, type IrExpr, STRING, arrayOf, isUnitType } from "../../../ir/ir.js"; /** Reflect.ownKeys uses native computed-key reflection. Reflect.apply(target, * thisArg, argsList) where the TARGET is a builtin @@ -63,6 +64,37 @@ export function lowerReflectCall( loc, }; } + // Reflect.deleteProperty over a value with a live property table: a + // checked-dynamic value, or a JavaScript class whose statics are runtime + // properties of its constructor + // (`Reflect.deleteProperty(Response, "getResponseState")`). + if ( + reflectMember === "deleteProperty" && + call.arguments.nodes.length === 2 && + !call.arguments.nodes.some(ts.isSpreadElement) + ) { + const loc = locOf(call); + let target = lowerer.lowerExpr(call.arguments.nodes[0]!); + const info = + target.type.kind === "classval" ? lowerer.classes.get(target.type.className) : undefined; + // A primitive target throws the TypeError, so it boxes as is. + const primitive = + isUnitType(target.type) || + ["bool", "f64", "string", "bigint", "symbol"].includes(target.type.kind); + if ( + primitive || + (info !== undefined && hasRuntimeStatics(info) && lowerer.dynConvertible(target.type)) + ) + target = lowerer.coerceToExpected(target, DYN); + if (target.type.kind !== "dyn") return null; + return { + kind: "libCall", + fn: "dyn.reflectDeleteProperty", + args: [target, lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN)], + type: BOOL, + loc, + }; + } if (reflectMember === "get" || reflectMember === "set") { const required = reflectMember === "get" ? 2 : 3; if ( diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index fc6d0a922..a943b9156 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -262,6 +262,7 @@ const KEY_CALLS = new Map([ ["dyn.propertyIsEnumerableComputed", "any"], ["dyn.hasOwnComputed", "value"], ["dyn.keyDeleteComputed", "value"], + ["dyn.reflectDeleteProperty", "object"], ["dyn.keySetComputed", "value"], ]); diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index f7ba7a115..db1c1938a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -57,6 +57,7 @@ import { contextualUndefReadType, fenceEarlyAliasUse, isTemporalGlobalRoot, + cjsNamespaceImportDep, lowerNsIdentifierValue, nsUndefRead, } from "../lower-namespaces.js"; @@ -958,6 +959,37 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // representation (ambient namespaces compile to Node's // ReferenceError instead — the object never exists at runtime). { + // `ns.name` through a namespace import of a CommonJS module, where Node's + // namespace has no `name` property (its lexer does not detect it), or + // `ns.default` (module.exports itself). + const cjsNamespace = cjsNamespaceImportDep(lowerer, expr); + if ( + cjsNamespace !== null && + ts.isPropertyAccessExpression(expr.parent) && + expr.parent.expression === expr && + ts.isIdentifier(expr.parent.name) + ) { + const name = expr.parent.name.text; + const { names, assignedLater } = lowerer.cjsNamespaceOf(cjsNamespace); + if (name === "default") + lowerer.unsupported( + "SC1090", + expr.parent, + `reads of 'default' through namespace imports of CommonJS modules (module.exports itself: read it through a default import)`, + ); + if (!names.has(name)) + lowerer.unsupported( + "SC1090", + expr.parent, + `reads of '${name}' through namespace imports of CommonJS modules (Node's lexer does not detect '${name}' among the module's exports, so Node reads undefined)`, + ); + if (assignedLater === "all" || assignedLater.has(name)) + lowerer.unsupported( + "SC1090", + expr.parent, + `reads of '${name}' through namespace imports of CommonJS modules whose functions assign exports (Node's namespace keeps the values from the end of the module's evaluation)`, + ); + } const moduleNs = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (moduleNs?.kind === "moduleNs") { return { kind: "moduleNsRef", moduleId: moduleNs.moduleId, type: moduleNs, loc }; diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index 6d0989e2b..c4563a234 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -175,6 +175,23 @@ export function declaredReturnType( // `() => void`), and throw-only callbacks are ordinary code // (`.action(() => { throw ... })`). `never` VALUES stay unmapped. if (retTsType.flags & ts.TypeFlags.Never) return VOID; + // A JS generator whose result the checker cannot infer (mutually + // recursive methods returning `yield*` of each other, as in yaml's + // Parser) has the return type `any`: a generator over untyped values. + if ( + isJsSourceFile(ts.getSourceFileOfNode(decl)) && + decl.asteriskToken && + retTsType.flags & ts.TypeFlags.Any + ) + return { + kind: "generator", + ...(decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) + ? { async: true as const } + : {}), + yieldT: DYN, + retT: DYN, + nextT: DYN, + }; let mappedReturn = lowerer.mapTypeOf(retTsType); // A generator declared to return Iterable returns a generator over T. if (mappedReturn && decl.asteriskToken) diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 14565aebc..4b833ee50 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -8464,7 +8464,17 @@ function lowerObjectStaticCall( ...(fnName !== null ? { fnName } : {}), }; } - if (target && isUnitType(target.type)) { + // A class value whose statics are runtime properties of its constructor + // boxes to that constructor function, as for Object.defineProperty above. + const runtimeStaticClass = + target?.type.kind === "classval" ? lowerer.classes.get(target.type.className) : undefined; + if ( + target && + (isUnitType(target.type) || + (runtimeStaticClass !== undefined && + hasRuntimeStatics(runtimeStaticClass) && + lowerer.dynConvertible(target.type))) + ) { target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments.nodes[0]!) }; } if (target?.type.kind === "dyn") { diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index b829b6ddc..e50466114 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -11,7 +11,7 @@ import { lowerDateStaticCall } from "./builtins/dates.js"; 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"; +import { dynUndefinedExpr, nodeThrowExpr, strLit, varRef } from "../../ir/build.js"; import { constructedFromValue, constructedValue, @@ -124,6 +124,7 @@ import { lowerWorkerNew, } from "./builtins/workers.js"; import { + noLoweringDiag, unsupportedPackageValueDiag, unsupportedDiag, type ScrDiagnostic, @@ -5687,15 +5688,22 @@ export function lowerStaticFieldRead( ): IrExpr | null { if (expr.name.text === "prototype") return null; if (expr.questionDotToken) return null; + const info = staticReceiverClass(lowerer, expr.expression); + if (!info) return null; + return lowerClassStaticRead(lowerer, info, expr.name.text, expr.expression, expr); +} + +/** The class a static member read through `receiver` reads exactly: the + * class a name binds (directly, through a require binding, or through a + * const alias), or null. */ +export function staticReceiverClass(lowerer: Lowerer, receiver: ts.Expression): ClassInfo | null { if ( - !ts.isIdentifier(expr.expression) && - expr.expression.kind !== ts.SyntaxKind.ThisKeyword && - exactClassOfReceiver(lowerer, expr.expression) === null + !ts.isIdentifier(receiver) && + receiver.kind !== ts.SyntaxKind.ThisKeyword && + exactClassOfReceiver(lowerer, receiver) === null ) return null; - const symbol = ts.isIdentifier(expr.expression) - ? lowerer.resolveValueSymbol(expr.expression) - : null; + const symbol = ts.isIdentifier(receiver) ? lowerer.resolveValueSymbol(receiver) : null; const info = (symbol ? lowerer.classBySymbol.get(symbol) : undefined) ?? // A require binding over `module.exports = class {…}` (the alias @@ -5703,7 +5711,7 @@ export function lowerStaticFieldRead( // or `C.name` below would fall through to paths that answer for // stdlib globals instead of this class. propertyAssignedClassInfoOf(lowerer, symbol) ?? - exactClassOfReceiver(lowerer, expr.expression) ?? + exactClassOfReceiver(lowerer, receiver) ?? undefined; if (!info) return null; // A decorated name that can REBIND (a replacing decorator): the @@ -5712,22 +5720,36 @@ export function lowerStaticFieldRead( // whose devirtualization and .name rules answer for every legal // runtime value. if (info.classDecorators?.valueGlobalId !== undefined) return null; - const loc = locOf(expr); - const found = findStaticOn(lowerer, info, expr.name.text); + return info; +} + +/** The static member `name` of `info` read through `receiver` (an exact + * reference to the class, see staticReceiverClass): `C.name` for the + * property access `site`, or a destructuring element over `C` whose + * initializer is `site`. Null when the class has no such member. */ +export function lowerClassStaticRead( + lowerer: Lowerer, + info: ClassInfo, + name: string, + receiver: ts.Expression, + site: ts.Expression, +): IrExpr | null { + const loc = locOf(site); + const found = findStaticOn(lowerer, info, name); // 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) + ["now", "parse", "UTC"].includes(name) && + !(ts.isCallExpression(site.parent) && site.parent.expression === site) ) - lowerer.noLowering(`Date.${expr.name.text} read through a class that extends Date`, expr); + lowerer.noLowering(`Date.${name} read through a class that extends Date`, site); if (hasRuntimeStatics(info) && !found?.field) return { kind: "dynKeyGet", - value: lowerer.coerceToExpected(classValueRef(lowerer, info, expr.expression), DYN), - key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + value: lowerer.coerceToExpected(classValueRef(lowerer, info, receiver), DYN), + key: { kind: "strLit", value: name, type: STRING, loc }, type: DYN, loc, }; @@ -5735,11 +5757,11 @@ export function lowerStaticFieldRead( // name: in JS the brand lives on that one constructor object, so // `D.#s` (a subclass receiver) throws Node's TypeError instead of // reaching up the chain — reported as SC1090 instead of resolved. - if (found && expr.name.text.startsWith("#") && found.declarer !== info) { + if (found && name.startsWith("#") && found.declarer !== info) { lowerer.unsupported( "SC1090", - expr, - `reading the private static '${expr.name.text}' through the subclass '${info.def.name.replace(/^%|^%m\d+\./, "")}' (JS brands the declaring class object alone — Node throws a TypeError here; spell the declaring class's name)`, + site, + `reading the private static '${name}' through the subclass '${info.def.name.replace(/^%|^%m\d+\./, "")}' (JS brands the declaring class object alone — Node throws a TypeError here; spell the declaring class's name)`, ); } if (found?.field !== undefined) { @@ -5749,29 +5771,31 @@ export function lowerStaticFieldRead( type: found.field.type, loc, }; - return found.field.runtimeOptional ? value : lowerer.maybeNarrow(value, expr); + return found.field.runtimeOptional || !ts.isPropertyAccessExpression(site) + ? value + : lowerer.maybeNarrow(value, site); } if (found) { - return staticMethodValue(lowerer, found.declarer, expr.name.text, found.method, expr, loc); + return staticMethodValue(lowerer, found.declarer, name, found.method, site, loc); } // A GENERIC static method as a VALUE: the pinned-value rule verbatim // (lowerGenericFnValue) — a slot spelling one concrete signature names // an instance, an unpinned reference fences by name. { - const gfound = findGenericStaticOn(lowerer, info, expr.name.text); - if (gfound) return lowerer.lowerGenericFnValue(expr, gfound.info); + const gfound = findGenericStaticOn(lowerer, info, name); + if (gfound) return lowerer.lowerGenericFnValue(site, gfound.info); } // `C.name` — the JS-observable class name, a compile-time constant on // the direct spelling (tsc rejects user statics named `name`, so the // chain above can never shadow it in TypeScript sources). A class of an // import cycle reads it from its class object instead, whose reference // checks the class's initialization (checkCycleBindingAccesses). - if (expr.name.text === "name" && info.def.jsName !== undefined) { + if (name === "name" && info.def.jsName !== undefined) { if (lowerer.classInitFlags.get(info.def.name)?.cycle === true) return { kind: "libCall", fn: "class.name", - args: [classValueRef(lowerer, info, expr.expression)], + args: [classValueRef(lowerer, info, receiver)], type: STRING, loc, }; @@ -5780,26 +5804,26 @@ export function lowerStaticFieldRead( if (info.callableBase) return { kind: "dynKeyGet", - value: lowerer.coerceToExpected(classValueRef(lowerer, info, expr.expression), DYN), - key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + value: lowerer.coerceToExpected(classValueRef(lowerer, info, receiver), DYN), + key: { kind: "strLit", value: name, type: STRING, loc }, type: DYN, loc, }; - const data = classStaticDataFor(lowerer, info, expr.name.text, loc); + const data = classStaticDataFor(lowerer, info, name, loc); if (data) { const value: IrExpr = { kind: "dynKeyGet", value: data, - key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + key: { kind: "strLit", value: name, type: STRING, loc }, type: DYN, loc, }; // JavaScript often assigns public statics after the class declaration. // Their storage stays mutable and checked, while reads retain the // checker's concrete type instead of losing the method receiver. - const type = isJsSourceFile(ts.getSourceFileOfNode(expr)) + const type = isJsSourceFile(ts.getSourceFileOfNode(site)) ? null - : lowerer.mapTypeOf(lowerer.typeOf(expr)); + : lowerer.mapTypeOf(lowerer.typeOf(site)); return type ? lowerer.coerceToExpected(value, type) : value; } return null; @@ -6625,7 +6649,7 @@ function staticCallOn( kind: "dynInvoke", recv: lowerer.lowerExprExpecting(access.expression, DYN), method: access.name.text, - calleeName: access.name.text, + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), type: DYN, loc, @@ -9902,34 +9926,47 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const args = expr.arguments?.nodes ?? []; const undefinedArg = (arg: ts.Expression): boolean => (lowerer.typeOf(arg).flags & ts.TypeFlags.Undefined) !== 0; - if (args.length > 2 || (args[0] && !undefinedArg(args[0]))) { + // A literal language tag: Unicode grapheme clusters have no locale + // tailoring. Only tags of the form language[-script][-region] are + // accepted; each is well-formed, so Node constructs the segmenter + // without the RangeError a malformed tag throws. + const literalLocale = (arg: ts.Expression): boolean => + ts.isStringLiteralLike(arg) && + /^[A-Za-z]{2,3}(-[A-Za-z]{4})?(-([A-Za-z]{2}|[0-9]{3}))?$/.test(arg.text); + if (args.length > 2 || (args[0] && !undefinedArg(args[0]) && !literalLocale(args[0]))) { lowerer.noLowering( "Intl.Segmenter locale negotiation", expr, - "omit the locale or pass undefined for default Unicode grapheme segmentation", + "omit the locale, pass undefined, or pass one language tag as a string literal", ); } + // JavaScript dependencies may construct word and sentence segmenters + // that they use only on some paths (pi-tui's shared word segmenter): + // the construction compiles and their segment() throws SC2020. + // TypeScript sources keep the build-time refusal. + const granularities = isJsSourceFile(ts.getSourceFileOfNode(expr)) + ? ["grapheme", "word", "sentence"] + : ["grapheme"]; const options = args[1]; - if ( - options && - !undefinedArg(options) && - !( - ts.isObjectLiteralExpression(options) && - options.properties.nodes.every( - (property) => - ts.isPropertyAssignment(property) && - (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && - property.name.text === "granularity" && - ts.isStringLiteral(property.initializer) && - property.initializer.text === "grapheme", - ) - ) - ) { - lowerer.noLowering( - "Intl.Segmenter options", - options, - 'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported', - ); + let granularity = "grapheme"; + if (options && !undefinedArg(options)) { + const properties = ts.isObjectLiteralExpression(options) ? options.properties.nodes : null; + const property = properties?.length === 1 ? properties[0] : undefined; + if ( + property === undefined || + !ts.isPropertyAssignment(property) || + !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) || + property.name.text !== "granularity" || + !ts.isStringLiteral(property.initializer) || + !granularities.includes(property.initializer.text) + ) { + lowerer.noLowering( + "Intl.Segmenter options", + options, + 'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported', + ); + } + granularity = property.initializer.text; } // Explicit undefined expressions can carry effects; retain argument order. const stmts: IrStmt[] = args.map((arg) => { @@ -9940,7 +9977,28 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { loc: locOf(arg), }; }); - const result: IrExpr = { kind: "libCall", fn: "intl.segmenterNew", args: [], type: DYN, loc }; + let result: IrExpr = { kind: "libCall", fn: "intl.segmenterNew", args: [], type: DYN, loc }; + // A word or sentence segmenter carries its segment() refusal, which + // coverage lists as deferred to runtime. + if (granularity !== "grapheme") { + const surface = `Intl.Segmenter segmentation with granularity '${granularity}'`; + lowerer.runtimeFences.push( + noLoweringDiag( + surface, + loc, + "only grapheme segmenters segment; word and sentence segmenters construct", + ), + ); + const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(expr), loc.start).line; + const refusal = `${surface} is not supported yet [SC2020 at ${loc.file}:${line + 1}]`; + result = { + kind: "libCall", + fn: "intl.segmenterRefusing", + args: [strLit(refusal, loc)], + type: DYN, + loc, + }; + } return stmts.length ? { kind: "seqExpr", stmts, result, type: DYN, loc } : result; } // A package declaration without a compiled constructor cannot provide @@ -11266,13 +11324,18 @@ function genericFieldFnNodeOf( } /** Whether a JavaScript class's own binding is the target of - * `Object.defineProperty`/`defineProperties` in its file - * (`Object.defineProperty(Definition, "name", { value })`): its constructor + * `Object.defineProperty`/`defineProperties` or `Reflect.deleteProperty` in + * its file (`Object.defineProperty(Definition, "name", { value })`, + * `Reflect.deleteProperty(Response, "getResponseState")`): its constructor * then needs a live own-property table. */ function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): boolean { const name = decl.name; const sf = ts.getSourceFileOfNode(decl); - if (name === undefined || !sf.text.includes("definePropert")) return false; + if ( + name === undefined || + (!sf.text.includes("definePropert") && !sf.text.includes("deleteProperty")) + ) + return false; const symbol = lowerer.checker.getSymbolAtLocation(name); if (symbol === undefined) return false; let found = false; @@ -11281,9 +11344,11 @@ function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaratio if ( ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && - (node.expression.name.text === "defineProperty" || + (((node.expression.name.text === "defineProperty" || node.expression.name.text === "defineProperties") && - lowerer.stdlibGlobalMember(node.expression, "Object") !== null + lowerer.stdlibGlobalMember(node.expression, "Object") !== null) || + (node.expression.name.text === "deleteProperty" && + lowerer.stdlibGlobalMember(node.expression, "Reflect") !== null)) ) { const target = node.arguments.nodes[0]; if ( diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index ea844ab1e..13d1c29fc 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -42,7 +42,7 @@ import { isPackageShimFile, isShimIntrinsicName, packageShimFor } from "../packa import type { Lowerer } from "./lowerer.js"; import { boundIdentifiersOf } from "./lowerer.js"; import type { FileParts } from "./lower-modules.js"; -import { locOf, resolveImport } from "../program.js"; +import { isCjsJsFile, isJsSourceFile, locOf, resolveImport } from "../program.js"; import { F64, type IrExpr, type IrStmt, type IrType, STRING } from "../../ir/ir.js"; import { TEMPORAL_GLOBAL } from "../builtin-modules.js"; @@ -311,6 +311,30 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile return null; } +/** The CommonJS module a JavaScript namespace import binding names + * (`import * as ns from "./m.cjs"`, `ns`), or null. */ +export function cjsNamespaceImportDep(lowerer: Lowerer, e: ts.Expression): ts.SourceFile | null { + if (!ts.isIdentifier(e)) return null; + const decl = lowerer.checker.getSymbolAtLocation(e)?.declarations.find(ts.isNamespaceImport); + const stmt = decl?.parent?.parent; + if ( + decl === undefined || + stmt === undefined || + !ts.isImportDeclaration(stmt) || + !ts.isStringLiteral(stmt.moduleSpecifier) + ) + return null; + const from = ts.getSourceFileOfNode(decl); + if (!isJsSourceFile(from)) return null; + const dep = resolveImport(lowerer.program, from, stmt.moduleSpecifier.text); + return dep !== null && + !dep.isDeclarationFile && + lowerer.fileTag.has(dep) && + isCjsJsFile(dep, lowerer.program) + ? dep + : null; +} + /** The member identifier of a qualified namespace reference, when `access` * reads a member declared in a lowered (or type-only) namespace block — * or an EXPORT of a program module read through its namespace-import @@ -330,6 +354,21 @@ export function nsMemberIdentOf( if (!ts.isIdentifier(access.name)) return null; const memberSym = lowerer.checker.getSymbolAtLocation(access.name); if (!memberSym) return null; + // A CommonJS module's namespace has the names Node's lexer detects, with + // the values they had when the module finished evaluating; the member + // then reads like the named import `import { name }`. `default` is + // module.exports itself. + const cjs = cjsNamespaceImportDep(lowerer, access.expression); + if (cjs !== null) { + const { names, assignedLater } = lowerer.cjsNamespaceOf(cjs); + const name = access.name.text; + return name !== "default" && + names.has(name) && + assignedLater !== "all" && + !assignedLater.has(name) + ? access.name + : null; + } if (nsBlockKindOfSymbol(lowerer, memberSym) !== null) return access.name; if (moduleNsSourceFileOf(lowerer, access.expression) !== null) return access.name; return null; @@ -857,6 +896,15 @@ export function nsWritableTarget( const nsMember = nsMemberIdentOf(lowerer, access); if (!nsMember) return null; const memberSym = lowerer.checker.getSymbolAtLocation(nsMember); + // An export read through a module namespace object (`import * as ns`): + // the object is read-only, and Node's assignment throws a TypeError. + if (memberSym && nsBlockKindOfSymbol(lowerer, memberSym) === null) { + lowerer.unsupported( + "SC1090", + access, + `assignment to the module namespace member '${nsMember.text}' (module namespace objects are read-only: Node throws a TypeError)`, + ); + } if (memberSym) fenceEarlyNsMemberRef(lowerer, access, memberSym); const resolved = lowerer.resolveValueSymbol(nsMember); const g = resolved ? lowerer.globalsBySymbol.get(resolved) : undefined; diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index c50676863..95e4f7e80 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -168,6 +168,7 @@ import { import { bindingNeverReassigned, deadUnmappableBinding, + fileWritesBinding, nullishExprUnitOf, nullishGenericBindingUnitOf, } from "./binding-analysis.js"; @@ -181,7 +182,12 @@ 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, superConditionLead } from "./lower-classes.js"; +import { + genericIfaceBindingKeepsClass, + lowerClassStaticRead, + staticReceiverClass, + superConditionLead, +} from "./lower-classes.js"; import { lowerStreamUnderscoreAssign, streamClassAliasDecl, @@ -3053,6 +3059,18 @@ export function lowerBindingPattern( ); return; } + // A class in a JavaScript file, destructured through its own name + // (`const { ANY, parse } = Comparator`): each element is the static + // read `Comparator.ANY` — the static getter, field or method as Node + // reads it, with the element's default applied when the read answers + // undefined. Other class-valued sources keep the refusal below. + if ( + srcType.kind === "classval" && + ts.isObjectBindingPattern(pattern) && + isJsSourceFile(ts.getSourceFileOfNode(pattern)) && + lowerClassValuePattern(lowerer, pattern, isLet, out) + ) + return; if (srcType.kind === "dyn") { // A runtime computed first key evaluates after RequireObjectCoercible: // a nullish source throws before the key's side effects run. Later @@ -4364,6 +4382,60 @@ function stdlibGlobalTokenDestructure( return out; } +/** `const { ANY, parse } = Comparator` in a JavaScript file, where the + * declaration's initializer names the class exactly (staticReceiverClass): + * one static read per element, left to right. False, with nothing + * emitted, for other sources (a parameter or nested pattern over a class + * value), which keep the class-value refusal. */ +function lowerClassValuePattern( + lowerer: Lowerer, + pattern: ts.ObjectBindingPattern, + isLet: boolean, + out: IrStmt[], +): boolean { + const decl = pattern.parent; + if (!ts.isVariableDeclaration(decl) || decl.initializer === undefined) return false; + let receiver: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(receiver)) receiver = receiver.expression; + const info = staticReceiverClass(lowerer, receiver); + if (info === null) return false; + for (const el of pattern.elements.nodes) { + if (el.name === undefined) continue; + if (el.dotDotDotToken) + lowerer.unsupported( + "SC1031", + el, + "rest bindings over class values (the rest object copies the class's own enumerable statics)", + ); + const prop = el.propertyName ?? el.name; + const name = + ts.isIdentifier(prop) || + ts.isComputedPropertyName(prop) || + ts.isStringLiteralLike(prop) || + ts.isNumericLiteral(prop) + ? patternKeyNameOf(lowerer, prop as ts.PropertyName) + : null; + if (name === null) + lowerer.unsupported( + "SC1031", + el, + "destructuring with computed keys that do not fold to one property name", + ); + if (name === "prototype" || name.startsWith("#")) + lowerer.unsupported("SC1031", el, `destructuring '${name}' from a class value`); + let value = lowerClassStaticRead(lowerer, info, name, receiver, receiver); + if (value === null) + lowerer.unsupported( + "SC1031", + el, + `destructuring the property '${name}' the class '${info.def.jsName ?? info.def.name}' does not declare as a static`, + ); + if (el.initializer) value = applyBindingDefault(lowerer, el, value); + lowerer.bindPatternTarget(el.name, value, isLet, out); + } + return true; +} + /** `x = dflt` on a pattern position whose source value always EXISTS * (tuple positions, record fields): JS evaluates the default exactly * when the value is undefined — the undefined-arm ternary, lazy, so @@ -7262,6 +7334,66 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt ); } +/** True when the `var`/`let` binding `nameNode` names cannot change after + * the top-level CommonJS export statement `exportNode` that exports it by + * reference, so importers read the value Node copies at the export: its + * module's top level declares it once, with an initializer that runs + * before the export, and nothing assigns it. The initializer runs earlier + * in the same module, or in a module the exporting module required before + * the export (`var hljs = require('./core'); module.exports = hljs;` over + * highlight.js's `var highlight = HLJS({}); module.exports = highlight;`). */ +function unchangedAfterExport( + lowerer: Lowerer, + nameNode: ts.Identifier, + exportNode: ts.Node, +): boolean { + const topLevelDeclaration = (symbol: ts.Symbol): ts.VariableDeclaration | null => { + const decl = symbol.declarations.length === 1 ? symbol.declarations[0]! : undefined; + return decl !== undefined && + ts.isVariableDeclaration(decl) && + ts.isIdentifier(decl.name) && + decl.initializer !== undefined && + ts.isVariableDeclarationList(decl.parent) && + ts.isVariableStatement(decl.parent.parent) && + ts.isSourceFile(decl.parent.parent.parent) + ? decl + : null; + }; + const exportFile = ts.getSourceFileOfNode(exportNode); + let exportStatement: ts.Node = exportNode; + for (let parent = exportNode.parent; parent !== undefined && !ts.isSourceFile(parent);) { + if (ts.isFunctionLike(parent) || ts.isClassDeclaration(parent) || ts.isClassExpression(parent)) + return false; + exportStatement = parent; + parent = parent.parent; + } + const runsBeforeExport = (decl: ts.VariableDeclaration): boolean => { + const statement = decl.parent?.parent; + return statement?.parent === exportFile && statement.end <= exportStatement.pos; + }; + const sym = lowerer.resolveValueSymbol(nameNode); + const decl = sym ? topLevelDeclaration(sym) : null; + if (!sym || !decl || !bindingNeverReassigned(lowerer, sym, decl)) return false; + if (ts.getSourceFileOfNode(decl) === exportFile) return runsBeforeExport(decl); + // A binding of another module: the exporting module's own binding + // `nameNode` names holds that module's require, evaluated before the + // export, and this file does not assign the binding either. + const local = lowerer.checker.getSymbolAtLocation(nameNode); + const localDecl = local ? topLevelDeclaration(local) : null; + if (!localDecl || !runsBeforeExport(localDecl)) return false; + let init: ts.Expression = localDecl.initializer!; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if ( + !ts.isCallExpression(init) || + !ts.isIdentifier(init.expression) || + init.expression.text !== "require" || + init.arguments.nodes.length !== 1 || + !ts.isStringLiteralLike(init.arguments.nodes[0]!) + ) + return false; + return !fileWritesBinding(sym, exportFile, (ident) => lowerer.resolveValueSymbol(ident)); +} + /** A top-level CommonJS export statement (see cjsExportAssignmentOf): * `exports.f = ` and each expression-valued property of * `module.exports = { ... }` assign their pre-registered export globals @@ -7445,7 +7577,7 @@ function lowerCjsExportStatement( const fenceMutable = (nameNode: ts.Identifier): void => { const sym = lowerer.resolveValueSymbol(nameNode); const g = sym ? lowerer.globalsBySymbol.get(sym) : undefined; - if (g?.mutable) { + if (g?.mutable && !unchangedAfterExport(lowerer, nameNode, cjs.expr)) { lowerer.unsupported( "SC1090", nameNode, @@ -7605,7 +7737,7 @@ function lowerCjsSingleValueExport( } const g = lowerer.globalsBySymbol.get(sym); if (g) { - if (g.mutable) { + if (g.mutable && !unchangedAfterExport(lowerer, rhs, expr)) { lowerer.unsupported( "SC1090", rhs, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 78980d322..59dda3d08 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,6 +1,6 @@ import { isDeclBoundaryFile } from "../decl-projection.js"; import { relieveHeapPressure } from "../../heap-pressure.js"; -import { moduleNamespaceObject } from "./module-namespace-elements.js"; +import { moduleNamespaceObject, moduleNamespaceValue } from "./module-namespace-elements.js"; import { keyedHelperName } from "./stable-names.js"; import { positionTag, StructuralPositions } from "./structural-positions.js"; import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; @@ -269,6 +269,8 @@ import { type StartupCrash, cjsExportAssignmentOf, cjsExportDiscardReason, + cjsExportsAssignedInFunctions, + cjsNamespaceExportNames, fallbackDtsPath, isCheckedJsSourceFile, isCjsExportTableLiteral, @@ -3998,6 +4000,31 @@ export class Lowerer { return null; } + /** For each CommonJS module read through a JavaScript namespace import: + * the property names of Node's namespace object + * (cjsNamespaceExportNames), and the exports the module assigns inside + * functions, which the namespace does not see + * (cjsExportsAssignedInFunctions). */ + private readonly cjsNamespaces = new Map< + ts.SourceFile, + { names: ReadonlySet; assignedLater: ReadonlySet | "all" } + >(); + + cjsNamespaceOf(dep: ts.SourceFile): { + names: ReadonlySet; + assignedLater: ReadonlySet | "all"; + } { + let namespace = this.cjsNamespaces.get(dep); + if (namespace === undefined) { + namespace = { + names: cjsNamespaceExportNames(this.program, dep), + assignedLater: cjsExportsAssignedInFunctions(dep), + }; + this.cjsNamespaces.set(dep, namespace); + } + return namespace; + } + sourceFileOfModuleNamespace(type: IrType): ts.SourceFile | null { if (type.kind !== "moduleNs" || !type.moduleId.startsWith("file:")) return null; const path = type.moduleId.slice("file:".length); @@ -9792,10 +9819,8 @@ export class Lowerer { // so its module binding is too (`ReleaseEmitter = EventEmitter` over // `const EventEmitter = require("events")`). if (expr.type.kind === "moduleNs" && isJsSourceFileName(expr.loc.file)) { - const object = moduleNamespaceObject( - this, - expr as IrExpr & { type: IrType & { kind: "moduleNs" } }, - ); + const ns = expr as IrExpr & { type: IrType & { kind: "moduleNs" } }; + const object = moduleNamespaceObject(this, ns) ?? moduleNamespaceValue(this, ns); if (object) return object; } if ( diff --git a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts index cf828c6f0..2c8e6a475 100644 --- a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts +++ b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts @@ -1,4 +1,5 @@ import { dynUndefinedExpr, strLit, varRef } from "../../ir/build.js"; +import { arrayOf, funcOf } from "../../ir/ir.js"; import * as ts from "../ts7/adapter.js"; import { DYN, @@ -9,7 +10,7 @@ import { type IrType, } from "../../ir/ir.js"; import { locOf } from "../program.js"; -import { newFnCtx, type Lowerer } from "./lowerer.js"; +import { newFnCtx, PoisonError, type Lowerer } from "./lowerer.js"; /** Computed namespace reads select live exports instead of copying a * namespace into a registry snapshot. Constructor registries may contain @@ -171,3 +172,124 @@ export function moduleNamespaceObject( loc, }; } + +/** A module namespace taken as an untyped value whose exports a copy + * cannot represent: a binding that can change after initialization (`let`, + * `var`), or an export the copy cannot name. The value is the runtime's + * namespace object (scr_module_namespace_value): one per module, whose + * property reads call a compiled getter for the export's current value and + * whose writes throw Node's TypeError. An export whose value has no + * checked-dynamic form throws its fence when it is read, and listing the + * object's keys or inspecting it throws the "not supported yet" error. */ +export function moduleNamespaceValue( + lowerer: Lowerer, + ns: IrExpr & { type: IrType & { kind: "moduleNs" } }, +): IrExpr | null { + const source = lowerer.sourceFileOfModuleNamespace(ns.type); + if (!source) return null; + const symbol = lowerer.checker.getSymbolAtLocation(source); + if (!symbol) return null; + const exports: { key: string; declaration: ts.Node | undefined }[] = []; + for (const exported of lowerer.checker.getExportsOfModule(symbol)) { + const target = + exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; + if (!(target.flags & ts.SymbolFlags.Value)) continue; + exports.push({ key: ts.symbolName(exported), declaration: target.valueDeclaration }); + } + exports.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); + const name = `%module.namespace:${ns.type.moduleId}`; + const loc = ns.loc; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const context = newFnCtx(false, null, null, DYN); + const previousClass = lowerer.currentClass; + lowerer.currentClass = null; + lowerer.fnStack.push(context); + try { + const body: IrStmt[] = []; + for (const { key, declaration } of exports) { + const node = declaration ?? source; + const diagsBefore = lowerer.diags.length; + let read: IrStmt[]; + try { + const identifier = declaration?.name as ts.Node | undefined; + if (!identifier || !ts.isIdentifier(identifier)) { + lowerer.unsupported( + "SC1090", + node, + `reading the unnamed export '${key}' through a module namespace object`, + ); + } + const value = lowerer.lowerExprExpecting(identifier, DYN); + if (value.type.kind !== "dyn") { + lowerer.unsupported( + "SC1101", + identifier, + `reading the export '${key}' ('${lowerer.fmt(value.type)}') through a module namespace object`, + ); + } + read = [{ kind: "return", value, loc }]; + } catch (err) { + if (!(err instanceof PoisonError)) throw err; + const fence = lowerer.deferToRuntimeFence(diagsBefore, node, { kind: "statement" }); + if (!fence) throw err; + read = [fence]; + } + body.push({ + kind: "if", + cond: { + kind: "strEq", + negated: false, + left: varRef("key", STRING, loc), + right: strLit(key, loc), + type: { kind: "bool" }, + loc, + }, + then: read, + else_: null, + loc, + }); + } + body.push({ kind: "return", value: dynUndefinedExpr(loc), loc }); + lowerer.liftedFns.push({ + name, + params: [{ localId: "key", name: "key", type: STRING }], + returnType: DYN, + locals: [{ id: "key", name: "key", type: STRING, mutable: false }, ...context.locals], + body, + loc, + }); + } finally { + lowerer.fnStack.pop(); + lowerer.currentClass = previousClass; + } + } + const value: IrExpr = { + kind: "libCall", + fn: "module.namespaceValue", + args: [ + strLit(ns.type.moduleId, loc), + { + kind: "arrayLit", + elems: exports.map(({ key }) => strLit(key, loc)), + type: arrayOf(STRING), + loc, + }, + { + kind: "dynFrom", + value: { kind: "closure", fnName: name, captures: [], type: funcOf([STRING], DYN), loc }, + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }; + if (ns.kind === "moduleNsRef") return value; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: ns, loc }], + result: value, + type: DYN, + loc, + }; +} diff --git a/packages/compiler/src/frontend/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts index 10beeaae8..06a6c4eca 100644 --- a/packages/compiler/src/frontend/package-shims.ts +++ b/packages/compiler/src/frontend/package-shims.ts @@ -55,6 +55,10 @@ interface ImplementationShim { /** Supported major versions and their entry modules, relative to the * shim root. */ versions: ReadonlyMap; + /** Major versions without an entry module that `--npm-static` compiles + * from the package's own published JavaScript. An installed version that + * is in neither list stays out of the program. */ + sourceVersions?: ReadonlySet; /** The entry has a default export (a CommonJS package's whole export * object, which a default import reads). */ defaultExport?: boolean; @@ -98,6 +102,7 @@ const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map< mainFile: "source/index.js", typesFile: "source/index.d.ts", versions: new Map([[5, "chalk/v5.ts"]]), + sourceVersions: new Set([6]), }, ], [ @@ -233,10 +238,17 @@ export function configurePackageShims(entryPath: string): void { unloadedPackages = packages; } -/** True for a package name the registry implements or the project marks - * never loaded: such a package never compiles its published JavaScript. */ -export function isPackageShimName(name: string): boolean { - return IMPLEMENTATION_SHIMS.has(name) || unloadedPackages.has(name); +/** True when `--npm-static` leaves the package `name` out of its package + * list: the project marks it never loaded, or the registry implements it + * and the version installed for `entryPath` has an entry module or is not + * one of the entry's `sourceVersions`. */ +export function npmStaticOmitsPackage(entryPath: string, name: string): boolean { + if (unloadedPackages.has(name)) return true; + const shim = IMPLEMENTATION_SHIMS.get(name); + if (shim === undefined) return false; + const dir = installedPackageDir(entryPath, name); + const major = dir === null ? null : installedMajorVersion(dir, name); + return major === null || shim.versions.has(major) || !shim.sourceVersions?.has(major); } /** True when `specifier` names a package the project marks never loaded. */ @@ -272,9 +284,9 @@ function installedPackageDir(fromFile: string, name: string): string | null { return found; } -/** The shim entry for an installed package directory, or null when the - * directory is not a registered package at a supported major version. */ -function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): string | null { +/** The installed package's name and major version from its package.json, + * or null when either is missing. */ +function installedPackageVersion(pkgDir: string): { name: string; major: number } | null { const text = trackedReadFile(join(pkgDir, "package.json")); if (text === null) return null; let pkg: { name?: unknown; version?: unknown }; @@ -284,11 +296,24 @@ function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): st return null; } if (typeof pkg.name !== "string" || typeof pkg.version !== "string") return null; - if (expectedName !== null && pkg.name !== expectedName) return null; - const shim = IMPLEMENTATION_SHIMS.get(pkg.name); + return { name: pkg.name, major: Number(/^(\d+)\./.exec(pkg.version)?.[1] ?? NaN) }; +} + +/** The major version of the package `name` installed at `pkgDir`, or null. */ +function installedMajorVersion(pkgDir: string, name: string): number | null { + const installed = installedPackageVersion(pkgDir); + return installed === null || installed.name !== name ? null : installed.major; +} + +/** The shim entry for an installed package directory, or null when the + * directory is not a registered package at a supported major version. */ +function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): string | null { + const installed = installedPackageVersion(pkgDir); + if (installed === null) return null; + if (expectedName !== null && installed.name !== expectedName) return null; + const shim = IMPLEMENTATION_SHIMS.get(installed.name); if (shim === undefined) return null; - const major = Number(/^(\d+)\./.exec(pkg.version)?.[1] ?? NaN); - const entry = shim.versions.get(major); + const entry = shim.versions.get(installed.major); if (entry === undefined) return null; return join(packageShimRoot(), shim.variant?.(pkgDir) ?? entry); } diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index bf75127c4..ce5f3c2bc 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -96,8 +96,8 @@ import type { import { configurePackageShims, isPackageShimFile, - isPackageShimName, isShimmedImport, + npmStaticOmitsPackage, } from "./package-shims.js"; import { provenanceEntryFor, provenancePaths } from "./provenance-registry.js"; import { cjsLexedExportsOfFile, cjsVisibleNames } from "./cjs-syntax.js"; @@ -1072,12 +1072,13 @@ export function loadProgram( } externalTypes.set(specifier, declarationPath); } - // Packages scriptc implements itself or the project marks never loaded - // (package-shims.ts) never compile their published JavaScript, even when - // named. + // Packages scriptc implements itself, and packages the project marks + // never loaded (package-shims.ts), never compile their published + // JavaScript, even when named. A registry entry's `sourceVersions` + // (chalk 6) compile from their own source. configurePackageShims(entryPath); const npmStaticPackages = [...new Set(opts?.npmStatic ?? [])].filter( - (name) => !isPackageShimName(name), + (name) => !npmStaticOmitsPackage(entryPath, name), ); // Resolve authored declaration entries before enabling the JS-only // package resolver. Safe overload metadata binds through the runtime @@ -5941,9 +5942,15 @@ function preflight7(load: LoadResult): { // to Node's no-op, the module still evaluates through its // ordinary init edge, and no member is ever read through the // lexer facade (self-imports of a CJS module included — the - // partially-built exports are never observed). Any OTHER use - // keeps the fence. - if (!nsBindingUsesAreBareStatements7(program, sf, clause.namedBindings.name)) { + // partially-built exports are never observed). In JavaScript + // sources every use passes here: member reads lower through the + // names Node's lexer detects, and the other reads refuse at + // their site (lower-namespaces.ts, cjsNamespaceImportDep). Any + // OTHER use in TypeScript keeps the fence. + if ( + !isJsSourceFileName(sf.fileName) && + !nsBindingUsesAreBareStatements7(program, sf, clause.namedBindings.name) + ) { diags.push( unsupportedDiag( "SC1013", @@ -6372,6 +6379,89 @@ function preflight7(load: LoadResult): { * Node refuses the program). This walk asks Node's question at compile * time, in Node's exact ORDER, and answers with Node's exact message. */ +/** The CommonJS program file a re-export (`module.exports = require(spec)`) + * of `from` resolves to, or null: reexport targets union in only when they + * resolve to CommonJS program files (Node's cjsPreparseModuleExports rule). */ +function cjsReexportDep7(program: ts.Program) { + return (from: ts.SourceFile, spec: string): ts.SourceFile | null => { + const dep = + resolveImport7(program, from, spec, "require") ?? + npmStaticDepSf7(program, from, spec, "require"); + return dep !== null && isCjsJsFile7(dep, program) ? dep : null; + }; +} + +/** Node's lexer analysis of a CommonJS file. A bundler rewrite spells some + * lexer-visible names as accessors. */ +function lexWithAccessorNames(sf: ts.SourceFile): ReturnType { + const lexed = cjsLexedExportsOfFile(sf); + for (const name of npmStaticLexerVisibleNames(sf.fileName)) lexed.exports.add(name); + return lexed; +} + +/** The property names of Node's namespace object for the CommonJS module + * `dep`: the names its lexer detects, CommonJS re-exports included (the + * rule of the named-import link check), plus `default` and + * `module.exports`. */ +export function cjsNamespaceExportNames(program: ts.Program, dep: ts.SourceFile): Set { + const names = cjsVisibleNames(dep, lexWithAccessorNames, cjsReexportDep7(program), new Map()); + return new Set([...names, "default", "module.exports"]); +} + +/** The export names a CommonJS module assigns inside functions + * (`exports.current = value` in a function body), or "all" when such code + * writes a computed name, replaces module.exports or redefines exports + * through Object.defineProperty. Node's namespace object of the module + * holds the values the exports had when the module finished evaluating, + * so it does not see those writes. */ +export function cjsExportsAssignedInFunctions(sf: ts.SourceFile): ReadonlySet | "all" { + const names = new Set(); + let all = false; + const exportsObject = (e: ts.Expression): boolean => + (ts.isIdentifier(e) && e.text === "exports") || isModuleExportsAccess(e); + const target = (e: ts.Expression): void => { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (isModuleExportsAccess(e)) all = true; + else if (ts.isPropertyAccessExpression(e) && exportsObject(e.expression)) + names.add(e.name.text); + else if (ts.isElementAccessExpression(e) && exportsObject(e.expression)) { + if (ts.isStringLiteralLike(e.argumentExpression)) names.add(e.argumentExpression.text); + else all = true; + } + }; + const visit = (node: ts.Node, inFunction: boolean): void => { + if (all) return; + const nested = inFunction || ts.isFunctionLike(node); + if (nested) { + if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + node.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) + target(node.left); + else if ( + (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && + (node.operator === ts.SyntaxKind.PlusPlusToken || + node.operator === ts.SyntaxKind.MinusMinusToken) + ) + target(node.operand); + else if (ts.isDeleteExpression(node)) target(node.expression); + else if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + ts.isIdentifier(node.expression.expression) && + node.expression.expression.text === "Object" && + node.arguments.nodes[0] !== undefined && + exportsObject(node.arguments.nodes[0]) + ) + all = true; + } + ts.forEachChild(node, (child) => visit(child, nested)); + }; + visit(sf, false); + return all ? "all" : names; +} + interface BadCjsImport { /** The export name requested from the CJS module. */ exportName: string; @@ -6412,20 +6502,7 @@ function cjsNamedImportLinkCheck( const npmStatic = npmStaticDepSf7(program, from, spec); return npmStatic; }; - // Reexport targets union in only when they resolve to CommonJS program - // files (Node's cjsPreparseModuleExports rule). - const resolveCjsDep = (from: ts.SourceFile, spec: string): ts.SourceFile | null => { - const dep = - resolveImport7(program, from, spec, "require") ?? - npmStaticDepSf7(program, from, spec, "require"); - return dep !== null && isCjsJsFile7(dep, program) ? dep : null; - }; - // A bundler rewrite spells some lexer-visible names as accessors. - const lexWithAccessorNames = (sf: ts.SourceFile): ReturnType => { - const lexed = cjsLexedExportsOfFile(sf); - for (const name of npmStaticLexerVisibleNames(sf.fileName)) lexed.exports.add(name); - return lexed; - }; + const resolveCjsDep = cjsReexportDep7(program); const visible = (dep: ts.SourceFile, name: string): boolean => // `default` is the module.exports binding itself — always provided. // The program already owns native source ASTs; reuse them without diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index e2d1c97cc..033d98d3d 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -595,6 +595,7 @@ async function compileExecutableNative( ...(features.wasm ? { wasm: true } : {}), ...(features.warningReplace ? { warningReplace: true } : {}), ...(features.stdioExtended ? { stdioExtended: true } : {}), + ...(features.moduleNamespace ? { moduleNamespace: true } : {}), zlib: features.zlib, ...(features.brotli ? { brotli: true } : {}), assert: features.assert, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 390c7fbc3..69b2d8dd0 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -683,6 +683,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fs.callbackCall", "dyn.keyDelete", "dyn.keyDeleteComputed", + "dyn.reflectDeleteProperty", "dyn.hasKeyComputed", "dyn.hasOwnComputed", "dyn.propertyIsEnumerableComputed", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 216e46a4d..072c006e9 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -196,6 +196,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * arguments; other receiver representations retain a runtime refusal. */ "dyn.keyDelete": { argTypes: [DYN, STRING, BOOL], result: VOID }, "dyn.keyDeleteComputed": { argTypes: [DYN, DYN, BOOL], result: VOID }, + "dyn.reflectDeleteProperty": { argTypes: [DYN, DYN], result: BOOL }, "dyn.hasKeyComputed": { argTypes: [DYN, DYN], result: BOOL }, "dyn.hasOwnComputed": { argTypes: [DYN, DYN], result: BOOL }, "dyn.propertyIsEnumerableComputed": { argTypes: [DYN, DYN], result: BOOL }, @@ -3425,6 +3426,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "process.builtinId": { argTypes: [DYN, arrayOf(STRING)], result: STRING }, "process.builtinModule": { argTypes: [STRING, DYN], result: DYN }, "process.builtinUnsupported": { argTypes: [STRING, STRING], result: DYN }, + /** A module namespace object as a checked-dynamic value + * (scr_module_namespace_value): the module id, its sorted export names + * and the compiled getter. One object per module. Never throws. */ + "module.namespaceValue": { argTypes: [STRING, arrayOf(STRING), DYN], result: DYN }, "fs.callbackValue": { argTypes: [STRING], result: DYN }, "fs.callbackCall": { argTypes: [STRING, DYN], result: DYN }, "process.hrtimeValue": { argTypes: [], result: DYN }, @@ -3710,6 +3715,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "ffi.argument": { argTypes: [DYN, STRING], result: DYN }, "ffi.memoryModule": { argTypes: [DYN], result: DYN }, "intl.segmenterNew": { argTypes: [], result: DYN }, + "intl.segmenterRefusing": { argTypes: [STRING], result: DYN }, "arrayBuffer.is": { argTypes: [DYN], result: BOOL }, "arrayBuffer.isView": { argTypes: [DYN], result: BOOL }, "arrayBuffer.byteLengthGetter": { argTypes: [], result: F64 }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 85b21af7b..bddb3f78b 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -1124,6 +1124,9 @@ const RUNTIME_FEATURE_RULES = { utilPromisify: rule({ fns: ["util.promisifyDyn"] }), /** process.emitWarning is assigned (process.setEmitWarning). */ warningReplace: rule({ fns: ["process.setEmitWarning"] }), + /** Module namespace objects as checked-dynamic values + * (scr_module_namespace.c). */ + moduleNamespace: rule({ fns: ["module.namespaceValue"] }), /** The WebAssembly API (scr_wasm.c). */ wasm: rule({ prefixes: ["wasm."] }), /** crypto.subtle importKey/sign/verify/digest and CryptoKey: @@ -1188,6 +1191,7 @@ function noRuntimeFeatures(): RuntimeFeatures { searchParams: false, urlIdna: false, stdioExtended: false, + moduleNamespace: false, stream: false, streamDyn: false, handleProps: false, diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 1415f2971..689d67367 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -87,6 +87,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( warningReplace: false, stdioExtended: false, brotli: false, + moduleNamespace: false, regex: true, copying: false, textDecoderLegacy: false, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8214-unshimmed-package-version/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8214-unshimmed-package-version/main.ts.json new file mode 100644 index 000000000..6a6a885dc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8214-unshimmed-package-version/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8214-unshimmed-package-version/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8216-module-namespace-live-values/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8216-module-namespace-live-values/main.ts.json new file mode 100644 index 000000000..d856e307e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8216-module-namespace-live-values/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8216-module-namespace-live-values/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8217-intl-segmenter-word-construct/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8217-intl-segmenter-word-construct/main.ts.json new file mode 100644 index 000000000..d2ad44948 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8217-intl-segmenter-word-construct/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8217-intl-segmenter-word-construct/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8218-cjs-namespace-imports/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8218-cjs-namespace-imports/main.mjs.json new file mode 100644 index 000000000..d627f9153 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8218-cjs-namespace-imports/main.mjs.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/8218-cjs-namespace-imports/table.cjs", + "/tests/corpus/8218-cjs-namespace-imports/client/transport.js", + "/tests/corpus/8218-cjs-namespace-imports/client/index.mjs", + "/tests/corpus/8218-cjs-namespace-imports/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8219-cjs-stable-var-exports/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8219-cjs-stable-var-exports/main.js.json new file mode 100644 index 000000000..71e59739d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8219-cjs-stable-var-exports/main.js.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/8219-cjs-stable-var-exports/lib/core.js", + "/tests/corpus/8219-cjs-stable-var-exports/lib/index.js", + "/tests/corpus/8219-cjs-stable-var-exports/lib/options.js", + "/tests/corpus/8219-cjs-stable-var-exports/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8220-js-class-static-destructuring/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8220-js-class-static-destructuring/main.js.json new file mode 100644 index 000000000..7a2ede696 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8220-js-class-static-destructuring/main.js.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/8220-js-class-static-destructuring/classes/comparator.js", + "/tests/corpus/8220-js-class-static-destructuring/ranges/outside.js", + "/tests/corpus/8220-js-class-static-destructuring/formdata.js", + "/tests/corpus/8220-js-class-static-destructuring/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8221-js-recursive-generator-returns/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8221-js-recursive-generator-returns/main.ts.json new file mode 100644 index 000000000..003137231 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8221-js-recursive-generator-returns/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8221-js-recursive-generator-returns/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8222-js-class-static-reflection.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8222-js-class-static-reflection.js.json new file mode 100644 index 000000000..2e961ad08 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8222-js-class-static-reflection.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8222-js-class-static-reflection.js" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 600637ba6..88348bdc2 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -80,6 +80,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_perf.c", "dynAsync"], // perf_hooks event-loop metrics, only for programs that read them. ["scr_perf_loop.c", "perfLoop"], + // Module namespace objects as checked-dynamic values. + ["scr_module_namespace.c", "moduleNamespace"], // Blob and File box their reads as dyn promises through scr_async_dyn.c. ["scr_blob.c", "dynAsync"], ["scr_zlib.c", "zlib"], diff --git a/packages/runtime/src/scr_blob.h b/packages/runtime/src/scr_blob.h index 15f4a10d4..0882e41b6 100644 --- a/packages/runtime/src/scr_blob.h +++ b/packages/runtime/src/scr_blob.h @@ -1,5 +1,6 @@ -/* scriptc runtime — Blob and File (scr_blob.c). Included by scr_runtime.h, - * which says where new declarations go. */ +/* scriptc runtime — Blob and File (scr_blob.c), and module namespace + * objects as checked-dynamic values (scr_module_namespace.c). Included by + * scr_runtime.h, which says where new declarations go. */ #ifndef SCR_BLOB_H #define SCR_BLOB_H @@ -23,9 +24,16 @@ ScrPromise *scr_blob_array_buffer(ScrDyn *self); bool scr_blob_is(const ScrDyn *value, double kind); ScrDyn *scr_intl_segmenter_new(void); +/* A word or sentence Intl.Segmenter whose segment() throws `refusal` + * (borrowed), the compiler's SC2020 message. */ +ScrDyn *scr_intl_segmenter_refusing(ScrStr *refusal); ScrStr *scr_process_builtin_id(ScrDyn *id, ScrArr *known); ScrDyn *scr_process_builtin_module(ScrStr *id, ScrDyn *getter); ScrDyn *scr_process_builtin_unsupported(ScrStr *id, ScrStr *member); +/* A module namespace object as a checked-dynamic value (scr_module_namespace.c): + * one handle per module `id`, whose reads call `getter` with the export + * name when `keys` lists it. Borrows all three; +1 handle. */ +ScrDyn *scr_module_namespace_value(ScrStr *id, ScrArr *keys, ScrDyn *getter); ScrDyn *scr_global_native(ScrArr *known); ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)); ScrDyn *scr_console_native(void); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 82fe1a6d9..cc2506b90 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -186,6 +186,7 @@ typedef enum { X(MATCH_SOURCE, 55) /* a match result's pending metadata (scr_regex.c) */ \ X(MESSAGE_EVENT, 50) /* ScrEvent with data — BroadcastChannel's 'message' */ \ X(MESSAGE_PORT, 36) \ + X(MODULE_NAMESPACE, 78) /* module namespace object read through its exports (scr_module_namespace.c) */ \ X(NET_BLOCK_LIST, 48) /* net.BlockList (scr_net.c) */ \ X(NET_SERVER, 3) /* ScrNetServer — net.Server (http.Server rides the same handle) */ \ X(NET_SOCKET, 2) /* ScrNetSocket — net.Socket */ \ @@ -195,6 +196,7 @@ typedef enum { X(REGEXP, 26) /* native RegExp, shared compiled pattern */ \ X(SEARCH_PARAMS, 33) /* live WHATWG URLSearchParams */ \ X(SEGMENTER, 19) /* default Unicode grapheme segmentation */ \ + X(SEGMENTER_REFUSING, 80) /* word or sentence Segmenter whose segment() throws (scr_grapheme.c) */ \ X(SEGMENTS, 20) /* iterable immutable segmentation result */ \ X(SET, 25) /* native Set, shared backing map */ \ X(SHARED_ARRAY_BUFFER, 37) \ @@ -875,6 +877,7 @@ bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key); bool scr_dyn_has_own_computed(const ScrDyn *value, const ScrDyn *key); bool scr_dyn_property_is_enumerable_computed(const ScrDyn *value, const ScrDyn *key); void scr_dyn_key_delete_computed(ScrDyn *value, const ScrDyn *key, bool strict); +bool scr_dyn_reflect_delete_property(ScrDyn *target, const ScrDyn *key); ScrStr *scr_dyn_string_constructor(const ScrDyn *value); ScrDyn *scr_dyn_property_key_value(const ScrDyn *value); ScrDyn *scr_dyn_to_numeric(const ScrDyn *value); diff --git a/packages/runtime/src/scr_grapheme.c b/packages/runtime/src/scr_grapheme.c index 2c93bc395..f9e5ca28d 100644 --- a/packages/runtime/src/scr_grapheme.c +++ b/packages/runtime/src/scr_grapheme.c @@ -201,3 +201,58 @@ ScrDyn *scr_intl_segmenter_new(void) { scr_dyn_handle_install(SCR_DYNH_SEGMENTS, &segments); return scr_graphemes_box(NULL, SCR_DYNH_SEGMENTER); } + +/* A word or sentence Segmenter, which JavaScript dependencies may build at + * module load and use on some paths only: it constructs, and its segment() + * throws the compiler's SC2020 message. */ +typedef struct { + size_t rc; + ScrStr *refusal; /* retained */ +} ScrRefusingSegmenter; + +static void *scr_refusing_segmenter_retain(void *ptr) { + ScrRefusingSegmenter *value = ptr; + value->rc++; + return value; +} + +static void scr_refusing_segmenter_release(void *ptr) { + ScrRefusingSegmenter *value = ptr; + if (--value->rc) return; + scr_str_release(value->refusal); + free(value); +} + +static ScrDyn *scr_refusing_segmenter_invoke(void *ptr, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + ScrRefusingSegmenter *value = ptr; + if (strcmp(method, "segment") == 0) { + ScrStr *input = scr_dyn_string_coerce_js(argc ? args[0] : scr_dyn_undefined()); + if (!input) return NULL; + scr_str_release(input); + scr_throw_error_msg_code(SCR_ERR_ERROR, value->refusal->data, value->refusal->len, "SC2020"); + return NULL; + } + return scr_graphemes_invoke(&(ScrGraphemes){ .rc = 1 }, self, method, args, argc, what); +} + +static ScrDyn *scr_refusing_segmenter_get(void *ptr, const char *key, size_t len) { + (void)ptr; + return scr_graphemes_get(&(ScrGraphemes){ .rc = 1 }, key, len); +} + +ScrDyn *scr_intl_segmenter_refusing(ScrStr *refusal) { + static const ScrDynHandleOps segmenter = { + .cls = "Segmenter", .retain = scr_refusing_segmenter_retain, + .release = scr_refusing_segmenter_release, .invoke = scr_refusing_segmenter_invoke, + .get = scr_refusing_segmenter_get, .set = scr_graphemes_set, + }; + scr_dyn_handle_install(SCR_DYNH_SEGMENTER_REFUSING, &segmenter); + ScrRefusingSegmenter *value = calloc(1, sizeof *value); + if (!value) scr_trap("scriptc: out of memory\n"); + value->rc = 1; + value->refusal = scr_str_retain(refusal); + ScrDyn *boxed = scr_dyn_new_handle(value, SCR_DYNH_SEGMENTER_REFUSING); + scr_refusing_segmenter_release(value); + return boxed; +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 49af072ed..4eb53bbec 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -8017,6 +8017,45 @@ void scr_dyn_key_delete_computed(ScrDyn *value, const ScrDyn *raw_key, bool stri scr_dyn_release(key); } +/* Reflect.deleteProperty(target, key): a TypeError for a primitive target, + * then the key's ToPropertyKey and a sloppy-mode delete. Answers whether + * the property is absent afterwards (false when it is non-configurable). + * A Proxy would observe the own-property check through its traps, and a + * compiled object's native fields keep their values after a delete through + * its checked-dynamic view, so both refuse. */ +bool scr_dyn_reflect_delete_property(ScrDyn *target, const ScrDyn *raw_key) { + switch (target->kind) { + case SCR_DYN_NULL: + case SCR_DYN_UNDEF: + case SCR_DYN_BOOL: + case SCR_DYN_NUM: + case SCR_DYN_STR: + case SCR_DYN_BIGINT: + case SCR_DYN_SYMBOL: { + static const char message[] = "Reflect.deleteProperty called on non-object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return false; + } + default: + break; + } + if (target->kind == SCR_DYN_PROXY) { + scr_dyn_proxy_unsupported("Reflect.deleteProperty"); + return false; + } + if (target->kind == SCR_DYN_TYPED_REF) { + static const char message[] = "Reflect.deleteProperty on a compiled object is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return false; + } + ScrDyn *key = scr_dyn_property_key_value(raw_key); + if (!key) return false; + scr_dyn_key_delete_computed(target, key, false); + bool deleted = !scr_exc_pending() && !scr_dyn_has_own_computed(target, key); + scr_dyn_release(key); + return deleted; +} + /* Native call/apply/bind lowering still owns these prototype operations. * Refuse overrides until every typed call site observes the property table. */ static bool scr_dyn_fn_property_fence(const ScrStr *key) { diff --git a/packages/runtime/src/scr_module_namespace.c b/packages/runtime/src/scr_module_namespace.c new file mode 100644 index 000000000..b0d7e80c4 --- /dev/null +++ b/packages/runtime/src/scr_module_namespace.c @@ -0,0 +1,121 @@ +/* Module namespace objects as checked-dynamic values + * (module-namespace-elements.ts lowers `import * as ns` values here). */ +#include "scr_runtime.h" + +#include +#include + +/* A module namespace object taken as a checked-dynamic value + * (module-namespace-elements.ts). The compiled getter answers an export's + * current value by name, so reads see live bindings; `keys` lists the + * namespace's export names. One object per module keeps its identity. + * Writes throw Node's strict-mode TypeError for a frozen namespace. */ +typedef struct ScrModuleNamespace { + size_t rc; + ScrStr *id; + ScrDyn *getter; + ScrArr *keys; + struct ScrModuleNamespace *next; +} ScrModuleNamespace; + +static SCR_TL ScrModuleNamespace *scr_module_namespaces; +static SCR_TL bool scr_module_namespace_cleanup_registered; + +static void *scr_module_namespace_retain(void *handle) { + ScrModuleNamespace *ns = handle; + ns->rc++; + return ns; +} + +static void scr_module_namespace_release(void *handle) { + ScrModuleNamespace *ns = handle; + if (--ns->rc) return; + scr_str_release(ns->id); + scr_dyn_release(ns->getter); + scr_arr_release(ns->keys); + free(ns); +} + +static void scr_module_namespace_cleanup(void) { + while (scr_module_namespaces) { + ScrModuleNamespace *ns = scr_module_namespaces; + scr_module_namespaces = ns->next; + scr_module_namespace_release(ns); + } +} + +static bool scr_module_namespace_has(const ScrModuleNamespace *ns, const char *key, size_t length) { + for (size_t i = 0; i < ns->keys->len; i++) { + ScrStr *entry = (ScrStr *)scr_arr_get_ref(ns->keys, (double)i); + bool same = entry->len == length && memcmp(entry->data, key, length) == 0; + scr_str_release(entry); + if (same) return true; + } + return false; +} + +static ScrDyn *scr_module_namespace_get(void *handle, const char *key, size_t length) { + ScrModuleNamespace *ns = handle; + if (!scr_module_namespace_has(ns, key, length)) return NULL; + ScrStr *name = scr_str_new(key, length); + ScrDyn *arg = scr_dyn_new_str(name); + scr_str_release(name); + ScrDyn *result = scr_dyn_call(ns->getter, &arg, 1, "module namespace export"); + scr_dyn_release(arg); + return result; +} + +static bool scr_module_namespace_set(void *handle, const char *key, size_t length, const ScrDyn *value) { + (void)value; + ScrModuleNamespace *ns = handle; + ScrJsonBuf text; + scr_jb_init(&text); + if (scr_module_namespace_has(ns, key, length)) { + scr_jb_puts(&text, "Cannot assign to read only property '"); + scr_jb_write(&text, key, length); + scr_jb_puts(&text, "' of object '[object Module]'"); + } else { + scr_jb_puts(&text, "Cannot add property "); + scr_jb_write(&text, key, length); + scr_jb_puts(&text, ", object is not extensible"); + } + ScrStr *message = scr_jb_finish(&text); + scr_throw_error_msg(SCR_ERR_TYPE, message->data, message->len); + scr_str_release(message); + return true; +} + +static ScrDyn *scr_module_namespace_invoke(void *handle, ScrDyn *self, const char *key, + ScrDyn *const *args, size_t argc, const char *what) { + ScrDyn *fn = scr_module_namespace_get(handle, key, strlen(key)); + if (fn == NULL) { + if (scr_exc_pending()) return NULL; + fn = scr_dyn_retain(scr_dyn_undefined()); + } + scr_dyn_this_push_dyn(self); + ScrDyn *result = scr_dyn_call(fn, args, argc, what); + scr_dyn_this_pop(); + scr_dyn_release(fn); + return result; +} + +ScrDyn *scr_module_namespace_value(ScrStr *id, ScrArr *keys, ScrDyn *getter) { + static const ScrDynHandleOps ops = { + .cls = "Module", .retain = scr_module_namespace_retain, .release = scr_module_namespace_release, + .get = scr_module_namespace_get, .set = scr_module_namespace_set, .invoke = scr_module_namespace_invoke, + }; + scr_dyn_handle_install(SCR_DYNH_MODULE_NAMESPACE, &ops); + for (ScrModuleNamespace *ns = scr_module_namespaces; ns; ns = ns->next) { + if (ns->id->len == id->len && memcmp(ns->id->data, id->data, id->len) == 0) + return scr_dyn_new_handle(ns, SCR_DYNH_MODULE_NAMESPACE); + } + if (!scr_module_namespace_cleanup_registered) { + scr_atexit(scr_module_namespace_cleanup); + scr_module_namespace_cleanup_registered = true; + } + ScrModuleNamespace *ns = malloc(sizeof(*ns)); + if (!ns) scr_trap("scriptc: out of memory\n"); + *ns = (ScrModuleNamespace){ 1, scr_str_retain(id), scr_dyn_retain(getter), scr_arr_retain(keys), scr_module_namespaces }; + scr_module_namespaces = ns; + return scr_dyn_new_handle(ns, SCR_DYNH_MODULE_NAMESPACE); +} diff --git a/tests/corpus/8214-unshimmed-package-version/main.ts b/tests/corpus/8214-unshimmed-package-version/main.ts new file mode 100644 index 000000000..d582a339b --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/main.ts @@ -0,0 +1,5 @@ +// A package that scriptc implements at some major versions (chalk 5) +// compiles from its own source at a version without an implementation. +import chalk, { level } from "chalk"; + +console.log(chalk.version, level, chalk.red("red"), chalk.bold("bold")); diff --git a/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/package.json b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/package.json new file mode 100644 index 000000000..7ab814eb0 --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/package.json @@ -0,0 +1 @@ +{ "name": "chalk", "version": "6.0.0", "type": "module", "exports": { "types": "./source/index.d.ts", "default": "./source/index.js" }, "imports": { "#styles": "./source/styles.js" } } diff --git a/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.d.ts b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.d.ts new file mode 100644 index 000000000..84dd75758 --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.d.ts @@ -0,0 +1,3 @@ +export declare const level: number; +declare const chalk: { version: number; red(text: string): string; bold(text: string): string }; +export default chalk; diff --git a/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.js b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.js new file mode 100644 index 000000000..2d85a4ae3 --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.js @@ -0,0 +1,16 @@ +import { styles } from "#styles"; + +export const level = 0; + +function paint(name, text) { + const [open, close] = styles[name]; + return level > 0 ? `\u001B[${open}m${text}\u001B[${close}m` : `${text}`; +} + +const chalk = { + version: 6, + red: (text) => paint("red", text), + bold: (text) => paint("bold", text), +}; + +export default chalk; diff --git a/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/styles.js b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/styles.js new file mode 100644 index 000000000..03e60b5e0 --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/styles.js @@ -0,0 +1 @@ +export const styles = { red: [31, 39], bold: [1, 22] }; diff --git a/tests/corpus/8216-module-namespace-live-values/main.ts b/tests/corpus/8216-module-namespace-live-values/main.ts new file mode 100644 index 000000000..419ce772e --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/main.ts @@ -0,0 +1,11 @@ +// A module namespace object stored as an untyped value, here in a registry +// keyed by module name (pi-coding-agent's VIRTUAL_MODULES): reads see the +// exports' current values, both registry entries are the same object, and +// writes throw Node's TypeError. +import { addThrough, bumpThrough, describe, same, writeThrough } from "ns-registry"; + +console.log(describe()); +bumpThrough(); +bumpThrough(); +console.log(describe()); +console.log(same(), writeThrough(), addThrough()); diff --git a/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/counter.js b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/counter.js new file mode 100644 index 000000000..b971201d8 --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/counter.js @@ -0,0 +1,7 @@ +export let count = 0; +export var legacy = "legacy"; +export const label = "counter"; +export function bump() { + count++; + legacy = `legacy ${count}`; +} diff --git a/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.d.ts b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.d.ts new file mode 100644 index 000000000..451d646f7 --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.d.ts @@ -0,0 +1,5 @@ +export declare function describe(): string; +export declare function bumpThrough(): void; +export declare function same(): boolean; +export declare function writeThrough(): string; +export declare function addThrough(): string; diff --git a/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.js b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.js new file mode 100644 index 000000000..51558bcba --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.js @@ -0,0 +1,40 @@ +import * as counter from "./counter.js"; + +export const registry = { + counter: counter, + "alias/counter": counter, +}; + +export function describe() { + const ns = registry["counter"]; + return `${ns.count} ${ns.legacy} ${ns.label} ${typeof ns.bump} ${typeof ns}`; +} + +export function bumpThrough() { + registry.counter.bump(); +} + +export function same() { + return registry.counter === registry["alias/counter"]; +} + +function assign(target, key, value) { + try { + target[key] = value; + } catch (error) { + return `${error.name}: ${error.message}`; + } + return "no error"; +} + +function lookup(name) { + return registry[name]; +} + +export function writeThrough() { + return assign(lookup("counter"), "count", 5); +} + +export function addThrough() { + return assign(lookup("alias/counter"), "extra", 5); +} diff --git a/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/package.json b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/package.json new file mode 100644 index 000000000..30fae6921 --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/package.json @@ -0,0 +1 @@ +{ "name": "ns-registry", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/8217-intl-segmenter-word-construct/main.ts b/tests/corpus/8217-intl-segmenter-word-construct/main.ts new file mode 100644 index 000000000..5a2a42e08 --- /dev/null +++ b/tests/corpus/8217-intl-segmenter-word-construct/main.ts @@ -0,0 +1,8 @@ +// A package builds its segmenters at module load (pi-tui, grok-mermaid): +// a grapheme segmenter with a locale tag, and a word segmenter that only +// some paths use. Grapheme segmentation matches Node; the word segmenter +// constructs, and this program never segments with it. +import { columns, describe } from "text-width"; + +console.log(describe()); +console.log(columns("éa👍🏽🇫🇷"), columns("abc")); diff --git a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts new file mode 100644 index 000000000..471b4fd0a --- /dev/null +++ b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts @@ -0,0 +1,2 @@ +export declare function columns(text: string): string; +export declare function describe(): string; diff --git a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js new file mode 100644 index 000000000..d58b4ccbb --- /dev/null +++ b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js @@ -0,0 +1,11 @@ +const graphemes = new Intl.Segmenter("en", { granularity: "grapheme" }); +const words = new Intl.Segmenter(undefined, { granularity: "word" }); +const sentences = new Intl.Segmenter(undefined, { granularity: "sentence" }); + +export function columns(text) { + return [...graphemes.segment(text)].map((part) => part.segment).join("|"); +} + +export function describe() { + return `${typeof words} ${typeof sentences}`; +} diff --git a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/package.json b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/package.json new file mode 100644 index 000000000..4f3af53f4 --- /dev/null +++ b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/package.json @@ -0,0 +1 @@ +{ "name": "text-width", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/8218-cjs-namespace-imports/client/index.mjs b/tests/corpus/8218-cjs-namespace-imports/client/index.mjs new file mode 100644 index 000000000..f0d49451f --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/client/index.mjs @@ -0,0 +1,12 @@ +import * as transport from './transport.js' + +export function connect (origin) { + const pool = new transport.Pool(origin, transport.defaults) + console.log(transport.setGlobalDispatcher(pool), pool.connections, transport.defaults.keepAlive) + return pool +} + +export function describe (pool) { + const format = transport.format + return format(pool.request('/status')) +} diff --git a/tests/corpus/8218-cjs-namespace-imports/client/package.json b/tests/corpus/8218-cjs-namespace-imports/client/package.json new file mode 100644 index 000000000..c866a30a1 --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/client/package.json @@ -0,0 +1 @@ +{ "name": "ns-client", "version": "1.0.0", "main": "index.mjs" } diff --git a/tests/corpus/8218-cjs-namespace-imports/client/transport.js b/tests/corpus/8218-cjs-namespace-imports/client/transport.js new file mode 100644 index 000000000..8d9efbe36 --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/client/transport.js @@ -0,0 +1,26 @@ +'use strict' +// A CommonJS module of the package: member exports, a class, and a table +// whose getter stops Node's lexer before the names after it. +class Pool { + constructor (origin, options) { + this.origin = origin + this.connections = options.connections + this.requests = 0 + } + + request (path) { + this.requests += 1 + return `${this.origin}${path} #${this.requests}` + } +} + +function setGlobalDispatcher (dispatcher) { + exports.current = dispatcher + return dispatcher.origin +} + +exports.Pool = Pool +exports.setGlobalDispatcher = setGlobalDispatcher +exports.current = null +exports.defaults = { connections: 4, keepAlive: true } +exports.format = function format (value) { return `[${value}]` } diff --git a/tests/corpus/8218-cjs-namespace-imports/main.mjs b/tests/corpus/8218-cjs-namespace-imports/main.mjs new file mode 100644 index 000000000..7d2d0e862 --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/main.mjs @@ -0,0 +1,13 @@ +// Namespace imports of CommonJS modules in JavaScript: member reads, calls, +// constructions and a method read as a value, through a package's ES +// module and directly. Each name is one Node's lexer detects. +import * as table from './table.cjs' +import { connect, describe } from './client/index.mjs' + +console.log(table.add(20, 22), table.scale) +const add = table.add +console.log(add(1, 2)) + +const pool = connect('http://localhost') +console.log(describe(pool)) +console.log(describe(pool)) diff --git a/tests/corpus/8218-cjs-namespace-imports/table.cjs b/tests/corpus/8218-cjs-namespace-imports/table.cjs new file mode 100644 index 000000000..38696c6f9 --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/table.cjs @@ -0,0 +1,10 @@ +'use strict' +const add = (a, b) => a + b +const scale = 10 +let calls = 0 +module.exports = { + add, + scale, + get calls () { return calls }, + bump () { calls += 1 } +} diff --git a/tests/corpus/8219-cjs-stable-var-exports/lib/core.js b/tests/corpus/8219-cjs-stable-var-exports/lib/core.js new file mode 100644 index 000000000..1210659ac --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/lib/core.js @@ -0,0 +1,31 @@ +// highlight.js's lib/core.js shape: the module builds one instance and +// exports the var that holds it. +'use strict'; + +const HLJS = function (options) { + const languages = new Map(); + const prefix = options.classPrefix || 'hljs-'; + + function registerLanguage(name, keywords) { + languages.set(name, keywords); + } + + function listLanguages() { + return Array.from(languages.keys()); + } + + function highlight(code, name) { + const keywords = languages.get(name); + if (keywords === undefined) throw new Error(`Unknown language: "${name}"`); + return code + .split(' ') + .map((word) => (keywords.includes(word) ? `${word}` : word)) + .join(' '); + } + + return { registerLanguage, listLanguages, highlight, versionString: '10.7.3' }; +}; + +var highlight = HLJS({}); + +module.exports = highlight; diff --git a/tests/corpus/8219-cjs-stable-var-exports/lib/index.js b/tests/corpus/8219-cjs-stable-var-exports/lib/index.js new file mode 100644 index 000000000..5f7726ea2 --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/lib/index.js @@ -0,0 +1,8 @@ +// highlight.js's lib/index.js shape: require the core instance, register +// languages on it and export the same var. +var hljs = require('./core'); + +hljs.registerLanguage('javascript', ['const', 'let', 'function', 'return']); +hljs.registerLanguage('python', ['def', 'return', 'lambda']); + +module.exports = hljs; diff --git a/tests/corpus/8219-cjs-stable-var-exports/lib/options.js b/tests/corpus/8219-cjs-stable-var-exports/lib/options.js new file mode 100644 index 000000000..17512396f --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/lib/options.js @@ -0,0 +1,12 @@ +// A let that is never reassigned, exported in a table. +'use strict'; + +let tabReplace = ' '; +let calls = []; + +function render(text) { + calls.push(text); + return text.split('\t').join(tabReplace); +} + +module.exports = { tabReplace, calls, render }; diff --git a/tests/corpus/8219-cjs-stable-var-exports/main.js b/tests/corpus/8219-cjs-stable-var-exports/main.js new file mode 100644 index 000000000..8607165a6 --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/main.js @@ -0,0 +1,21 @@ +// A var and a let exported by reference, where neither changes after the +// export: importers see the same instance and value as Node. +'use strict'; + +const hljs = require('./lib'); +const core = require('./lib/core'); +const options = require('./lib/options'); + +console.log(hljs === core, hljs.versionString); +console.log(hljs.listLanguages()); +console.log(hljs.highlight('const x = function', 'javascript')); +hljs.registerLanguage('ruby', ['def', 'end']); +console.log(core.listLanguages().length, core.highlight('def run end', 'ruby')); +try { + hljs.highlight('x', 'cobol'); +} catch (error) { + console.log(error.message); +} + +console.log(JSON.stringify(options.tabReplace), options.render('a\tb'), options.render('\tc')); +console.log(options.calls); diff --git a/tests/corpus/8219-cjs-stable-var-exports/package.json b/tests/corpus/8219-cjs-stable-var-exports/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/corpus/8220-js-class-static-destructuring/classes/comparator.js b/tests/corpus/8220-js-class-static-destructuring/classes/comparator.js new file mode 100644 index 000000000..3b128847c --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/classes/comparator.js @@ -0,0 +1,22 @@ +'use strict' +// semver's Comparator: a static getter returning a module symbol. +const ANY = Symbol('SemVer ANY') + +class Comparator { + static get ANY () { + return ANY + } + + static loose = false + + static parse (text) { + return new Comparator(text) + } + + constructor (text) { + this.semver = text === '' ? ANY : text + this.value = text === '' ? '' : `>=${text}` + } +} + +module.exports = Comparator diff --git a/tests/corpus/8220-js-class-static-destructuring/formdata.js b/tests/corpus/8220-js-class-static-destructuring/formdata.js new file mode 100644 index 000000000..851d3ba28 --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/formdata.js @@ -0,0 +1,26 @@ +'use strict' +// undici's FormData: statics that read private instance state, taken off +// the class before the class is exposed. +class FormData { + #state = [] + + append (name, value) { + this.#state.push({ name, value }) + } + + static getFormDataState (formData) { + return formData.#state + } + + static setFormDataState (formData, state) { + formData.#state = state + } + + static get boundary () { + return undefined + } +} + +const { getFormDataState, setFormDataState, boundary = '----formdata-default' } = FormData + +module.exports = { FormData, getFormDataState, setFormDataState, boundary } diff --git a/tests/corpus/8220-js-class-static-destructuring/main.js b/tests/corpus/8220-js-class-static-destructuring/main.js new file mode 100644 index 000000000..6cc17b94e --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/main.js @@ -0,0 +1,39 @@ +'use strict' +// Object patterns over a class value read its static getters, fields and +// methods as Node does, defaults included. +const Comparator = require('./classes/comparator') +const { outside, ANY, any, isLoose, name } = require('./ranges/outside') +const { FormData, getFormDataState, setFormDataState, boundary } = require('./formdata') + +console.log(typeof ANY, ANY === Comparator.ANY, any === ANY, isLoose, name) +console.log(outside(''), outside('1.2.3')) +console.log(Comparator.parse('').semver === ANY, Comparator.parse('2.0.0').semver === ANY) + +const form = new FormData() +form.append('a', 1) +form.append('b', 'two') +console.log(getFormDataState(form)) +setFormDataState(form, [{ name: 'c', value: true }]) +console.log(getFormDataState(form), boundary) + +class Defaults { + static count = 0 + static missing = undefined + static nested = { inner: 'value' } +} +const { count = 5, missing = 'fallback', nested: { inner, other = 'other' } } = Defaults +console.log(count, missing, inner, other) + +// Inherited statics, and a static getter reading the class it is read +// through. +class Base { + static tag = 'base' + static get label () { return `label:${this.tag}` } + static make () { return this.tag } +} +class Sub extends Base { + static tag = 'sub' +} +const { tag, label, make, name: subName } = Sub +const { label: baseLabel } = Base +console.log(tag, label, typeof make, subName, baseLabel) diff --git a/tests/corpus/8220-js-class-static-destructuring/package.json b/tests/corpus/8220-js-class-static-destructuring/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/corpus/8220-js-class-static-destructuring/ranges/outside.js b/tests/corpus/8220-js-class-static-destructuring/ranges/outside.js new file mode 100644 index 000000000..a8ffe06f2 --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/ranges/outside.js @@ -0,0 +1,13 @@ +'use strict' +// semver's ranges/outside.js and ranges/subset.js destructure the class's +// statics at module load. +const Comparator = require('../classes/comparator') +const { ANY } = Comparator +const { ANY: any, loose: isLoose = true, parse, name } = Comparator + +const outside = (text) => { + const comparator = parse(text) + return comparator.semver === ANY ? 'any' : comparator.value +} + +module.exports = { outside, ANY, any, isLoose, name } diff --git a/tests/corpus/8221-js-recursive-generator-returns/main.ts b/tests/corpus/8221-js-recursive-generator-returns/main.ts new file mode 100644 index 000000000..72b9a7b10 --- /dev/null +++ b/tests/corpus/8221-js-recursive-generator-returns/main.ts @@ -0,0 +1,10 @@ +// A JavaScript package's parser of mutually recursive generator methods that +// return `yield*` of each other (yaml 2.9.0's parse/parser.js): the checker +// cannot infer their results. Tokens, values passed to next() and return +// values reach each generator in the same order as in Node. +import { parseTokens, drive, spreadTypes, walkAll } from "token-parser"; + +for (const token of parseTokens("a { b [ c d ] e } f")) console.log(token); +console.log(drive("{ x } y ! z")); +console.log(spreadTypes("[ 1 2 3 ]")); +console.log(await walkAll("a ( b c ) d ( e )")); diff --git a/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.d.ts b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.d.ts new file mode 100644 index 000000000..763a34ae4 --- /dev/null +++ b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.d.ts @@ -0,0 +1,4 @@ +export declare function parseTokens(source: string): string[]; +export declare function drive(source: string): string; +export declare function spreadTypes(source: string): string; +export declare function walkAll(source: string): Promise; diff --git a/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.js b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.js new file mode 100644 index 000000000..aab64a1ac --- /dev/null +++ b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.js @@ -0,0 +1,131 @@ +'use strict' + +class Parser { + constructor (source) { + this.tokens = source.split(' ') + this.index = 0 + this.stack = [] + } + + * parse () { + while (this.index < this.tokens.length) { + const done = yield * this.step() + if (done) return 'stopped' + } + return yield * this.end() + } + + * step () { + const token = this.tokens[this.index++] + if (token === '{') return yield * this.blockMap(token) + if (token === '[') return yield * this.flowSeq(token) + if (token === '!') return true + return yield * this.scalar(token) + } + + * blockMap (open) { + this.stack.push(open) + yield { type: 'map-start', depth: this.stack.length } + while (this.index < this.tokens.length && this.tokens[this.index] !== '}') { + const stop = yield * this.step() + if (stop) return stop + } + this.index++ + this.stack.pop() + const reply = yield { type: 'map-end', depth: this.stack.length + 1 } + if (reply !== undefined) yield { type: 'reply', value: reply } + return false + } + + * flowSeq (open) { + this.stack.push(open) + let count = 0 + while (this.index < this.tokens.length && this.tokens[this.index] !== ']') { + yield * this.step() + count++ + } + this.index++ + this.stack.pop() + yield { type: 'seq', count } + return false + } + + * scalar (token) { + const value = yield { type: 'scalar', source: token } + if (value !== undefined) return yield * this.scalar(String(value)) + return false + } + + * end () { + yield { type: 'end', depth: this.stack.length } + return this.index + } +} + +function parseTokens (source) { + const tokens = [] + for (const token of new Parser(source).parse()) tokens.push(JSON.stringify(token)) + return tokens +} + +// next(value) reaches the innermost delegated generator, and return values +// pass back through each yield*. +function drive (source) { + const parser = new Parser(source).parse() + const log = [] + let result = parser.next() + while (!result.done) { + const value = result.value + log.push(JSON.stringify(value)) + result = parser.next(value.type === 'map-end' ? 'ack' : value.source === 'x' ? 'x2' : undefined) + } + log.push('return ' + result.value) + return log.join('\n') +} + +function spreadTypes (source) { + const tokens = [...new Parser(source).parse()] + return tokens.length + ' ' + tokens.map((token) => token.type).join(',') +} + +// Async generator methods with the same mutual recursion. +class AsyncWalker { + constructor (source) { + this.tokens = source.split(' ') + this.index = 0 + } + + async * walk () { + while (this.index < this.tokens.length) yield * this.next() + return this.index + } + + async * next () { + const token = this.tokens[this.index++] + await null + if (token === '(') return yield * this.group() + yield token + return token + } + + async * group () { + const inner = [] + while (this.tokens[this.index] !== ')') inner.push(yield * this.next()) + this.index++ + yield 'group:' + inner.join('+') + return inner.length + } +} + +async function walkAll (source) { + const seen = [] + const walker = new AsyncWalker(source).walk() + let result = await walker.next() + while (!result.done) { + seen.push(result.value) + result = await walker.next() + } + return seen.join(' ') + ' -> ' + result.value +} + +module.exports = { parseTokens, drive, spreadTypes, walkAll } diff --git a/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/package.json b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/package.json new file mode 100644 index 000000000..1fa77c2eb --- /dev/null +++ b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/package.json @@ -0,0 +1 @@ +{ "name": "token-parser", "version": "1.0.0", "main": "dist/index.js", "types": "dist/index.d.ts" } diff --git a/tests/corpus/8222-js-class-static-reflection.js b/tests/corpus/8222-js-class-static-reflection.js new file mode 100644 index 000000000..74027cc64 --- /dev/null +++ b/tests/corpus/8222-js-class-static-reflection.js @@ -0,0 +1,94 @@ +'use strict' +// A class's statics extracted, then removed with Reflect.deleteProperty, and +// redefined with Object.defineProperties (undici's FormData and Response). + +const kEnumerableProperty = { __proto__: null, enumerable: true } + +class FormData { + #state = [] + append (name, value) { this.#state.push({ name, value }) } + static getFormDataState (formData) { return formData.#state } + static setFormDataState (formData, state) { formData.#state = state } +} + +const getFormDataState = FormData.getFormDataState +const setFormDataState = FormData.setFormDataState +console.log(Reflect.deleteProperty(FormData, 'getFormDataState')) +console.log(Reflect.deleteProperty(FormData, 'setFormDataState')) +console.log(Reflect.deleteProperty(FormData, 'missing')) +console.log(typeof FormData.getFormDataState, typeof FormData.setFormDataState) + +const form = new FormData() +form.append('a', 1) +form.append('b', 'two') +console.log(getFormDataState(form)) +setFormDataState(form, [{ name: 'c', value: true }]) +console.log(getFormDataState(form).length, getFormDataState(form)[0].name) +try { + FormData.getFormDataState(form) +} catch (error) { + console.log(error.constructor.name, error.message) +} + +class Response { + #status + constructor (status) { this.#status = status } + get status () { return this.#status } + static json () { return new Response(200) } + static error () { return new Response(0) } + static getResponseState (response) { return response.#status } +} + +const getResponseState = Response.getResponseState +Reflect.deleteProperty(Response, 'getResponseState') +Object.defineProperties(Response, { + json: kEnumerableProperty, + error: kEnumerableProperty +}) +const response = Response.json() +console.log(response.status, Response.error().status, getResponseState(response)) +for (const key of ['json', 'error', 'getResponseState']) { + const descriptor = Object.getOwnPropertyDescriptor(Response, key) + console.log(key, descriptor === undefined ? 'absent' : [descriptor.enumerable, descriptor.writable, descriptor.configurable, typeof descriptor.value]) +} + +// Statics a subclass inherits, a static field and a static accessor. +class Base { + static helper () { return 'helper' } + static count = 3 + static get label () { return 'label' } +} +class Child extends Base {} +console.log(Child.helper(), Child.count, Child.label) +console.log(Reflect.deleteProperty(Base, 'helper'), Reflect.deleteProperty(Base, 'count')) +console.log(typeof Child.helper, Child.count, typeof Base.helper, Base.count) +console.log(Reflect.deleteProperty(Base, 'label'), Base.label, Child.label) +Base.helper = () => 'replaced' +console.log(Base.helper(), Child.helper()) +Base.count = 4 +const countKey = { toString () { return 'count' } } +console.log(Reflect.deleteProperty(Base, countKey), Base.count) +class Key { + constructor (name) { this.name = name } + toString () { return this.name } +} +Base.count = 5 +console.log(Reflect.deleteProperty(Base, new Key('count')), Base.count) + +// Checked-dynamic objects and primitive targets. +const parsed = JSON.parse('{"a":1,"b":2}') +console.log(Reflect.deleteProperty(parsed, 'a'), parsed) +const frozen = Object.freeze(JSON.parse('{"a":1}')) +console.log(Reflect.deleteProperty(frozen, 'a'), frozen) +const attempt = (run) => { + try { + run() + } catch (error) { + console.log(error.constructor.name, error.message) + } +} +attempt(() => Reflect.deleteProperty(1, 'a')) +attempt(() => Reflect.deleteProperty('text', 'a')) +attempt(() => Reflect.deleteProperty(null, 'a')) +attempt(() => Reflect.deleteProperty(undefined, 'a')) +attempt(() => Reflect.deleteProperty(true, 'a')) diff --git a/tests/coverage-fixtures/cjs-mutable-exports/main.js b/tests/coverage-fixtures/cjs-mutable-exports/main.js new file mode 100644 index 000000000..ac16cc4ec --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/main.js @@ -0,0 +1,5 @@ +const reassigned = require('./reassigned'); +const written = require('./written'); +const redeclared = require('./redeclared'); +const stable = require('./stable'); +console.log(reassigned.n, written.tab, redeclared.twice, stable.n); diff --git a/tests/coverage-fixtures/cjs-mutable-exports/package.json b/tests/coverage-fixtures/cjs-mutable-exports/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/coverage-fixtures/cjs-mutable-exports/reassigned.js b/tests/coverage-fixtures/cjs-mutable-exports/reassigned.js new file mode 100644 index 000000000..6c77f3b43 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/reassigned.js @@ -0,0 +1,4 @@ +// Reassigned after the export: Node's importers keep the first object. +var counter = { n: 1 }; +module.exports = counter; +counter = { n: 2 }; diff --git a/tests/coverage-fixtures/cjs-mutable-exports/redeclared.js b/tests/coverage-fixtures/cjs-mutable-exports/redeclared.js new file mode 100644 index 000000000..3150903f6 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/redeclared.js @@ -0,0 +1,4 @@ +// Declared twice: the second declaration assigns the binding again. +var twice = 1; +module.exports = { twice }; +var twice = 2; diff --git a/tests/coverage-fixtures/cjs-mutable-exports/stable.js b/tests/coverage-fixtures/cjs-mutable-exports/stable.js new file mode 100644 index 000000000..c2846bbc9 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/stable.js @@ -0,0 +1,3 @@ +// Initialized before the export and never assigned. +var stable = { n: 3 }; +module.exports = stable; diff --git a/tests/coverage-fixtures/cjs-mutable-exports/written.js b/tests/coverage-fixtures/cjs-mutable-exports/written.js new file mode 100644 index 000000000..8e0c29cd2 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/written.js @@ -0,0 +1,4 @@ +// Written by a function: the table holds the value at the export. +let tab = ' '; +function setTab(next) { tab = next; } +module.exports = { tab, setTab }; diff --git a/tests/diagnostics/intl-segmenter.ts b/tests/diagnostics/intl-segmenter.ts index 42bdae846..0cf30eabb 100644 --- a/tests/diagnostics/intl-segmenter.ts +++ b/tests/diagnostics/intl-segmenter.ts @@ -1,5 +1,5 @@ -// The native implementation covers default Unicode grapheme segmentation. -new Intl.Segmenter("en-US"); +// The native implementation covers Unicode grapheme segmentation. +new Intl.Segmenter(["en-US", "de"]); new Intl.Segmenter(undefined, { granularity: "word" }); new Intl.Segmenter(undefined, { granularity: "sentence" }); // End of the diagnostic fixture. diff --git a/tests/fixtures/npm-static/node_modules/word-segments/index.d.ts b/tests/fixtures/npm-static/node_modules/word-segments/index.d.ts new file mode 100644 index 000000000..2fd45310e --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/word-segments/index.d.ts @@ -0,0 +1 @@ +export declare function splitWords(text: string): string; diff --git a/tests/fixtures/npm-static/node_modules/word-segments/index.js b/tests/fixtures/npm-static/node_modules/word-segments/index.js new file mode 100644 index 000000000..a67aa5381 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/word-segments/index.js @@ -0,0 +1,9 @@ +const words = new Intl.Segmenter(undefined, { granularity: "word" }); + +export function splitWords(text) { + try { + return [...words.segment(text)].map((part) => part.segment).join("|"); + } catch (error) { + return `${error.code}: ${error.message}`; + } +} diff --git a/tests/fixtures/npm-static/node_modules/word-segments/package.json b/tests/fixtures/npm-static/node_modules/word-segments/package.json new file mode 100644 index 000000000..37b0d6f73 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/word-segments/package.json @@ -0,0 +1 @@ +{ "name": "word-segments", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm-static/word-segments-cli.ts b/tests/fixtures/npm-static/word-segments-cli.ts new file mode 100644 index 000000000..28e3bd5e1 --- /dev/null +++ b/tests/fixtures/npm-static/word-segments-cli.ts @@ -0,0 +1,3 @@ +import { splitWords } from "word-segments"; + +console.log(splitWords("two words")); diff --git a/tests/harness/__snapshots__/intl-segmenter.ts.txt b/tests/harness/__snapshots__/intl-segmenter.ts.txt index e50205eec..19a13ef4f 100644 --- a/tests/harness/__snapshots__/intl-segmenter.ts.txt +++ b/tests/harness/__snapshots__/intl-segmenter.ts.txt @@ -1,15 +1,15 @@ intl-segmenter.ts:2:1 - error SC2020: 'Intl.Segmenter locale negotiation' is part of the standard library types but has no scriptc lowering yet - 1 | // The native implementation covers default Unicode grapheme segmentation. - 2 | new Intl.Segmenter("en-US"); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ + 1 | // The native implementation covers Unicode grapheme segmentation. + 2 | new Intl.Segmenter(["en-US", "de"]); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | new Intl.Segmenter(undefined, { granularity: "word" }); - hint: omit the locale or pass undefined for default Unicode grapheme segmentation + hint: omit the locale, pass undefined, or pass one language tag as a string literal intl-segmenter.ts:3:31 - error SC2020: 'Intl.Segmenter options' is part of the standard library types but has no scriptc lowering yet - 2 | new Intl.Segmenter("en-US"); + 2 | new Intl.Segmenter(["en-US", "de"]); 3 | new Intl.Segmenter(undefined, { granularity: "word" }); | ^~~~~~~~~~~~~~~~~~~~~~~ 4 | new Intl.Segmenter(undefined, { granularity: "sentence" }); diff --git a/tests/harness/corpus-costs/8214-unshimmed-package-version/main.ts.json b/tests/harness/corpus-costs/8214-unshimmed-package-version/main.ts.json new file mode 100644 index 000000000..d5d1c5f65 --- /dev/null +++ b/tests/harness/corpus-costs/8214-unshimmed-package-version/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3500 } diff --git a/tests/harness/corpus-costs/8216-module-namespace-live-values/main.ts.json b/tests/harness/corpus-costs/8216-module-namespace-live-values/main.ts.json new file mode 100644 index 000000000..d5d1c5f65 --- /dev/null +++ b/tests/harness/corpus-costs/8216-module-namespace-live-values/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3500 } diff --git a/tests/harness/corpus-costs/8217-intl-segmenter-word-construct/main.ts.json b/tests/harness/corpus-costs/8217-intl-segmenter-word-construct/main.ts.json new file mode 100644 index 000000000..d5d1c5f65 --- /dev/null +++ b/tests/harness/corpus-costs/8217-intl-segmenter-word-construct/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3500 } diff --git a/tests/harness/corpus-costs/8218-cjs-namespace-imports/main.mjs.json b/tests/harness/corpus-costs/8218-cjs-namespace-imports/main.mjs.json new file mode 100644 index 000000000..ffabf028f --- /dev/null +++ b/tests/harness/corpus-costs/8218-cjs-namespace-imports/main.mjs.json @@ -0,0 +1 @@ +{ "plain": 7000, "san": 7200 } diff --git a/tests/harness/corpus-costs/8219-cjs-stable-var-exports/main.js.json b/tests/harness/corpus-costs/8219-cjs-stable-var-exports/main.js.json new file mode 100644 index 000000000..f8d95630e --- /dev/null +++ b/tests/harness/corpus-costs/8219-cjs-stable-var-exports/main.js.json @@ -0,0 +1 @@ +{ "plain": 8300, "san": 7400 } diff --git a/tests/harness/corpus-costs/8220-js-class-static-destructuring/main.js.json b/tests/harness/corpus-costs/8220-js-class-static-destructuring/main.js.json new file mode 100644 index 000000000..8b730689b --- /dev/null +++ b/tests/harness/corpus-costs/8220-js-class-static-destructuring/main.js.json @@ -0,0 +1 @@ +{ "plain": 10100, "san": 9400 } diff --git a/tests/harness/corpus-costs/8221-js-recursive-generator-returns/main.ts.json b/tests/harness/corpus-costs/8221-js-recursive-generator-returns/main.ts.json new file mode 100644 index 000000000..e6c42bf47 --- /dev/null +++ b/tests/harness/corpus-costs/8221-js-recursive-generator-returns/main.ts.json @@ -0,0 +1 @@ +{ "plain": 6600, "san": 6000 } diff --git a/tests/harness/corpus-costs/8222-js-class-static-reflection.js.json b/tests/harness/corpus-costs/8222-js-class-static-reflection.js.json new file mode 100644 index 000000000..7f7902ca1 --- /dev/null +++ b/tests/harness/corpus-costs/8222-js-class-static-reflection.js.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 4100 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index e1831c609..5b541cc54 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -543,6 +543,16 @@ test("settled generic rest-order fences count each source statement once", () => expect(coverage.stats.statementsFailed).toBe(2); }); +test("CommonJS var and let exports that can change after the export keep their refusal", () => { + // JavaScript refusals defer to run time: they are the runtime fences. + const { coverage } = analyze(fixture("cjs-mutable-exports/main.js")); + const refused = [...coverage.diagnostics, ...(coverage.runtimeFences ?? [])] + .map((d) => /exporting the mutable 'let' binding '(\w+)'/.exec(d.message)?.[1]) + .filter((name) => name !== undefined) + .sort(); + expect(refused).toEqual(["counter", "tab", "twice"]); +}); + test("inferred JavaScript compiles natively", async () => { // The js-gap fixture turns noImplicitAny off. Its inferred parameter // uses checked native values alongside the JSDoc-typed neighbor. diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 388da2138..838302af4 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -523,6 +523,28 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { } }); + test("a word segmenter in a package constructs, and its segment() throws SC2020", async () => { + const entry = join(pilotRoot, "word-segments-cli.ts"); + const { coverage } = analyze(entry, { npmStatic: "auto" }); + expect(coverage.npmStatic).toEqual([{ package: "word-segments", status: "static" }]); + expect(coverage.diagnostics).toHaveLength(0); + const outDir = join(cacheDir, "word-segments", sanitize ? "san" : "plain"); + const result = await compile(entry, { + backend: "llvm", + npmStatic: "auto", + sanitize, + outDir, + outPath: join(outDir, "program"), + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const nativeRes = await runBinary(result.binaryPath, []); + expect(nativeRes.stdout.toString()).toMatch( + /^SC2020: Intl\.Segmenter segmentation with granularity 'word' is not supported yet \[SC2020 at .*index\.js:1\]\n$/, + ); + expect(nativeRes.exitCode).toBe(0); + }); + test.each([ 'import { Console as Logger } from "node:console"; new Logger({ stdout: process.stdout }).dir(1);', 'import * as output from "console"; new output.Console(process.stdout).table([1, 2]);', diff --git a/tests/harness/package-shims.test.ts b/tests/harness/package-shims.test.ts index ca3d1589b..a21c4e768 100644 --- a/tests/harness/package-shims.test.ts +++ b/tests/harness/package-shims.test.ts @@ -8,7 +8,8 @@ * stays unloaded: its require throws MODULE_NOT_FOUND; * - a shimmed package that is not installed, or whose major version has no * shim, resolves exactly as before (here: refused like any missing or - * uncompiled package); + * uncompiled package), and `--npm-static` does not compile a version the + * registry entry does not list in `sourceVersions`; * - the undici shims keep memory bounded against a peer that sends endless * chunk-size lines and large or compressed bodies, and the 5.x profile * rejects a Host value with CR or LF as undici 5.29 does (the vendored @@ -253,6 +254,33 @@ describe("package shims", () => { } }, 180_000); + test("--npm-static leaves out an implemented package at a version it does not list", async () => { + const dir = join(scratch, "undici-v6-npm-static"); + const pkg = join(dir, "node_modules/undici"); + mkdirSync(pkg, { recursive: true }); + writeFileSync( + join(pkg, "package.json"), + '{ "name": "undici", "version": "6.21.0", "main": "index.js" }\n', + ); + writeFileSync(join(pkg, "index.js"), "exports.version = 6;\n"); + writeFileSync( + join(dir, "main.mjs"), + 'import { version } from "undici";\nconsole.log(version);\n', + ); + const result = await compile(join(dir, "main.mjs"), { + outPath: join(dir, ".scriptc/program"), + outDir: join(dir, ".scriptc"), + npmStatic: ["undici"], + sanitize, + }); + // The package's JavaScript is not part of the program, so the import + // has no declarations. + expect(result.ok).toBe(false); + expect(result.diagnostics.map((d) => d.message).join("\n")).toContain( + "Could not find a declaration file for module 'undici'", + ); + }, 180_000); + for (const name of ["undici7", "undici5"]) { test(`${name}: endless chunk-size lines and large bodies stay in bounded memory`, async () => { const { dir, binary } = await buildInCase(name, "undici-bounded.ts"); From 41664c903b18e943bb26e94c0cd743692b364036 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 12:03:38 -0500 Subject: [PATCH 067/273] Walk files iteratively in the untyped collection inference The declaration and reference sweeps recursed through ts.forEachChild, two JS frames per AST level, so a 7000-level expression overflowed the stack before the expression lowering's 200-level SC1090 fence. Both sweeps use ts.walkPreorder now, and primitiveOf answers null past the same 200 levels. --- .../frontend/lowering/js-collection-types.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/js-collection-types.ts b/packages/compiler/src/frontend/lowering/js-collection-types.ts index 2be3c067f..8820ade4a 100644 --- a/packages/compiler/src/frontend/lowering/js-collection-types.ts +++ b/packages/compiler/src/frontend/lowering/js-collection-types.ts @@ -55,6 +55,8 @@ const MAP_LOOKUPS = new Set(["get", "has", "delete"]); const SET_LOOKUPS = new Set(["has", "delete"]); const ARRAY_READS = new Set(["pop", "shift"]); const ARRAY_SEARCHES = new Set(["includes", "indexOf"]); +/** The expression lowering refuses deeper nesting (SC1090). */ +const MAX_DEPTH = 200; /** The inferred type of the binding a `new Map()` or `new Set()` initializes, * which the construction adopts. */ @@ -153,9 +155,8 @@ function inferFile(lowerer: Lowerer, file: ts.SourceFile): Candidate[] { value: undefined, }); } - ts.forEachChild(node, declarations); }; - declarations(file); + ts.walkPreorder(file, declarations); if (directEval || candidates.size === 0) return []; const names = new Set( @@ -171,9 +172,8 @@ function inferFile(lowerer: Lowerer, file: ts.SourceFile): Candidate[] { else refused.add(symbol); } } - ts.forEachChild(node, references); }; - references(file); + ts.walkPreorder(file, references); for (const symbol of refused) candidates.delete(symbol); const proven = [...candidates.values()].filter( (c) => c.valueWrites.length > 0 || c.keyWrites.length > 0, @@ -381,16 +381,20 @@ function checkerPrimitive(type: ts.Type): Primitive | null { /** The primitive type `e` always evaluates to, or null. The checker answers * where it knows; where it types `e` as `any`, the value's structure does: * a read of a decided candidate, arithmetic and comparisons over such - * values, and constants defined from them. */ + * values, and constants defined from them. An expression nested deeper than + * the lowering's nesting limit answers null. */ function primitiveOf( lowerer: Lowerer, bySymbol: Map, e: ts.Expression, visiting: Set, + depth = 0, ): Primitive | null { + if (depth > MAX_DEPTH) return null; const type = lowerer.checker.getTypeAtLocation(e); if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) === 0) return checkerPrimitive(type); - const of = (x: ts.Expression): Primitive | null => primitiveOf(lowerer, bySymbol, x, visiting); + const of = (x: ts.Expression): Primitive | null => + primitiveOf(lowerer, bySymbol, x, visiting, depth + 1); if (ts.isParenthesizedExpression(e)) return of(e.expression); if (ts.isPrefixUnaryExpression(e)) { if (e.operator === ts.SyntaxKind.ExclamationToken) return "boolean"; @@ -461,7 +465,7 @@ function primitiveOf( (declaration.parent.flags & ts.NodeFlags.Const) !== 0 ) { visiting.add(e); - const p = primitiveOf(lowerer, bySymbol, declaration.initializer, visiting); + const p = primitiveOf(lowerer, bySymbol, declaration.initializer, visiting, depth + 1); visiting.delete(e); return p; } From 684a0abd59e0645f609566a400a1052f782c76b7 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 24 Oct 2026 09:22:05 -0700 Subject: [PATCH 068/273] Store AbortController and AbortSignal handles in class fields A class field typed AbortController or AbortSignal (plain, optional, nullable, or in an array) maps to checked-dynamic storage, and the class-field rule refused it (SC1090) although the same handles already live in locals, Map values, and closures. They now take the checked-dynamic slot, like the fetch Headers, Request, and Response handles. controller?.abort() on a nullable controller was refused as a method call (also for locals): the static abort lowering ran only without an optional chain. It now runs again once the chain has narrowed the receiver. Found by pi's TUI Editor (autocompleteAbort: AbortController | undefined). Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-stdlib.ts | 12 +++-- .../compiler/src/frontend/lowering/lowerer.ts | 3 +- packages/compiler/src/frontend/type-mapper.ts | 18 ++++--- .../class-abort-controller-fields.ts.json | 6 +++ tests/corpus/class-abort-controller-fields.ts | 52 +++++++++++++++++++ .../class-abort-controller-fields.ts.json | 1 + 6 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-abort-controller-fields.ts.json create mode 100644 tests/corpus/class-abort-controller-fields.ts create mode 100644 tests/harness/corpus-costs/class-abort-controller-fields.ts.json diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index dc9d3def8..3b5be4d23 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -1630,16 +1630,22 @@ export function lowerStaticAbortControllerCall( call: ts.CallExpression, ): IrExpr | null { const access = call.expression; + // An optional chain (`controller?.abort()`) lowers the call again once it + // has narrowed the receiver (Lowerer.chainHandled). if ( - call.questionDotToken || + (call.questionDotToken && !lowerer.chainHandled.has(call)) || call.arguments.nodes.some((argument) => ts.isSpreadElement(argument)) || (!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) || - access.questionDotToken || + (access.questionDotToken && !lowerer.chainHandled.has(access)) || staticResponseMemberName(lowerer, access) !== "abort" ) { return null; } - const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); + const receiverTs = lowerer.checker.getBaseTypeOfLiteralType( + access.questionDotToken + ? lowerer.checker.getNonNullableType(lowerer.typeOf(access.expression)) + : lowerer.typeOf(access.expression), + ); const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( symbol === undefined || diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 78980d322..988b4b804 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3910,7 +3910,8 @@ export class Lowerer { /** Declared class fields may store a DYN value only when its static type * 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 + * handle (a Worker or MessagePort, a fetch Headers/Request/Response or + * AbortController/AbortSignal, or * 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. */ diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 161f73d27..e52361dc8 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1747,9 +1747,17 @@ export function mapAsyncGeneratorAnnotation(type: ts.Type, ctx: TypeMapperCtx): return { kind: "generator", async: true, ...channels }; } -/** The native fetch handles (Headers, Request, Response), optionally in - * arrays or with null/undefined: checked-dynamic handles wherever they are - * stored, like the worker handles above. */ +const FETCH_HANDLE_NAMES: ReadonlySet = new Set([ + "Headers", + "Request", + "Response", + "AbortController", + "AbortSignal", +]); + +/** The native fetch handles (Headers, Request, Response, AbortController, + * AbortSignal), optionally in arrays or with null/undefined: checked-dynamic + * handles wherever they are stored, like the worker handles above. */ export function isFetchHandleType( type: ts.Type, ctx: Pick, @@ -1765,9 +1773,7 @@ export function isFetchHandleType( const symbol = member.getSymbol(); if ( !symbol || - (ts.symbolName(symbol) !== "Headers" && - ts.symbolName(symbol) !== "Request" && - ts.symbolName(symbol) !== "Response") || + !FETCH_HANDLE_NAMES.has(ts.symbolName(symbol)) || !symbol.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-abort-controller-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-abort-controller-fields.ts.json new file mode 100644 index 000000000..80466c474 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-abort-controller-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-abort-controller-fields.ts" + ], + "diags": [] +} diff --git a/tests/corpus/class-abort-controller-fields.ts b/tests/corpus/class-abort-controller-fields.ts new file mode 100644 index 000000000..b82f11a0a --- /dev/null +++ b/tests/corpus/class-abort-controller-fields.ts @@ -0,0 +1,52 @@ +// Class fields typed AbortController and AbortSignal (plain, optional, +// `| undefined`, `| null`, in arrays) hold the live handles: aborting +// through the field is seen through every other reference, and optional +// calls on a nullable field skip an empty slot. +class Editor { + private abort: AbortController | undefined; + private signal: AbortSignal | null = null; + private readonly lifetime = new AbortController(); + history: AbortController[] = []; + last?: AbortSignal; + start(): AbortSignal { + this.abort?.abort(); + this.abort = new AbortController(); + this.history.push(this.abort); + this.signal = this.abort.signal; + this.last = this.signal; + return this.signal; + } + cancel(reason?: string): boolean { + this.abort?.abort(reason); + return this.signal?.aborted ?? false; + } + reason(): unknown { + return this.signal?.reason; + } + dispose(): boolean { + this.lifetime.abort(); + return this.lifetime.signal.aborted; + } +} + +const idle = new Editor(); +console.log(idle.cancel(), idle.reason()); + +const e = new Editor(); +const first = e.start(); +let events = 0; +first.addEventListener("abort", () => { + events++; +}); +const second = e.start(); +console.log(first.aborted, second.aborted, events, e.history.length); +console.log(e.cancel("stop"), e.reason(), second.aborted, second.reason); +console.log(e.history[0]!.signal === first, e.last === second, e.dispose()); + +// A nullable local takes the same optional call. +let maybe: AbortController | undefined = e.history.length > 5 ? new AbortController() : undefined; +maybe?.abort(); +console.log(maybe?.signal.aborted); +maybe = new AbortController(); +maybe?.abort("late"); +console.log(maybe?.signal.aborted, maybe?.signal.reason); diff --git a/tests/harness/corpus-costs/class-abort-controller-fields.ts.json b/tests/harness/corpus-costs/class-abort-controller-fields.ts.json new file mode 100644 index 000000000..28efce0e8 --- /dev/null +++ b/tests/harness/corpus-costs/class-abort-controller-fields.ts.json @@ -0,0 +1 @@ +{ "plain": 4302 } From 864feec4cf66894f88598507829b0f2a57b5d4c5 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 24 Oct 2026 09:53:28 -0700 Subject: [PATCH 069/273] Map resolved empty mapped types as the empty object type A mapped type with no members was refused (SC2001) to keep `Partial` inside a generic body, where keyof T is still unknown, from interning a wrong empty record. That also refused mapped types the checker resolved from concrete inputs whose key filter keeps no key: `Record`, `Partial<{}>`, and a catalog type such as `{ [K in KeyOf as Id]: Model }` for data without image models. When the mapped type comes through an alias whose arguments mention no type parameter (or a module-level alias without arguments), it is the empty object type and lowers like `{}`. pi-ai declares its per-provider image and classifier model catalogs this way; most providers have none, which refused 105 declaration groups. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/frontend/type-mapper.ts | 45 ++++++++++++++++++- .../corpus/empty-mapped-alias-types.ts.json | 6 +++ tests/corpus/empty-mapped-alias-types.ts | 33 ++++++++++++++ .../empty-mapped-alias-types.ts.json | 1 + 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/empty-mapped-alias-types.ts.json create mode 100644 tests/corpus/empty-mapped-alias-types.ts create mode 100644 tests/harness/corpus-costs/empty-mapped-alias-types.ts.json diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 161f73d27..9d450716f 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -7015,6 +7015,43 @@ function immutableSourceType(type: ts.Type, ctx: TypeMapperCtx): ts.Type | null return null; } +/** A mapped type reached through a type alias whose arguments mention no + * type parameter (`Record`, `Catalog`, + * a module-level alias without arguments): + * the checker computed its members from concrete inputs, so an empty + * result really is the empty object type, unlike `Partial` inside a + * generic body. */ +function resolvedMappedAlias(t: ts.Type, ctx: TypeMapperCtx): boolean { + if (!isMappedShape(t) || t.getAliasSymbol() === undefined) return false; + const args = t.getAliasTypeArguments(); + const seen = new Set(); + const concrete = (part: ts.Type): boolean => { + if (part.flags & (ts.TypeFlags.TypeParameter | ts.TypeFlags.Index)) return false; + if (part.flags & (ts.TypeFlags.IndexedAccess | ts.TypeFlags.Conditional)) return false; + if (seen.has(part)) return true; + seen.add(part); + if (part.isUnionType() || part.isIntersectionType()) + return ts.constituentTypes(part).every(concrete); + if (!part.getAliasTypeArguments().every(concrete)) return false; + return ( + (part.flags & ts.TypeFlags.Object) === 0 || + ((part as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) === 0 || + ctx.checker.getTypeArguments(part as ts.TypeReference).every(concrete) + ); + }; + // An alias without type arguments is concrete when it is declared at + // module level (a function body's alias may mention that function's type + // parameters). + if (args.length === 0) + return t + .getAliasSymbol()! + .declarations.every( + (d) => + ts.isTypeAliasDeclaration(d) && (ts.isSourceFile(d.parent) || ts.isModuleBlock(d.parent)), + ); + return args.every(concrete); +} + /** True for MAPPED-type results — `Partial`, `Record<"a", n>`, * whatever `Pick`/`Omit` reduce to. Their shape is computed by the checker, * not declared anywhere: the only declaration behind them is the utility @@ -7969,8 +8006,12 @@ function mapRecordTypeInner( // still unknown there) — interning `{}` would give a wrong type. The // degenerate empties (`Partial<{}>`, `Omit`) go with it. // 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; + // has zero declared members — the signature is the shape. So is a mapped + // alias instantiated over concrete arguments (`Record`, a + // catalog type whose key filter keeps no key): the checker resolved it, + // and it is the empty object type below. + if (computed && props.length === 0 && !indexValue && !resolvedMappedAlias(widened, ctx)) + return null; // 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, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/empty-mapped-alias-types.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/empty-mapped-alias-types.ts.json new file mode 100644 index 000000000..08ec4c0f3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/empty-mapped-alias-types.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/empty-mapped-alias-types.ts" + ], + "diags": [] +} diff --git a/tests/corpus/empty-mapped-alias-types.ts b/tests/corpus/empty-mapped-alias-types.ts new file mode 100644 index 000000000..43f3ed831 --- /dev/null +++ b/tests/corpus/empty-mapped-alias-types.ts @@ -0,0 +1,33 @@ +// Mapped types instantiated over concrete arguments whose key filter keeps +// no key (a per-kind model catalog with no models of that kind, +// `Record`, `Partial<{}>`) are the empty object type: values flow +// through them like any `{}`-typed value. +type Groups = Record>; +type Key = { [A in keyof G]: keyof G[A] }[keyof G] & string; +type KeyOf = Extract, `${T}:${string}`>; +type Id = K extends `${string}:${infer M}` ? M : never; +type Catalog = { + [K in KeyOf as Id]: { id: Id; name: string }; +}; + +const data = { api: { "chat:a": { id: "a", name: "A" }, "chat:b": { id: "b", name: "B" } } }; + +const chat: Catalog = { a: { id: "a", name: "A" }, b: { id: "b", name: data.api["chat:b"].name } }; +const image: Catalog = {}; +const video: Catalog = image; +console.log(chat.a.name, chat.b.id, JSON.stringify(image), video === image); + +const none: Record = {}; +const partial: Partial<{}> = { extra: 1 } as {}; +console.log(JSON.stringify(none), JSON.stringify(partial)); + +function describe(value: Catalog): string { + return JSON.stringify(value); +} +const catalogs: Catalog[] = [image, {}, none]; +console.log(describe(image), describe({}), catalogs.map(describe).join(" ")); + +// A module-level alias without type arguments whose filter keeps no key. +type Flags = { [K in "a" | "b" as K extends "c" ? K : never]: boolean }; +const flags: Flags = {}; +console.log(JSON.stringify(flags), JSON.stringify([flags, none])); diff --git a/tests/harness/corpus-costs/empty-mapped-alias-types.ts.json b/tests/harness/corpus-costs/empty-mapped-alias-types.ts.json new file mode 100644 index 000000000..855a18b97 --- /dev/null +++ b/tests/harness/corpus-costs/empty-mapped-alias-types.ts.json @@ -0,0 +1 @@ +{ "plain": 2395 } From 50ccfbcbe290b0751723e3af04012217d41f273b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 13:41:29 -0700 Subject: [PATCH 070/273] Report inferred shapes for untyped npm JavaScript (shape inference, stage 1) SCRIPTC_DEBUG_SHAPES=1 runs a whole-program allocation-site points-to analysis over opted-in npm packages' JavaScript and the user files that call into them, and prints which object and array literals a native record would represent exactly, the inferred class field types, the census of writes and reflection on untracked receivers, and the hot functions' parameter and return types. Report only: nothing reaches the lowering, and the analysis runs only when the variable is set. Design: results/design-shape-inference/DESIGN.md (scriptc-perf). Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/frontend/program.ts | 5 + .../src/frontend/shape-inference/analyze.ts | 2864 +++++++++++++++++ .../src/frontend/shape-inference/domain.ts | 153 + .../src/frontend/shape-inference/index.ts | 21 + .../src/frontend/shape-inference/report.ts | 374 +++ .../compiler/test/shape-inference.test.ts | 166 + 6 files changed, 3583 insertions(+) create mode 100644 packages/compiler/src/frontend/shape-inference/analyze.ts create mode 100644 packages/compiler/src/frontend/shape-inference/domain.ts create mode 100644 packages/compiler/src/frontend/shape-inference/index.ts create mode 100644 packages/compiler/src/frontend/shape-inference/report.ts create mode 100644 packages/compiler/test/shape-inference.test.ts diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index bf75127c4..8e582af89 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -140,6 +140,7 @@ import { } from "./type-only-imports.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; import { planDeclProjection, type DeclProjection } from "./decl-projection.js"; +import { reportShapes } from "./shape-inference/index.js"; import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js"; import { containsJsx, @@ -1260,6 +1261,10 @@ export function loadProgram( npmStaticPackages.length > 0, projector, ); + // Shape inference, stage S1: a report only (SCRIPTC_DEBUG_SHAPES). + const shapes = process.env["SCRIPTC_DEBUG_SHAPES"]; + if (shapes !== undefined && shapes !== "" && shapes !== "0" && npmStaticPackages.length > 0) + reportShapes(load.program, shapes); return { ...load, services, diff --git a/packages/compiler/src/frontend/shape-inference/analyze.ts b/packages/compiler/src/frontend/shape-inference/analyze.ts new file mode 100644 index 000000000..55e0e0107 --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/analyze.ts @@ -0,0 +1,2864 @@ +/* Shape inference (results/design-shape-inference/DESIGN.md), stage S1: + * a whole-program allocation-site points-to analysis over the JavaScript of + * opted-in npm packages and the user files that call into them. + * + * Stage S1 is report-only: the analysis runs when SCRIPTC_DEBUG_SHAPES is + * set and prints its plan (report.ts). Nothing it computes reaches the + * lowering yet, so every program compiles exactly as before. + * + * The analysis (DESIGN.md §4): + * - Andersen-style inclusion constraints, solved with a worklist. Locals + * and parameters are one value node per binding (flow-insensitive in S1); + * heap fields are one node per (atom, name); arrays one element node. + * - Calls resolve by the callee's atoms: functions, class constructors, + * methods by the receiver's class chain (inherited members, getters and + * setters, `this.constructor`), closures stored in fields, `bind`, and + * builtin summaries for the common library surface. + * - Escape: an atom escapes when it reaches code the analysis cannot see + * (a ⊤ callee, a builtin without a summary, a store into a ⊤ object). + * Escaped functions run with ⊤ arguments; everything reachable from an + * escaped atom escapes. + * - Census: writes and reflection whose receiver may be ⊤ apply to every + * escaped atom (§5.4); the report uses them to decide which allocation + * sites could become native records (§5.5). */ +import * as ts from "../ts7/adapter.js"; +import { isJsSourceFile } from "../program.js"; +import { npmStaticPackageOfPath } from "../npm-static.js"; +import { AtomTable, MODELED_GLOBALS, STRING_METHOD_RESULTS, type Atom } from "./domain.js"; + +type Node = ts.Node; + +export interface ValueNode { + readonly id: number; + readonly atoms: Set; + readonly succ: Set; + /** Constraints to run for each atom that arrives. */ + readonly onAtom: ((atom: Atom) => void)[]; + /** Every atom that arrives escapes. */ + escapeSink: string | null; +} + +export interface FunctionInfo { + readonly node: Node; + /** The analysis context: "" or a method's receiver (`C`). */ + readonly ctx: string; + /** The function whose clone the context is (bindings inside it are per context). */ + readonly root: Node | null; + /** Bodies are walked on first call: code that never runs adds no facts. */ + walked: boolean; + readonly label: string; + readonly params: number[]; + /** Index of a rest parameter, or -1. */ + readonly rest: number; + readonly thisNode: number; + readonly ret: number; + readonly isArrow: boolean; + readonly isAsync: boolean; + readonly isGenerator: boolean; + /** Reads `arguments` other than through `...arguments` forwarding. */ + usesArguments: boolean; + /** `f(...arguments)` forwarding: one node per argument position. */ + argPos: number[] | null; + /** The class whose member this function is (method, accessor, constructor). */ + readonly ownerClass: Node | null; + readonly inPackage: boolean; +} + +export interface LiteralSite { + readonly atom: Atom; + readonly node: Node; + readonly kind: "object" | "array"; + /** Static keys in creation order; null when a spread or computed key makes them unknown. */ + readonly keys: string[] | null; + readonly hasAccessor: boolean; + readonly hasProto: boolean; + readonly hasComputedKey: boolean; + readonly spreads: number[]; + readonly inPackage: boolean; +} + +export interface Op { + readonly kind: + | "read" + | "write" + | "computedRead" + | "computedWrite" + | "delete" + | "freeze" + | "seal" + | "defineProperty" + | "setPrototypeOf" + | "assign" + | "proxy" + | "lengthWrite"; + readonly recv: number; + readonly name: string | null; + readonly node: Node; +} + +export interface CallEdge { + readonly caller: FunctionInfo | null; + readonly callee: FunctionInfo; + readonly inLoop: boolean; +} + +export interface ShapeAnalysis { + readonly atoms: AtomTable; + readonly nodes: ValueNode[]; + readonly functions: Map; + readonly sites: LiteralSite[]; + readonly ops: Op[]; + readonly calls: CallEdge[]; + readonly escaped: Map; + readonly topWriteNames: Map; + readonly topComputedWrites: Node[]; + /** Fields per atom: name → node id. */ + readonly fields: Map>; + readonly elements: Map; + readonly top: Atom; + readonly files: readonly ts.SourceFile[]; + readonly packageFiles: number; + readonly checker: ts.TypeChecker; + readonly budgetExceeded: boolean; + readonly steps: number; + readonly topCause: ReadonlyMap; + readonly nodeLabel: ReadonlyMap; +} + +/** Array.prototype members: other names on an array are own properties. */ +const ARRAY_METHODS: ReadonlySet = new Set([ + "at", + "concat", + "copyWithin", + "entries", + "every", + "fill", + "filter", + "find", + "findIndex", + "findLast", + "findLastIndex", + "flat", + "flatMap", + "forEach", + "includes", + "indexOf", + "join", + "keys", + "lastIndexOf", + "map", + "pop", + "push", + "reduce", + "reduceRight", + "reverse", + "shift", + "slice", + "some", + "sort", + "splice", + "toLocaleString", + "toReversed", + "toSorted", + "toSpliced", + "toString", + "unshift", + "values", + "with", +]); +const MAX_STEPS = 20_000_000; + +function list(arr: { readonly nodes: readonly T[] } | undefined): readonly T[] { + return arr?.nodes ?? []; +} + +function isStatic(node: Node): boolean { + const mods = (node as { modifiers?: { nodes: readonly Node[] } }).modifiers; + return list(mods).some((m) => m.kind === ts.SyntaxKind.StaticKeyword); +} + +function unparen(node: ts.Expression): ts.Expression { + let n = node; + for (;;) { + if (ts.isParenthesizedExpression(n)) n = n.expression; + else if (ts.isNonNullExpression(n)) n = n.expression; + else if (ts.isAsExpression(n) || ts.isSatisfiesExpression(n)) n = n.expression; + else return n; + } +} + +function propertyNameText(name: Node): string | null { + if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name)) return name.text; + if (ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)) return name.text; + if (ts.isNumericLiteral(name)) return String(Number(name.text)); + if (ts.isComputedPropertyName(name)) { + const e = unparen(name.expression); + if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)) return e.text; + if (ts.isNumericLiteral(e)) return String(Number(e.text)); + } + return null; +} + +function shortFile(fileName: string): string { + const norm = fileName.split("\\").join("/"); + const nm = norm.lastIndexOf("/node_modules/"); + if (nm >= 0) { + const rest = norm.slice(nm + "/node_modules/".length).split("/"); + const pkgLen = rest[0]?.startsWith("@") ? 2 : 1; + const pkg = rest.slice(0, pkgLen).join("/"); + return `${pkg}/${rest[rest.length - 1]}`; + } + return norm.slice(norm.lastIndexOf("/") + 1); +} + +export function locLabel(node: Node): string { + const sf = node.getSourceFile(); + const { line } = ts.getLineAndCharacterOfPosition(sf, node.getStart(sf)); + return `${shortFile(sf.fileName)}:${line + 1}`; +} + +/** Whether a source file belongs to an opted-in package's JavaScript. */ +export function isPackageJsFile(sf: ts.SourceFile): boolean { + return isJsSourceFile(sf) && npmStaticPackageOfPath(sf.fileName) !== null; +} + +function isLoop(node: Node): boolean { + return ( + ts.isForStatement(node) || + ts.isForOfStatement(node) || + ts.isForInStatement(node) || + ts.isWhileStatement(node) || + ts.isDoStatement(node) + ); +} + +const LOOP_CALLBACK_METHODS: ReadonlySet = new Set([ + "map", + "forEach", + "filter", + "some", + "every", + "find", + "findIndex", + "findLast", + "findLastIndex", + "reduce", + "reduceRight", + "flatMap", + "sort", +]); + +export function analyzeShapes(program: ts.Program, checker: ts.TypeChecker): ShapeAnalysis { + return new Analyzer(program, checker).run(); +} + +class Analyzer { + readonly atoms = new AtomTable(); + readonly nodes: ValueNode[] = []; + readonly functions = new Map(); + readonly sites: LiteralSite[] = []; + private readonly siteAtoms = new Set(); + readonly ops: Op[] = []; + readonly calls: CallEdge[] = []; + readonly escaped = new Map(); + readonly topWriteNames = new Map(); + readonly topComputedWrites: Node[] = []; + readonly fields = new Map>(); + readonly elements = new Map(); + readonly top: Atom; + private readonly bindings = new Map(); + private readonly exprMemo = new Map(); + /** Current analysis context and the function whose clone it is. */ + private ctx = ""; + private ctxRoot: Node | null = null; + private readonly classInfo = new Map(); + private readonly boundCalls = new Set(); + private readonly escapedFunctionCalls = new Set(); + private readonly worklist: [number, Atom][] = []; + private steps = 0; + private budgetExceeded = false; + private currentFn: FunctionInfo | null = null; + private loopDepth = 0; + private files: ts.SourceFile[] = []; + private packageFiles = 0; + private readonly prims = new Map(); + /** Per call site: the calling function and whether the site is in a loop + * (constraints fire during solving, after the walk has moved on). */ + private readonly siteCtx = new Map(); + private forceLoop = false; + + noteSite(site: Node): void { + if (!this.siteCtx.has(site)) + this.siteCtx.set(site, { caller: this.currentFn, inLoop: this.loopDepth > 0 }); + } + + edgeFor(site: Node, callee: FunctionInfo): void { + const ctx = this.siteCtx.get(site); + this.calls.push({ + caller: ctx?.caller ?? null, + callee, + inLoop: (ctx?.inLoop ?? false) || this.forceLoop, + }); + } + + constructor( + private readonly program: ts.Program, + private readonly checker: ts.TypeChecker, + ) { + this.top = this.atoms.intern("top", () => ({ kind: "top", label: "⊤" })); + } + + run(): ShapeAnalysis { + for (const sf of this.program.getSourceFiles()) { + if (sf.isDeclarationFile) continue; + const name = sf.fileName.split("\\").join("/"); + const inPackage = isPackageJsFile(sf); + if (!inPackage && name.includes("/node_modules/")) continue; + this.files.push(sf); + if (inPackage) this.packageFiles++; + } + for (const sf of this.files) this.checker.prefetchSourceFile(sf); + for (const sf of this.files) this.moduleBody(sf); + this.finish(); + return { + atoms: this.atoms, + nodes: this.nodes, + functions: this.functions, + sites: this.sites, + ops: this.ops, + calls: this.calls, + escaped: this.escaped, + topWriteNames: this.topWriteNames, + topComputedWrites: this.topComputedWrites, + fields: this.fields, + elements: this.elements, + top: this.top, + files: this.files, + packageFiles: this.packageFiles, + checker: this.checker, + budgetExceeded: this.budgetExceeded, + steps: this.steps, + topCause: this.topCause, + nodeLabel: this.nodeLabel, + }; + } + + // ---- value nodes ------------------------------------------------------- + + newNode(): number { + const id = this.nodes.length; + this.nodes.push({ id, atoms: new Set(), succ: new Set(), onAtom: [], escapeSink: null }); + return id; + } + + /** Debugging (SCRIPTC_DEBUG_SHAPES_WHY): where each node's ⊤ came from. */ + readonly topCause = new Map(); + readonly nodeLabel = new Map(); + private cause: number | null = null; + private readonly traceTop = process.env["SCRIPTC_DEBUG_SHAPES_WHY"] !== undefined; + + add(node: number, atom: Atom): void { + const n = this.nodes[node]!; + if (n.atoms.has(atom)) return; + n.atoms.add(atom); + if (this.traceTop && atom === this.top && !this.topCause.has(node)) + this.topCause.set( + node, + this.cause ?? + (new Error().stack ?? "") + .split("\n") + .slice(2, 4) + .map((l) => l.trim()) + .join(" < "), + ); + this.worklist.push([node, atom]); + } + + edge(from: number, to: number): void { + if (from === to) return; + const n = this.nodes[from]!; + if (n.succ.has(to)) return; + n.succ.add(to); + const saved = this.cause; + this.cause = from; + for (const a of n.atoms) this.add(to, a); + this.cause = saved; + } + + on(node: number, fn: (atom: Atom) => void): void { + const n = this.nodes[node]!; + n.onAtom.push(fn); + for (const a of [...n.atoms]) fn(a); + } + + sink(node: number, reason: string): void { + const n = this.nodes[node]!; + if (n.escapeSink !== null) return; + n.escapeSink = reason; + for (const a of [...n.atoms]) this.escape(a, reason); + } + + constNode(atom: Atom): number { + const n = this.newNode(); + this.add(n, atom); + return n; + } + + prim(name: string): Atom { + let a = this.prims.get(name); + if (a === undefined) { + a = this.atoms.intern(`prim:${name}`, () => ({ kind: "prim", label: name })); + this.prims.set(name, a); + } + return a; + } + + builtin(path: string, receiver?: Atom): Atom { + const key = receiver === undefined ? `builtin:${path}` : `builtin:${path}@${receiver.id}`; + return this.atoms.intern(key, () => ({ kind: "builtin", label: path, path, receiver })); + } + + solve(): void { + while (this.worklist.length > 0) { + if (++this.steps > MAX_STEPS) { + this.budgetExceeded = true; + return; + } + const [id, atom] = this.worklist.pop()!; + const n = this.nodes[id]!; + this.cause = id; + for (const s of n.succ) this.add(s, atom); + this.cause = null; + for (const f of n.onAtom) f(atom); + if (n.escapeSink !== null) this.escape(atom, n.escapeSink); + } + } + + // ---- heap ---------------------------------------------------------------- + + field(atom: Atom, name: string): number { + let m = this.fields.get(atom); + if (m === undefined) { + m = new Map(); + this.fields.set(atom, m); + } + let id = m.get(name); + if (id === undefined) { + id = this.newNode(); + m.set(name, id); + if (this.traceTop) this.nodeLabel.set(id, `${atom.label}.${name}`); + const reason = this.escaped.get(atom); + if (reason !== undefined) { + this.sink(id, reason); + if (this.topComputedWrites.length > 0 || this.topWriteNames.has(name)) + this.add(id, this.top); + } + } + return id; + } + + element(atom: Atom): number { + let id = this.elements.get(atom); + if (id === undefined) { + id = this.newNode(); + this.elements.set(atom, id); + const reason = this.escaped.get(atom); + if (reason !== undefined) { + this.sink(id, reason); + if (this.topComputedWrites.length > 0) this.add(id, this.top); + } + } + return id; + } + + escape(atom: Atom, reason: string): void { + if (atom.kind === "prim" || atom.kind === "top") return; + if (atom.kind === "builtin" && atom.receiver === undefined) return; + if (this.escaped.has(atom)) return; + this.escaped.set(atom, reason); + const via = `via ${atom.label}`; + const fm = this.fields.get(atom); + if (fm !== undefined) { + for (const [name, id] of fm) { + this.sink(id, via); + if (this.topComputedWrites.length > 0 || this.topWriteNames.has(name)) + this.add(id, this.top); + } + } + const el = this.elements.get(atom); + if (el !== undefined) { + this.sink(el, via); + if (this.topComputedWrites.length > 0) this.add(el, this.top); + } + if (atom.receiver !== undefined) this.escape(atom.receiver, via); + if (atom.kind === "function") { + const info = this.infoOfAtom(atom); + if (info !== undefined) this.escapeFunction(info, via); + } + if ((atom.kind === "instance" || atom.kind === "class") && atom.node !== undefined) { + // Unknown code may call any member with any arguments. + for (const member of this.classMembers(atom.node, atom.kind === "class")) { + const info = this.functionInfo(member, this.ownerOf(member), this.receiverCtx(atom)); + this.add(info.thisNode, atom); + this.escapeFunction(info, via); + } + } + if (atom.kind === "promise") this.sink(this.field(atom, "value"), via); + } + + /** A function unknown code may call: ⊤ arguments and receiver; its + * results escape. */ + escapeFunction(info: FunctionInfo, reason: string): void { + if (this.escapedFunctionCalls.has(info)) return; + this.escapedFunctionCalls.add(info); + this.activate(info); + for (const p of info.params) this.add(p, this.top); + if (!info.isArrow && info.ownerClass === null) this.add(info.thisNode, this.top); + this.sink(info.ret, reason); + } + + /** Property names that untyped code writes on receivers that may be ⊤ (census). */ + noteTopWrite(name: string | null, node: Node): void { + if (name === null) { + const first = this.topComputedWrites.length === 0; + this.topComputedWrites.push(node); + if (first) { + for (const atom of [...this.escaped.keys()]) { + for (const id of (this.fields.get(atom) ?? new Map()).values()) + this.add(id, this.top); + const el = this.elements.get(atom); + if (el !== undefined) this.add(el, this.top); + } + } + return; + } + if (this.topWriteNames.has(name)) return; + this.topWriteNames.set(name, node); + for (const atom of [...this.escaped.keys()]) { + const id = this.fields.get(atom)?.get(name); + if (id !== undefined) this.add(id, this.top); + } + } + + // ---- classes --------------------------------------------------------------- + + /** The base class declaration of `decl`, via the checker; null at a + * builtin base or none; undefined when unresolved. */ + baseClass(decl: Node): Node | null | undefined { + const heritage = list((decl as ts.ClassLikeDeclaration).heritageClauses).find( + (c) => c.token === ts.SyntaxKind.ExtendsKeyword, + )?.types?.nodes[0]; + if (heritage === undefined) return null; + const e = unparen(heritage.expression); + if (!ts.isIdentifier(e) && !ts.isPropertyAccessExpression(e)) return undefined; + let d = this.resolve(ts.isIdentifier(e) ? e : e.name); + // `var C = class C { … }` (bundler output) + if ( + d !== undefined && + ts.isVariableDeclaration(d) && + d.initializer !== undefined && + ts.isClassExpression(unparen(d.initializer)) + ) + d = unparen(d.initializer); + if (d !== undefined && ts.isClassLikeDeclaration(d)) return d; + if (d !== undefined && d.getSourceFile().isDeclarationFile) return null; + return undefined; + } + + classChain(decl: Node): Node[] { + const out: Node[] = []; + let cur: Node | null | undefined = decl; + while (cur && out.length < 64 && !out.includes(cur)) { + out.push(cur); + cur = this.baseClass(cur); + } + return out; + } + + /** Method, accessor and constructor declarations of a class chain. */ + classMembers(decl: Node, statics: boolean): Node[] { + const out: Node[] = []; + for (const c of this.classChain(decl)) { + for (const m of list((c as ts.ClassLikeDeclaration).members)) { + if (isStatic(m) !== statics) continue; + if ( + ts.isMethodDeclaration(m) || + ts.isGetAccessorDeclaration(m) || + ts.isSetAccessorDeclaration(m) + ) + out.push(m); + else if (ts.isPropertyDeclaration(m) && m.initializer !== undefined) { + const init = unparen(m.initializer); + if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) out.push(init); + } + } + } + return out; + } + + lookupMember( + decl: Node, + name: string, + statics: boolean, + ): { + kind: "method" | "getter" | "setter" | "both"; + get?: Node; + set?: Node; + method?: Node; + } | null { + for (const c of this.classChain(decl)) { + let get: Node | undefined; + let set: Node | undefined; + for (const m of list((c as ts.ClassLikeDeclaration).members)) { + if (isStatic(m) !== statics) continue; + const mn = (m as { name?: Node }).name; + if (mn === undefined || propertyNameText(mn) !== name) continue; + if (ts.isMethodDeclaration(m)) return { kind: "method", method: m }; + if (ts.isGetAccessorDeclaration(m)) get = m; + if (ts.isSetAccessorDeclaration(m)) set = m; + if (ts.isPropertyDeclaration(m)) return null; // an own field: data + } + if (get || set) + return { + kind: get && set ? "both" : get ? "getter" : "setter", + ...(get ? { get } : {}), + ...(set ? { set } : {}), + }; + } + return null; + } + + // ---- symbols ----------------------------------------------------------------- + + resolve(id: Node): Node | undefined { + let sym = this.checker.getSymbolAtLocation(id); + if (sym === undefined) return undefined; + if (sym.flags & ts.SymbolFlags.Alias) { + try { + sym = this.checker.getAliasedSymbol(sym); + } catch { + return undefined; + } + } + return sym.valueDeclaration ?? sym.declarations[0]; + } + + binding(decl: Node): number { + const key = this.ctxKey(decl); + let id = this.bindings.get(key); + if (id === undefined) { + id = this.newNode(); + this.bindings.set(key, id); + if (this.traceTop) + this.nodeLabel.set( + id, + `binding ${(decl as { name?: { text?: string } }).name?.text ?? "?"}@${locLabel(decl)}${this.ctx !== "" ? ` [${this.ctx}]` : ""}`, + ); + } + return id; + } + + /** Bindings and expressions inside the function being cloned belong to + * the clone's context; everything else to the default context. */ + ctxKey(node: Node): string { + const root = this.ctxRoot; + const inside = + root !== null && + node.getSourceFile() === root.getSourceFile() && + node.pos >= root.pos && + node.end <= root.end; + return `${inside ? this.ctx : ""}|${this.nodeKey(node)}`; + } + + receiverCtx(atom: Atom): string { + return atom.kind === "instance" || atom.kind === "class" ? `C${atom.id}` : ""; + } + + infoOfAtom(atom: Atom): FunctionInfo | undefined { + if (atom.node === undefined) return undefined; + return this.functions.get(`${atom.ctx ?? ""}|${this.nodeKey(atom.node)}`); + } + + // ---- functions --------------------------------------------------------------- + + functionInfo(node: Node, ownerClass: Node | null, ctxArg = ""): FunctionInfo { + // A nested closure walked inside a clone shares the clone's context. + const inherit = + ctxArg === "" && + this.ctxRoot !== null && + node.getSourceFile() === this.ctxRoot.getSourceFile() && + node.pos >= this.ctxRoot.pos && + node.end <= this.ctxRoot.end; + const ctx = ctxArg !== "" ? ctxArg : inherit ? this.ctx : ""; + const root = ctxArg !== "" ? node : inherit ? this.ctxRoot : null; + const fkey = `${ctx}|${this.nodeKey(node)}`; + const existing = this.functions.get(fkey); + if (existing !== undefined) return existing; + const savedCtx = this.ctx; + const savedRoot = this.ctxRoot; + this.ctx = ctx; + this.ctxRoot = root; + const fn = node as ts.FunctionLikeDeclaration; + const isArrow = ts.isArrowFunction(node); + const params: number[] = []; + let rest = -1; + for (const p of list(fn.parameters)) { + const id = this.binding(p); + if (p.dotDotDotToken !== undefined) rest = params.length; + params.push(id); + } + const mods = list((fn as { modifiers?: { nodes: readonly Node[] } }).modifiers); + const sf = node.getSourceFile(); + const nameNode = (fn as { name?: Node }).name; + const name = nameNode !== undefined ? (propertyNameText(nameNode) ?? "") : null; + const owner = ownerClass !== null ? this.className(ownerClass) : null; + const label = `${owner !== null ? `${owner}.` : ""}${name ?? (ts.isConstructorDeclaration(node) ? "constructor" : "")}@${locLabel(node)}`; + const info: FunctionInfo = { + node, + ctx, + root, + walked: false, + label: ctx === "" ? label : `${label} [${this.ctxLabel(ctx)}]`, + params, + rest, + thisNode: isArrow && this.currentFn !== null ? this.currentFn.thisNode : this.newNode(), + ret: this.newNode(), + isArrow, + isAsync: mods.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword), + isGenerator: (fn as { asteriskToken?: Node }).asteriskToken !== undefined, + usesArguments: false, + argPos: null, + ownerClass, + inPackage: isPackageJsFile(sf), + }; + this.functions.set(fkey, info); + this.ctx = savedCtx; + this.ctxRoot = savedRoot; + return info; + } + + /** Walks a function's body the first time it can run. */ + activate(info: FunctionInfo): void { + if (info.walked) return; + info.walked = true; + if (ts.isSourceFile(info.node) || ts.isClassLikeDeclaration(info.node)) return; + const fn = info.node as ts.FunctionLikeDeclaration; + const savedCtx = this.ctx; + const savedRoot = this.ctxRoot; + const saved = this.currentFn; + const savedLoop = this.loopDepth; + this.ctx = info.ctx; + this.ctxRoot = info.root; + this.currentFn = info; + this.loopDepth = 0; + for (const p of list(fn.parameters)) { + if (p.initializer !== undefined) this.edge(this.expr(p.initializer), this.binding(p)); + if (p.dotDotDotToken !== undefined) { + const arr = this.atoms.intern(`rest:${this.nodes.length}`, () => ({ + kind: "array", + label: `rest@${locLabel(p)}`, + node: p, + })); + this.add(this.binding(p), arr); + } + this.bindPattern(p.name, this.binding(p)); + } + const body = fn.body; + if (body !== undefined) { + if (ts.isBlock(body)) this.stmts(list(body.statements)); + else this.edge(this.expr(body as ts.Expression), info.ret); + } + this.currentFn = saved; + this.loopDepth = savedLoop; + this.ctx = savedCtx; + this.ctxRoot = savedRoot; + if (info.usesArguments) { + for (const p of info.params) this.add(p, this.top); + } + } + + /** The atom of a function node (memoized by its info). */ + fnAtom(node: Node, ownerClass: Node | null, ctx = ""): Atom { + const info = this.functionInfo(node, ownerClass, ctx); + return this.atoms.intern(`fn:${info.ret}`, () => ({ + kind: "function", + label: info.label, + node, + ctx: info.ctx, + })); + } + + ctxLabel(ctx: string): string { + if (ctx.startsWith("C")) return `this=${this.atoms.atoms[Number(ctx.slice(1))]?.label ?? ctx}`; + return ctx; + } + + className(decl: Node): string { + const own = (decl as ts.ClassLikeDeclaration).name?.text; + if (own !== undefined) return own; + const p = decl.parent; + if (p !== undefined && ts.isVariableDeclaration(p) && ts.isIdentifier(p.name)) + return p.name.text; + return ``; + } + + classAtoms(decl: Node): { cls: Atom; inst: Atom } { + const name = this.className(decl); + const cls = this.atoms.intern(`class:${this.nodeKey(decl)}`, () => ({ + kind: "class", + label: `class ${name}`, + node: decl, + })); + const inst = this.atoms.intern(`inst:${this.nodeKey(decl)}`, () => ({ + kind: "instance", + label: name, + node: decl, + })); + this.ensureClass(decl); + return { cls, inst }; + } + + private readonly nodeKeys = new Map(); + nodeKey(node: Node): number { + let k = this.nodeKeys.get(node); + if (k === undefined) { + k = this.nodeKeys.size; + this.nodeKeys.set(node, k); + } + return k; + } + + /** Registers a class: member functions, field initializers (as a pseudo + * function run at construction) and static initializers. */ + ensureClass(decl: Node): { initFn: FunctionInfo; staticInit: FunctionInfo } { + const existing = this.classInfo.get(decl); + if (existing !== undefined) return existing; + const c = decl as ts.ClassLikeDeclaration; + const label = this.className(decl); + const mk = (suffix: string): FunctionInfo => ({ + node: decl, + ctx: "", + root: null, + walked: true, + label: `${label}.${suffix}@${locLabel(decl)}`, + params: [], + rest: -1, + thisNode: this.newNode(), + ret: this.newNode(), + isArrow: false, + isAsync: false, + isGenerator: false, + usesArguments: false, + argPos: null, + ownerClass: decl, + inPackage: isPackageJsFile(decl.getSourceFile()), + }); + const entry = { initFn: mk(""), staticInit: mk("") }; + this.classInfo.set(decl, entry); + const cls = this.atoms.intern(`class:${this.nodeKey(decl)}`, () => ({ + kind: "class", + label: `class ${label}`, + node: decl, + })); + this.add(entry.staticInit.thisNode, cls); + const saved = this.currentFn; + for (const m of list(c.members)) { + const statics = isStatic(m); + if ( + ts.isMethodDeclaration(m) || + ts.isGetAccessorDeclaration(m) || + ts.isSetAccessorDeclaration(m) || + ts.isConstructorDeclaration(m) + ) { + continue; + } + if (ts.isPropertyDeclaration(m)) { + const name = propertyNameText(m.name); + const init = statics ? entry.staticInit : entry.initFn; + this.currentFn = init; + const value = + m.initializer !== undefined + ? this.exprIn(m.initializer, decl) + : this.constNode(this.prim("undefined")); + if (name !== null) this.storeConstraint(init.thisNode, name, value, m); + continue; + } + if (ts.isClassStaticBlockDeclaration(m)) { + this.currentFn = entry.staticInit; + this.stmts(list(m.body.statements)); + } + } + this.currentFn = saved; + return entry; + } + + /** An initializer expression: arrow functions in field initializers take + * the instance as `this` and belong to the class. */ + exprIn(e: ts.Expression, ownerClass: Node): number { + const u = unparen(e); + if (ts.isArrowFunction(u) || ts.isFunctionExpression(u)) { + const n = this.newNode(); + this.add(n, this.fnAtom(u, ts.isArrowFunction(u) ? null : ownerClass)); + return n; + } + return this.expr(e); + } + + /** `new C(args)` on a class atom: the instance, the field initializers of + * every class in the chain, and the constructor (or the implicit one). */ + construct( + decl: Node, + args: number[], + spread: boolean, + dst: number, + site: Node, + inst?: Atom, + ): void { + const atoms = this.classAtoms(decl); + const instance = inst ?? atoms.inst; + this.add(dst, instance); + this.callConstructorChain(decl, instance, args, spread, site); + } + + callConstructorChain( + decl: Node, + instance: Atom, + args: number[], + spread: boolean, + site: Node, + ): void { + const key = `ctor:${this.nodeKey(decl)}:${instance.id}:${this.nodeKey(site)}`; + if (this.boundCalls.has(key)) return; + this.boundCalls.add(key); + const entry = this.ensureClass(decl); + this.add(entry.initFn.thisNode, instance); + const ctor = list((decl as ts.ClassLikeDeclaration).members).find((m) => + ts.isConstructorDeclaration(m), + ); + if (ctor !== undefined) { + const info = this.functionInfo(ctor, decl, this.receiverCtx(instance)); + this.activate(info); + this.add(info.thisNode, instance); + this.bindArgs(info, args, spread); + this.edgeFor(site, info); + return; + } + const base = this.baseClass(decl); + if (base) this.callConstructorChain(base, instance, args, spread, site); + } + + bindArgs(info: FunctionInfo, args: number[], spread: boolean): void { + if (info.argPos !== null) { + for (let i = 0; i < info.argPos.length; i++) { + if (spread || i >= info.argPos.length - 1) { + for (const a of args.slice(i)) this.edge(a, info.argPos[i]!); + if (spread) this.add(info.argPos[i]!, this.top); + } else if (i < args.length) this.edge(args[i]!, info.argPos[i]!); + else this.add(info.argPos[i]!, this.prim("undefined")); + } + } + if (spread) { + for (const p of info.params) this.add(p, this.top); + for (const a of args) this.sink(a, `spread into ${info.label}`); + return; + } + for (let i = 0; i < info.params.length; i++) { + if (i === info.rest) { + const restAtoms = [...this.nodes[info.params[i]!]!.atoms]; + for (const ra of restAtoms) + for (let j = i; j < args.length; j++) this.edge(args[j]!, this.element(ra)); + break; + } + if (i < args.length) this.edge(args[i]!, info.params[i]!); + else this.add(info.params[i]!, this.prim("undefined")); + } + } + + /** Call of a function atom with a receiver node. */ + callFunction( + info: FunctionInfo, + thisNode: number | null, + args: number[], + spread: boolean, + dst: number, + site: Node, + ): void { + // `() => Object.create(null)` helpers: each call site gets its own + // dictionary, as if the helper were inlined. + if (this.isFactory(info.node)) { + const d = this.atoms.intern(`dict:${this.nodeKey(site)}`, () => ({ + kind: "dictionary", + label: `Object.create@${locLabel(site)}`, + node: site, + })); + this.add(dst, d); + return; + } + this.activate(info); + const fnNode = info.node; + const key = `call:${info.ctx}|${this.nodeKey(fnNode)}:${this.nodeKey(site)}:${thisNode ?? -1}:${dst}`; + if (this.boundCalls.has(key)) return; + this.boundCalls.add(key); + if (thisNode !== null && !info.isArrow) this.edge(thisNode, info.thisNode); + this.bindArgs(info, args, spread); + if (info.isAsync) { + const p = this.promiseAtom(fnNode, `async ${info.label}`); + this.awaitInto(info.ret, this.field(p, "value")); + this.add(dst, p); + } else if (info.isGenerator) { + this.add(dst, this.top); + } else this.edge(info.ret, dst); + this.edgeFor(site, info); + } + + private readonly factories = new Map(); + isFactory(node: Node): boolean { + let r = this.factories.get(node); + if (r === undefined) { + r = false; + const fn = node as ts.FunctionLikeDeclaration; + let e: Node | undefined = fn.body; + if (e !== undefined && ts.isBlock(e)) { + const st = e.statements.nodes; + e = st.length === 1 && ts.isReturnStatement(st[0]!) ? st[0]!.expression : undefined; + } + if (e !== undefined && !ts.isSourceFile(node)) { + const u = unparen(e as ts.Expression); + r = + ts.isCallExpression(u) && + u.expression.getText() === "Object.create" && + u.arguments.nodes.length === 1 && + u.arguments.nodes[0]!.kind === ts.SyntaxKind.NullKeyword; + } + this.factories.set(node, r); + } + return r; + } + + promiseAtom(site: Node, label: string): Atom { + return this.atoms.intern(`promise:${this.nodeKey(site)}`, () => ({ + kind: "promise", + label: `Promise@${label}`, + node: site, + })); + } + + /** dst ⊇ awaited(src). */ + awaitInto(src: number, dst: number): void { + this.on(src, (a) => { + if (a.kind === "promise") this.edge(this.field(a, "value"), dst); + else this.add(dst, a); + }); + } + + syntheticArray(site: Node, tag: string): Atom { + return this.atoms.intern(`arr:${tag}:${this.nodeKey(site)}`, () => ({ + kind: "array", + label: `${tag}[]@${locLabel(site)}`, + node: site, + })); + } + + // ---- statements ---------------------------------------------------------------- + + moduleBody(sf: ts.SourceFile): void { + const info: FunctionInfo = { + node: sf, + ctx: "", + root: null, + walked: true, + label: ``, + params: [], + rest: -1, + thisNode: this.constNode(this.prim("undefined")), + ret: this.newNode(), + isArrow: false, + isAsync: false, + isGenerator: false, + usesArguments: false, + argPos: null, + ownerClass: null, + inPackage: isPackageJsFile(sf), + }; + this.functions.set(`|${this.nodeKey(sf)}`, info); + this.currentFn = info; + this.stmts(list(sf.statements)); + this.currentFn = null; + } + + stmts(stmts: readonly Node[]): void { + for (const s of stmts) this.stmt(s); + } + + stmt(s: Node): void { + if (ts.isVariableStatement(s)) { + for (const d of list(s.declarationList.declarations)) this.varDecl(d); + return; + } + if (ts.isExpressionStatement(s)) { + this.expr(s.expression); + return; + } + if (ts.isReturnStatement(s)) { + if (s.expression !== undefined && this.currentFn !== null) + this.edge(this.expr(s.expression), this.currentFn.ret); + return; + } + if (ts.isFunctionDeclaration(s)) { + const b = this.binding(s); + const saved = this.currentFn; + this.currentFn = null; + this.add(b, this.fnAtom(s, null)); + this.currentFn = saved; + return; + } + if (ts.isClassDeclaration(s)) { + const b = this.binding(s); + this.add(b, this.classAtoms(s).cls); + return; + } + if (isLoop(s)) { + if (ts.isForOfStatement(s) || ts.isForInStatement(s)) { + const src = this.expr(s.expression); + const target = this.newNode(); + if (ts.isForInStatement(s)) this.add(target, this.prim("string")); + else this.iterateInto(src, target, s); + const init = s.initializer; + if (ts.isVariableDeclarationList(init)) { + for (const d of list(init.declarations)) { + this.edge(target, this.binding(d)); + this.bindPattern(d.name, this.binding(d)); + } + } else this.assignTo(init as ts.Expression, target); + } + this.loopDepth++; + if (ts.isForStatement(s)) { + if (s.initializer !== undefined) { + if (ts.isVariableDeclarationList(s.initializer)) + for (const d of list(s.initializer.declarations)) this.varDecl(d); + else this.expr(s.initializer as ts.Expression); + } + if (s.condition !== undefined) this.expr(s.condition); + if (s.incrementor !== undefined) this.expr(s.incrementor); + } + if (ts.isWhileStatement(s) || ts.isDoStatement(s)) this.expr(s.expression); + const body = (s as { statement?: Node }).statement; + if (body !== undefined) this.stmt(body); + this.loopDepth--; + return; + } + if (ts.isTryStatement(s)) { + this.stmts(list(s.tryBlock.statements)); + if (s.catchClause !== undefined) { + const v = s.catchClause.variableDeclaration; + if (v !== undefined) { + this.add(this.binding(v), this.top); + this.bindPattern(v.name, this.binding(v)); + } + this.stmts(list(s.catchClause.block.statements)); + } + if (s.finallyBlock !== undefined) this.stmts(list(s.finallyBlock.statements)); + return; + } + if (ts.isThrowStatement(s)) { + this.sink(this.expr(s.expression), `thrown at ${locLabel(s)}`); + return; + } + if ( + ts.isImportDeclaration(s) || + ts.isExportDeclaration(s) || + ts.isInterfaceDeclaration(s) || + ts.isTypeAliasDeclaration(s) + ) + return; + if (ts.isExportAssignment(s)) { + this.expr(s.expression); + return; + } + // Blocks, ifs, switches, labels: walk children, evaluating expressions. + ts.forEachChild(s, (child) => { + if (ts.isExpression(child)) this.expr(child); + else this.stmt(child); + return undefined; + }); + } + + varDecl(d: ts.VariableDeclaration): void { + const b = this.binding(d); + if (d.initializer !== undefined) this.edge(this.expr(d.initializer), b); + this.bindPattern(d.name, b); + } + + /** Destructuring: binding elements read from the source node. */ + bindPattern(name: Node, src: number): void { + if (ts.isIdentifier(name)) return; + if (ts.isObjectBindingPattern(name)) { + for (const el of list(name.elements)) { + const b = this.binding(el); + if (el.dotDotDotToken !== undefined) { + this.add(b, this.top); + continue; + } + const keyNode = el.propertyName ?? el.name; + const key = keyNode !== undefined ? propertyNameText(keyNode) : null; + if (key !== null) this.loadConstraint(src, key, b, el); + else this.add(b, this.top); + if (el.initializer !== undefined) this.edge(this.expr(el.initializer), b); + if (el.name !== undefined) this.bindPattern(el.name, b); + } + return; + } + if (ts.isArrayBindingPattern(name)) { + let i = 0; + for (const el of list(name.elements)) { + if (ts.isBindingElement(el)) { + const b = this.binding(el); + if (el.dotDotDotToken !== undefined) this.add(b, this.top); + else this.iterateInto(src, b, el, i); + if (el.initializer !== undefined) this.edge(this.expr(el.initializer), b); + if (el.name !== undefined) this.bindPattern(el.name, b); + } + i++; + } + } + } + + /** Element values of an iterable. */ + iterateInto(src: number, dst: number, site: Node, _index?: number): void { + this.on(src, (a) => { + if (a.kind === "array" || a.kind === "collection") this.edge(this.element(a), dst); + else if (a.kind === "prim" && a.label === "string") this.add(dst, this.prim("string")); + else if (a.kind === "prim" || a.kind === "object" || a.kind === "dictionary") { + /* not iterable (a TypeError): no value. Literals with a + Symbol.iterator member have a computed key; they are G sites. */ + } else { + this.add(dst, this.top); + if (a.kind !== "top") this.escape(a, `iterated at ${locLabel(site)}`); + } + }); + } + + // ---- expressions --------------------------------------------------------------- + + expr(e: ts.Expression): number { + const key = this.ctxKey(e); + const memo = this.exprMemo.get(key); + if (memo !== undefined) return memo; + const id = this.exprUncached(e); + this.exprMemo.set(key, id); + if (this.traceTop && !this.nodeLabel.has(id)) + this.nodeLabel.set( + id, + `expr ${e.getText().slice(0, 40).replace(/\s+/g, " ")}@${locLabel(e)}`, + ); + return id; + } + + private exprUncached(e: ts.Expression): number { + const u = unparen(e); + if (u !== e) return this.expr(u); + switch (e.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.TemplateExpression: + if (ts.isTemplateExpression(e)) + for (const span of list(e.templateSpans)) this.expr(span.expression); + return this.constNode(this.prim("string")); + case ts.SyntaxKind.NumericLiteral: + return this.constNode(this.prim("number")); + case ts.SyntaxKind.BigIntLiteral: + return this.constNode(this.prim("bigint")); + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + return this.constNode(this.prim("boolean")); + case ts.SyntaxKind.NullKeyword: + return this.constNode(this.prim("null")); + case ts.SyntaxKind.RegularExpressionLiteral: + return this.constNode(this.builtin("RegExp.instance")); + case ts.SyntaxKind.ThisKeyword: + return this.currentFn !== null + ? this.currentFn.thisNode + : this.constNode(this.prim("undefined")); + case ts.SyntaxKind.SuperKeyword: + return this.constNode(this.top); + } + if (ts.isIdentifier(e)) return this.identifier(e); + if (ts.isObjectLiteralExpression(e)) return this.objectLiteral(e); + if (ts.isArrayLiteralExpression(e)) return this.arrayLiteral(e); + if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) { + const n = this.newNode(); + this.add(n, this.fnAtom(e, null)); + if (ts.isFunctionExpression(e) && e.name !== undefined) + this.add(this.binding(e), this.fnAtom(e, null)); + return n; + } + if (ts.isClassExpression(e)) { + const n = this.newNode(); + this.add(n, this.classAtoms(e).cls); + return n; + } + if (ts.isPropertyAccessExpression(e)) { + this.noteSite(e); + const recv = this.expr(e.expression); + const name = e.name.text; + const dst = this.newNode(); + this.loadConstraint(recv, name, dst, e); + this.ops.push({ kind: "read", recv, name, node: e }); + return dst; + } + if (ts.isElementAccessExpression(e)) { + const recv = this.expr(e.expression); + const key = unparen(e.argumentExpression); + const dst = this.newNode(); + const name = + ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key) ? key.text : null; + if (name !== null) { + this.loadConstraint(recv, name, dst, e); + this.ops.push({ kind: "read", recv, name, node: e }); + } else { + this.expr(e.argumentExpression); + this.computedLoad(recv, dst, e); + this.ops.push({ kind: "computedRead", recv, name: null, node: e }); + } + return dst; + } + if (ts.isCallExpression(e)) return this.call(e); + if (ts.isNewExpression(e)) return this.newExpr(e); + if (ts.isBinaryExpression(e)) return this.binary(e); + if (ts.isConditionalExpression(e)) { + this.expr(e.condition); + const n = this.newNode(); + this.edge(this.expr(e.whenTrue), n); + this.edge(this.expr(e.whenFalse), n); + return n; + } + if (ts.isAwaitExpression(e)) { + const n = this.newNode(); + this.awaitInto(this.expr(e.expression), n); + return n; + } + if (ts.isTypeOfExpression(e)) { + this.expr(e.expression); + return this.constNode(this.prim("string")); + } + if (ts.isVoidExpression(e)) { + this.expr(e.expression); + return this.constNode(this.prim("undefined")); + } + if (ts.isDeleteExpression(e)) { + const target = unparen(e.expression); + if (ts.isPropertyAccessExpression(target) || ts.isElementAccessExpression(target)) { + const recv = this.expr(target.expression); + const name = ts.isPropertyAccessExpression(target) ? target.name.text : null; + this.ops.push({ kind: "delete", recv, name, node: e }); + } + return this.constNode(this.prim("boolean")); + } + if (ts.isPrefixUnaryExpression(e) || ts.isPostfixUnaryExpression(e)) { + const operand = e.operand; + this.expr(operand); + if ( + e.operator === ts.SyntaxKind.PlusPlusToken || + e.operator === ts.SyntaxKind.MinusMinusToken + ) + this.assignTo(operand, this.constNode(this.prim("number"))); + if (ts.isPrefixUnaryExpression(e) && e.operator === ts.SyntaxKind.ExclamationToken) + return this.constNode(this.prim("boolean")); + return this.constNode(this.prim("number")); + } + if (ts.isSpreadElement(e)) return this.expr(e.expression); + if (ts.isTaggedTemplateExpression(e)) { + this.expr(e.tag); + return this.constNode(this.top); + } + if (ts.isYieldExpression(e)) { + if (e.expression !== undefined) + this.sink(this.expr(e.expression), `yielded at ${locLabel(e)}`); + return this.constNode(this.top); + } + if (ts.isMetaProperty(e)) return this.constNode(this.top); + // Anything else: evaluate children, answer ⊤. + ts.forEachChild(e, (child) => { + if (ts.isExpression(child)) this.expr(child); + return undefined; + }); + return this.constNode(this.top); + } + + identifier(id: ts.Identifier): number { + if (id.text === "undefined") return this.constNode(this.prim("undefined")); + if (id.text === "arguments") { + const fn = this.enclosingNonArrow(); + if (fn !== null) { + const parent = id.parent; + const forwarding = parent !== undefined && ts.isSpreadElement(parent); + if (!forwarding) fn.usesArguments = true; + const arr = this.syntheticArray(id, "arguments"); + for (const p of fn.params) this.edge(p, this.element(arr)); + return this.constNode(arr); + } + } + const decl = this.resolve(id); + if (decl === undefined) return this.globalNode(id.text); + if (decl.getSourceFile().isDeclarationFile) return this.globalNode(id.text); + if (!this.files.includes(decl.getSourceFile())) return this.constNode(this.top); + if (ts.isFunctionDeclaration(decl)) { + const b = this.binding(decl); + const saved = this.currentFn; + this.currentFn = null; + this.add(b, this.fnAtom(decl, null)); + this.currentFn = saved; + return b; + } + if (ts.isClassDeclaration(decl) || ts.isClassExpression(decl)) { + const b = this.binding(decl); + this.add(b, this.classAtoms(decl).cls); + return b; + } + if (ts.isFunctionExpression(decl)) return this.binding(decl); + if ( + ts.isImportSpecifier(decl) || + ts.isImportClause(decl) || + ts.isNamespaceImport(decl) || + ts.isImportEqualsDeclaration(decl) + ) + return this.constNode(this.top); + if (ts.isSourceFile(decl) || ts.isModuleDeclaration(decl)) return this.namespaceNode(decl); + return this.narrowed(id, decl, this.binding(decl)); + } + + /** Flow narrowing at guarded references (S1 handles the guards hot code + * uses: `x instanceof C` and calls of predicate functions whose body is + * `… && x instanceof C`, in the true arm of an `if`, `?:` or `&&`). The + * narrowed value keeps the guard's class arms and ⊤. */ + narrowed(id: ts.Identifier, decl: Node, value: number): number { + let child: Node = id; + let p = id.parent; + while (p !== undefined && !ts.isFunctionLike(p) && !ts.isSourceFile(p)) { + let cond: ts.Expression | undefined; + if (ts.isIfStatement(p) && p.thenStatement === child) cond = p.expression; + else if (ts.isConditionalExpression(p) && p.whenTrue === child) cond = p.condition; + else if ( + ts.isBinaryExpression(p) && + p.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && + p.right === child + ) + cond = p.left; + if (cond !== undefined) { + const cls = this.guardClass(cond, decl); + if (cls !== null) { + const out = this.newNode(); + this.on(value, (a) => { + if (a.kind === "top") this.add(out, a); + else if ( + cls === "Promise" + ? a.kind === "promise" + : a.kind === "instance" && + a.node !== undefined && + this.classChain(a.node).includes(cls) + ) + this.add(out, a); + }); + return out; + } + } + child = p; + p = p.parent; + } + return value; + } + + /** The class a condition proves `decl`'s value an instance of, if any. */ + guardClass(cond: ts.Expression, decl: Node): Node | "Promise" | null { + const e = unparen(cond); + if (ts.isBinaryExpression(e)) { + if (e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) + return this.guardClass(e.left, decl) ?? this.guardClass(e.right, decl); + if (e.operatorToken.kind !== ts.SyntaxKind.InstanceOfKeyword) return null; + const l = unparen(e.left); + if (!ts.isIdentifier(l) || this.resolve(l) !== decl) return null; + return this.instanceofTarget(e.right); + } + if ( + ts.isCallExpression(e) && + ts.isIdentifier(unparen(e.expression)) && + e.arguments.nodes.length === 1 + ) { + const arg = unparen(e.arguments.nodes[0]!); + if (!ts.isIdentifier(arg) || this.resolve(arg) !== decl) return null; + const fnDecl = this.resolve(unparen(e.expression)); + let fn: Node | undefined = fnDecl; + if (fn !== undefined && ts.isVariableDeclaration(fn) && fn.initializer !== undefined) + fn = unparen(fn.initializer); + if ( + fn === undefined || + !(ts.isArrowFunction(fn) || ts.isFunctionDeclaration(fn) || ts.isFunctionExpression(fn)) + ) + return null; + const param = fn.parameters.nodes[0]; + if (param === undefined || !ts.isIdentifier(param.name)) return null; + let body: Node | undefined = fn.body; + if (body !== undefined && ts.isBlock(body)) { + const stmts = body.statements.nodes; + body = + stmts.length === 1 && ts.isReturnStatement(stmts[0]!) ? stmts[0]!.expression : undefined; + } + if (body === undefined) return null; + return this.predicateClass(body as ts.Expression, param); + } + return null; + } + + /** `… && p instanceof C` in a predicate body over parameter `p`. */ + predicateClass(e: ts.Expression, param: Node): Node | "Promise" | null { + const u = unparen(e); + if (!ts.isBinaryExpression(u)) return null; + if (u.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) + return this.predicateClass(u.right, param) ?? this.predicateClass(u.left, param); + if (u.operatorToken.kind !== ts.SyntaxKind.InstanceOfKeyword) return null; + const l = unparen(u.left); + if (!ts.isIdentifier(l) || this.resolve(l) !== param) return null; + return this.instanceofTarget(u.right); + } + + instanceofTarget(right: ts.Expression): Node | "Promise" | null { + const r = unparen(right); + if (!ts.isIdentifier(r)) return null; + const d = this.resolve(r); + if (d === undefined || d.getSourceFile().isDeclarationFile) + return r.text === "Promise" ? "Promise" : null; + return ts.isClassLikeDeclaration(d) ? d : null; + } + + private readonly namespaces = new Map(); + /** `import * as ns`: an object whose fields are the module's exports. */ + namespaceNode(mod: Node): number { + const existing = this.namespaces.get(mod); + if (existing !== undefined) return existing; + const n = this.newNode(); + this.namespaces.set(mod, n); + const atom = this.atoms.intern(`ns:${this.nodeKey(mod)}`, () => ({ + kind: "object", + label: `namespace ${shortFile((mod as ts.SourceFile).fileName ?? "")}`, + node: mod, + })); + this.add(n, atom); + const sym = this.checker.getSymbolAtLocation(mod); + if (sym !== undefined) { + for (const ex of this.checker.getExportsOfModule(sym)) { + let target = ex; + if (target.flags & ts.SymbolFlags.Alias) { + try { + target = this.checker.getAliasedSymbol(target); + } catch { + continue; + } + } + const d = target.valueDeclaration ?? target.declarations[0]; + if (d === undefined) continue; + const value = this.declValue(d); + if (value !== null) this.edge(value, this.field(atom, String(ex.escapedName))); + } + } + return n; + } + + declValue(d: Node): number | null { + if (d.getSourceFile().isDeclarationFile || !this.files.includes(d.getSourceFile())) return null; + if (ts.isFunctionDeclaration(d)) { + const b = this.binding(d); + const saved = this.currentFn; + this.currentFn = null; + this.add(b, this.fnAtom(d, null)); + this.currentFn = saved; + return b; + } + if (ts.isClassDeclaration(d)) { + const b = this.binding(d); + this.add(b, this.classAtoms(d).cls); + return b; + } + if (ts.isVariableDeclaration(d) || ts.isBindingElement(d) || ts.isParameter(d)) + return this.binding(d); + if (ts.isSourceFile(d)) return this.namespaceNode(d); + return null; + } + + globalNode(name: string): number { + if (MODELED_GLOBALS.has(name)) return this.constNode(this.builtin(name)); + if (name === "NaN" || name === "Infinity") return this.constNode(this.prim("number")); + return this.constNode(this.top); + } + + enclosingNonArrow(): FunctionInfo | null { + const fn = this.currentFn; + if (fn === null) return null; + if (!fn.isArrow) return fn; + // An arrow's `arguments` is its enclosing function's. + let p: Node | undefined = fn.node.parent; + while (p !== undefined) { + if (ts.isFunctionLike(p) && !ts.isArrowFunction(p)) + return ( + this.functions.get(this.ctxKey(p)) ?? this.functions.get(`|${this.nodeKey(p)}`) ?? null + ); + p = p.parent; + } + return null; + } + + objectLiteral(e: ts.ObjectLiteralExpression): number { + const keys: string[] = []; + let keysKnown = true; + let hasAccessor = false; + let hasProto = false; + let hasComputedKey = false; + const spreads: number[] = []; + const inPackage = isPackageJsFile(e.getSourceFile()); + const label = (): string => + `{${keys.slice(0, 6).join(",")}${keys.length > 6 ? ",…" : ""}${keysKnown ? "" : ",…"}}@${locLabel(e)}`; + // keys first, so the label is complete + for (const p of list(e.properties)) { + if (ts.isSpreadAssignment(p)) { + keysKnown = false; + continue; + } + const name = (p as { name?: Node }).name; + const key = name !== undefined ? propertyNameText(name) : null; + if (key === null) { + hasComputedKey = true; + keysKnown = false; + continue; + } + if (key === "__proto__" && ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) + hasProto = true; + if (!keys.includes(key)) keys.push(key); + } + const atom = this.atoms.intern(`obj:${this.nodeKey(e)}`, () => ({ + kind: "object", + label: label(), + node: e, + })); + const n = this.newNode(); + this.add(n, atom); + for (const p of list(e.properties)) { + if (ts.isSpreadAssignment(p)) { + const src = this.expr(p.expression); + spreads.push(src); + this.copyFields(src, atom, p); + continue; + } + if (ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p)) { + hasAccessor = true; + const info = this.functionInfo(p, null); + this.activate(info); + this.add(info.thisNode, atom); + const key = propertyNameText(p.name); + if (key !== null && ts.isGetAccessorDeclaration(p)) + this.edge(info.ret, this.field(atom, key)); + continue; + } + const name = (p as { name?: Node }).name; + const key = name !== undefined ? propertyNameText(name) : null; + if (name !== undefined && ts.isComputedPropertyName(name)) this.expr(name.expression); + let value: number; + if (ts.isPropertyAssignment(p)) value = this.expr(p.initializer); + else if (ts.isShorthandPropertyAssignment(p)) { + const sym = this.checker.getShorthandAssignmentValueSymbol(p); + const d = sym?.valueDeclaration ?? sym?.declarations[0]; + const id = p.name as ts.Identifier; + value = d !== undefined ? (this.declValue(d) ?? this.identifier(id)) : this.identifier(id); + } else if (ts.isMethodDeclaration(p)) { + const fa = this.fnAtom(p, null); + this.add(this.infoOfAtom(fa)!.thisNode, atom); + value = this.constNode(fa); + } else continue; + if (key !== null) this.edge(value, this.field(atom, key)); + else this.edge(value, this.anyField(atom)); + } + if (!this.siteAtoms.has(atom) && this.siteAtoms.add(atom)) + this.sites.push({ + atom, + node: e, + kind: "object", + keys: keysKnown ? keys : null, + hasAccessor, + hasProto, + hasComputedKey, + spreads, + inPackage, + }); + return n; + } + + /** Values written under unknown keys. */ + anyField(atom: Atom): number { + return this.field(atom, "*"); + } + + copyFields(src: number, dstAtom: Atom, site: Node): void { + this.on(src, (a) => { + if (a.kind === "top") { + this.edge(this.constNode(this.top), this.anyField(dstAtom)); + return; + } + if (a.kind === "prim" || a.kind === "builtin") return; + if (a.kind === "array") { + this.edge(this.element(a), this.anyField(dstAtom)); + return; + } + // Copy every own field the source has, now and later. + const fm = this.fields.get(a); + const copied = new Set(); + const copy = (): void => { + for (const [name, id] of this.fields.get(a) ?? new Map()) { + if (copied.has(name)) continue; + copied.add(name); + this.edge(id, this.field(dstAtom, name)); + } + }; + copy(); + if (fm === undefined) this.fields.set(a, new Map()); + this.lateCopies.push(copy); + if (a.kind === "instance" || a.kind === "dictionary" || a.kind === "collection") + this.edge(this.constNode(this.top), this.anyField(dstAtom)); + void site; + }); + } + + /** Field copies re-run after solving rounds (fields created later). */ + private readonly lateCopies: (() => void)[] = []; + + arrayLiteral(e: ts.ArrayLiteralExpression): number { + const atom = this.atoms.intern(`arr:${this.nodeKey(e)}`, () => ({ + kind: "array", + label: `[]@${locLabel(e)}`, + node: e, + })); + const n = this.newNode(); + this.add(n, atom); + const el = this.element(atom); + let hasHole = false; + for (const item of list(e.elements)) { + if (ts.isOmittedExpression(item)) { + hasHole = true; + continue; + } + if (ts.isSpreadElement(item)) this.iterateInto(this.expr(item.expression), el, item); + else this.edge(this.expr(item), el); + } + if (!this.siteAtoms.has(atom) && this.siteAtoms.add(atom)) + this.sites.push({ + atom, + node: e, + kind: "array", + keys: null, + hasAccessor: false, + hasProto: hasHole, + hasComputedKey: false, + spreads: [], + inPackage: isPackageJsFile(e.getSourceFile()), + }); + return n; + } + + binary(e: ts.BinaryExpression): number { + const op = e.operatorToken.kind; + if (op === ts.SyntaxKind.EqualsToken) { + const value = this.expr(e.right); + this.assignTo(e.left, value); + return value; + } + if ( + op === ts.SyntaxKind.BarBarEqualsToken || + op === ts.SyntaxKind.QuestionQuestionEqualsToken || + op === ts.SyntaxKind.AmpersandAmpersandEqualsToken + ) { + const n = this.newNode(); + this.edge(this.expr(e.left), n); + const value = this.expr(e.right); + this.edge(value, n); + this.assignTo(e.left, value); + return n; + } + if (op >= ts.SyntaxKind.FirstCompoundAssignment && op <= ts.SyntaxKind.LastCompoundAssignment) { + this.expr(e.left); + this.expr(e.right); + const result = this.constNode( + op === ts.SyntaxKind.PlusEqualsToken ? this.top : this.prim("number"), + ); + if (op === ts.SyntaxKind.PlusEqualsToken) { + this.nodes[result]!.atoms.clear(); + this.add(result, this.prim("string")); + this.add(result, this.prim("number")); + } + this.assignTo(e.left, result); + return result; + } + if ( + op === ts.SyntaxKind.BarBarToken || + op === ts.SyntaxKind.QuestionQuestionToken || + op === ts.SyntaxKind.AmpersandAmpersandToken + ) { + const n = this.newNode(); + this.edge(this.expr(e.left), n); + this.edge(this.expr(e.right), n); + return n; + } + if (op === ts.SyntaxKind.CommaToken) { + this.expr(e.left); + return this.expr(e.right); + } + const l = this.expr(e.left); + const r = this.expr(e.right); + switch (op) { + case ts.SyntaxKind.EqualsEqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + case ts.SyntaxKind.EqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsToken: + case ts.SyntaxKind.LessThanToken: + case ts.SyntaxKind.LessThanEqualsToken: + case ts.SyntaxKind.GreaterThanToken: + case ts.SyntaxKind.GreaterThanEqualsToken: + case ts.SyntaxKind.InstanceOfKeyword: + case ts.SyntaxKind.InKeyword: + if (op === ts.SyntaxKind.InKeyword) + this.ops.push({ kind: "computedRead", recv: r, name: null, node: e }); + return this.constNode(this.prim("boolean")); + case ts.SyntaxKind.PlusToken: { + const n = this.newNode(); + const strOrNum = (a: Atom): void => { + this.add( + n, + a.kind === "prim" && a.label === "number" ? this.prim("number") : this.prim("string"), + ); + if ( + a.kind === "prim" && + (a.label === "boolean" || a.label === "null" || a.label === "undefined") + ) + this.add(n, this.prim("number")); + if (a.kind === "top") this.add(n, this.prim("number")); + }; + this.on(l, strOrNum); + this.on(r, strOrNum); + return n; + } + default: + return this.constNode(this.prim("number")); + } + } + + assignTo(target: ts.Expression, value: number): void { + const t = unparen(target); + if (ts.isIdentifier(t)) { + const decl = this.resolve(t); + if (decl === undefined || decl.getSourceFile().isDeclarationFile) { + this.sink(value, `global write at ${locLabel(t)}`); + return; + } + this.edge(value, this.binding(decl)); + return; + } + if (ts.isPropertyAccessExpression(t)) { + const recv = this.expr(t.expression); + this.storeConstraint(recv, t.name.text, value, t); + this.ops.push({ + kind: t.name.text === "length" ? "lengthWrite" : "write", + recv, + name: t.name.text, + node: t, + }); + return; + } + if (ts.isElementAccessExpression(t)) { + const recv = this.expr(t.expression); + const key = unparen(t.argumentExpression); + if (ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) { + this.storeConstraint(recv, key.text, value, t); + this.ops.push({ kind: "write", recv, name: key.text, node: t }); + } else { + this.expr(t.argumentExpression); + this.computedStore(recv, value, t); + this.ops.push({ kind: "computedWrite", recv, name: null, node: t }); + } + return; + } + if (ts.isObjectLiteralExpression(t)) { + for (const p of list(t.properties)) { + if (ts.isPropertyAssignment(p)) { + const key = propertyNameText(p.name); + const n = this.newNode(); + if (key !== null) this.loadConstraint(value, key, n, p); + else this.add(n, this.top); + this.assignTo(p.initializer, n); + } else if (ts.isShorthandPropertyAssignment(p)) { + const n = this.newNode(); + const id = p.name as ts.Identifier; + this.loadConstraint(value, id.text, n, p); + this.assignTo(id, n); + } + } + return; + } + if (ts.isArrayLiteralExpression(t)) { + for (const el of list(t.elements)) { + if (ts.isOmittedExpression(el)) continue; + const n = this.newNode(); + this.iterateInto(value, n, el); + this.assignTo(ts.isSpreadElement(el) ? el.expression : el, n); + } + } + } + + // ---- loads and stores ------------------------------------------------------------ + + loadConstraint(recv: number, name: string, dst: number, site: Node): void { + this.on(recv, (a) => this.loadAtom(a, name, dst, site)); + } + + loadAtom(a: Atom, name: string, dst: number, site: Node): void { + switch (a.kind) { + case "top": + this.add(dst, this.top); + return; + case "prim": + if (name === "length" && a.label === "string") this.add(dst, this.prim("number")); + else if (a.label === "string" || a.label === "number" || a.label === "boolean") + this.add(dst, this.builtin(`${a.label}.${name}`, a)); + return; + case "instance": { + const decl = a.node!; + if (name === "constructor") { + this.add(dst, this.classAtoms(decl).cls); + return; + } + const member = this.lookupMember(decl, name, false); + if (member?.kind === "method") + this.add( + dst, + this.fnAtom(member.method!, this.ownerOf(member.method!), this.receiverCtx(a)), + ); + if (member !== null && member.get !== undefined) { + const g = this.functionInfo(member.get, this.ownerOf(member.get), this.receiverCtx(a)); + this.activate(g); + this.add(g.thisNode, a); + this.edge(g.ret, dst); + this.edgeFor(site, g); + return; + } + if (this.classChain(decl).some((c) => this.baseClass(c) === undefined)) + this.add(dst, this.top); + this.edge(this.field(a, name), dst); + return; + } + case "class": { + const decl = a.node!; + if (name === "prototype") { + this.add(dst, this.top); + return; + } + if (name === "name") { + this.add(dst, this.prim("string")); + return; + } + const member = this.lookupMember(decl, name, true); + if (member?.kind === "method") + this.add( + dst, + this.fnAtom(member.method!, this.ownerOf(member.method!), this.receiverCtx(a)), + ); + if (member?.get !== undefined) { + const g = this.functionInfo(member.get, this.ownerOf(member.get), this.receiverCtx(a)); + this.activate(g); + this.add(g.thisNode, a); + this.edge(g.ret, dst); + return; + } + // static fields of the class and its bases + for (const c of this.classChain(decl)) { + const cls = this.classAtoms(c).cls; + this.edge(this.field(cls, name), dst); + } + return; + } + case "function": + if (name === "call" || name === "apply" || name === "bind") + this.add(dst, this.builtin(`function.${name}`, a)); + else if (name === "prototype") this.add(dst, this.top); + else if (name === "name") this.add(dst, this.prim("string")); + else if (name === "length") this.add(dst, this.prim("number")); + else this.edge(this.field(a, name), dst); + return; + case "array": + if (name === "length") this.add(dst, this.prim("number")); + else if (/^\d+$/.test(name)) this.edge(this.element(a), dst); + else if (ARRAY_METHODS.has(name)) this.add(dst, this.builtin(`array.${name}`, a)); + else this.edge(this.field(a, name), dst); + return; + case "object": + this.edge(this.field(a, name), dst); + this.edge(this.anyField(a), dst); + if (name === "constructor" || name === "hasOwnProperty" || name === "toString") + this.add(dst, this.builtin(`object.${name}`, a)); + return; + case "dictionary": + this.edge(this.anyField(a), dst); + this.edge(this.field(a, name), dst); + return; + case "promise": + if (name === "then" || name === "catch" || name === "finally") + this.add(dst, this.builtin(`promise.${name}`, a)); + return; + case "collection": + if (name === "size") this.add(dst, this.prim("number")); + else this.add(dst, this.builtin(`collection.${name}`, a)); + return; + case "builtin": + this.loadBuiltin(a, name, dst); + return; + } + void site; + } + + ownerOf(member: Node): Node | null { + const p = member.parent; + return p !== undefined && ts.isClassLikeDeclaration(p) ? p : null; + } + + loadBuiltin(a: Atom, name: string, dst: number): void { + const path = a.path ?? ""; + if ( + path === "Number" && + (name === "MAX_SAFE_INTEGER" || + name === "MIN_SAFE_INTEGER" || + name === "EPSILON" || + name === "MAX_VALUE" || + name === "MIN_VALUE" || + name === "POSITIVE_INFINITY" || + name === "NEGATIVE_INFINITY" || + name === "NaN") + ) + this.add(dst, this.prim("number")); + else if (path === "Math" && /^[A-Z0-9_]+$/.test(name)) this.add(dst, this.prim("number")); + else if (path === "Symbol") this.add(dst, this.prim("symbol")); + else if (path === "Error.instance" || path === "error") { + if (name === "message" || name === "stack" || name === "name") + this.add(dst, this.prim("string")); + else this.add(dst, this.top); + } else if ( + a.receiver === undefined && + MODELED_GLOBALS.has(path.split(".")[0]!) && + path.split(".").length < 4 + ) + this.add(dst, this.builtin(`${path}.${name}`)); + else if (path === "RegExp.instance") this.add(dst, this.builtin(`regexp.${name}`, a)); + else this.add(dst, this.top); + } + + storeConstraint(recv: number, name: string, value: number, site: Node): void { + this.on(recv, (a) => { + switch (a.kind) { + case "top": + this.noteTopWrite(name, site); + this.sink(value, `stored into ⊤.${name} at ${locLabel(site)}`); + return; + case "instance": { + const member = this.lookupMember(a.node!, name, false); + if (member?.set !== undefined) { + const s = this.functionInfo(member.set, this.ownerOf(member.set), this.receiverCtx(a)); + this.activate(s); + this.add(s.thisNode, a); + if (s.params[0] !== undefined) this.edge(value, s.params[0]); + return; + } + this.edge(value, this.field(a, name)); + return; + } + case "class": + case "function": + case "object": + case "dictionary": + this.edge(value, this.field(a, name)); + return; + case "array": + if (/^\d+$/.test(name)) this.edge(value, this.element(a)); + return; + case "prim": + return; + default: + this.sink(value, `stored into ${a.label} at ${locLabel(site)}`); + } + }); + } + + computedLoad(recv: number, dst: number, site: Node): void { + this.on(recv, (a) => { + switch (a.kind) { + case "top": + this.add(dst, this.top); + return; + case "array": + case "collection": + this.edge(this.element(a), dst); + return; + case "prim": + if (a.label === "string") this.add(dst, this.prim("string")); + return; + case "object": + case "dictionary": + case "instance": + case "class": + case "function": { + // every field, now and later + const seen = new Set(); + const copy = (): void => { + for (const [name, id] of this.fields.get(a) ?? new Map()) { + if (seen.has(name)) continue; + seen.add(name); + this.edge(id, dst); + } + }; + copy(); + this.lateCopies.push(copy); + if (a.kind === "instance" || a.kind === "class") this.add(dst, this.top); + return; + } + default: + this.add(dst, this.top); + } + void site; + }); + } + + computedStore(recv: number, value: number, site: Node): void { + this.on(recv, (a) => { + switch (a.kind) { + case "top": + this.noteTopWrite(null, site); + this.sink(value, `stored into ⊤[k] at ${locLabel(site)}`); + return; + case "array": + this.edge(value, this.element(a)); + return; + case "prim": + return; + default: + this.edge(value, this.anyField(a)); + } + }); + } + + // ---- calls --------------------------------------------------------------------------- + + args(e: ts.CallExpression | ts.NewExpression): { args: number[]; spread: boolean } { + const out: number[] = []; + let spread = false; + for (const a of list(e.arguments)) { + if (ts.isSpreadElement(a)) { + const inner = unparen(a.expression); + if (ts.isIdentifier(inner) && inner.text === "arguments") { + // `f(...arguments)`: forward the enclosing function's parameters. + const fn = this.enclosingNonArrow(); + if (fn !== null && out.length === 0) { + if (fn.argPos === null) fn.argPos = [0, 1, 2, 3, 4, 5].map(() => this.newNode()); + out.push(...fn.argPos); + continue; + } + } + spread = true; + out.push(this.expr(a.expression)); + continue; + } + out.push(this.expr(a)); + } + return { args: out, spread }; + } + + call(e: ts.CallExpression): number { + this.noteSite(e); + const callee = unparen(e.expression); + const dst = this.newNode(); + if (callee.kind === ts.SyntaxKind.SuperKeyword) { + const { args, spread } = this.args(e); + const fn = this.enclosingNonArrow(); + const owner = fn?.ownerClass ?? null; + const base = owner !== null ? this.baseClass(owner) : undefined; + if (fn !== null && base) { + this.on(fn.thisNode, (inst) => { + if (inst.kind === "instance") this.callConstructorChain(base, inst, args, spread, e); + }); + } else if (base === undefined) + for (const a of args) this.sink(a, `super(...) at ${locLabel(e)}`); + return dst; + } + if (ts.isPropertyAccessExpression(callee) || ts.isElementAccessExpression(callee)) { + const recvExpr = unparen(callee.expression); + const { args, spread } = this.args(e); + if (recvExpr.kind === ts.SyntaxKind.SuperKeyword && ts.isPropertyAccessExpression(callee)) { + // super.m(...): the base class member with the current receiver. + const fn = this.enclosingNonArrow(); + const owner = fn?.ownerClass ?? null; + const base = owner !== null ? this.baseClass(owner) : undefined; + if (fn !== null && base) { + const member = this.lookupMember(base, callee.name.text, isStatic(fn.node)); + if (member?.method !== undefined) { + this.callFunction( + this.functionInfo(member.method, this.ownerOf(member.method), fn.ctx), + fn.thisNode, + args, + spread, + dst, + e, + ); + } + } + return dst; + } + const recv = this.expr(recvExpr); + let name: string | null = null; + if (ts.isPropertyAccessExpression(callee)) name = callee.name.text; + else { + const k = unparen(callee.argumentExpression); + if (ts.isStringLiteral(k) || ts.isNoSubstitutionTemplateLiteral(k)) name = k.text; + else this.expr(callee.argumentExpression); + } + if (name !== null) this.ops.push({ kind: "read", recv, name, node: callee }); + // Per receiver atom: the member it resolves to runs with that atom as `this`. + const member = name; + this.on(recv, (a) => { + const fnNode = this.newNode(); + const self = this.constNode(a); + if (member !== null) this.loadAtom(a, member, fnNode, callee); + else this.computedLoad(self, fnNode, callee); + this.invoke(fnNode, self, args, spread, dst, e); + }); + return dst; + } + const calleeNode = this.expr(callee); + const { args, spread } = this.args(e); + this.invoke(calleeNode, null, args, spread, dst, e); + return dst; + } + + invoke( + callee: number, + recv: number | null, + args: number[], + spread: boolean, + dst: number, + site: ts.CallExpression, + ): void { + const undefNode = recv === null ? this.constNode(this.prim("undefined")) : recv; + this.on(callee, (a) => { + switch (a.kind) { + case "function": + this.callFunction(this.infoOfAtom(a)!, undefNode, args, spread, dst, site); + return; + case "builtin": + this.callBuiltin(a, args, spread, dst, site, recv); + return; + case "class": + return; // calling a class throws + case "prim": + return; + default: + for (const x of args) this.sink(x, `argument of ⊤ call at ${locLabel(site)}`); + if (recv !== null && a.kind === "top") + this.sink(recv, `receiver of ⊤ call at ${locLabel(site)}`); + this.add(dst, this.top); + } + }); + } + + newExpr(e: ts.NewExpression): number { + this.noteSite(e); + const dst = this.newNode(); + const callee = this.expr(e.expression); + const { args, spread } = this.args(e); + this.on(callee, (a) => { + switch (a.kind) { + case "class": + this.construct(a.node!, args, spread, dst, e); + return; + case "function": { + // An ES5 constructor function: one instance atom per function. + const inst = this.atoms.intern(`fninst:${this.nodeKey(a.node!)}`, () => ({ + kind: "object", + label: `new ${a.label}`, + node: a.node, + })); + this.add(dst, inst); + const info = this.infoOfAtom(a); + if (info !== undefined) { + this.activate(info); + this.add(info.thisNode, inst); + this.bindArgs(info, args, spread); + this.edge(info.ret, dst); + } + return; + } + case "builtin": + this.constructBuiltin(a.path ?? "", args, dst, e); + return; + default: + for (const x of args) this.sink(x, `argument of ⊤ constructor at ${locLabel(e)}`); + this.add(dst, this.top); + } + }); + return dst; + } + + constructBuiltin(path: string, args: number[], dst: number, site: Node): void { + switch (path) { + case "Map": + case "Set": + case "WeakMap": + case "WeakSet": { + const c = this.atoms.intern(`coll:${this.nodeKey(site)}`, () => ({ + kind: "collection", + label: `${path}@${locLabel(site)}`, + node: site, + })); + this.add(dst, c); + if (args[0] !== undefined) this.iterateInto(args[0], this.element(c), site); + return; + } + case "Error": + case "TypeError": + case "RangeError": + case "SyntaxError": + this.add(dst, this.builtin("Error.instance")); + return; + case "Promise": { + const p = this.promiseAtom(site, locLabel(site)); + this.add(this.field(p, "value"), this.top); + this.add(dst, p); + if (args[0] !== undefined) { + const resolver = this.constNode(this.builtin("promise.resolver")); + this.on(args[0], (f) => { + if (f.kind === "function") + this.callFunction( + this.infoOfAtom(f)!, + null, + [resolver, resolver], + false, + this.newNode(), + site, + ); + }); + } + return; + } + case "Date": + this.add(dst, this.builtin("Date.instance")); + return; + case "RegExp": + this.add(dst, this.builtin("RegExp.instance")); + return; + case "Array": { + this.add(dst, this.syntheticArray(site, "Array")); + return; + } + default: + for (const x of args) this.sink(x, `argument of new ${path} at ${locLabel(site)}`); + this.add(dst, this.top); + } + } + + /** Builtin call summaries; anything unmodeled makes its arguments escape. */ + callBuiltin( + a: Atom, + args: number[], + spread: boolean, + dst: number, + site: ts.CallExpression, + recvNode: number | null, + ): void { + const path = a.path ?? ""; + const recv = a.receiver; + const arg = (i: number): number => args[i] ?? this.constNode(this.prim("undefined")); + const prim = (p: string): void => this.add(dst, this.prim(p)); + const callback = (i: number, cbArgs: number[], ret: number | null): void => { + if (args[i] === undefined) return; + this.on(args[i]!, (f) => { + if (f.kind === "function") { + const saved = this.forceLoop; + this.forceLoop = LOOP_CALLBACK_METHODS.has(path.slice(path.indexOf(".") + 1)); + this.callFunction(this.infoOfAtom(f)!, null, cbArgs, false, ret ?? this.newNode(), site); + this.forceLoop = saved; + } else if (f.kind === "top" || f.kind === "builtin") { + for (const x of cbArgs) this.sink(x, `argument of ⊤ callback at ${locLabel(site)}`); + if (ret !== null) this.add(ret, this.top); + } + }); + }; + const escapeAll = (): void => { + for (const x of args) this.sink(x, `argument of ${path} at ${locLabel(site)}`); + this.add(dst, this.top); + }; + // Methods of builtin receivers. + if (recv !== undefined) { + const method = path.slice(path.indexOf(".") + 1); + if (path.startsWith("array.")) { + const el = this.element(recv); + const num = this.constNode(this.prim("number")); + const recvN = this.constNode(recv); + switch (method) { + case "push": + case "unshift": + for (const x of args) { + if (spread) this.iterateInto(x, el, site); + else this.edge(x, el); + } + prim("number"); + return; + case "pop": + case "shift": + case "at": + this.edge(el, dst); + this.add(dst, this.prim("undefined")); + return; + case "find": + case "findLast": + callback(0, [el, num, recvN], null); + this.edge(el, dst); + this.add(dst, this.prim("undefined")); + return; + case "forEach": + callback(0, [el, num, recvN], null); + prim("undefined"); + return; + case "some": + case "every": + case "includes": + callback(0, [el, num, recvN], null); + prim("boolean"); + return; + case "findIndex": + case "findLastIndex": + case "indexOf": + case "lastIndexOf": + callback(0, [el, num, recvN], null); + prim("number"); + return; + case "join": + case "toString": + prim("string"); + return; + case "map": + case "flatMap": { + const out = this.syntheticArray(site, method); + const outEl = this.element(out); + if (method === "map") callback(0, [el, num, recvN], outEl); + else { + const r = this.newNode(); + callback(0, [el, num, recvN], r); + this.iterateInto(r, outEl, site); + } + this.add(dst, out); + return; + } + case "filter": + case "slice": + case "concat": + case "reverse": + case "sort": + case "splice": + case "toSorted": + case "toReversed": + case "flat": { + if (method === "filter") callback(0, [el, num, recvN], null); + if (method === "sort" || method === "toSorted") callback(0, [el, el], null); + if (method === "reverse" || method === "sort") { + this.add(dst, recv); + return; + } + const out = this.syntheticArray(site, method); + this.edge(el, this.element(out)); + if (method === "concat") + for (const x of args) this.iterateInto(x, this.element(out), site); + if (method === "splice") for (const x of args.slice(2)) this.edge(x, el); + this.add(dst, out); + return; + } + case "reduce": + case "reduceRight": { + const acc = this.newNode(); + if (args[1] !== undefined) this.edge(args[1], acc); + else this.edge(el, acc); + callback(0, [acc, el, num, recvN], acc); + this.edge(acc, dst); + return; + } + case "keys": + case "values": + case "entries": { + const out = this.syntheticArray(site, method); + if (method === "values") this.edge(el, this.element(out)); + else if (method === "keys") this.add(this.element(out), this.prim("number")); + else { + const pair = this.syntheticArray(site, "entry"); + this.add(this.element(pair), this.prim("number")); + this.edge(el, this.element(pair)); + this.add(this.element(out), pair); + } + this.add(dst, out); + return; + } + case "fill": + for (const x of args) this.edge(x, el); + this.add(dst, recv); + return; + } + escapeAll(); + this.escape(recv, `array.${method} at ${locLabel(site)}`); + return; + } + if (path.startsWith("string.") || path.startsWith("number.") || path.startsWith("boolean.")) { + const r = STRING_METHOD_RESULTS.get(method); + if (r === "array") { + const out = this.syntheticArray(site, method); + this.add(this.element(out), this.prim("string")); + this.add(this.element(out), this.prim("undefined")); + this.add(dst, out); + this.add(dst, this.prim("null")); + } else if (r !== undefined) prim(r); + else if (method === "replace" || method === "replaceAll") prim("string"); + else escapeAll(); + if (method === "replace" || method === "replaceAll") + callback(1, [this.constNode(this.prim("string"))], null); + return; + } + if (path.startsWith("regexp.")) { + if (method === "test") prim("boolean"); + else if (method === "exec") { + const out = this.syntheticArray(site, "exec"); + this.add(this.element(out), this.prim("string")); + this.add(dst, out); + prim("null"); + } else escapeAll(); + return; + } + if (path.startsWith("promise.")) { + const out = this.promiseAtom(site, locLabel(site)); + const value = this.field(recv, "value"); + const ret = this.newNode(); + if (method === "then") { + callback(0, [value], ret); + callback(1, [this.constNode(this.top)], ret); + } else if (method === "catch") { + callback(0, [this.constNode(this.top)], ret); + this.edge(value, ret); + } else { + callback(0, [], null); + this.edge(value, ret); + } + this.awaitInto(ret, this.field(out, "value")); + this.add(dst, out); + return; + } + if (path.startsWith("collection.")) { + const el = this.element(recv); + switch (method) { + case "add": + case "set": + for (const x of args) this.edge(x, el); + this.add(dst, recv); + return; + case "get": + this.edge(el, dst); + prim("undefined"); + return; + case "has": + case "delete": + prim("boolean"); + return; + case "clear": + prim("undefined"); + return; + case "forEach": + callback(0, [el, el], null); + prim("undefined"); + return; + case "keys": + case "values": + case "entries": { + const out = this.syntheticArray(site, method); + this.edge(el, this.element(out)); + if (method === "entries") { + const pair = this.syntheticArray(site, "entry"); + this.edge(el, this.element(pair)); + this.nodes[this.element(out)]!.atoms.clear(); + this.add(this.element(out), pair); + } + this.add(dst, out); + return; + } + } + escapeAll(); + return; + } + if (path.startsWith("function.")) { + if (recv.kind !== "function" || recv.node === undefined) { + escapeAll(); + return; + } + if (method === "bind") { + const info = this.infoOfAtom(recv); + if (info !== undefined && args[0] !== undefined && !info.isArrow) + this.edge(args[0], info.thisNode); + if (info !== undefined && args.length > 1) { + this.activate(info); + for (const p of info.params) this.add(p, this.top); + for (const x of args.slice(1)) this.sink(x, `bound argument at ${locLabel(site)}`); + } + this.add(dst, recv); + return; + } + if (method === "call") { + this.callFunction( + this.infoOfAtom(recv)!, + args[0] ?? this.constNode(this.prim("undefined")), + args.slice(1), + spread, + dst, + site, + ); + return; + } + // apply: arguments from an array + const info = this.infoOfAtom(recv); + if (info !== undefined) { + this.activate(info); + if (args[0] !== undefined && !info.isArrow) this.edge(args[0], info.thisNode); + for (const p of info.params) + if (args[1] !== undefined) this.iterateInto(args[1], p, site); + this.edge(info.ret, dst); + } + return; + } + if (path.startsWith("object.")) { + if (method === "hasOwnProperty") prim("boolean"); + else if (method === "toString") prim("string"); + else escapeAll(); + return; + } + escapeAll(); + return; + } + // `Object.prototype.m.call(o, …)`-style calls of builtin prototype methods. + if (/^(Object|Array|String|Function)\.prototype\.\w+\.(call|apply)$/.test(path)) { + const m = path.split(".")[2]!; + if (m === "hasOwnProperty" || m === "propertyIsEnumerable" || m === "isPrototypeOf") + prim("boolean"); + else if (m === "toString") prim("string"); + else escapeAll(); + return; + } + switch (path) { + case "Object.keys": + case "Object.getOwnPropertyNames": { + const out = this.syntheticArray(site, "keys"); + this.add(this.element(out), this.prim("string")); + this.add(dst, out); + return; + } + case "Object.values": + case "Object.entries": { + const out = this.syntheticArray(site, path === "Object.values" ? "values" : "entries"); + const vals = this.newNode(); + if (args[0] !== undefined) this.computedLoad(args[0], vals, site); + if (path === "Object.values") this.edge(vals, this.element(out)); + else { + const pair = this.syntheticArray(site, "entry"); + this.add(this.element(pair), this.prim("string")); + this.edge(vals, this.element(pair)); + this.add(this.element(out), pair); + } + this.add(dst, out); + return; + } + case "Object.freeze": + case "Object.seal": + case "Object.preventExtensions": + this.ops.push({ + kind: path === "Object.freeze" ? "freeze" : "seal", + recv: arg(0), + name: null, + node: site, + }); + this.edge(arg(0), dst); + return; + case "Object.isFrozen": + case "Object.isSealed": + case "Object.isExtensible": + case "Array.isArray": + case "Number.isInteger": + case "Number.isNaN": + case "Number.isFinite": + case "Number.isSafeInteger": + case "isNaN": + case "isFinite": + prim("boolean"); + return; + case "Object.assign": { + this.ops.push({ kind: "assign", recv: arg(0), name: null, node: site }); + this.on(arg(0), (t) => { + for (const s of args.slice(1)) this.copyFields(s, t, site); + if (t.kind === "top") + for (const s of args.slice(1)) + this.sink(s, `Object.assign into ⊤ at ${locLabel(site)}`); + }); + this.edge(arg(0), dst); + return; + } + case "Object.defineProperty": + case "Object.defineProperties": + case "Reflect.defineProperty": + this.ops.push({ kind: "defineProperty", recv: arg(0), name: null, node: site }); + this.sink(arg(2), `property descriptor at ${locLabel(site)}`); + this.sink(arg(1), `property descriptor at ${locLabel(site)}`); + this.edge(arg(0), dst); + return; + case "Object.setPrototypeOf": + case "Reflect.setPrototypeOf": + this.ops.push({ kind: "setPrototypeOf", recv: arg(0), name: null, node: site }); + this.sink(arg(1), `prototype at ${locLabel(site)}`); + this.edge(arg(0), dst); + return; + case "Object.create": { + const d = this.atoms.intern(`dict:${this.nodeKey(site)}`, () => ({ + kind: "dictionary", + label: `Object.create@${locLabel(site)}`, + node: site, + })); + this.add(dst, d); + return; + } + case "Object.fromEntries": { + const d = this.atoms.intern(`dict:${this.nodeKey(site)}`, () => ({ + kind: "dictionary", + label: `fromEntries@${locLabel(site)}`, + node: site, + })); + const pairs = this.newNode(); + if (args[0] !== undefined) this.iterateInto(args[0], pairs, site); + const vals = this.newNode(); + this.iterateInto(pairs, vals, site); + this.edge(vals, this.anyField(d)); + this.add(dst, d); + return; + } + case "Object.getPrototypeOf": + case "Reflect.getPrototypeOf": + this.add(dst, this.top); + return; + case "Object.is": + prim("boolean"); + return; + case "Array.from": + case "Array.of": { + const out = this.syntheticArray(site, path); + if (path === "Array.of") for (const x of args) this.edge(x, this.element(out)); + else if (args[1] !== undefined) { + const els = this.newNode(); + if (args[0] !== undefined) this.iterateInto(args[0], els, site); + callback(1, [els, this.constNode(this.prim("number"))], this.element(out)); + } else if (args[0] !== undefined) this.iterateInto(args[0], this.element(out), site); + this.add(dst, out); + return; + } + case "JSON.parse": + this.add(dst, this.top); + return; + case "JSON.stringify": + prim("string"); + prim("undefined"); + return; + case "String": + case "encodeURIComponent": + case "decodeURIComponent": + case "encodeURI": + case "decodeURI": + prim("string"); + return; + case "Number": + case "parseInt": + case "parseFloat": + case "Number.parseInt": + case "Number.parseFloat": + prim("number"); + return; + case "Boolean": + prim("boolean"); + return; + case "Symbol": + case "Symbol.for": + prim("symbol"); + return; + case "BigInt": + prim("bigint"); + return; + case "Promise.resolve": { + const p = this.promiseAtom(site, locLabel(site)); + if (args[0] !== undefined) this.awaitInto(args[0], this.field(p, "value")); + else this.add(this.field(p, "value"), this.prim("undefined")); + this.add(dst, p); + return; + } + case "Promise.reject": { + const p = this.promiseAtom(site, locLabel(site)); + this.add(dst, p); + if (args[0] !== undefined) this.sink(args[0], `rejection at ${locLabel(site)}`); + return; + } + case "Promise.all": + case "Promise.allSettled": + case "Promise.race": + case "Promise.any": { + const p = this.promiseAtom(site, locLabel(site)); + const items = this.newNode(); + if (args[0] !== undefined) this.iterateInto(args[0], items, site); + if (path === "Promise.all") { + const out = this.syntheticArray(site, "all"); + this.awaitInto(items, this.element(out)); + this.add(this.field(p, "value"), out); + } else if (path === "Promise.race" || path === "Promise.any") + this.awaitInto(items, this.field(p, "value")); + else this.add(this.field(p, "value"), this.top); + this.add(dst, p); + return; + } + case "promise.resolver": + for (const x of args) this.sink(x, `promise resolution at ${locLabel(site)}`); + return; + } + if (path.startsWith("console.")) { + prim("undefined"); + return; + } + if (path.startsWith("Math.")) { + prim("number"); + return; + } + if (path === "Date.now") { + prim("number"); + return; + } + if (path.startsWith("Date.instance") || path === "Error.instance") { + escapeAll(); + return; + } + void recvNode; + escapeAll(); + } + + /** Re-run deferred field copies until nothing new flows. */ + finish(): void { + for (;;) { + const before = this.steps; + for (const c of this.lateCopies) c(); + this.solve(); + if (this.steps === before || this.budgetExceeded) return; + } + } +} diff --git a/packages/compiler/src/frontend/shape-inference/domain.ts b/packages/compiler/src/frontend/shape-inference/domain.ts new file mode 100644 index 000000000..36978d4c9 --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/domain.ts @@ -0,0 +1,153 @@ +/* Shape inference (results/design-shape-inference/DESIGN.md), stage S1: + * the abstract domain of the allocation-site points-to analysis. + * + * An abstract value is a set of atoms. An atom names a set of runtime + * values: a primitive kind, an allocation site (object or array literal, or + * a value a builtin creates at one call site), the instances of one class, + * one class value, one function (closure site), a builtin object, or ⊤ — + * every value the analysis does not track. Value nodes hold atom sets and + * flow into each other along inclusion edges; the solver (analyze.ts) + * attaches load, store and call constraints that fire as atoms arrive. */ +import type * as ts from "../ts7/adapter.js"; + +export type AtomKind = + | "prim" + | "top" + | "object" + | "array" + | "instance" + | "class" + | "function" + | "builtin" + | "promise" + | "collection" + | "dictionary"; + +export interface Atom { + readonly id: number; + readonly kind: AtomKind; + /** Short display name, e.g. `string`, `ZodString`, `{status,value}@types.js:804`. */ + readonly label: string; + /** The syntax that creates the atom: the literal, class or function + * declaration, or the builtin call. Undefined for prims, ⊤ and builtins. */ + readonly node?: ts.Node | undefined; + /** builtin atoms: the dotted path (`Object.keys`, `array.push`). */ + readonly path?: string | undefined; + /** method atoms of builtin receivers: the receiver atom. */ + readonly receiver?: Atom | undefined; + /** function atoms: the analysis context of the function's clone (a + * method's receiver class), "" for the context-insensitive default. */ + readonly ctx?: string | undefined; +} + +export class AtomTable { + readonly atoms: Atom[] = []; + private readonly byKey = new Map(); + + intern(key: string, make: (id: number) => Omit): Atom { + let atom = this.byKey.get(key); + if (atom === undefined) { + const id = this.atoms.length; + atom = { id, ...make(id) }; + this.atoms.push(atom); + this.byKey.set(key, atom); + } + return atom; + } +} + +/** Object.prototype members: a read of one of these names on a plain + * object resolves on the prototype, never to an absent own key. */ +export const OBJECT_PROTOTYPE_MEMBERS: ReadonlySet = new Set([ + "constructor", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "toLocaleString", + "toString", + "valueOf", + "__defineGetter__", + "__defineSetter__", + "__lookupGetter__", + "__lookupSetter__", + "__proto__", +]); + +/** Global names the analysis models as builtin objects. Any other global is ⊤. */ +export const MODELED_GLOBALS: ReadonlySet = new Set([ + "Object", + "Array", + "JSON", + "Math", + "Number", + "String", + "Boolean", + "Symbol", + "Promise", + "Map", + "Set", + "WeakMap", + "WeakSet", + "Error", + "TypeError", + "RangeError", + "SyntaxError", + "Date", + "RegExp", + "console", + "Reflect", + "parseInt", + "parseFloat", + "isNaN", + "isFinite", + "encodeURIComponent", + "decodeURIComponent", + "encodeURI", + "decodeURI", + "BigInt", +]); + +/** Results of string (and number) methods, by name. Anything else is ⊤. */ +type MethodResult = "string" | "number" | "boolean" | "array"; +export const STRING_METHOD_RESULTS: ReadonlyMap = new Map< + string, + MethodResult +>([ + ["at", "string"], + ["charAt", "string"], + ["charCodeAt", "number"], + ["codePointAt", "number"], + ["concat", "string"], + ["endsWith", "boolean"], + ["includes", "boolean"], + ["indexOf", "number"], + ["lastIndexOf", "number"], + ["localeCompare", "number"], + ["match", "array"], + ["matchAll", "array"], + ["normalize", "string"], + ["padEnd", "string"], + ["padStart", "string"], + ["repeat", "string"], + ["replace", "string"], + ["replaceAll", "string"], + ["search", "number"], + ["slice", "string"], + ["split", "array"], + ["startsWith", "boolean"], + ["substring", "string"], + ["substr", "string"], + ["toLowerCase", "string"], + ["toUpperCase", "string"], + ["toLocaleLowerCase", "string"], + ["toLocaleUpperCase", "string"], + ["toString", "string"], + ["trim", "string"], + ["trimEnd", "string"], + ["trimStart", "string"], + ["valueOf", "string"], + ["test", "boolean"], + ["toFixed", "string"], + ["toPrecision", "string"], + ["toExponential", "string"], +]); diff --git a/packages/compiler/src/frontend/shape-inference/index.ts b/packages/compiler/src/frontend/shape-inference/index.ts new file mode 100644 index 000000000..88c2100ee --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/index.ts @@ -0,0 +1,21 @@ +/* Shape inference for untyped npm JavaScript + * (results/design-shape-inference/DESIGN.md). Stage S1: report only. */ +import type * as ts from "../ts7/adapter.js"; +import { analyzeShapes } from "./analyze.js"; +import { buildShapeReport } from "./report.js"; + +export { analyzeShapes } from "./analyze.js"; +export { buildShapeReport } from "./report.js"; + +/** SCRIPTC_DEBUG_SHAPES: `1` reports package code in hot functions, + * `verbose` every package site, `all` user files too, any other value only + * files whose path contains it. Writes to stderr; changes nothing else. */ +export function reportShapes(program: ts.Program, filter: string): void { + const start = performance.now(); + const analysis = analyzeShapes(program, program.getTypeChecker()); + const report = buildShapeReport(analysis, filter); + const ms = (performance.now() - start).toFixed(0); + process.stderr.write( + `[shapes] ${ms} ms\n${report.lines.map((l) => `[shapes] ${l}`).join("\n")}\n`, + ); +} diff --git a/packages/compiler/src/frontend/shape-inference/report.ts b/packages/compiler/src/frontend/shape-inference/report.ts new file mode 100644 index 000000000..c18d9eb56 --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/report.ts @@ -0,0 +1,374 @@ +/* Shape inference stage S1: the plan the analysis would hand the lowering, + * as a report (DESIGN.md §4.5, §5.5). Nothing here changes compilation. + * + * Per allocation site of package code, the verdict: + * P local — a native record would be exact: no reflection, no added + * keys, no computed writes, and its objects never escape; + * P escaping — exact as a record behind identity-keeping capsules: it + * escapes, but the census leaves every later operation + * answerable (fields untyped code may write become dyn); + * G — keeps today's dynamic object, with the rule that decided. + * Plus class field types, the census, and the hot functions' value types. */ +import * as ts from "../ts7/adapter.js"; +import { OBJECT_PROTOTYPE_MEMBERS, type Atom } from "./domain.js"; +import { locLabel, type FunctionInfo, type LiteralSite, type ShapeAnalysis } from "./analyze.js"; + +export interface SiteVerdict { + readonly site: LiteralSite; + readonly label: string; + readonly verdict: "P-local" | "P-escaping" | "G"; + readonly reasons: string[]; + /** P-escaping: fields untyped code may write (they become dyn slots). */ + readonly writableByTop: string[]; + readonly freezable: boolean; + readonly brand: boolean; +} + +export interface ShapeReport { + readonly sites: SiteVerdict[]; + readonly hot: Set; + readonly lines: string[]; +} + +const MAX_ATOMS_SHOWN = 6; + +function fieldKeys(fields: ReadonlyMap | undefined): string[] { + return fields === undefined ? [] : [...fields.keys()]; +} + +function fieldEntries(fields: ReadonlyMap | undefined): [string, number][] { + return fields === undefined ? [] : [...fields.entries()]; +} + +export function describeValue(analysis: ShapeAnalysis, node: number): string { + const atoms = [...analysis.nodes[node]!.atoms]; + if (atoms.length === 0) return "∅"; + const labels = [...new Set(atoms.map((a) => a.label))]; + labels.sort((a, b) => (a === "⊤" ? 1 : b === "⊤" ? -1 : a.localeCompare(b))); + const shown = labels.slice(0, MAX_ATOMS_SHOWN).join(" | "); + if (labels.length <= MAX_ATOMS_SHOWN) return shown; + return `${shown} | +${labels.length - MAX_ATOMS_SHOWN}${labels.includes("⊤") ? " | ⊤" : ""}`; +} + +function atomsOf(analysis: ShapeAnalysis, node: number): Set { + return analysis.nodes[node]!.atoms; +} + +/** Functions reached from call sites in loops (or loop callbacks), and + * everything they call: the static hotness heuristic (DESIGN.md §8.1). */ +export function hotFunctions(analysis: ShapeAnalysis): Set { + const hot = new Set(); + const callees = new Map(); + const work: FunctionInfo[] = []; + for (const c of analysis.calls) { + let l = callees.get(c.caller); + if (l === undefined) callees.set(c.caller, (l = [])); + l.push(c.callee); + if (c.inLoop && !hot.has(c.callee)) { + hot.add(c.callee); + work.push(c.callee); + } + } + while (work.length > 0) { + const f = work.pop()!; + for (const g of callees.get(f) ?? []) { + if (!hot.has(g)) { + hot.add(g); + work.push(g); + } + } + } + return hot; +} + +export function buildShapeReport(analysis: ShapeAnalysis, filter: string): ShapeReport { + const lines: string[] = []; + const all = filter === "all" || filter === "verbose"; + const verbose = filter === "verbose"; + const fileFilter = filter !== "1" && !all ? filter : null; + const wanted = (n: ts.Node, inPackage: boolean): boolean => { + if (fileFilter !== null) return n.getSourceFile().fileName.includes(fileFilter); + return all || inPackage; + }; + + // Census facts: operations whose receiver may be ⊤ reach every escaped atom. + + // Index ops by the site atoms they can reach directly. + const byAtom = new Map(); + const topOps: typeof analysis.ops = []; + for (const o of analysis.ops) { + const atoms = atomsOf(analysis, o.recv); + for (const a of atoms) { + if (a.kind !== "object" && a.kind !== "array") continue; + let l = byAtom.get(a); + if (l === undefined) byAtom.set(a, (l = [])); + l.push(o); + } + if (atoms.has(analysis.top)) topOps.push(o); + } + + // Key-order groups: sites with the same key set and different orders need brands. + const orders = new Map>(); + for (const s of analysis.sites) { + if (s.kind !== "object" || s.keys === null) continue; + const set = [...s.keys].sort().join(","); + let o = orders.get(set); + if (o === undefined) orders.set(set, (o = new Set())); + o.add(s.keys.join(",")); + } + + const verdicts: SiteVerdict[] = []; + for (const s of analysis.sites) { + if (!s.inPackage && !all && fileFilter === null) continue; + const reasons: string[] = []; + const escapedWhy = analysis.escaped.get(s.atom); + const escapes = escapedWhy !== undefined; + if (s.hasAccessor) reasons.push("accessor in literal"); + if (s.hasProto) reasons.push(s.kind === "array" ? "hole in literal" : "__proto__ in literal"); + if (s.hasComputedKey) reasons.push("computed key in literal"); + for (const sp of s.spreads) + if (atomsOf(analysis, sp).has(analysis.top)) reasons.push("spread of ⊤"); + const direct = byAtom.get(s.atom) ?? []; + const ops = escapes ? [...direct, ...topOps] : direct; + const keys = new Set(s.keys ?? []); + const fieldsOf = analysis.fields.get(s.atom); + const knownKeys = + s.keys !== null ? keys : new Set([...fieldKeys(fieldsOf)].filter((k) => k !== "*")); + let freezable = false; + const seen = new Set(); + const note = (r: string): void => { + if (!seen.has(r)) { + seen.add(r); + reasons.push(r); + } + }; + for (const o of ops) { + const viaTop = !atomsOf(analysis, o.recv).has(s.atom); + const where = `${locLabel(o.node)}${viaTop ? " via ⊤" : ""}`; + switch (o.kind) { + case "delete": + case "defineProperty": + case "setPrototypeOf": + case "seal": + case "proxy": + note(`${o.kind} at ${where}`); + break; + case "assign": + note(`Object.assign target at ${where}`); + break; + case "freeze": + freezable = true; + break; + case "computedWrite": + if (!viaTop) + note(s.kind === "array" ? `index write at ${where}` : `computed write at ${where}`); + break; + case "lengthWrite": + if (s.kind === "array") note(`length write at ${where}`); + break; + case "write": + if (s.kind === "object" && !viaTop && o.name !== null && !knownKeys.has(o.name)) + note(`adds key '${o.name}' at ${where}`); + break; + case "read": + if ( + s.kind === "object" && + !underPromiseGuard(o.node) && + escapes && + o.name !== null && + !viaTop && + !knownKeys.has(o.name) && + !OBJECT_PROTOTYPE_MEMBERS.has(o.name) + ) + note(`reads absent key '${o.name}' at ${where} (escapes)`); + break; + default: + break; + } + } + let brand = false; + if (s.kind === "object" && s.keys !== null) { + const o = orders.get([...s.keys].sort().join(",")); + if (o !== undefined && o.size > 1) brand = true; + } + const writableByTop: string[] = []; + if (escapes) { + if (analysis.topComputedWrites.length > 0) writableByTop.push("*"); + else for (const k of knownKeys) if (analysis.topWriteNames.has(k)) writableByTop.push(k); + } + const verdict: SiteVerdict["verdict"] = + reasons.length > 0 ? "G" : escapes ? "P-escaping" : "P-local"; + const label = s.atom.label; + if (escapes) reasons.push(`escapes: ${escapedWhy}`); + verdicts.push({ site: s, label, verdict, reasons, writableByTop, freezable, brand }); + } + + const hot = hotFunctions(analysis); + const hotNodes = new Set([...hot].map((f) => f.node)); + + // ---- print ---- + const pkgFns = [...analysis.functions.values()].filter((f) => f.inPackage); + lines.push( + `analyzed ${analysis.files.length} files (${analysis.packageFiles} package JS), ${analysis.functions.size} functions (${pkgFns.length} in packages), ${analysis.atoms.atoms.length} atoms, ${analysis.nodes.length} value nodes, ${analysis.steps} steps${analysis.budgetExceeded ? " — BUDGET EXCEEDED, results incomplete" : ""}`, + ); + const topNames = [...analysis.topWriteNames.entries()].map( + ([n, node]) => `${n} (${locLabel(node)})`, + ); + lines.push( + `census: writes on ⊤ receivers: ${topNames.length === 0 ? "none" : topNames.slice(0, 20).join(", ")}${topNames.length > 20 ? ` +${topNames.length - 20}` : ""}`, + ); + lines.push( + `census: computed writes on ⊤ receivers: ${analysis.topComputedWrites.length === 0 ? "none" : analysis.topComputedWrites.slice(0, 8).map(locLabel).join(", ")}`, + ); + const topReflect = topOps.filter( + (o) => + o.kind !== "read" && + o.kind !== "computedRead" && + o.kind !== "write" && + o.kind !== "computedWrite", + ); + lines.push( + `census: reflection on ⊤ receivers: ${topReflect.length === 0 ? "none" : topReflect.map((o) => `${o.kind} (${locLabel(o.node)})`).join(", ")}`, + ); + const escapedPkg = [...analysis.escaped.keys()].filter( + (a) => + a.node !== undefined && + wanted(a.node, a.node.getSourceFile().fileName.includes("/node_modules/")), + ); + lines.push( + `escaped atoms: ${analysis.escaped.size} (${escapedPkg.length} shown below where relevant)`, + ); + const firstEscapes = [...analysis.escaped.entries()] + .filter(([, why]) => !why.startsWith("via ")) + .slice(0, 12); + for (const [atom, why] of firstEscapes) lines.push(` escape root: ${atom.label}: ${why}`); + + const counts = { "P-local": 0, "P-escaping": 0, G: 0 }; + const hotCounts = { "P-local": 0, "P-escaping": 0, G: 0 }; + const inHot = (n: ts.Node): boolean => { + let p: ts.Node | undefined = n.parent; + while (p !== undefined) { + if (hotNodes.has(p)) return true; + p = p.parent; + } + return false; + }; + for (const v of verdicts) { + counts[v.verdict]++; + if (inHot(v.site.node)) hotCounts[v.verdict]++; + } + lines.push( + `sites: ${verdicts.length} (P local ${counts["P-local"]}, P escaping ${counts["P-escaping"]}, G ${counts.G}); in hot functions: P local ${hotCounts["P-local"]}, P escaping ${hotCounts["P-escaping"]}, G ${hotCounts.G}`, + ); + for (const v of verdicts) { + if (!wanted(v.site.node, v.site.inPackage)) continue; + if (!verbose && !inHot(v.site.node)) continue; + const extra: string[] = []; + if (v.brand) extra.push("needs brand (key order)"); + if (v.freezable) extra.push("freezable"); + if (v.writableByTop.length > 0) extra.push(`⊤-writable: ${v.writableByTop.join(",")}`); + const fields = analysis.fields.get(v.site.atom); + const ftypes = + v.site.kind === "array" + ? `elem: ${analysis.elements.has(v.site.atom) ? describeValue(analysis, analysis.elements.get(v.site.atom)!) : "∅"}` + : fieldEntries(fields) + .filter(([k]) => k !== "*") + .map(([k, id]) => `${k}: ${describeValue(analysis, id)}`) + .join("; "); + lines.push( + `site ${v.label}${inHot(v.site.node) ? " [hot]" : ""}: ${v.verdict}${v.reasons.length > 0 ? ` — ${v.reasons.join("; ")}` : ""}${extra.length > 0 ? ` [${extra.join("; ")}]` : ""}`, + ); + if (ftypes.length > 0) lines.push(` ${ftypes}`); + } + + // Class fields. + for (const atom of analysis.atoms.atoms) { + if (atom.kind !== "instance" || atom.node === undefined) continue; + const inPackage = atom.node.getSourceFile().fileName.includes("/node_modules/"); + if (!wanted(atom.node, inPackage)) continue; + const fm = analysis.fields.get(atom); + if (fm === undefined || fm.size === 0) continue; + const esc = analysis.escaped.get(atom); + const parts = [...fm.entries()].map(([k, id]) => { + const w = + esc !== undefined && + !k.startsWith("#") && + (analysis.topComputedWrites.length > 0 || analysis.topWriteNames.has(k)) + ? " (⊤-writable)" + : ""; + return `${k}: ${describeValue(analysis, id)}${w}`; + }); + lines.push( + `class ${atom.label}${esc !== undefined ? ` (escapes: ${esc})` : ""}: ${parts.join("; ")}`, + ); + } + + // Hot functions. + const hotPkg = [...hot].filter((info) => wanted(info.node, info.inPackage)); + lines.push(`hot functions: ${hotPkg.length} (clones per receiver class counted separately)`); + for (const info of hotPkg) { + const n = info.node; + const params = info.params.map((p, i) => `p${i}=${describeValue(analysis, p)}`).join(", "); + const flags = [ + info.usesArguments ? "uses arguments" : "", + analysis.escaped.size > 0 && [...analysis.escaped.keys()].some((a) => a.node === n) + ? "escapes" + : "", + ].filter((x) => x !== ""); + lines.push( + `fn ${info.label}${flags.length > 0 ? ` [${flags.join(", ")}]` : ""}: this=${describeValue(analysis, info.thisNode)}${params.length > 0 ? `, ${params}` : ""}, ret=${describeValue(analysis, info.ret)}`, + ); + } + const why = process.env["SCRIPTC_DEBUG_SHAPES_WHY"]; + if (why !== undefined) { + for (const info of analysis.functions.values()) { + if (!info.label.startsWith(why)) continue; + for (const [what, node] of [ + ["ret", info.ret], + ...info.params.map((p, i) => [`p${i}`, p] as const), + ] as const) { + if (!analysis.nodes[node]!.atoms.has(analysis.top)) continue; + lines.push(`why ⊤ in ${info.label} ${what}:`); + let cur: number | string | undefined = node; + for (let i = 0; i < 40 && typeof cur === "number"; i++) { + lines.push(` ${analysis.nodeLabel.get(cur) ?? `node ${cur}`}`); + cur = analysis.topCause.get(cur); + } + if (typeof cur === "string") lines.push(` seed: ${cur}`); + } + } + } + return { sites: verdicts, hot, lines }; +} + +/** A read inside the true arm of a promise test (`isAsync(x)`, + * `x instanceof Promise`): flow-sensitive narrowing removes the record + * arms there. Stage S1's locals are flow-insensitive, so the report + * recognizes the guard syntactically. */ +function underPromiseGuard(node: ts.Node): boolean { + let child: ts.Node = node; + let p = node.parent; + while (p !== undefined && !ts.isFunctionLike(p)) { + let cond: ts.Expression | undefined; + if (ts.isIfStatement(p) && p.thenStatement === child) cond = p.expression; + if (ts.isConditionalExpression(p) && p.whenTrue === child) cond = p.condition; + if (cond !== undefined && isPromiseTest(cond)) return true; + child = p; + p = p.parent; + } + return false; +} + +function isPromiseTest(e: ts.Expression): boolean { + if (ts.isParenthesizedExpression(e)) return isPromiseTest(e.expression); + if (ts.isBinaryExpression(e)) { + if (e.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) + return ts.isIdentifier(e.right) && e.right.text === "Promise"; + if (e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) + return isPromiseTest(e.left) || isPromiseTest(e.right); + return false; + } + if (ts.isCallExpression(e) && ts.isIdentifier(e.expression)) + return /^is(Async|Promise|Thenable)$/.test(e.expression.text); + return false; +} diff --git a/packages/compiler/test/shape-inference.test.ts b/packages/compiler/test/shape-inference.test.ts new file mode 100644 index 000000000..3cd6e809d --- /dev/null +++ b/packages/compiler/test/shape-inference.test.ts @@ -0,0 +1,166 @@ +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, test, vi } from "vitest"; +import { compile } from "../src/index.js"; + +// A package shaped like a validation library: schema classes with a virtual +// `_parse`, per-field context and result literals, a class holding the +// context, and an output object built with computed keys. +const packageSource = ` +export class Status { + constructor() { this.value = "valid"; } + dirty() { if (this.value === "valid") this.value = "dirty"; } +} +class LazyPath { + constructor(parent, data, key) { this.parent = parent; this.data = data; this.key = key; } + get path() { return [this.key]; } +} +export class Base { + constructor(def) { this._def = def; } + ctxOf(input) { return { common: input.parent.common, data: input.data, parent: input.parent }; } + parse(data) { + const root = { common: { issues: [] }, data, parent: null }; + const result = this._parse({ data, parent: root }); + return result.value; + } +} +export class Str extends Base { + _parse(input) { + const status = new Status(); + if (typeof input.data !== "string") status.dirty(); + return { status: status.value, value: input.data }; + } +} +export class Obj extends Base { + _parse(input) { + const ctx = this.ctxOf(input); + const pairs = []; + for (const key of Object.keys(this._def.shape)) { + const field = this._def.shape[key]; + pairs.push({ key, value: field._parse(new LazyPath(ctx, ctx.data[key], key)) }); + } + const out = {}; + for (const pair of pairs) out[pair.key] = pair.value.value; + return { status: "valid", value: out }; + } +} +export const str = () => new Str({ checks: [] }); +export const obj = (shape) => new Obj({ shape }); +export function leak(value) { globalThis.leaked = value; return value; } +export function reflect() { const o = { a: 1 }; delete o.a; return o; } +`; + +const packageTypes = ` +export declare class Base { parse(data: unknown): unknown; } +export declare class Str extends Base {} +export declare class Obj extends Base {} +export declare const str: () => Str; +export declare const obj: (shape: Record) => Obj; +export declare function leak(value: T): T; +export declare function reflect(): object; +`; + +const mainSource = ` +import { leak, obj, reflect, str } from "shapes-fixture"; +const schema = obj({ a: str(), b: str() }); +const input: unknown = JSON.parse('{"a":"x","b":"y"}'); +console.log(JSON.stringify(schema.parse(input))); +console.log(JSON.stringify(leak({ kept: 1 })), JSON.stringify(reflect())); +`; + +let dir: string; +let saved: string | undefined; +beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "scriptc-shapes-")); + const pkg = join(dir, "node_modules", "shapes-fixture"); + await mkdir(pkg, { recursive: true }); + await writeFile( + join(pkg, "package.json"), + JSON.stringify({ + name: "shapes-fixture", + version: "1.0.0", + type: "module", + main: "index.js", + types: "index.d.ts", + }), + ); + await writeFile(join(pkg, "index.js"), packageSource); + await writeFile(join(pkg, "index.d.ts"), packageTypes); + await writeFile( + join(dir, "package.json"), + JSON.stringify({ type: "module", dependencies: { "shapes-fixture": "1.0.0" } }), + ); + await writeFile(join(dir, "main.ts"), mainSource); + saved = process.env["SCRIPTC_DEBUG_SHAPES"]; +}); +afterEach(async () => { + if (saved === undefined) delete process.env["SCRIPTC_DEBUG_SHAPES"]; + else process.env["SCRIPTC_DEBUG_SHAPES"] = saved; + vi.restoreAllMocks(); + await rm(dir, { recursive: true, force: true }); +}); + +async function report(): Promise { + process.env["SCRIPTC_DEBUG_SHAPES"] = "verbose"; + const lines: string[] = []; + vi.spyOn(process.stderr, "write").mockImplementation((chunk: string | Uint8Array) => { + const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"); + for (const l of text.split("\n")) if (l.startsWith("[shapes] ")) lines.push(l.slice(9)); + return true; + }); + const entry = join(dir, "main.ts"); + await compile(entry, { + outDir: dir, + outPath: join(dir, "main.ir.json"), + outputKind: "ir", + npmStatic: ["shapes-fixture"], + }); + return lines; +} + +function site(lines: string[], label: RegExp): string { + const line = lines.find((l) => l.startsWith("site ") && label.test(l)); + if (line === undefined) throw new Error(`no site ${label}:\n${lines.join("\n")}`); + return line; +} + +test("S1 report: per-field temporaries are provable records, dynamic objects stay G", async () => { + const lines = await report(); + // result literals of both _parse implementations, the pair literal, the + // context and the root context never escape: native records would be exact + // the leaf schema's result: in a function called from a loop (hot) + expect(site(lines, /^site \{status,value\}@shapes-fixture\/index\.js:23 \[hot\]/)).toMatch( + /: P-local/, + ); + expect(site(lines, /^site \{status,value\}@shapes-fixture\/index\.js:36/)).toMatch(/: P-local/); + expect(site(lines, /^site \{key,value\}@/)).toMatch(/: P-local/); + expect(site(lines, /^site \{common,data,parent\}@shapes-fixture\/index\.js:12/)).toMatch( + /: P-local/, + ); + expect(site(lines, /^site \[\]@shapes-fixture\/index\.js:29/)).toMatch(/: P-local/); + // the output object is built with computed keys + expect(site(lines, /^site \{\}@shapes-fixture\/index\.js:34/)).toMatch(/: G — computed write/); + // a deleted key and a value stored into the global object + expect(site(lines, /^site \{a\}@/)).toMatch(/: G — delete at/); + expect(site(lines, /^site \{kept\}@main\.ts/)).toMatch(/: P-escaping — escapes: stored into ⊤/); + // receiver-typed dispatch: the field's validators are the schema classes + expect(lines.find((l) => l.startsWith("fn Str._parse@"))).toMatch(/p0=LazyPath/); + expect(lines.find((l) => l.startsWith("class LazyPath"))).toMatch( + /parent: \{common,data,parent\}@shapes-fixture\/index\.js:12;/, + ); + expect(lines.find((l) => l.startsWith("class Str"))).toMatch(/_def: \{checks\}@/); +}); + +test("S1 report is off by default and changes nothing", async () => { + delete process.env["SCRIPTC_DEBUG_SHAPES"]; + const writes = vi.spyOn(process.stderr, "write"); + const entry = join(dir, "main.ts"); + await compile(entry, { + outDir: dir, + outPath: join(dir, "main.ir.json"), + outputKind: "ir", + npmStatic: ["shapes-fixture"], + }); + expect(writes.mock.calls.some(([c]) => String(c).includes("[shapes]"))).toBe(false); +}); From c244ddc4c53b5746b7443c6dadbd42eea1f616f8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 13:18:19 -0500 Subject: [PATCH 071/273] Answer 'send', 'disconnect' and 'channel' in a child process from its IPC channel A child process has these members only when it was created with an IPC channel (fork, or an "ipc" stdio entry), and keeps them after the channel closes and after the child exits. The 'in' test on native handles answered true for every declared member, so a child spawned without IPC reported them and entered IPC-only branches. The three names now answer from the runtime: scr_child_ipc_present reports whether the child was created with a channel. Corpus 8290 tests a forked and a spawned child before and after disconnect and exit. --- .../compiler/src/backend/llvm/lib-process.ts | 7 +++++ .../lowering/expressions/relational.ts | 8 ++++++ .../compiler/src/ir/builtin-signatures.ts | 1 + .../8290-child-channel-members/main.ts.json | 6 ++++ packages/runtime/src/scr_child.c | 3 ++ packages/runtime/src/scr_child.h | 1 + .../corpus/8290-child-channel-members/main.ts | 28 +++++++++++++++++++ .../8290-child-channel-members/worker.ts | 1 + 8 files changed, 55 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8290-child-channel-members/main.ts.json create mode 100644 tests/corpus/8290-child-channel-members/main.ts create mode 100644 tests/corpus/8290-child-channel-members/worker.ts diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 3e8a9bff5..c44febada 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -118,6 +118,13 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr B.line(`call void @scr_child_on_error(ptr ${child.name}, ptr ${cb.name}, ptr @${adapter})`); return { name: "", type: e.type }; } + if (e.fn === "child.hasChannel") { + const child = host.emitExpr(e.args[0]!); + host.declare(`declare zeroext i1 @scr_child_ipc_present(ptr)`); + const out = B.tmp(); + B.line(`${out} = call i1 @scr_child_ipc_present(ptr ${child.name})`); + return { name: out, type: e.type }; + } if (e.fn === "child.connected") { const child = host.emitExpr(e.args[0]!); host.declare(`declare zeroext i1 @scr_child_ipc_connected(ptr)`); diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index edec5ba5f..9b74e3f10 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1762,6 +1762,11 @@ export function lowerInExpression( }; } } + // A child process has `send`, `disconnect` and `channel` only when it was + // created with an IPC channel (fork, or an "ipc" stdio entry), and keeps + // them after the channel closes: the runtime answers from the child. + if (recv.type.kind === "child" && CHILD_CHANNEL_MEMBERS.has(key)) + return { kind: "libCall", fn: "child.hasChannel", args: [recv], type: BOOL, loc }; // A native Node handle (an http.Server, a socket, a child process...): // a member Node's declarations give the class is on its prototype, so // `"closeAllConnections" in server` answers true. Undeclared names can @@ -2094,6 +2099,9 @@ const NATIVE_HANDLE_KINDS: ReadonlySet = new Set([ "http2Stream", ]); +/** Child process members that exist only on a child with an IPC channel. */ +const CHILD_CHANNEL_MEMBERS: ReadonlySet = new Set(["send", "disconnect", "channel"]); + /** Whether every constituent of `t` declares a required member `key` in * a declaration file (Node's @types/node). */ function nodeDeclaredMember(lowerer: Lowerer, t: ts.Type, key: string): boolean { diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 51a0ec778..0449be07f 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2570,6 +2570,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ result: CHILD_T, }, "child.connected": { argTypes: [CHILD_T], result: BOOL }, + "child.hasChannel": { argTypes: [CHILD_T], result: BOOL }, "child.send": { argTypes: [CHILD_T, STRING], result: BOOL }, "child.sendCb": { argTypes: [CHILD_T, STRING, null], result: BOOL }, "child.disconnect": { argTypes: [CHILD_T], result: VOID }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8290-child-channel-members/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8290-child-channel-members/main.ts.json new file mode 100644 index 000000000..3519c4e6c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8290-child-channel-members/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8290-child-channel-members/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index df7543500..234b3c115 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -6456,6 +6456,9 @@ double scr_process_fork_target(double target_count) { return target; } +/* The child was created with an IPC channel; it keeps it until it is freed. */ +bool scr_child_ipc_present(ScrChild *c) { return c->ipc != NULL; } + bool scr_child_ipc_connected(ScrChild *c) { return c->ipc != NULL && c->ipc->connected; } diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h index a5853ad59..8b0d4d3f1 100644 --- a/packages/runtime/src/scr_child.h +++ b/packages/runtime/src/scr_child.h @@ -176,6 +176,7 @@ void scr_child_release_v(void *p); void scr_child_on_exit(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildExitFn fn); void scr_child_on_close(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildExitFn fn); void scr_child_on_error(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildErrFn fn); +bool scr_child_ipc_present(ScrChild *c); bool scr_child_ipc_connected(ScrChild *c); bool scr_child_ipc_send(ScrChild *c, ScrStr *json); bool scr_child_ipc_send_cb(ScrChild *c, ScrStr *json, diff --git a/tests/corpus/8290-child-channel-members/main.ts b/tests/corpus/8290-child-channel-members/main.ts new file mode 100644 index 000000000..78ed52de1 --- /dev/null +++ b/tests/corpus/8290-child-channel-members/main.ts @@ -0,0 +1,28 @@ +import { fork, spawn } from "node:child_process"; + +// `send`, `disconnect` and `channel` exist only on a child created with an +// IPC channel, and stay after the channel closes; the other members are +// on every child. +const describe = (label: string, child: ReturnType): string => + `${label}: send ${"send" in child} disconnect ${"disconnect" in child} channel ${"channel" in child} kill ${"kill" in child} pid ${"pid" in child}`; + +const lines: string[] = []; +const plain = spawn("/bin/sh", ["-c", "exit 0"], { stdio: "ignore" }); +lines.push(describe("spawn", plain)); +const forked = fork(new URL("./worker.ts", import.meta.url), [], { + stdio: ["ignore", "ignore", "inherit", "ipc"], +}); +lines.push(describe("fork", forked)); +forked.once("message", () => { + forked.disconnect(); + lines.push(describe("fork after disconnect", forked)); +}); +forked.once("exit", () => { + lines.push(describe("fork after exit", forked)); +}); +plain.once("exit", () => { + lines.push(describe("spawn after exit", plain)); +}); +process.once("exit", () => { + console.log(lines.sort().join("\n")); +}); diff --git a/tests/corpus/8290-child-channel-members/worker.ts b/tests/corpus/8290-child-channel-members/worker.ts new file mode 100644 index 000000000..96401ea1a --- /dev/null +++ b/tests/corpus/8290-child-channel-members/worker.ts @@ -0,0 +1 @@ +process.send?.({ value: 1 }); From 587443428cfb1e2600fd642195731cd4e68814ba Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 13:22:26 -0500 Subject: [PATCH 072/273] Link Linux executables with lld when the driver finds it Linux executables linked through the clang driver with its default linker, GNU ld. lld links the same objects in about half the time. - Native and Node-hosted Linux exe links pass -fuse-ld=lld when ld.lld is beside the driver's executable (where clang looks first) or on PATH, and the default driver is in use (no SCRIPTC_LINKER, no sanitizer link). - A link with lld that fails runs again with the driver's default linker, whose error is the one reported when that fails too. --- packages/compiler/src/backend/linker.ts | 14 ++- packages/compiler/src/backend/lld.test.ts | 61 ++++++++++++ packages/compiler/src/backend/lld.ts | 49 +++++++++ packages/compiler/src/native/compiler.test.ts | 99 ++++++++++++++++++- packages/compiler/src/native/compiler.ts | 20 +++- 5 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/src/backend/lld.test.ts create mode 100644 packages/compiler/src/backend/lld.ts diff --git a/packages/compiler/src/backend/linker.ts b/packages/compiler/src/backend/linker.ts index af53f74df..a54956ed9 100644 --- a/packages/compiler/src/backend/linker.ts +++ b/packages/compiler/src/backend/linker.ts @@ -21,6 +21,7 @@ import { import { type NativeArtifactDependency } from "./native/contracts.js"; import { RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js"; import type { NativeLinkPlan } from "./link-plan.js"; +import { lldLinkerArgs } from "./lld.js"; import { createDarwinDebugSymbols, installDarwinDebugSymbols, @@ -240,8 +241,11 @@ export async function linkNativeExecutable( try { const staged = await stageRuntimePackArtifacts(plan.runtimePack); stagedRoot = staged.root; + // Linux links with lld when the default driver finds it. + const lld = explicitLinker === undefined ? lldLinkerArgs(plan.target.platform, linker) : []; const args = [ ...(explicitLinker === undefined ? plan.target.defaultLinkerArgs : []), + ...lld, ...plan.driverFlags, ...plan.inputs.map((input) => staged.replacements.get(input) ?? input), ...plan.systemLibraries.map((name) => `-l${name}`), @@ -276,7 +280,15 @@ export async function linkNativeExecutable( ...(await snapshotDependencies([...toolchain, ...additionalDependencyPaths])), ]) .catch(() => null); - await execFileAsync(linker, args); + await execFileAsync(linker, args).catch(async (error: unknown) => { + // A failed link with lld runs again with the driver's default linker, + // whose error (if any) is the one reported. + if (lld.length === 0) throw error; + await execFileAsync( + linker, + args.filter((arg) => !lld.includes(arg)), + ); + }); const output = await stat(privateOut); if (!output.isFile() || output.size === 0) throw new Error("linker produced no executable"); if (plan.darwinDebugSymbols) { diff --git a/packages/compiler/src/backend/lld.test.ts b/packages/compiler/src/backend/lld.test.ts new file mode 100644 index 000000000..b0a466cf6 --- /dev/null +++ b/packages/compiler/src/backend/lld.test.ts @@ -0,0 +1,61 @@ +import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { driverFindsLld, lldLinkerArgs } from "./lld.js"; + +const directories: string[] = []; +afterEach(() => { + for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true }); +}); + +function tool(path: string): string { + writeFileSync(path, "#!/bin/sh\n"); + chmodSync(path, 0o755); + return path; +} + +function fixture() { + const root = mkdtempSync(join(tmpdir(), "scriptc-lld-")); + directories.push(root); + const llvm = join(root, "llvm", "bin"); + const usr = join(root, "usr", "bin"); + const other = join(root, "other"); + for (const directory of [llvm, usr, other]) mkdirSync(directory, { recursive: true }); + return { root, llvm, usr, other }; +} + +test.skipIf(process.platform === "win32")( + "a driver finds ld.lld beside itself, beside its real executable, or on PATH", + () => { + const f = fixture(); + const clang = tool(join(f.llvm, "clang")); + // Without ld.lld anywhere. + expect(driverFindsLld(clang, { PATH: f.other })).toBe(false); + // Beside the driver. + tool(join(f.llvm, "ld.lld")); + expect(driverFindsLld(clang, { PATH: f.other })).toBe(true); + // A PATH entry that links to the driver: clang looks beside its real executable. + symlinkSync(clang, join(f.usr, "clang")); + expect(driverFindsLld("clang", { PATH: f.usr })).toBe(true); + // A driver elsewhere finds ld.lld on PATH, and not otherwise. + const gcc = tool(join(f.other, "gcc")); + expect(driverFindsLld(gcc, { PATH: f.other })).toBe(false); + expect(driverFindsLld(gcc, { PATH: [f.other, f.llvm].join(":") })).toBe(true); + expect(driverFindsLld("missing-driver", { PATH: f.llvm })).toBe(false); + }, +); + +test.skipIf(process.platform === "win32")( + "Linux links with the default driver use lld when the driver finds it", + () => { + const f = fixture(); + const clang = tool(join(f.llvm, "clang")); + tool(join(f.llvm, "ld.lld")); + const env = { PATH: f.other }; + expect(lldLinkerArgs("linux", clang, env)).toEqual(["-fuse-ld=lld"]); + expect(lldLinkerArgs("darwin", clang, env)).toEqual([]); + expect(lldLinkerArgs("windows", clang, env)).toEqual([]); + expect(lldLinkerArgs("linux", clang, { ...env, SCRIPTC_LINKER: clang })).toEqual([]); + }, +); diff --git a/packages/compiler/src/backend/lld.ts b/packages/compiler/src/backend/lld.ts new file mode 100644 index 000000000..790e07062 --- /dev/null +++ b/packages/compiler/src/backend/lld.ts @@ -0,0 +1,49 @@ +import { realpathSync, statSync } from "node:fs"; +import { delimiter, dirname, isAbsolute, join, resolve } from "node:path"; + +function isFile(path: string): boolean { + try { + return statSync(path).isFile(); + } catch { + return false; + } +} + +/** The executable `command` names: a path, or the first match on PATH. */ +function commandFile(command: string, env: NodeJS.ProcessEnv): string | null { + if (command.includes("/")) return isFile(command) ? resolve(command) : null; + for (const directory of (env["PATH"] ?? "/usr/bin:/bin").split(delimiter)) { + const candidate = join(directory || ".", command); + if (isFile(candidate)) return isAbsolute(candidate) ? candidate : resolve(candidate); + } + return null; +} + +/** + * Whether the compiler driver `driver` finds ld.lld for `-fuse-ld=lld`: in + * the directory of the driver's executable (where clang looks first) or on + * PATH. Linking still falls back to the driver's default linker when a link + * with lld fails. + */ +export function driverFindsLld(driver: string, env: NodeJS.ProcessEnv = process.env): boolean { + const file = commandFile(driver, env); + if (file === null) return false; + try { + for (const directory of new Set([dirname(file), dirname(realpathSync(file))])) + if (isFile(join(directory, "ld.lld"))) return true; + } catch { + return false; + } + return commandFile("ld.lld", env) !== null; +} + +/** `-fuse-ld=lld` for Linux links whose driver finds ld.lld. */ +export function lldLinkerArgs( + platform: string, + driver: string, + env: NodeJS.ProcessEnv = process.env, +): string[] { + return platform === "linux" && env["SCRIPTC_LINKER"] === undefined && driverFindsLld(driver, env) + ? ["-fuse-ld=lld"] + : []; +} diff --git a/packages/compiler/src/native/compiler.test.ts b/packages/compiler/src/native/compiler.test.ts index 7a1cee62a..a085356a0 100644 --- a/packages/compiler/src/native/compiler.test.ts +++ b/packages/compiler/src/native/compiler.test.ts @@ -1,5 +1,13 @@ import { execFileSync, spawnSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { + chmodSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { afterEach, expect, test, vi } from "vitest"; @@ -7,7 +15,7 @@ import { NativeCompiler } from "./compiler.js"; import type { NativeToolchain } from "./toolchain.js"; import { runNativeFrontend } from "../frontend/pipeline-native.js"; import { emitNativeObject } from "../backend/native-tools.js"; -import { MACOS_ARM64_TARGET } from "../backend/targets.js"; +import { LINUX_X64_GNU_TARGET, MACOS_ARM64_TARGET } from "../backend/targets.js"; import { VOID, type IrModule } from "../ir/ir.js"; import { contentDigest, NativeCache } from "./cache.js"; import { NativeExecutableCache, openNativeExecutableCache } from "./executable-cache.js"; @@ -283,3 +291,90 @@ test.each([false, true])( expect(readdirSync(directory).sort()).toEqual(["output.o", "output.o.ll"]); }, ); + +test.skipIf(process.platform === "win32").each([false, true])( + "Linux links use lld when the driver finds it, and the default linker when lld fails (%s)", + (lldFails) => { + const directory = mkdtempSync(join("/tmp", "scriptc-native-lld-")); + directories.push(directory); + const bin = join(directory, "bin"); + mkdirSync(bin); + const clang = join(bin, "clang"); + writeFileSync(clang, "#!/bin/sh\n"); + chmodSync(clang, 0o755); + writeFileSync(join(bin, "ld.lld"), "#!/bin/sh\n"); + chmodSync(join(bin, "ld.lld"), 0o755); + const entry = join(directory, "entry.ts"); + const output = join(directory, "program"); + const module: IrModule = { + irVersion: 17, + sourceFile: entry, + entry: "%main", + functions: [ + { + name: "%main", + params: [], + locals: [], + body: [], + returnType: VOID, + loc: { file: entry, start: 0, end: 0 }, + }, + ], + }; + vi.mocked(runNativeFrontend).mockReturnValue({ + preflight: [], + entryText: () => "", + entryExports: () => new Map(), + entryContract: () => { + throw new Error("unexpected contract request"); + }, + sourceTexts: () => new Map(), + npmStatic: [], + npmImportSites: new Map(), + dispose: () => {}, + lower: () => ({ + module, + diagnostics: [], + runtimeFences: [], + stats: { statementsTotal: 0, statementsFailed: 0, functionsSkipped: 0 }, + }), + }); + vi.mocked(emitNativeObject).mockImplementation((options) => { + writeFileSync(options.outputPath, "object"); + return [options.outputPath]; + }); + vi.mocked(openNativeExecutableCache).mockReturnValue(null); + const links: string[][] = []; + vi.mocked(execFileSync).mockImplementation((_command, args) => { + const list = args as string[]; + links.push(list); + if (lldFails && list.includes("-fuse-ld=lld")) throw new Error("lld failed"); + writeFileSync(list[list.indexOf("-o") + 1]!, "executable"); + return ""; + }); + const target = LINUX_X64_GNU_TARGET; + const compiler = new NativeCompiler({ + compilerVersion: "0.0.0", + target, + helper: target.helper, + ts7Executable: "unused", + helperExecutable: "unused", + helperPackageRoot: "unused", + runtimePackRoot: "unused", + linker: clang, + linkerArgs: [], + dsymutil: "unused", + }); + const result = compiler.compile(entry, { + outDir: directory, + outPath: output, + backend: "llvm", + optimization: "release", + strip: true, + }); + expect(result).toMatchObject({ ok: true, binaryPath: output }); + expect(links[0]).toContain("-fuse-ld=lld"); + expect(links.length).toBe(lldFails ? 2 : 1); + if (lldFails) expect(links[1]).not.toContain("-fuse-ld=lld"); + }, +); diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts index a4582ec49..0188dccf7 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -65,6 +65,7 @@ import { import { splitLlvmLibraryProgram } from "../backend/llvm/split.js"; import { prepareNativeExecutable, prepareNativeLibrary } from "./prepare.js"; import { buildSanitizedRuntime, sanitizerDriver, sanitizerFlags } from "./sanitizer.js"; +import { lldLinkerArgs } from "../backend/lld.js"; import { openNativeExecutableCache } from "./executable-cache.js"; import { nativeFileIdentity } from "./file-identity.js"; @@ -494,8 +495,11 @@ export class NativeCompiler { : { windowsSubsystem: options.windowsSubsystem }), ...(profile?.generate === undefined ? {} : { profileGenerate: true }), }); + // Linux links with lld when the driver finds it. + const lld = options.sanitize ? [] : lldLinkerArgs(target.platform, toolchain.linker); const linkArgs = [ ...(options.sanitize ? ["-fsanitize=address"] : toolchain.linkerArgs), + ...lld, ...plan.driverFlags, ...plan.inputs, ...plan.systemLibraries.map((name) => `-l${name}`), @@ -506,8 +510,20 @@ export class NativeCompiler { timing("link-inputs"); // A successful dependency trace already produced this executable. // Keep its verified output instead of asking the linker to repeat it. - if (!cacheable) - runNativeTool(options.sanitize ? sanitizerDriver(toolchain) : toolchain.linker, linkArgs); + if (!cacheable) { + const driver = options.sanitize ? sanitizerDriver(toolchain) : toolchain.linker; + try { + runNativeTool(driver, linkArgs); + } catch (error) { + // A failed link with lld runs again with the driver's default + // linker, whose error (if any) is the one reported. + if (lld.length === 0) throw error; + runNativeTool( + driver, + linkArgs.filter((arg) => !lld.includes(arg)), + ); + } + } timing("native-link"); requireNativeArtifact(stagedOutput); if (target.platform === "darwin" && optimization === "dev" && !options.strip) { From a8c0fa772b9c14005f55e378df588059bed0cb0a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 13:28:53 -0500 Subject: [PATCH 073/273] Use fixed timestamps in the fs.cp preserveTimestamps test --- tests/corpus/5570-fs-cp-async.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/corpus/5570-fs-cp-async.ts b/tests/corpus/5570-fs-cp-async.ts index d83750095..23747bcb3 100644 --- a/tests/corpus/5570-fs-cp-async.ts +++ b/tests/corpus/5570-fs-cp-async.ts @@ -88,8 +88,13 @@ const stamp = new Date("2021-05-06T07:08:09.123Z"); fs.utimesSync("file.txt", stamp, stamp); await fsp.cp("file.txt", "p-stamped.txt", { preserveTimestamps: true }); console.log(new Date(fs.statSync("p-stamped.txt").mtimeMs).toISOString()); +// The source tree gets fixed times: its own times come from the clock, and +// whether a copy equals them depends on their sub-millisecond digits. +fs.utimesSync("src/a.txt", stamp, stamp); +fs.utimesSync("src", stamp, stamp); await fsp.cp("src", "p-stamped-dir", { recursive: true, preserveTimestamps: true }); -console.log(fs.statSync("p-stamped-dir").mtimeMs === fs.statSync("src").mtimeMs); +for (const path of ["p-stamped-dir", "p-stamped-dir/a.txt"]) + console.log(path, new Date(fs.statSync(path).mtimeMs).toISOString()); const rejected = await fsp.cp("missing", "x").then( () => "fulfilled", From 908d87d3e0bdcb3ba91e3d6ad21722bd9a819392 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 13:32:14 -0500 Subject: [PATCH 074/273] Key widened returns by their type-parameter bindings and skip self-calls Two review findings on the widened-return cache: - A nested function lowered under different type-parameter bindings reused the record layout of the first instance it was widened in. In `function outer(x: T) { const inner = () => ...; return inner(); }`, the closures of outer and outer returned the records of outer, and returning one threw a TypeError. A kept result now records the bindings it was mapped under (typeParamBindings, its checker-type and implicit-any twins, and whether an erased body is lowering) and answers only under the same ones. - A function that calls itself on one return cut its own widening, so its result was never kept and a chain of such functions forwarding to each other on two branches was scanned 2^depth times. A call of the function itself returns its own type, whose arms add nothing, so the scan now skips it. --- .../src/frontend/lowering/coercions/state.ts | 18 +++++- .../src/frontend/lowering/function-returns.ts | 25 ++++++++- .../compiler/test/widened-returns.test.ts | 55 ++++++++++++++++++- 3 files changed, 92 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/coercions/state.ts b/packages/compiler/src/frontend/lowering/coercions/state.ts index 27667197c..cbc577634 100644 --- a/packages/compiler/src/frontend/lowering/coercions/state.ts +++ b/packages/compiler/src/frontend/lowering/coercions/state.ts @@ -1,5 +1,6 @@ import type { IrType } from "../../../ir/ir.js"; import type * as ts from "../../ts7/adapter.js"; +import type { Lowerer } from "../lowerer.js"; /** Per-compilation conversion state. Recursive plans are scoped to the * active planning stack; only fully validated copy routes are memoized. */ @@ -24,6 +25,19 @@ export class CoercionState { * width-converting extraction (widthNarrowHelper). */ readonly widenedReturns = new Set(); /** widenReducedReturn's results by declaration and inferred return type, - * for the results computed without cutting a recursive widening. */ - readonly widenedReturnTypes = new Map>(); + * for the results computed without cutting a widening cycle through + * another function. Each keeps the type-parameter bindings it was mapped + * under and answers only under the same bindings. */ + readonly widenedReturnTypes = new Map>(); +} + +/** A widened inferred return type and the lowering state that mapped it + * (Lowerer.typeParamBindings and its checker-type and implicit-any twins, + * and whether an erased generic body is lowering). */ +export interface WidenedReturn { + type: IrType; + bindings: Lowerer["typeParamBindings"]; + tsBindings: Lowerer["typeParamTsBindings"]; + implicit: Lowerer["implicitParamTypes"]; + erasing: boolean; } diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index 0b87710e0..4d586fcc3 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -584,15 +584,30 @@ function widenReducedReturn( return mapped; } // Functions that forward to the same callees along several return - // branches read each callee's widened type once. + // branches read each callee's widened type once. A nested function + // lowered under other type-parameter bindings maps its returns again. const known = lowerer.coercions.widenedReturnTypes.get(decl)?.get(retTsType); - if (known !== undefined) return known; + const erasing = lowerer.erasingDepth > 0; + if ( + known !== undefined && + known.bindings === lowerer.typeParamBindings && + known.tsBindings === lowerer.typeParamTsBindings && + known.implicit === lowerer.implicitParamTypes && + known.erasing === erasing + ) + return known.type; const cutsBefore = wideningCuts; const widened = widenReturnArms(lowerer, decl, retTsType, mapped, arms, recordArms); if (wideningCuts === cutsBefore) { let byType = lowerer.coercions.widenedReturnTypes.get(decl); if (byType === undefined) lowerer.coercions.widenedReturnTypes.set(decl, (byType = new Map())); - byType.set(retTsType, widened); + byType.set(retTsType, { + type: widened, + bindings: lowerer.typeParamBindings, + tsBindings: lowerer.typeParamTsBindings, + implicit: lowerer.implicitParamTypes, + erasing, + }); } return widened; } @@ -638,6 +653,10 @@ function widenReturnArms( (callee as { body?: ts.Node }).body !== undefined && callee.type === undefined ) { + // A call of the function itself returns its own type, whose arms + // add nothing. Reading it would cut the widening and keep this + // result from being reused. + if (callee === decl) return; let t: IrType | null = null; widening.add(decl); try { diff --git a/packages/compiler/test/widened-returns.test.ts b/packages/compiler/test/widened-returns.test.ts index 63d24c1da..fe78c6b75 100644 --- a/packages/compiler/test/widened-returns.test.ts +++ b/packages/compiler/test/widened-returns.test.ts @@ -22,7 +22,7 @@ async function lowerModule(source: string): Promise { /** Functions that each forward to the previous one along two return * branches, above one whose inferred type the checker reduced. */ -function forwardingChain(depth: number): string { +function forwardingChain(depth: number, recursive = false): string { const lines = [ "export {};", "type Short = { status: string };", @@ -35,6 +35,7 @@ function forwardingChain(depth: number): string { for (let i = 1; i <= depth; i++) lines.push( `function f${i}(a: Short, b: Long, n: number) {`, + ...(recursive ? [` if (n > 20) return f${i}(a, b, n - 1);`] : []), ` if (n > 1) return f${i - 1}(a, b, n - 1);`, ` if (n > 0) return f${i - 1}(a, b, n + 1);`, " return a;", @@ -53,3 +54,55 @@ test("return widening reads each forwarded callee's type once", { timeout: 60_00 expect(top).toBeDefined(); expect(top!.returnType.kind).toBe("union"); }); + +test( + "return widening reads each forwarded callee's type once when the callees call themselves", + { timeout: 60_000 }, + async () => { + const mod = await lowerModule(forwardingChain(40, true)); + const top = mod.functions.find((f) => f.name === "f40"); + expect(top).toBeDefined(); + expect(top!.returnType.kind).toBe("union"); + }, +); + +test("a nested function's widened return follows each instance's type arguments", async () => { + const mod = await lowerModule( + [ + "export {};", + "function outer(x: T, n: number) {", + " const inner = () => {", + " if (n > 0) return { status: x, value: n };", + " return { status: x };", + " };", + " return inner();", + "}", + "console.log(JSON.stringify(outer(1, 1)));", + 'console.log(JSON.stringify(outer("s", 1)));', + "console.log(JSON.stringify(outer(true, 0)));", + ].join("\n"), + ); + const shapes = new Map((mod.records ?? []).map((r) => [r.id, r])); + const unions = new Map((mod.unions ?? []).map((u) => [u.id, u])); + // Each closure returns records whose `status` has the type its captured + // `x` has. + const closures = mod.functions.filter( + (f) => f.name.startsWith("%fn.") && !f.name.includes("adapt"), + ); + expect(closures.length).toBe(3); + for (const closure of closures) { + const captured = closure.captures?.find((c) => c.type.kind !== "f64")?.type.kind ?? "f64"; + const ret = closure.returnType; + expect(ret.kind).toBe("union"); + const arms = ret.kind === "union" ? (unions.get(ret.unionId)?.arms ?? []) : []; + expect(arms.length).toBe(2); + for (const arm of arms) { + expect(arm.kind).toBe("record"); + const status = + arm.kind === "record" + ? shapes.get(arm.shapeId)?.fields.find((f) => f.name === "status") + : undefined; + expect(status?.type.kind).toBe(captured); + } + } +}); From 3f03c90e864352a95c31857baf7143fd87829827 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 13:47:17 -0500 Subject: [PATCH 075/273] Keep versioned loops' slow copies free of speculative integer copies The copy of a dense-array, byte-window, counted, callback or typed-array versioned loop that runs when its guard fails no longer gives its loops speculative integer or int32 kind copies, which emitted each loop again for a path that rarely runs. Proven integer loops, which emit one copy, are kept in it. --- .../src/backend/llvm/emit-function.ts | 1 + .../src/backend/llvm/emit-statements.ts | 26 +++- packages/compiler/src/backend/llvm/emitter.ts | 3 + .../test/speculative-loop-emission.test.ts | 44 +++++- .../7902-versioned-loop-slow-copies.ts.json | 6 + .../corpus/7902-versioned-loop-slow-copies.ts | 139 ++++++++++++++++++ 6 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7902-versioned-loop-slow-copies.ts.json create mode 100644 tests/corpus/7902-versioned-loop-slow-copies.ts diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index e015c2d72..327d2019b 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -136,6 +136,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator; beginFunctionLoops(emitter, fn, numericFn); emitter.speculativeLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled; + emitter.loopCopiesEnabled = true; emitter.speculativeFunction = fn.name; emitter.speculation = null; emitter.speculationResume = null; diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index d356bfa3d..794ba1798 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -994,7 +994,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { LOOPS.of(emitter).denseLoopsEnabled = false; try { B.startBlock(slow); - emitter.emitStmt(s); + emitSlowCopy(emitter, s); B.br(done); B.startBlock(fast); LOOPS.of(emitter).denseLoop = dense; @@ -1018,7 +1018,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { LOOPS.of(emitter).byteWindowsEnabled = false; try { B.startBlock(slow); - emitter.emitStmt(s); + emitSlowCopy(emitter, s); B.br(done); B.startBlock(fast); emitter.integerRanges = window.ranges; @@ -1072,7 +1072,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.condBr(safe, fast, slow); B.startBlock(slow); LOOPS.of(emitter).countedLoopsEnabled = false; - emitter.emitStmt(s); + emitSlowCopy(emitter, s); LOOPS.of(emitter).countedLoopsEnabled = true; B.br(countedJoin); B.startBlock(fast); @@ -2049,7 +2049,7 @@ function emitCallbackVersionedLoop( emitter.callbackVersioning.add(s); try { B.startBlock(slow); - emitter.emitStmt(s); + emitSlowCopy(emitter, s); B.br(done); B.startBlock(fast); for (const { ref } of bindings) emitter.knownCallbacks.add(ref.localId); @@ -2062,6 +2062,20 @@ function emitCallbackVersionedLoop( B.startBlock(done); } +/** The copy of a versioned loop that runs when its guard fails. Its loops + * get no speculative or int32 kind copies, which would emit each loop again + * for a path that rarely runs. Proven integer loops, which emit one copy, + * are kept. */ +function emitSlowCopy(emitter: LlEmitter, s: IrStmt): void { + const copies = emitter.loopCopiesEnabled; + emitter.loopCopiesEnabled = false; + try { + emitter.emitStmt(s); + } finally { + emitter.loopCopiesEnabled = copies; + } +} + /** Entries in a row whose fast copy bailed before a speculative loop site * stops trying it. */ const SPECULATION_BAILS = 8; @@ -2089,7 +2103,7 @@ function emitVersionedLoop( LOOPS.of(emitter).bytesLoopVersioning.add(s); try { B.startBlock(slow); - emitter.emitStmt(s); + emitSlowCopy(emitter, s); B.br(done); B.startBlock(fast); emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); @@ -2120,6 +2134,7 @@ export function emitLoopCopies( const plan = allowed && emitter.speculativeLoopsEnabled && + emitter.loopCopiesEnabled && emitter.speculation === null && emitter.speculationResume === null ? planSpeculativeLoop( @@ -2147,6 +2162,7 @@ export function emitLoopCopies( const kinds = allowed && emitter.speculativeLoopsEnabled && + emitter.loopCopiesEnabled && emitter.int32Slots.checkedInt32Arrays.size > 0 && emitter.int32Kinds === null && emitter.speculation === null && diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 6729da197..e6ec6e702 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1461,6 +1461,9 @@ export class LlEmitter { integerTempsEnabled = false; /** Speculative integer loops (speculative-integers.ts) run in this function. */ speculativeLoopsEnabled = false; + /** False inside the copy of a versioned loop that runs when its guard + * fails: loops there get no speculative or int32 kind copies. */ + loopCopiesEnabled = true; /** The fast copy of a speculative integer loop being emitted: its * integer-computed writes and where each resumes in the ordinary copy. */ speculation: { diff --git a/packages/compiler/test/speculative-loop-emission.test.ts b/packages/compiler/test/speculative-loop-emission.test.ts index a81348102..3f6aef158 100644 --- a/packages/compiler/test/speculative-loop-emission.test.ts +++ b/packages/compiler/test/speculative-loop-emission.test.ts @@ -47,9 +47,8 @@ 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); - // 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. + // Each speculative 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; @@ -91,3 +90,42 @@ test("a loop site stops entering its fast copy after repeated bails", async () = 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`); }); + +test("the slow copy of a dense-array loop has no speculative 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("dense.loop.slow")); + const fast = order.findIndex((l) => l.startsWith("dense.loop.fast")); + expect(slow).toBeGreaterThanOrEqual(0); + expect(fast).toBeGreaterThan(slow); + // The slow copy runs when the guard fails; only the fast copy speculates. + expect(order.slice(slow, fast).filter((l) => l.startsWith("spec."))).toEqual([]); + expect(order.slice(fast).some((l) => l.startsWith("spec.fast"))).toBe(true); + expect(llvm.match(/^@sc_spec_bails_\d+ = /gm)).toHaveLength(1); +}); + +test("the slow copy of a typed-array version keeps its proven integer loop", async () => { + // The program can start a worker, so typed-array loops are versioned on + // thread ownership. + const llvm = await emitLlvm(` +import { Worker, isMainThread } from "node:worker_threads"; +function count(bytes: Uint8Array): number { + let lines = 0; + for (let i = 0; i < bytes.length; i++) if (bytes[i] === 10) lines++; + return lines; +} +if (isMainThread) { + if (process.argv[2] === "spawn") new Worker(new URL(import.meta.url)); + console.log(count(new Uint8Array([10, 1, 10]))); +} +`); + const fn = /^define internal double @sc_bf_\w*count\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + const order = labels(fn); + const slow = order.findIndex((l) => l.startsWith("bytes.version.slow")); + const fast = order.findIndex((l) => l.startsWith("bytes.version.fast")); + expect(slow).toBeGreaterThanOrEqual(0); + expect(fast).toBeGreaterThan(slow); + expect(order.slice(slow, fast).some((l) => l.startsWith("integer.leave"))).toBe(true); + expect(order.slice(fast).some((l) => l.startsWith("integer.leave"))).toBe(true); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7902-versioned-loop-slow-copies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7902-versioned-loop-slow-copies.ts.json new file mode 100644 index 000000000..0147937af --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7902-versioned-loop-slow-copies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7902-versioned-loop-slow-copies.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7902-versioned-loop-slow-copies.ts b/tests/corpus/7902-versioned-loop-slow-copies.ts new file mode 100644 index 000000000..91b769f47 --- /dev/null +++ b/tests/corpus/7902-versioned-loop-slow-copies.ts @@ -0,0 +1,139 @@ +// Loops whose versioning guard can fail, so they run the ordinary copy: +// arrays whose holes were filled later, counted loops that read past the +// end, arrays that change length inside the loop, typed-array windows past +// the end, fractional and NaN bounds, and callbacks that are replaced. The +// loops keep integer counters and sums that would also suit integer +// speculation, with values past 2^31 and 2^53, negative zero and fractions. + +function sum(values: number[]): string { + let total = 0; + let count = 0; + let seed = 12345; + let grow = 1; + for (let i = 0; i < values.length; i++) { + total += values[i]!; + count++; + seed = (seed * 1103515245 + 12345) % 2147483648; + grow = grow * 7 + i; + } + return `${total} ${count} ${seed} ${grow}`; +} + +function product(values: number[], limit: number): string { + let p = 1; + let steps = 0; + for (let i = 0; i < limit; i++) { + p *= values[i] ?? 1; + steps += 3; + } + return `${p} ${steps}`; +} + +function shrinking(values: number[]): string { + let total = 0; + for (let i = 0; i < values.length; i++) { + total += values[i]! * i; + if (values[i]! % 3 === 0) values.pop(); + } + return `${total} ${values.length}`; +} + +function windowSum(a: Int32Array, from: number, to: number): string { + let total = 0; + let mix = 1; + for (let i = from; i < to; i++) { + total += a[i] ?? -1; + mix = (mix * 31 + 17) % 65521; + } + return `${total} ${mix}`; +} + +function bytes(a: Uint8Array, start: number, n: number): string { + let hash = 0; + let steps = 0; + for (let i = start; i < start + n; i++) { + hash = (hash * 31 + (a[i] ?? 7)) | 0; + steps++; + } + return `${hash} ${steps}`; +} + +function counted(values: Float64Array, n: number, step: number): string { + let total = 0; + let odd = 0; + for (let i = 0; i < n; i += step) { + values[i] = i * 2; + total += i; + odd = odd * 2 + 1; + } + return `${total} ${odd} ${values.join()}`; +} + +function mapped(values: number[], f: (x: number) => number): string { + let total = 0; + let k = 0; + for (let i = 0; i < values.length; i++) { + total += f(values[i]!); + k = k * 10 + 1; + } + return `${total} ${k}`; +} + +function lastNegativeZero(values: number[]): string { + let last = 0; + let zero = 0; + for (let i = 0; i < values.length; i++) { + last = values[i]! * -1; + zero = zero * -1; + } + return `${Object.is(last, -0) ? "-0" : String(last)} ${Object.is(zero, -0) ? "-0" : zero}`; +} + +/** An array written out of order: index 2 first, then the hole at 1. */ +function filledLate(): number[] { + const values: number[] = [1]; + values[2] = 3; + values[1] = 2; + return values; +} + +console.log(sum([1, 2, 3, 4, 5])); +console.log(sum(filledLate())); +console.log(sum([1, 2.5, 3])); +console.log(sum([2147483647, 2147483647, 2147483647])); +console.log(sum([9007199254740991, 2, 2])); +console.log(sum(Array.from({ length: 40 }, (_, i) => (i % 2 === 0 ? i : -i)))); + +console.log(product([1, 2, 3, 4], 4)); +console.log(product([1, 2, 3, 4], 6)); +console.log(product([65536, 65536, 65536], 3)); +console.log(product([0, -1], 2), product([-1, 0], 2)); + +console.log(shrinking([1, 2, 3, 4, 5, 6, 7, 8, 9])); +console.log(shrinking([3, 3, 3, 3])); + +const ints = new Int32Array([10, 20, 30, 40]); +console.log(windowSum(ints, 0, 4), windowSum(ints, 2, 7), windowSum(ints, -2, 2)); +console.log(windowSum(ints, 0.5, 3.5), windowSum(ints, 0, NaN)); + +const text = new Uint8Array([104, 101, 108, 108, 111]); +console.log(bytes(text, 0, 5), bytes(text, 3, 5), bytes(text, -1, 3)); + +console.log(counted(new Float64Array(12), 10, 1), counted(new Float64Array(12), 10.5, 1)); +console.log(counted(new Float64Array(12), 10, 2.5), counted(new Float64Array(4), NaN, 1)); +console.log(counted(new Float64Array(4), 8, 1), counted(new Float64Array(4), 3, 0.75)); +console.log(counted(new Float64Array(60), 60, 1).split(" ").slice(0, 2).join(" ")); + +let calls = 0; +const double = (x: number): number => { + calls++; + return x * 2; +}; +const half = (x: number): number => { + calls++; + return x / 2; +}; +console.log(mapped([1, 2, 3], double), mapped([1, 2, 3], half), calls); +console.log(mapped(filledLate(), double), mapped([1, 2.5], (x) => x * x), calls); + +console.log(lastNegativeZero([1, 2, 0]), lastNegativeZero([1, 2, 3]), lastNegativeZero([])); From 40faf6a9f1b2cf2beb2e3dee7c10abf1cc735466 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 14:16:32 -0500 Subject: [PATCH 076/273] Collect the native compiler profile on the release commit The release workflow's new cli-profile job runs scripts/refresh-cli-profile.mjs on Linux x64 and uploads the profile. Every platform's native CLI build uses it through SCRIPTC_NATIVE_PROFILE_USE, and the GitHub release attaches it as scriptc--cli.profdata.gz. When the job fails, the CLI builds use the committed profile. build-native-cli.mts accepts a gzip-compressed profile in SCRIPTC_NATIVE_PROFILE_USE when the name ends in .gz. refresh-cli-profile.mjs creates the directory of --output before it starts. --- .github/workflows/release.yml | 90 ++++++++++++++++++++++++++++++++- RELEASING.md | 2 +- scripts/build-native-cli.mts | 12 +++-- scripts/refresh-cli-profile.mjs | 19 ++++--- 4 files changed, 109 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fff1931ae..799337e0a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,10 +100,59 @@ jobs: if (receiptRun) appendFileSync(process.env.GITHUB_OUTPUT, `receipt_run=${receiptRun}\n`); NODE - build-native-packages: - name: Build native package (${{ matrix.platform }}) + # Collects the native compiler's profile on this commit + # (scripts/refresh-cli-profile.mjs), so every platform's CLI build optimizes + # with a profile of the code it compiles. The committed profile falls behind + # main within a day or two. If this job fails, the CLI builds use the + # committed profile. + cli-profile: + name: Collect the native compiler profile needs: check-release if: needs.check-release.outputs.should_release == 'true' && needs.check-release.outputs.reuse_artifacts != 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + continue-on-error: true + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 11.1.3 + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: pnpm + - uses: ./.github/actions/llvm-linux + - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/zig + - uses: ./.github/actions/native-packages + with: + target: linux-x64-gnu + - run: pnpm --filter @scriptc/compiler --filter scriptc build + - name: Check out tsc-ts + run: | + git clone --quiet --depth 1 https://github.com/vercel-labs/tsc-ts "$RUNNER_TEMP/tsc-ts" + echo "tsc-ts $(git -C "$RUNNER_TEMP/tsc-ts" rev-parse HEAD)" + npm ci --prefix "$RUNNER_TEMP/tsc-ts" + - name: Collect the profile + run: >- + node scripts/refresh-cli-profile.mjs --tsc-ts "$RUNNER_TEMP/tsc-ts" --jobs 4 + --output "$RUNNER_TEMP/cli-profile/cli.profdata.gz" + - uses: actions/upload-artifact@v4 + with: + name: cli-profile + path: ${{ runner.temp }}/cli-profile/cli.profdata.gz + retention-days: 7 + if-no-files-found: error + + build-native-packages: + name: Build native package (${{ matrix.platform }}) + needs: [check-release, cli-profile] + # Runs when the profile job fails; the CLI builds then use the committed profile. + if: >- + !cancelled() && needs.check-release.outputs.should_release == 'true' && + needs.check-release.outputs.reuse_artifacts != 'true' strategy: fail-fast: false matrix: @@ -243,6 +292,28 @@ jobs: - name: Build host helper for cross-libc bootstrap if: matrix.seed_helper != '' run: pnpm --filter @scriptc/${{ matrix.seed_helper }} build:native + - name: Download the profile collected on this commit + id: cli-profile + if: matrix.cli != '' + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: cli-profile + path: ${{ runner.temp }}/cli-profile + # Without the artifact, SCRIPTC_NATIVE_PROFILE_USE stays unset and + # build-native-cli.mts uses the committed profile. + - name: Select the native compiler profile + if: matrix.cli != '' + shell: bash + env: + COLLECTED: ${{ steps.cli-profile.outcome == 'success' && format('{0}/cli-profile/cli.profdata.gz', runner.temp) || '' }} + run: | + if [ -n "$COLLECTED" ]; then + echo "SCRIPTC_NATIVE_PROFILE_USE=$COLLECTED" >> "$GITHUB_ENV" + echo "The native CLI builds with the profile collected on $GITHUB_SHA" + else + echo "::warning::No profile was collected on this commit; the native CLI builds with the committed profile" + fi - name: Build native CLI distribution if: matrix.cli != '' env: @@ -558,6 +629,21 @@ jobs: - name: Package standalone compilers run: node scripts/package-native-cli.mjs packages /tmp/scriptc-release-assets + # The profile the native CLIs were built with. Runs that reuse native + # builds from before the cli-profile job, or whose profile job failed, + # have none; their CLIs used the committed profile. + - name: Download the native compiler profile + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RUN: ${{ needs.check-release.outputs.artifact_run }} + VERSION: ${{ needs.check-release.outputs.version }} + run: | + if gh run download "$RUN" --repo "$GITHUB_REPOSITORY" --name cli-profile --dir "$RUNNER_TEMP/cli-profile"; then + mv "$RUNNER_TEMP/cli-profile/cli.profdata.gz" "/tmp/scriptc-release-assets/scriptc-$VERSION-cli.profdata.gz" + else + echo "Run $RUN has no cli-profile artifact" + fi + - name: Create GitHub Release run: | VERSION="${{ needs.check-release.outputs.version }}" diff --git a/RELEASING.md b/RELEASING.md index b6c5239ee..42bfe75ad 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -16,7 +16,7 @@ CI (`.github/workflows/release.yml`) compares the version in `packages/cli/packa The release job builds each pinned LLVM helper, precompiled runtime pack, and native CLI on its matching host. The native CLI bundles its TypeScript checker, LLVM helper, runtime pack, declarations, and comptime evaluator. npm postinstall installs the matching native executable; Node is needed for npm installation but not native compilation or execution. The user's toolchain performs the final platform link. Sanitized builds also use it to instrument program LLVM and compile the C runtime. Platform packages publish ahead of `@scriptc/compiler` and `scriptc`. The GitHub release attaches standalone compiler archives, their SHA-256 checksums, and the surface manifest after npm publication succeeds. -Release builds of the native CLI optimize the compiler with the committed profile `packages/compiler/native/cli.profdata` on every platform. Code the profile does not describe, or describes from older compiler sources, is optimized as without a profile. Refresh the profile on a quiet Linux x64 host when compiler code has changed substantially since the last refresh: `node scripts/refresh-cli-profile.mjs --tsc-ts ` builds an instrumented compiler, runs it on tsc-ts, a slice of the corpus, and the runtime benchmark workloads, and writes the merged sparse profile. Commit the new profile on its own. +Release builds of the native CLI optimize the compiler with a profile collected on the release commit. The `cli-profile` job runs `node scripts/refresh-cli-profile.mjs` on Linux x64, which builds an instrumented compiler, runs it on tsc-ts, a slice of the corpus, and the runtime benchmark workloads, and writes the merged sparse profile. Every platform's CLI build uses that profile (`SCRIPTC_NATIVE_PROFILE_USE`), and the GitHub release attaches it as `scriptc--cli.profdata.gz`. Code the profile does not describe, or describes from older compiler sources, is optimized as without a profile. When the profile job fails, the CLI builds use the committed profile `packages/compiler/native/cli.profdata.gz`, which local and CI builds of the native compiler also use. Refresh the committed profile when compiler code has changed substantially since the last refresh: run `node scripts/refresh-cli-profile.mjs --tsc-ts ` on a quiet Linux x64 host and commit the new profile on its own. Publishing uses npm trusted publishing (OIDC) — there is no npm token secret. Each published package must have a GitHub Actions trusted publisher for diff --git a/scripts/build-native-cli.mts b/scripts/build-native-cli.mts index e539cea1d..0b26b0965 100644 --- a/scripts/build-native-cli.mts +++ b/scripts/build-native-cli.mts @@ -17,8 +17,9 @@ const sanitize = process.env["SCRIPTC_SAN"] === "1"; // SCRIPTC_NATIVE_PROFILE_GENERATE= builds a compiler that writes a // raw profile per run (scripts/refresh-cli-profile.mjs). Optimized builds // otherwise use the committed profile, or the indexed profile that -// SCRIPTC_NATIVE_PROFILE_USE names; an empty SCRIPTC_NATIVE_PROFILE_USE -// builds without one. +// SCRIPTC_NATIVE_PROFILE_USE names (gzip-compressed when the name ends in +// .gz, as refresh-cli-profile.mjs writes it); an empty +// SCRIPTC_NATIVE_PROFILE_USE builds without one. const profileGenerate = process.env["SCRIPTC_NATIVE_PROFILE_GENERATE"] || undefined; const committedProfile = join(root, "packages/compiler/native/cli.profdata.gz"); const requestedProfile = process.env["SCRIPTC_NATIVE_PROFILE_USE"]; @@ -45,10 +46,11 @@ const seed = join(dirname(output), ".scriptc", basename(output) + "-seed"); for (const dir of [bin, lib, seed]) mkdirSync(dir, { recursive: true }); let profileUse: string | undefined; if (useProfile) { - profileUse = requestedProfile; - if (profileUse === undefined) { + profileUse = requestedProfile ?? committedProfile; + if (profileUse.endsWith(".gz")) { + const compressed = profileUse; profileUse = join(seed, "cli.profdata"); - writeFileSync(profileUse, gunzipSync(readFileSync(committedProfile))); + writeFileSync(profileUse, gunzipSync(readFileSync(compressed))); } } const compiler = process.env["SCRIPTC_CC"] ?? target.defaultLinker; diff --git a/scripts/refresh-cli-profile.mjs b/scripts/refresh-cli-profile.mjs index e9554e911..1285b5503 100644 --- a/scripts/refresh-cli-profile.mjs +++ b/scripts/refresh-cli-profile.mjs @@ -1,6 +1,8 @@ #!/usr/bin/env node -/* Refreshes packages/compiler/native/cli.profdata.gz, the profile that release - * builds of the native compiler optimize with (scripts/build-native-cli.mts). +/* Refreshes packages/compiler/native/cli.profdata.gz, the profile that builds + * of the native compiler optimize with (scripts/build-native-cli.mts). The + * release workflow's cli-profile job runs it with --output on the release + * commit, and release builds use that profile instead of the committed one. * * Builds an instrumented native compiler for this Linux host, runs it on the * training builds below, and merges the raw profiles it writes into one @@ -26,7 +28,7 @@ import { writeFileSync, } from "node:fs"; import { cpus, tmpdir } from "node:os"; -import { join, resolve } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; import { constants, gzipSync } from "node:zlib"; @@ -55,6 +57,9 @@ const tscTsPackage = join(tscTs, "packages/tsc-ts"); if (!existsSync(join(tscTsPackage, "src/main.ts"))) throw new Error(`${tscTs} is not a tsc-ts checkout with installed dependencies`); const jobs = Number(values.jobs); +// The output's directory is created up front, before the instrumented build. +const profileOutput = resolve(values.output); +mkdirSync(dirname(profileOutput), { recursive: true }); const work = resolve(values.work ?? mkdtempSync(join(tmpdir(), "scriptc-cli-profile-"))); const raw = join(work, "raw"); const builds = join(work, "builds"); @@ -196,10 +201,12 @@ const merged = spawnSync(scriptc, ["merge-profiles", "--sparse", "-o", indexed, }); if (merged.status !== 0) throw new Error("merge-profiles failed"); // The indexed profile compresses to about an eighth of its size. -const output = resolve(values.output); -writeFileSync(output, gzipSync(readFileSync(indexed), { level: constants.Z_BEST_COMPRESSION })); +writeFileSync( + profileOutput, + gzipSync(readFileSync(indexed), { level: constants.Z_BEST_COMPRESSION }), +); console.log( - `wrote ${output} (${statSync(output).size} bytes; ${statSync(indexed).size} uncompressed)`, + `wrote ${profileOutput} (${statSync(profileOutput).size} bytes; ${statSync(indexed).size} uncompressed)`, ); // A work directory the caller named stays for inspection. if (values.work === undefined) rmSync(work, { recursive: true, force: true }); From 55740d094a6b461abb886be66f395a577fe21205 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 14:33:02 -0500 Subject: [PATCH 077/273] Resolve corpus promises 7503 and 7504 with an explicit undefined tsgo checks JavaScript calls to an untyped Promise's resolve against its one parameter, so resolve() with no argument is an error there while tsc-ts accepts it. The two programs pass undefined, which both checkers accept and which settles the promise the same way. --- tests/corpus/7503-pipeline-untyped-sources.mjs | 10 +++++----- tests/corpus/7504-pipeline-untyped-source-errors.mjs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/corpus/7503-pipeline-untyped-sources.mjs b/tests/corpus/7503-pipeline-untyped-sources.mjs index 3dbc90eee..e676fd607 100644 --- a/tests/corpus/7503-pipeline-untyped-sources.mjs +++ b/tests/corpus/7503-pipeline-untyped-sources.mjs @@ -58,7 +58,7 @@ async function run(name) { queueMicrotask(() => log.push(`${name} microtask`)); pipeline(src, sink(name, objectMode), (err) => { log.push(`${name} done ${err ? err.code ?? err.message : err}`); - resolve(); + resolve(undefined); }); log.push(`${name} after call`); }); @@ -72,7 +72,7 @@ await new Promise((resolve) => src2, new Transform({ transform(c, _e, cb) { cb(null, c.toString().toUpperCase()); } }), sink("transform"), - (err) => { log.push(`transform done ${err}`); resolve(); }, + (err) => { log.push(`transform done ${err}`); resolve(undefined); }, ), ); const src3 = new Set(["f1", "f2"]); @@ -81,7 +81,7 @@ await new Promise((resolve) => src3, async function* (source) { for await (const c of source) yield c + "!"; }, sink("fnstage"), - (err) => { log.push(`fnstage done ${err}`); resolve(); }, + (err) => { log.push(`fnstage done ${err}`); resolve(undefined); }, ), ); // promises form @@ -92,11 +92,11 @@ log.push("promises resolved"); function* bad() { yield "ok"; throw new Error("source failed"); } const src5 = bad(); await new Promise((resolve) => - pipeline(src5, sink("bad"), (err) => { log.push(`bad done ${err && err.message}`); resolve(); }), + pipeline(src5, sink("bad"), (err) => { log.push(`bad done ${err && err.message}`); resolve(undefined); }), ); // a source of non-string values into a byte-mode writable const src6 = [1, 2]; await new Promise((resolve) => - pipeline(src6, sink("numbers"), (err) => { log.push(`numbers done ${err && err.code}`); resolve(); }), + pipeline(src6, sink("numbers"), (err) => { log.push(`numbers done ${err && err.code}`); resolve(undefined); }), ); console.log(log.join("\n")); diff --git a/tests/corpus/7504-pipeline-untyped-source-errors.mjs b/tests/corpus/7504-pipeline-untyped-source-errors.mjs index fcbfa9d60..9c677c011 100644 --- a/tests/corpus/7504-pipeline-untyped-source-errors.mjs +++ b/tests/corpus/7504-pipeline-untyped-source-errors.mjs @@ -30,7 +30,7 @@ await new Promise((resolve) => { }); pipeline(pick(counted("f", 5)), dst, (err) => { console.log("fail done", err && err.message); - resolve(); + resolve(undefined); }); }); @@ -45,7 +45,7 @@ await new Promise((resolve) => { }); pipeline(pick(counted("b", 3)), dst, (err) => { console.log("slow done", err); - resolve(); + resolve(undefined); }); }); @@ -85,6 +85,6 @@ await new Promise((resolve) => { pipeline( pick(new Map([["k", 1]])), new Writable({ objectMode: true, write(c, _e, cb) { console.log("map write", JSON.stringify(c)); cb(); } }), - (err) => { console.log("map done", err); resolve(); }, + (err) => { console.log("map done", err); resolve(undefined); }, ); }); From ab341be202cfcb5d56c802f994045c0c0c45a141 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 15:19:33 -0500 Subject: [PATCH 078/273] Settle BigInt quotients from the top limbs, and vectorize limb shifts A quotient-only BigInt division with a one-limb quotient returns Knuth's estimate without a pass over the operands when the top limbs prove it: with rhat * B + u0 - qhat * d0 at least qhat, a - qhat * b stays positive whatever the lower limbs hold, since they subtract less than qhat * B^(n-2). Otherwise one pass settles the estimate as before. The add and subtract loops process eight limbs a pass, which rebuilds the carry half as often. Left and right shifts, including the operand normalization of the general division, form each limb from two source limbs in a restrict-qualified helper with no dependency between limbs, so the loops vectorize. --- packages/runtime/src/scr_bigint.c | 60 +++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/packages/runtime/src/scr_bigint.c b/packages/runtime/src/scr_bigint.c index 5af2eafaa..69b7009fc 100644 --- a/packages/runtime/src/scr_bigint.c +++ b/packages/runtime/src/scr_bigint.c @@ -269,11 +269,15 @@ static inline bi_limb bi_subc(bi_limb x, bi_limb y, bi_limb *borrow) { static bi_limb bi_add_n(bi_limb *r, const bi_limb *a, const bi_limb *b, size_t n) { bi_limb carry = 0; size_t i = 0; - for (; i + 4 <= n; i += 4) { + for (; i + 8 <= n; i += 8) { r[i] = bi_addc(a[i], b[i], &carry); r[i + 1] = bi_addc(a[i + 1], b[i + 1], &carry); r[i + 2] = bi_addc(a[i + 2], b[i + 2], &carry); r[i + 3] = bi_addc(a[i + 3], b[i + 3], &carry); + r[i + 4] = bi_addc(a[i + 4], b[i + 4], &carry); + r[i + 5] = bi_addc(a[i + 5], b[i + 5], &carry); + r[i + 6] = bi_addc(a[i + 6], b[i + 6], &carry); + r[i + 7] = bi_addc(a[i + 7], b[i + 7], &carry); } for (; i < n; i++) r[i] = bi_addc(a[i], b[i], &carry); return carry; @@ -283,11 +287,15 @@ static bi_limb bi_add_n(bi_limb *r, const bi_limb *a, const bi_limb *b, size_t n static bi_limb bi_sub_n(bi_limb *r, const bi_limb *a, const bi_limb *b, size_t n) { bi_limb borrow = 0; size_t i = 0; - for (; i + 4 <= n; i += 4) { + for (; i + 8 <= n; i += 8) { r[i] = bi_subc(a[i], b[i], &borrow); r[i + 1] = bi_subc(a[i + 1], b[i + 1], &borrow); r[i + 2] = bi_subc(a[i + 2], b[i + 2], &borrow); r[i + 3] = bi_subc(a[i + 3], b[i + 3], &borrow); + r[i + 4] = bi_subc(a[i + 4], b[i + 4], &borrow); + r[i + 5] = bi_subc(a[i + 5], b[i + 5], &borrow); + r[i + 6] = bi_subc(a[i + 6], b[i + 6], &borrow); + r[i + 7] = bi_subc(a[i + 7], b[i + 7], &borrow); } for (; i < n; i++) r[i] = bi_subc(a[i], b[i], &borrow); return borrow; @@ -409,6 +417,23 @@ static bi_limb bi_submul1(bi_limb *r, const bi_limb *a, size_t n, bi_limb m) { return carry; } +/* y[0..n) = the low n limbs of x << shift, for 0 < shift < BI_LIMB_BITS + * and y disjoint from x. Each limb is formed from two source limbs with no + * dependency between limbs, so the loop vectorizes. */ +static void bi_lshift(bi_limb *restrict y, const bi_limb *restrict x, size_t n, unsigned shift) { + unsigned back = BI_LIMB_BITS - shift; + y[0] = x[0] << shift; + for (size_t i = 1; i < n; i++) y[i] = (x[i] << shift) | (x[i - 1] >> back); +} + +/* y[0..n) = x[0..n) >> shift, for 0 < shift < BI_LIMB_BITS and y disjoint + * from x; like bi_lshift, each limb reads two source limbs. */ +static void bi_rshift(bi_limb *restrict y, const bi_limb *restrict x, size_t n, unsigned shift) { + unsigned back = BI_LIMB_BITS - shift; + for (size_t i = 0; i + 1 < n; i++) y[i] = (x[i] >> shift) | (x[i + 1] << back); + y[n - 1] = x[n - 1] >> shift; +} + /* Schoolbook product: r[0..an+bn) = a * b, r disjoint from a and b. */ static void bi_mul_school(bi_limb *r, const bi_limb *a, size_t an, const bi_limb *b, size_t bn) { r[an] = bi_mul1(r, a, an, b[0], 0); @@ -589,11 +614,9 @@ static void bi_divmod_knuth(bi_limb *q, bi_limb *r, const bi_limb *a, size_t an, bi_limb *un = bi_limbs(an + 1 + bn); bi_limb *vn = un + an + 1; if (s) { - for (size_t i = bn - 1; i > 0; i--) vn[i] = (b[i] << s) | (b[i - 1] >> (BI_LIMB_BITS - s)); - vn[0] = b[0] << s; + bi_lshift(vn, b, bn, s); + bi_lshift(un, a, an, s); un[an] = a[an - 1] >> (BI_LIMB_BITS - s); - for (size_t i = an - 1; i > 0; i--) un[i] = (a[i] << s) | (a[i - 1] >> (BI_LIMB_BITS - s)); - un[0] = a[0] << s; } else { memcpy(vn, b, bn * sizeof(bi_limb)); memcpy(un, a, an * sizeof(bi_limb)); @@ -631,8 +654,7 @@ static void bi_divmod_knuth(bi_limb *q, bi_limb *r, const bi_limb *a, size_t an, } if (r) { if (s) { - for (size_t i = 0; i + 1 < bn; i++) r[i] = (un[i] >> s) | (un[i + 1] << (BI_LIMB_BITS - s)); - r[bn - 1] = un[bn - 1] >> s; + bi_rshift(r, un, bn, s); } else { memcpy(r, un, bn * sizeof(bi_limb)); } @@ -674,6 +696,15 @@ static bi_limb bi_div_qlimb(bi_limb *r, const bi_limb *a, const bi_limb *b, size rhat += d1; rhat_big = rhat < d1; } + /* Without a remainder, the top limbs decide when they leave room: + * a - qhat * b >= (rhat * B + u0 - qhat * d0) * B^(n-2) - qhat * B^(n-2) + * as normalized, so a margin of at least qhat proves qhat. */ + if (!r && !rhat_big) { + bi_wide top = ((bi_wide)rhat << BI_LIMB_BITS) | u0, sub = (bi_wide)qhat * d0; + if (top - sub >= qhat) return qhat; + } else if (!r) { + return qhat; /* rhat >= B: the remainder exceeds B^(n-1) >= qhat * b's low part */ + } /* a - qhat * b: negative when the product's high limb or the borrow * reaches past a's n limbs */ bi_limb carry = 0, borrow = 0; @@ -926,13 +957,8 @@ static ScrBigInt *bi_shl_abs(const ScrBigInt *a, size_t count, int sign) { if (!shift) { memcpy(r->limb + words, a->limb, a->len * sizeof(bi_limb)); } else { - bi_limb carry = 0; - for (size_t i = 0; i < a->len; i++) { - bi_limb v = a->limb[i]; - r->limb[i + words] = (v << shift) | carry; - carry = v >> (BI_LIMB_BITS - shift); - } - if (grow) r->limb[n] = carry; + bi_lshift(r->limb + words, a->limb, a->len, shift); + if (grow) r->limb[n] = a->limb[a->len - 1] >> (BI_LIMB_BITS - shift); } r->len = n + grow; r->sign = sign; @@ -955,9 +981,7 @@ static ScrBigInt *bi_shr_abs(const ScrBigInt *a, size_t count, int sign, bool *d if (!shift) { memcpy(r->limb, a->limb + words, n * sizeof(bi_limb)); } else { - for (size_t i = 0; i + 1 < n; i++) - r->limb[i] = (a->limb[i + words] >> shift) | (a->limb[i + words + 1] << (BI_LIMB_BITS - shift)); - r->limb[n - 1] = a->limb[a->len - 1] >> shift; + bi_rshift(r->limb, a->limb + words, n, shift); } return bi_finish(r, n, sign); } From 9b6a25f57e48507b2819867c03f3efc0d557ad4f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 15:20:53 -0500 Subject: [PATCH 079/273] Resolve each package file's module edges once when pruning npm re-exports planNpmStaticReexports visited a file again each time another of its export names was demanded. Every visit resolved the file's import and export declarations again and walked its whole syntax tree for import() and require() calls. The purity check kept only pure trees and the failing root, so once a walk reached an impure file, the next namespace re-export walked the same files again. Each file's edges are now collected once per plan; the calls come from moduleSourceCandidates, which fork and worker discovery already fill. A repeated visit re-checks only the file's namespace re-exports, the only edges whose pruning depends on the demand. Purity is decided per strongly connected component of the module graph and kept for every file, and a walk that reaches an impure file marks every file on its stack impure. The pruned re-exports and the module order are unchanged. --- .../compiler/src/frontend/npm-static-prune.ts | 266 ++++++++++-------- 1 file changed, 154 insertions(+), 112 deletions(-) diff --git a/packages/compiler/src/frontend/npm-static-prune.ts b/packages/compiler/src/frontend/npm-static-prune.ts index 84a58d561..f02a6e0f6 100644 --- a/packages/compiler/src/frontend/npm-static-prune.ts +++ b/packages/compiler/src/frontend/npm-static-prune.ts @@ -11,6 +11,7 @@ import { npmStaticPackageOfPath } from "./npm-static.js"; import { trackedReadFile } from "./input-tracker.js"; import { npmPackageNameOf } from "./workspace-registry.js"; import { typeOnlyNamedImport } from "./type-only-imports.js"; +import { moduleSourceCandidates } from "./module-source-candidates.js"; export function isPrunedNpmReexport(program: ts.Program, stmt: ts.ExportDeclaration): boolean { return program.analysis.prunedNpmReexports.has(stmt); @@ -117,6 +118,21 @@ function pureExternalPackage( type Demand = Set | null; // null requests the entire namespace +/** One module edge of a package file, resolved once per plan. */ +interface ModuleEdge { + spec: string; + dep: ts.SourceFile | null; +} + +/** A file's runtime module edges: its import declarations with the names + * they request (null: the whole module), its `export … from` declarations, + * and its `import()` and `require()` calls with one string argument. */ +interface ModuleEdges { + imports: (ModuleEdge & { names: readonly string[] | null })[]; + reexports: (ModuleEdge & { stmt: ts.ExportDeclaration })[]; + calls: ModuleEdge[]; +} + /** Package files outside the demanded module graph are absent from the * executable program. All ordinary project files retain their preflight * behavior, including files not imported by the entry. */ @@ -151,6 +167,70 @@ export function planNpmStaticReexports( } }; + // The demand walk visits a file again whenever another of its names is + // requested, and every purity check walks the files it reaches: both read + // these edges instead of resolving the file's syntax each time. + const edgeMemo = new Map(); + const edgesOf = (sf: ts.SourceFile): ModuleEdges => { + const cached = edgeMemo.get(sf); + if (cached !== undefined) return cached; + const edges: ModuleEdges = { imports: [], reexports: [], calls: [] }; + for (const stmt of sf.statements.nodes) { + if (ts.isImportDeclaration(stmt)) { + if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue; + const clause = stmt.importClause; + if (clause?.phaseModifier === ts.SyntaxKind.TypeKeyword) continue; + const spec = stmt.moduleSpecifier.text; + let names: string[] | null = null; + if ( + clause !== undefined && + (clause.namedBindings === undefined || + !( + ts.isNamespaceImport(clause.namedBindings) || + (ts.isNamedImports(clause.namedBindings) && + clause.namedBindings.elements.nodes.length === 0) + )) && + // `import { type A }` still loads the module, like `import {}`. + !typeOnlyNamedImport(stmt) + ) { + names = []; + if (clause.name !== undefined) names.push("default"); + if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) { + for (const element of clause.namedBindings.elements.nodes) { + if (!element.isTypeOnly) names.push((element.propertyName ?? element.name).text); + } + } + } + edges.imports.push({ spec, dep: resolveEdge(sf, spec), names }); + } else if (ts.isExportDeclaration(stmt)) { + // `export { type A } from` still loads the module under Node's + // type stripping; only `export type … from` erases. + if ( + stmt.isTypeOnly || + stmt.moduleSpecifier === undefined || + !ts.isStringLiteral(stmt.moduleSpecifier) + ) + continue; + const spec = stmt.moduleSpecifier.text; + edges.reexports.push({ spec, dep: resolveEdge(sf, spec), stmt }); + } + } + for (const node of moduleSourceCandidates(program, sf).calls) { + if (node.arguments.nodes.length !== 1) continue; + const arg = node.arguments.nodes[0]; + if (arg === undefined || !ts.isStringLiteralLike(arg)) continue; + const kind = + node.expression.kind === ts.SyntaxKind.ImportKeyword + ? "import" + : ts.isIdentifier(node.expression) && node.expression.text === "require" + ? "require" + : null; + if (kind !== null) edges.calls.push({ spec: arg.text, dep: resolveEdge(sf, arg.text, kind) }); + } + edgeMemo.set(sf, edges); + return edges; + }; + for (const sf of files) { if (npmStaticPackageOfPath(sf.fileName) === null) request(sf, null); } @@ -178,57 +258,65 @@ export function planNpmStaticReexports( return false; } }; + // An edge this program does not resolve: the conservative + // whole-dependency promise of the external package it names. + const externalPure = (sf: ts.SourceFile, spec: string): boolean => { + const resolved = resolveBareModule(sf.fileName, spec, "js-only"); + const pkg = resolved === null ? null : npmPackageNameOf(resolved.typesFile); + const root = + pkg === null || resolved === null + ? null + : packageRootJsonPath(resolved.typesFile, pkg, packageJsons); + return root !== null && pureExternalPackage(root, packageJsons); + }; // Check the actual imported closure: metadata for one file never vouches // for its side-effectful descendants, even across a package or a cycle. - const pureModuleTree = (root: ts.SourceFile): boolean => { - const cached = pureMemo.get(root); - if (cached !== undefined) return cached; - const visited = new Set(); - const visit = (sf: ts.SourceFile): boolean => { - if (visited.has(sf)) return true; - if (pureMemo.get(sf) === false || !filePure(sf)) return false; - visited.add(sf); - const edges: { spec: string; kind?: "import" | "require" }[] = []; - for (const stmt of sf.statements.nodes) { - if ( - ts.isImportDeclaration(stmt) && - stmt.importClause?.phaseModifier !== ts.SyntaxKind.TypeKeyword && - ts.isStringLiteral(stmt.moduleSpecifier) - ) - edges.push({ spec: stmt.moduleSpecifier.text }); - else if ( - ts.isExportDeclaration(stmt) && - !stmt.isTypeOnly && - stmt.moduleSpecifier && - ts.isStringLiteral(stmt.moduleSpecifier) - ) - edges.push({ spec: stmt.moduleSpecifier.text }); + // A file is pure when every file it reaches is. The walk finds the + // strongly connected components of the module graph (Tarjan): a finished + // component is pure as a whole. On the first impure file the walk stops, + // and every file still on its stack reaches that file, so each is impure. + const order = new Map(); + const stack: ts.SourceFile[] = []; + /** The lowest order the file's walk reaches (Infinity for a finished + * component), or -1 once it reaches an impure file. */ + const visit = (sf: ts.SourceFile): number => { + const known = pureMemo.get(sf); + if (known !== undefined) return known ? Infinity : -1; + const seen = order.get(sf); + if (seen !== undefined) return seen; + if (!filePure(sf)) { + pureMemo.set(sf, false); + return -1; + } + const own = order.size; + order.set(sf, own); + stack.push(sf); + let low = own; + const edges = edgesOf(sf); + for (const list of [edges.imports, edges.reexports, edges.calls]) { + for (const edge of list) { + const reached = + edge.dep !== null ? visit(edge.dep) : externalPure(sf, edge.spec) ? low : -1; + if (reached < 0) return -1; + if (reached < low) low = reached; } - ts.walkPreorder(sf, (node) => { - if (!ts.isCallExpression(node) || node.arguments.nodes.length !== 1) return; - const arg = node.arguments.nodes[0]; - if (!arg || !ts.isStringLiteralLike(arg)) return; - if (node.expression.kind === ts.SyntaxKind.ImportKeyword) - edges.push({ spec: arg.text, kind: "import" }); - else if (ts.isIdentifier(node.expression) && node.expression.text === "require") - edges.push({ spec: arg.text, kind: "require" }); - }); - return edges.every((edge) => { - const dep = resolveEdge(sf, edge.spec, edge.kind); - if (dep !== null) return visit(dep); - const resolved = resolveBareModule(sf.fileName, edge.spec, "js-only"); - const pkg = resolved === null ? null : npmPackageNameOf(resolved.typesFile); - const root = - pkg === null || resolved === null - ? null - : packageRootJsonPath(resolved.typesFile, pkg, packageJsons); - return root !== null && pureExternalPackage(root, packageJsons); - }); - }; - const pure = visit(root); - if (pure) for (const sf of visited) pureMemo.set(sf, true); - else pureMemo.set(root, false); - return pure; + } + if (low === own) { + for (;;) { + const member = stack.pop()!; + order.delete(member); + pureMemo.set(member, true); + if (member === sf) break; + } + } + return low; + }; + const pureModuleTree = (root: ts.SourceFile): boolean => { + if (visit(root) >= 0) return true; + for (const sf of stack) pureMemo.set(sf, false); + stack.length = 0; + order.clear(); + return false; }; const canPrune = ( sf: ts.SourceFile, @@ -245,78 +333,32 @@ export function planNpmStaticReexports( return names !== undefined && names !== null && !names.has(stmt.exportClause.name.text); }; + // A file is queued again only when its demand grows. Its imports and + // calls request the same modules every time; only a namespace re-export + // the new demand names stops being prunable. + const walked = new Set(); while (queue.length > 0) { const sf = queue.shift()!; - for (const stmt of sf.statements.nodes) { - if (ts.isImportDeclaration(stmt)) { - if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue; - const clause = stmt.importClause; - if (clause?.phaseModifier === ts.SyntaxKind.TypeKeyword) continue; - const dep = resolveEdge(sf, stmt.moduleSpecifier.text); - if ( - clause === undefined || - (clause.namedBindings !== undefined && - (ts.isNamespaceImport(clause.namedBindings) || - (ts.isNamedImports(clause.namedBindings) && - clause.namedBindings.elements.nodes.length === 0))) || - // `import { type A }` still loads the module, like `import {}`. - typeOnlyNamedImport(stmt) - ) { - request(dep, null); - } else { - if (clause.name !== undefined) request(dep, "default"); - if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) { - for (const element of clause.namedBindings.elements.nodes) { - if (!element.isTypeOnly) request(dep, (element.propertyName ?? element.name).text); - } - } - } - } else if (ts.isExportDeclaration(stmt)) { - // `export { type A } from` still loads the module under Node's - // type stripping; only `export type … from` erases. - if ( - stmt.isTypeOnly || - stmt.moduleSpecifier === undefined || - !ts.isStringLiteral(stmt.moduleSpecifier) - ) - continue; - const dep = resolveEdge(sf, stmt.moduleSpecifier.text); - if (canPrune(sf, stmt, dep)) continue; - request(dep, null); + const edges = edgesOf(sf); + const first = !walked.has(sf); + walked.add(sf); + if (first) { + for (const edge of edges.imports) { + if (edge.names === null) request(edge.dep, null); + else for (const name of edge.names) request(edge.dep, name); } } - ts.walkPreorder(sf, (node) => { - if (!ts.isCallExpression(node) || node.arguments.nodes.length !== 1) return undefined; - const arg = node.arguments.nodes[0]; - if (arg === undefined || !ts.isStringLiteralLike(arg)) return undefined; - if ( - node.expression.kind === ts.SyntaxKind.ImportKeyword || - (ts.isIdentifier(node.expression) && node.expression.text === "require") - ) { - request( - resolveEdge( - sf, - arg.text, - node.expression.kind === ts.SyntaxKind.ImportKeyword ? "import" : "require", - ), - null, - ); - } - return undefined; - }); + for (const edge of edges.reexports) { + if (canPrune(sf, edge.stmt, edge.dep)) continue; + request(edge.dep, null); + } + if (first) for (const edge of edges.calls) request(edge.dep, null); } const pruned = new Set(); for (const sf of demanded.keys()) { - for (const stmt of sf.statements.nodes) { - if ( - !ts.isExportDeclaration(stmt) || - stmt.moduleSpecifier === undefined || - !ts.isStringLiteral(stmt.moduleSpecifier) - ) - continue; - if (canPrune(sf, stmt, resolveEdge(sf, stmt.moduleSpecifier.text))) pruned.add(stmt); - } + for (const edge of edgesOf(sf).reexports) + if (canPrune(sf, edge.stmt, edge.dep)) pruned.add(edge.stmt); } program.analysis.prunedNpmReexports.clear(); for (const stmt of pruned) program.analysis.prunedNpmReexports.add(stmt); From e5d386add79ad909c1b5d25a06ae1f2b2eed6acb Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 15:27:31 -0500 Subject: [PATCH 080/273] List caches and open pull requests in full before pruning --- .github/workflows/prune-caches.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/prune-caches.yml b/.github/workflows/prune-caches.yml index 008baaa43..1509d56ce 100644 --- a/.github/workflows/prune-caches.yml +++ b/.github/workflows/prune-caches.yml @@ -24,13 +24,18 @@ jobs: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} run: | - open=" $(gh api --paginate "repos/$REPO/pulls?state=open&per_page=100" --jq '.[].number' | tr '\n' ' ') " - gh api --paginate "repos/$REPO/actions/caches?per_page=100" \ - --jq '.actions_caches[] | select(.ref | startswith("refs/pull/")) | "\(.id) \(.ref)"' | + set -euo pipefail + # Read both lists in full before deleting anything: deleting while the cache + # listing pages shifts later pages, and a failed request must stop the job + # instead of leaving open pull requests out of the list. + open=$(gh api --paginate "repos/$REPO/pulls?state=open&per_page=100" --jq '.[].number') + caches=$(gh api --paginate "repos/$REPO/actions/caches?per_page=100" \ + --jq '.actions_caches[] | select(.ref | startswith("refs/pull/")) | "\(.id) \(.ref)"') while read -r id ref; do + [ -n "$id" ] || continue number=$(echo "$ref" | cut -d/ -f3) - case "$open" in - *" $number "*) ;; - *) gh api --method DELETE "repos/$REPO/actions/caches/$id" >/dev/null && echo "deleted cache $id ($ref)" ;; - esac - done + if ! grep -qx "$number" <<<"$open"; then + gh api --method DELETE "repos/$REPO/actions/caches/$id" >/dev/null + echo "deleted cache $id ($ref)" + fi + done <<<"$caches" From dd57655bb14bbc36cc72790b0ca1ef572d572be1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 15:38:16 -0500 Subject: [PATCH 081/273] Parse npm-static declaration and package files only when a rewrite can apply Loading a program with --npm-static parses package files on their own before the checker reads them: - The declaration closure of each named package was walked twice, once for class overloads and once for class properties, and the one-entry parse cache could not serve the second walk. One walk now reads both from each parse. - The nullable class field and Array.find return rewrites parsed every package JavaScript file. They now parse only files whose text contains `class` and `null` (or `find`), or a Unicode escape that could spell them. - The JSDoc namepath rewrite parsed every file containing `#` or `~`. It now parses only files with a namepath segment joined by one of them (`a#b`, `a~b`), the pattern the rewrite replaces. The rewritten sources are unchanged. --- packages/compiler/src/frontend/program.ts | 43 ++++++++-------------- packages/compiler/src/frontend/services.ts | 13 ++++++- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 8e582af89..5a3f3d20c 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -488,43 +488,33 @@ function visitNpmStaticDeclarationClosure( visit(entryPath); } -function npmStaticDeclarationOverloadsOf( +/** The overloads and properties of the classes a package's declaration + * closure declares, read from one parse of each declaration file. */ +function npmStaticDeclarationMetadataOf( services: FrontendServices, entryPath: string, -): NpmStaticDeclarationOverloads { - const classes = new Map>(); +): { overloads: NpmStaticDeclarationOverloads; properties: NpmStaticDeclarationProperties } { + const overloads = new Map>(); + const properties = new Map>(); visitNpmStaticDeclarationClosure(services, entryPath, (path, source) => { - for (const [className, methods] of parseNpmStaticDeclarationOverloads( - services.parse(path, source, "ts"), - )) { + const sourceFile = services.parse(path, source, "ts"); + for (const [className, methods] of parseNpmStaticDeclarationOverloads(sourceFile)) { const target = - classes.get(className) ?? new Map(); - classes.set(className, target); + overloads.get(className) ?? new Map(); + overloads.set(className, target); for (const [methodName, signatures] of methods) { if (!target.has(methodName)) target.set(methodName, signatures); } } - }); - return classes; -} - -function npmStaticDeclarationPropertiesOf( - services: FrontendServices, - entryPath: string, -): NpmStaticDeclarationProperties { - const classes = new Map>(); - visitNpmStaticDeclarationClosure(services, entryPath, (path, source) => { - for (const [className, properties] of parseNpmStaticDeclarationProperties( - services.parse(path, source, "ts"), - )) { - const target = classes.get(className) ?? new Map(); - classes.set(className, target); - for (const [propertyName, type] of properties) { + for (const [className, fields] of parseNpmStaticDeclarationProperties(sourceFile)) { + const target = properties.get(className) ?? new Map(); + properties.set(className, target); + for (const [propertyName, type] of fields) { if (!target.has(propertyName)) target.set(propertyName, type); } } }); - return classes; + return { overloads, properties }; } /** See LoadResult.startupCrash: Node's exact error message, the IR error @@ -1096,8 +1086,7 @@ export function loadProgram( ) { continue; } - const overloads = npmStaticDeclarationOverloadsOf(services, resolved.typesFile); - const properties = npmStaticDeclarationPropertiesOf(services, resolved.typesFile); + const { overloads, properties } = npmStaticDeclarationMetadataOf(services, resolved.typesFile); const classNames = new Set([...overloads.keys(), ...properties.keys()]); if (classNames.size === 0) continue; const runtime = resolveBareModule(entryPath, pkg, "js-only"); diff --git a/packages/compiler/src/frontend/services.ts b/packages/compiler/src/frontend/services.ts index 14b366bfa..a0acb5818 100644 --- a/packages/compiler/src/frontend/services.ts +++ b/packages/compiler/src/frontend/services.ts @@ -20,6 +20,12 @@ import * as syntax from "./npm-static-declaration-syntax.js"; import { normalizeCjsExportsVar } from "./npm-static-cjs-exports-var.js"; import type { SourceFile } from "./ts7/ast-types.js"; +/** Whether the source can contain `word` as a keyword or identifier: it + * holds the word itself or a Unicode escape that might spell it. */ +function spells(source: string, word: string): boolean { + return source.includes(word) || source.includes("\\u"); +} + /** Evaluate an isolated, self-contained TypeScript callback under a finite * budget. Hosts supply execution; shared lowering owns capture checks and * verifies the returned value before baking it into the program. */ @@ -150,13 +156,18 @@ export class FrontendServices { return normalizeCjsExportsVar(this.parse(path, source, "js")); } nullableClassFields(path: string, source: string): syntax.NpmStaticOverloadRewrite | null { + // Rewrites `null`-initialized fields of class declarations. + if (!spells(source, "class") || !spells(source, "null")) return null; return syntax.applyNpmStaticNullableClassFields(this.parse(path, source, "js"), source); } jsDocNamepaths(path: string, source: string): string | null { - if (!source.includes("~") && !source.includes("#")) return null; + // A namepath segment joined by `#` or `~` (applyNpmStaticJsDocNamepaths). + if (!/[$\w][#~][$A-Z_a-z]/.test(source)) return null; return syntax.applyNpmStaticJsDocNamepaths(this.parse(path, source, "js"), source); } findReturnWidening(path: string, source: string): syntax.NpmStaticOverloadRewrite | null { + // Widens `find` calls returned by methods of class declarations. + if (!spells(source, "class") || !spells(source, "find")) return null; return syntax.applyNpmStaticFindReturnWidening(this.parse(path, source, "js"), source); } declarationProperties( From d4fcd787a8f6f11b3ea4a7be786b06f7d06f799c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 15:38:37 -0500 Subject: [PATCH 082/273] Reference the unwinder weakly from the runtime's raise scr_unwind.c is linked into every Linux executable, and its references to _Unwind_ForcedUnwind and _Unwind_GetCFA pulled the unwinder out of libgcc_eh (or Zig's libunwind) even for programs that never raise, which grew a static hello world past its size ceiling. The references are now weak: programs that raise reference the personality routine from their landing pads, and its unit links the unwinder in. --- packages/runtime/src/scr_unwind.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/scr_unwind.c b/packages/runtime/src/scr_unwind.c index 9bdf2dbd2..50474e82e 100644 --- a/packages/runtime/src/scr_unwind.c +++ b/packages/runtime/src/scr_unwind.c @@ -10,6 +10,13 @@ #include #include +/* Weak, so the runtime unit alone does not link the unwinder into every + * program: programs that raise reference the personality routine from + * their landing pads, and its unit references the unwinder's entry points, + * which link it in. */ +#pragma weak _Unwind_ForcedUnwind +#pragma weak _Unwind_GetCFA + #if defined(__SANITIZE_ADDRESS__) #define SCR_UNWIND_ASAN 1 #elif defined(__has_feature) @@ -69,7 +76,7 @@ void scr_unwind_raise(void) { #ifdef SCR_UNWIND_ASAN scr_unwind_low = (uintptr_t)__builtin_frame_address(0); #endif - _Unwind_ForcedUnwind(exc, scr_unwind_stop, NULL); + if (_Unwind_ForcedUnwind != NULL) _Unwind_ForcedUnwind(exc, scr_unwind_stop, NULL); fputs("scriptc: the native unwinder could not raise an exception\n", stderr); abort(); } From d837036f87a64711db49b138fc5dd009b788aebe Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 24 Oct 2026 11:27:25 -0700 Subject: [PATCH 083/273] Report constant object graphs in untyped JavaScript (static objects, stage 1) SCRIPTC_DEBUG_SHAPES=const reports the constant object graphs shape inference finds (DESIGN-constant-objects.md, stage 1): objects created by code that runs at most once (module top level, IIFEs, functions reached only through one call site from such code) whose fields are written only there, that never meet a reflective or computed write, and that never escape. Namespaces (`var ns = ns || {}; ns.util = {}`), revealing modules, `module.exports` tables, and ES5 constructors' prototype objects qualify. Each near miss names its rule: allocated repeatedly, created by an init factory (one object per call site), escapes, reflective or computed write, literal form, written after init, or lazy cache. The report counts the member reads and calls a later stage can fold. The analysis now models ES5 constructors (a function's prototype object, instances reading through it) and links CommonJS require() and export bindings to the analyzed module's exports, which it previously read as nothing. Code generation is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/shape-inference/analyze.ts | 111 ++++- .../shape-inference/constant-graphs.ts | 421 ++++++++++++++++++ .../src/frontend/shape-inference/index.ts | 11 +- .../compiler/test/constant-graphs.test.ts | 106 +++++ 4 files changed, 642 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/src/frontend/shape-inference/constant-graphs.ts create mode 100644 packages/compiler/test/constant-graphs.test.ts diff --git a/packages/compiler/src/frontend/shape-inference/analyze.ts b/packages/compiler/src/frontend/shape-inference/analyze.ts index 55e0e0107..ee07098e7 100644 --- a/packages/compiler/src/frontend/shape-inference/analyze.ts +++ b/packages/compiler/src/frontend/shape-inference/analyze.ts @@ -100,6 +100,8 @@ export interface CallEdge { readonly caller: FunctionInfo | null; readonly callee: FunctionInfo; readonly inLoop: boolean; + /** The call (or `new`, getter read, builtin callback) site. */ + readonly site: Node; } export interface ShapeAnalysis { @@ -123,6 +125,10 @@ export interface ShapeAnalysis { readonly steps: number; readonly topCause: ReadonlyMap; readonly nodeLabel: ReadonlyMap; + /** Functions unknown code may call (their atoms escaped). */ + readonly escapedFunctions: ReadonlySet; + /** ES5 constructors: each function's default prototype object. */ + readonly protoOf: ReadonlyMap; } /** Array.prototype members: other names on an array are own properties. */ @@ -298,6 +304,7 @@ class Analyzer { caller: ctx?.caller ?? null, callee, inLoop: (ctx?.inLoop ?? false) || this.forceLoop, + site, }); } @@ -340,6 +347,8 @@ class Analyzer { steps: this.steps, topCause: this.topCause, nodeLabel: this.nodeLabel, + escapedFunctions: this.escapedFunctionCalls, + protoOf: this.protoOf, }; } @@ -413,6 +422,25 @@ class Analyzer { return a; } + /** ES5 constructor functions: the function's default prototype object, + * and which function each `new F()` instance atom belongs to. */ + readonly protoOf = new Map(); + readonly fnOfInstance = new Map(); + protoAtom(fn: Atom): Atom { + let p = this.protoOf.get(fn); + if (p === undefined) { + p = this.atoms.intern(`proto:${this.nodeKey(fn.node!)}:${fn.ctx ?? ""}`, () => ({ + kind: "object", + label: `${fn.label}.prototype`, + node: fn.node, + })); + this.protoOf.set(fn, p); + this.add(this.field(p, "constructor"), fn); + if (this.escaped.has(fn)) this.escape(p, `via ${fn.label}`); + } + return p; + } + builtin(path: string, receiver?: Atom): Atom { const key = receiver === undefined ? `builtin:${path}` : `builtin:${path}@${receiver.id}`; return this.atoms.intern(key, () => ({ kind: "builtin", label: path, path, receiver })); @@ -477,6 +505,11 @@ class Analyzer { if (this.escaped.has(atom)) return; this.escaped.set(atom, reason); const via = `via ${atom.label}`; + // Unknown code reaches a constructor's prototype through the function + // and through its instances. + const ctor = atom.kind === "function" ? atom : this.fnOfInstance.get(atom); + if (ctor !== undefined && ctor.node !== undefined && (atom !== ctor || this.protoOf.has(ctor))) + this.escape(this.protoAtom(ctor), via); const fm = this.fields.get(atom); if (fm !== undefined) { for (const [name, id] of fm) { @@ -1424,6 +1457,15 @@ class Analyzer { ) return this.constNode(this.top); if (ts.isSourceFile(decl) || ts.isModuleDeclaration(decl)) return this.namespaceNode(decl); + // A CommonJS export the checker resolved an import or require binding + // to (`module.exports = { name }`, `exports.name = v`): the member of the + // module's exports. + const exported = cjsExportName(decl); + if (exported !== null && decl.getSourceFile() !== id.getSourceFile()) { + const dst = this.newNode(); + this.loadConstraint(this.namespaceNode(decl.getSourceFile()), exported, dst, id); + return dst; + } return this.narrowed(id, decl, this.binding(decl)); } @@ -1531,6 +1573,9 @@ class Analyzer { } private readonly namespaces = new Map(); + /** Module namespaces: a CommonJS module's `module.exports` value is the + * namespace's `exports` field, and member reads see its members too. */ + private readonly namespaceAtoms = new Set(); /** `import * as ns`: an object whose fields are the module's exports. */ namespaceNode(mod: Node): number { const existing = this.namespaces.get(mod); @@ -1542,6 +1587,7 @@ class Analyzer { label: `namespace ${shortFile((mod as ts.SourceFile).fileName ?? "")}`, node: mod, })); + this.namespaceAtoms.add(atom); this.add(n, atom); const sym = this.checker.getSymbolAtLocation(mod); if (sym !== undefined) { @@ -1983,8 +2029,11 @@ class Analyzer { case "function": if (name === "call" || name === "apply" || name === "bind") this.add(dst, this.builtin(`function.${name}`, a)); - else if (name === "prototype") this.add(dst, this.top); - else if (name === "name") this.add(dst, this.prim("string")); + else if (name === "prototype") { + // The default prototype object, or what the program assigns. + this.add(dst, this.protoAtom(a)); + this.edge(this.field(a, "prototype"), dst); + } else if (name === "name") this.add(dst, this.prim("string")); else if (name === "length") this.add(dst, this.prim("number")); else this.edge(this.field(a, name), dst); return; @@ -1994,12 +2043,28 @@ class Analyzer { else if (ARRAY_METHODS.has(name)) this.add(dst, this.builtin(`array.${name}`, a)); else this.edge(this.field(a, name), dst); return; - case "object": + case "object": { this.edge(this.field(a, name), dst); this.edge(this.anyField(a), dst); + if (this.namespaceAtoms.has(a) && name !== "exports") { + this.loadConstraint(this.field(a, "exports"), name, dst, site); + } + // `new F()` instances read through F's prototype objects. + const fn = this.fnOfInstance.get(a); + if (fn !== undefined) { + const protos = this.newNode(); + this.add(protos, this.protoAtom(fn)); + this.edge(this.field(fn, "prototype"), protos); + this.on(protos, (proto) => { + if (proto.kind === "object") this.edge(this.field(proto, name), dst); + else if (proto.kind === "top") this.add(dst, this.top); + }); + return; + } if (name === "constructor" || name === "hasOwnProperty" || name === "toString") this.add(dst, this.builtin(`object.${name}`, a)); return; + } case "dictionary": this.edge(this.anyField(a), dst); this.edge(this.field(a, name), dst); @@ -2190,6 +2255,27 @@ class Analyzer { for (const a of args) this.sink(a, `super(...) at ${locLabel(e)}`); return dst; } + // CommonJS `require("")` of an analyzed module: its exports (the + // module's namespace, which holds `module.exports` as its `exports` + // field and the checker's view of its exports). + if (ts.isIdentifier(callee) && callee.text === "require" && e.arguments.nodes.length === 1) { + const spec = e.arguments.nodes[0]!; + if (ts.isStringLiteral(spec)) { + let target: Node | undefined; + try { + const sym = this.checker.getSymbolAtLocation(spec); + target = sym?.valueDeclaration ?? sym?.declarations[0]; + } catch { + target = undefined; + } + if (target !== undefined && ts.isSourceFile(target) && this.files.includes(target)) { + const ns = this.namespaceNode(target); + this.edge(ns, dst); + this.on(ns, (a) => this.edge(this.field(a, "exports"), dst)); + return dst; + } + } + } if (ts.isPropertyAccessExpression(callee) || ts.isElementAccessExpression(callee)) { const recvExpr = unparen(callee.expression); const { args, spread } = this.args(e); @@ -2286,6 +2372,7 @@ class Analyzer { label: `new ${a.label}`, node: a.node, })); + this.fnOfInstance.set(inst, a); this.add(dst, inst); const info = this.infoOfAtom(a); if (info !== undefined) { @@ -2862,3 +2949,21 @@ class Analyzer { } } } + +/** The export name of a CommonJS export declaration the checker resolves + * bindings to: a member of the `module.exports` literal, or the target of + * `exports.name =` / `module.exports.name =`. */ +function cjsExportName(decl: Node): string | null { + if (ts.isShorthandPropertyAssignment(decl) || ts.isPropertyAssignment(decl)) { + const n = decl.name; + return ts.isIdentifier(n) || ts.isStringLiteral(n) ? n.text : null; + } + if (ts.isPropertyAccessExpression(decl) && ts.isIdentifier(decl.name)) return decl.name.text; + if ( + ts.isBinaryExpression(decl) && + ts.isPropertyAccessExpression(decl.left) && + ts.isIdentifier(decl.left.name) + ) + return decl.left.name.text; + return null; +} diff --git a/packages/compiler/src/frontend/shape-inference/constant-graphs.ts b/packages/compiler/src/frontend/shape-inference/constant-graphs.ts new file mode 100644 index 000000000..dffa8d17d --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/constant-graphs.ts @@ -0,0 +1,421 @@ +/* Static-object specialization, stage 1 (DESIGN-constant-objects.md): + * constant object graphs. + * + * An object literal is a SINGLETON when the code that creates it runs at + * most once: at module top level, or in a function every call path to which + * starts at module evaluation through exactly one call site outside loops + * (IIFEs, revealing modules, init helpers). A singleton's field is CONSTANT + * when every store to it runs in such once-only code, no reflective or + * computed write reaches the object, and the object never escapes to code + * the analysis cannot see. A constant object has only constant fields and a + * fixed key set; constant objects whose constant fields point at each other + * form a constant graph (`ns`, `ns.util`, `ns.util.trim`). + * + * This stage computes the graphs and reports them, with the rule that + * demoted every near miss, and the reads and calls a later stage could fold. + * Nothing reaches code generation. */ +import * as ts from "../ts7/adapter.js"; +import type { Atom } from "./domain.js"; +import type { FunctionInfo, Op, ShapeAnalysis } from "./analyze.js"; +import { npmStaticPackageOfPath } from "../npm-static.js"; + +type Node = ts.Node; + +export interface ConstantField { + readonly key: string; + /** Distinct atoms the field may hold. */ + readonly values: readonly Atom[]; + /** Stores (the literal's own member included). */ + readonly stores: number; +} + +/** An object the analysis tracks as one allocation: an object literal, a + * function object (a namespace or an ES5 constructor), or a constructor's + * default prototype object. */ +export interface Candidate { + readonly atom: Atom; + /** The syntax that creates it (the literal or the function). */ + readonly node: Node; + readonly kind: "literal" | "function" | "prototype"; + /** Keys the creating syntax itself defines. */ + readonly keys: readonly string[]; + /** Whether the creating syntax is a plain data literal. */ + readonly plain: boolean; + readonly inPackage: boolean; +} + +export interface ConstantObject { + readonly site: Candidate; + readonly fields: readonly ConstantField[]; + readonly frozen: boolean; +} + +export interface NearMiss { + readonly site: Candidate; + /** Null when the whole object was demoted. */ + readonly key: string | null; + readonly rule: + | "init-factory" + | "allocated-repeatedly" + | "escapes" + | "reflective-write" + | "computed-write" + | "literal-form" + | "written-after-init" + | "lazy-cache"; + readonly detail: string; +} + +export interface ConstantGraphs { + readonly objects: readonly ConstantObject[]; + readonly nearMisses: readonly NearMiss[]; + /** Reads `e.k` whose receiver is exactly one constant object and whose + * field is constant, and the subset that are callees. */ + readonly foldableReads: number; + readonly foldableCalls: number; + /** Reads of a constant field through a receiver that may also be other + * values. */ + readonly mixedReads: number; + readonly once: (fn: Node) => boolean; +} + +function isLoop(n: Node): boolean { + return ( + ts.isForStatement(n) || + ts.isForInStatement(n) || + ts.isForOfStatement(n) || + ts.isWhileStatement(n) || + ts.isDoStatement(n) + ); +} + +/** The function (or source file) whose body contains `n`. */ +function ownerFunction(n: Node): Node { + for (let p = n.parent; p !== undefined; p = p.parent) { + if (ts.isSourceFile(p)) return p; + if (ts.isFunctionLike(p) && (p as { body?: Node }).body !== undefined) return p; + } + return n.getSourceFile(); +} + +/** Whether `n` sits in a loop of its own function's body. */ +function inLoop(n: Node, owner: Node): boolean { + for (let p = n.parent; p !== undefined && p !== owner; p = p.parent) if (isLoop(p)) return true; + return false; +} + +function loc(n: Node): string { + const sf = n.getSourceFile(); + const file = sf.fileName.split("/node_modules/").pop() ?? sf.fileName; + return `${file.split("/").slice(-2).join("/")}:${sf.getLineAndCharacterOfPosition(ts.tokenStartOfNode(n)).line + 1}`; +} + +/** `x.k || (x.k = v)`, `x.k ??= v`, `x.k ||= v`, `if (!x.k) x.k = v`. */ +function isCacheStore(write: Node): boolean { + let assign: Node | undefined = write.parent; + while (assign !== undefined && ts.isParenthesizedExpression(assign)) assign = assign.parent; + if (assign === undefined || !ts.isBinaryExpression(assign)) return false; + const op = assign.operatorToken.kind; + if (op === ts.SyntaxKind.BarBarEqualsToken || op === ts.SyntaxKind.QuestionQuestionEqualsToken) + return true; + if (op !== ts.SyntaxKind.EqualsToken) return false; + const text = write.getText(); + let up: Node | undefined = assign.parent; + while (up !== undefined && ts.isParenthesizedExpression(up)) up = up.parent; + if ( + up !== undefined && + ts.isBinaryExpression(up) && + (up.operatorToken.kind === ts.SyntaxKind.BarBarToken || + up.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) && + up.left.getText() === text + ) + return true; + for (let p: Node | undefined = assign.parent; p !== undefined; p = p.parent) { + if (ts.isIfStatement(p)) { + const cond = p.expression.getText().replace(/\s+/g, ""); + return cond === `!${text}` || cond === `${text}===undefined` || cond === `${text}==null`; + } + if (ts.isFunctionLike(p) || ts.isSourceFile(p)) break; + } + return false; +} + +const OBJECT_DEMOTING: ReadonlySet = new Set([ + "computedWrite", + "delete", + "defineProperty", + "setPrototypeOf", + "assign", + "proxy", +]); + +export function computeConstantGraphs(analysis: ShapeAnalysis): ConstantGraphs { + const infosByNode = new Map(); + for (const info of analysis.functions.values()) { + let l = infosByNode.get(info.node); + if (l === undefined) infosByNode.set(info.node, (l = [])); + l.push(info); + } + const edgesTo = new Map(); + for (const e of analysis.calls) { + let l = edgesTo.get(e.callee.node); + if (l === undefined) edgesTo.set(e.callee.node, (l = [])); + l.push(e); + } + const escapedNodes = new Set([...analysis.escapedFunctions].map((f) => f.node)); + + const onceMemo = new Map(); + const once = (fn: Node): boolean => { + if (ts.isSourceFile(fn)) return true; + const known = onceMemo.get(fn); + if (known !== undefined) return known; + onceMemo.set(fn, false); // a cycle is not once-only + let result = false; + const edges = edgesTo.get(fn) ?? []; + if ( + infosByNode.has(fn) && + !escapedNodes.has(fn) && + edges.length > 0 && + new Set(edges.map((e) => e.site)).size === 1 + ) { + result = edges.every( + (e) => + !e.inLoop && e.caller !== null && !inLoop(e.site, e.caller.node) && once(e.caller.node), + ); + } + onceMemo.set(fn, result); + return result; + }; + + // Whether every call of a function comes from once-only code outside + // loops, through any number of call sites: an init factory, whose + // allocations are one object per call site (schema builders called at + // module initialization). + const initOnlyMemo = new Map(); + const initOnly = (fn: Node): boolean => { + if (ts.isSourceFile(fn)) return true; + const known = initOnlyMemo.get(fn); + if (known !== undefined) return known; + initOnlyMemo.set(fn, false); + const edges = edgesTo.get(fn) ?? []; + const result = + infosByNode.has(fn) && + !escapedNodes.has(fn) && + edges.length > 0 && + edges.every( + (e) => + !e.inLoop && + e.caller !== null && + !inLoop(e.site, e.caller.node) && + (once(e.caller.node) || initOnly(e.caller.node)), + ); + initOnlyMemo.set(fn, result); + return result; + }; + + const opsByAtom = new Map(); + for (const op of analysis.ops) { + for (const a of analysis.nodes[op.recv]!.atoms) { + let l = opsByAtom.get(a); + if (l === undefined) opsByAtom.set(a, (l = [])); + l.push(op); + } + } + + const objects: ConstantObject[] = []; + const nearMisses: NearMiss[] = []; + const constantKeys = new Map>(); + const candidates: Candidate[] = []; + for (const site of analysis.sites) { + if (site.kind !== "object") continue; + candidates.push({ + atom: site.atom, + node: site.node, + kind: "literal", + keys: site.keys ?? [], + plain: !( + site.hasAccessor || + site.hasComputedKey || + site.spreads.length > 0 || + site.keys === null + ), + inPackage: site.inPackage, + }); + } + const inPackage = (n: Node): boolean => + npmStaticPackageOfPath(n.getSourceFile().fileName) !== null; + for (const atom of analysis.atoms.atoms) { + if (atom.kind !== "function" || atom.node === undefined) continue; + const hasFields = (analysis.fields.get(atom)?.size ?? 0) > 0; + const proto = analysis.protoOf.get(atom); + if (!hasFields && proto === undefined) continue; + candidates.push({ + atom, + node: atom.node, + kind: "function", + keys: [], + plain: true, + inPackage: inPackage(atom.node), + }); + if (proto !== undefined) + candidates.push({ + atom: proto, + node: atom.node, + kind: "prototype", + keys: ["constructor"], + plain: true, + inPackage: inPackage(atom.node), + }); + } + for (const site of candidates) { + // A function object is created where its expression or declaration + // runs; its prototype with it. + const owner = ownerFunction(site.node); + if (!once(owner) || inLoop(site.node, owner)) { + const factory = !inLoop(site.node, owner) && initOnly(owner); + if (site.inPackage || factory) + nearMisses.push({ + site, + key: null, + rule: factory ? "init-factory" : "allocated-repeatedly", + detail: factory + ? `one object per call of ${loc(owner)} (${new Set((edgesTo.get(owner) ?? []).map((e) => e.site)).size} call sites)` + : `created in ${owner.kind === ts.SyntaxKind.SourceFile ? "a module loop" : loc(owner)}`, + }); + continue; + } + const escaped = analysis.escaped.get(site.atom); + if (escaped !== undefined) { + nearMisses.push({ site, key: null, rule: "escapes", detail: escaped }); + continue; + } + if (!site.plain) { + nearMisses.push({ + site, + key: null, + rule: "literal-form", + detail: "accessor, computed key or spread in the literal", + }); + continue; + } + const ops = opsByAtom.get(site.atom) ?? []; + const demoting = ops.find((op) => OBJECT_DEMOTING.has(op.kind)); + if (demoting !== undefined) { + nearMisses.push({ + site, + key: null, + rule: demoting.kind === "computedWrite" ? "computed-write" : "reflective-write", + detail: `${demoting.kind} at ${loc(demoting.node)}`, + }); + continue; + } + const frozen = ops.some((op) => op.kind === "freeze"); + const stores = new Map(); + for (const k of site.keys) stores.set(k, 1); + // A function's own data fields come only from stores. + const demotedKeys = new Map(); + for (const op of ops) { + // A `length` store on an object (not an array) is an ordinary write. + if ((op.kind !== "write" && op.kind !== "lengthWrite") || op.name === null) continue; + stores.set(op.name, (stores.get(op.name) ?? 0) + 1); + const fn = ownerFunction(op.node); + if (once(fn) && !inLoop(op.node, fn)) continue; + if (demotedKeys.has(op.name)) continue; + demotedKeys.set(op.name, { + site, + key: op.name, + rule: isCacheStore(op.node) ? "lazy-cache" : "written-after-init", + detail: loc(op.node), + }); + } + for (const miss of demotedKeys.values()) nearMisses.push(miss); + const fieldMap = analysis.fields.get(site.atom) ?? new Map(); + const fields: ConstantField[] = []; + const keys = new Set(); + for (const [key, count] of stores) { + if (demotedKeys.has(key)) continue; + const id = fieldMap.get(key); + const values = id === undefined ? [] : [...analysis.nodes[id]!.atoms]; + fields.push({ key, values, stores: count }); + keys.add(key); + } + constantKeys.set(site.atom, keys); + if (demotedKeys.size === 0) objects.push({ site, fields, frozen }); + } + + let foldableReads = 0, + foldableCalls = 0, + mixedReads = 0; + for (const op of analysis.ops) { + if (op.kind !== "read" || op.name === null) continue; + const atoms = analysis.nodes[op.recv]!.atoms; + const constant = [...atoms].filter((a) => constantKeys.get(a)?.has(op.name!) === true); + if (constant.length === 0) continue; + if (atoms.size === 1) { + foldableReads++; + const p = op.node.parent; + if (p !== undefined && ts.isCallExpression(p) && p.expression === op.node) foldableCalls++; + } else mixedReads++; + } + return { objects, nearMisses, foldableReads, foldableCalls, mixedReads, once }; +} + +/** Report lines (SCRIPTC_DEBUG_SHAPES=const): per package, the constant + * graphs with their roots, the near misses by rule, and the foldable reads. */ +export function constantGraphReport(analysis: ShapeAnalysis, graphs: ConstantGraphs): string[] { + const lines: string[] = []; + const pkgOf = (site: Candidate): string => + npmStaticPackageOfPath(site.node.getSourceFile().fileName) ?? ""; + const byPkg = new Map(); + const entry = (pkg: string) => { + let e = byPkg.get(pkg); + if (e === undefined) byPkg.set(pkg, (e = { objects: [], misses: [] })); + return e; + }; + for (const o of graphs.objects) entry(pkgOf(o.site)).objects.push(o); + for (const m of graphs.nearMisses) entry(pkgOf(m.site)).misses.push(m); + const constantAtoms = new Set(graphs.objects.map((o) => o.site.atom)); + const referenced = new Set(); + for (const o of graphs.objects) + for (const f of o.fields) + if (f.values.length === 1 && constantAtoms.has(f.values[0]!)) referenced.add(f.values[0]!); + const describe = (o: ConstantObject, depth: number, seen: Set): string => { + const parts = o.fields.map((f) => { + if (f.values.length !== 1) + return `${f.key}: ${f.values.length === 0 ? "?" : f.values.map((a) => a.label).join("|")}`; + const v = f.values[0]!; + const child = graphs.objects.find((x) => x.site.atom === v); + if (child !== undefined && depth < 3 && !seen.has(v)) { + seen.add(v); + return `${f.key}: ${describe(child, depth + 1, seen)}`; + } + return `${f.key}: ${v.kind === "function" && child === undefined ? "fn" : v.label}`; + }); + const shown = parts.length > 8 ? [...parts.slice(0, 8), `… ${parts.length - 8} more`] : parts; + return `{${shown.join(", ")}}${o.frozen ? " (frozen)" : ""}`; + }; + lines.push( + `analysis ${analysis.steps} steps${analysis.budgetExceeded ? " (BUDGET EXCEEDED: facts incomplete)" : ""}, ${analysis.files.length} files`, + ); + lines.push( + `constant objects ${graphs.objects.length}, near misses ${graphs.nearMisses.length}; foldable reads ${graphs.foldableReads} (calls ${graphs.foldableCalls}), mixed-receiver reads ${graphs.mixedReads}`, + ); + for (const [pkg, e] of [...byPkg].sort((a, b) => a[0].localeCompare(b[0]))) { + const fields = e.objects.reduce((n, o) => n + o.fields.length, 0); + const rules = new Map(); + for (const m of e.misses) rules.set(m.rule, (rules.get(m.rule) ?? 0) + 1); + lines.push( + `${pkg}: ${e.objects.length} constant objects (${fields} fields); near misses ${[...rules].map(([r, n]) => `${r} ${n}`).join(", ") || "none"}`, + ); + for (const o of e.objects) { + if (referenced.has(o.site.atom)) continue; + lines.push(` graph ${loc(o.site.node)} ${describe(o, 0, new Set([o.site.atom]))}`); + } + for (const m of e.misses.filter((x) => x.rule !== "allocated-repeatedly").slice(0, 40)) { + lines.push( + ` near miss ${loc(m.site.node)}${m.key === null ? "" : ` .${m.key}`}: ${m.rule} (${m.detail})`, + ); + } + } + return lines; +} diff --git a/packages/compiler/src/frontend/shape-inference/index.ts b/packages/compiler/src/frontend/shape-inference/index.ts index 88c2100ee..402a788b1 100644 --- a/packages/compiler/src/frontend/shape-inference/index.ts +++ b/packages/compiler/src/frontend/shape-inference/index.ts @@ -3,6 +3,7 @@ import type * as ts from "../ts7/adapter.js"; import { analyzeShapes } from "./analyze.js"; import { buildShapeReport } from "./report.js"; +import { computeConstantGraphs, constantGraphReport } from "./constant-graphs.js"; export { analyzeShapes } from "./analyze.js"; export { buildShapeReport } from "./report.js"; @@ -13,9 +14,11 @@ export { buildShapeReport } from "./report.js"; export function reportShapes(program: ts.Program, filter: string): void { const start = performance.now(); const analysis = analyzeShapes(program, program.getTypeChecker()); - const report = buildShapeReport(analysis, filter); + // `const`: the constant object graphs (DESIGN-constant-objects.md, stage 1). + const lines = + filter === "const" + ? constantGraphReport(analysis, computeConstantGraphs(analysis)) + : buildShapeReport(analysis, filter).lines; const ms = (performance.now() - start).toFixed(0); - process.stderr.write( - `[shapes] ${ms} ms\n${report.lines.map((l) => `[shapes] ${l}`).join("\n")}\n`, - ); + process.stderr.write(`[shapes] ${ms} ms\n${lines.map((l) => `[shapes] ${l}`).join("\n")}\n`); } diff --git a/packages/compiler/test/constant-graphs.test.ts b/packages/compiler/test/constant-graphs.test.ts new file mode 100644 index 000000000..258c83b08 --- /dev/null +++ b/packages/compiler/test/constant-graphs.test.ts @@ -0,0 +1,106 @@ +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, test, vi } from "vitest"; +import { compile } from "../src/index.js"; + +// A library in the pre-module idiom: guarded namespace declarations, a +// revealing module, an ES5 constructor with prototype methods, and the +// cases that keep an object from being constant. +const packageSource = ` +var lib = lib || {}; +lib.math = lib.math || {}; +lib.math.TAU = 6.283185307179586; +lib.math.half = function (x) { return x / 2; }; +lib.text = (function () { + function upper(s) { return s.toUpperCase(); } + return { upper: upper }; +})(); +lib.Point = function (x, y) { this.x = x; this.y = y; }; +lib.Point.prototype.norm = function () { return Math.sqrt(this.x * this.x + this.y * this.y); }; +lib.counter = { hits: 0 }; +lib.bump = function () { lib.counter.hits++; }; +lib.cache = { table: null }; +lib.lookup = function () { return lib.cache.table || (lib.cache.table = [1, 2, 3]); }; +lib.scratch = { a: 1, b: 2 }; +delete lib.scratch.b; +lib.shared = { value: 1 }; +globalThis.sharedFromLib = lib.shared; +function option(name) { return { name: name }; } +lib.options = { first: option("a"), second: option("b") }; +module.exports = { lib }; +`; + +const mainSource = ` +const { lib } = require("ns-fixture"); +let total = 0; +for (let i = 0; i < 3; i++) { + total += lib.math.half(i) + new lib.Point(i, i).norm() + lib.lookup().length; + lib.bump(); +} +console.log(total, lib.text.upper("x"), lib.counter.hits); +`; + +let dir: string; +let saved: string | undefined; +beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "scriptc-constant-graphs-")); + const pkg = join(dir, "node_modules", "ns-fixture"); + await mkdir(pkg, { recursive: true }); + await writeFile( + join(pkg, "package.json"), + JSON.stringify({ name: "ns-fixture", version: "1.0.0", main: "index.js", types: "index.d.ts" }), + ); + await writeFile(join(pkg, "index.js"), packageSource); + await writeFile(join(pkg, "index.d.ts"), "declare const lib: any;\nexport { lib };\n"); + await writeFile( + join(dir, "package.json"), + JSON.stringify({ dependencies: { "ns-fixture": "1.0.0" } }), + ); + await writeFile(join(dir, "main.cjs"), mainSource); + saved = process.env["SCRIPTC_DEBUG_SHAPES"]; +}); +afterEach(async () => { + if (saved === undefined) delete process.env["SCRIPTC_DEBUG_SHAPES"]; + else process.env["SCRIPTC_DEBUG_SHAPES"] = saved; + vi.restoreAllMocks(); + await rm(dir, { recursive: true, force: true }); +}); + +async function report(): Promise { + process.env["SCRIPTC_DEBUG_SHAPES"] = "const"; + const lines: string[] = []; + vi.spyOn(process.stderr, "write").mockImplementation((chunk: string | Uint8Array) => { + const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"); + for (const l of text.split("\n")) if (l.startsWith("[shapes] ")) lines.push(l.slice(9)); + return true; + }); + await compile(join(dir, "main.cjs"), { + outDir: dir, + outPath: join(dir, "main.ir.json"), + outputKind: "ir", + npmStatic: ["ns-fixture"], + }); + return lines; +} + +test("constant graphs: namespaces, revealing modules and prototypes, with near misses", async () => { + const lines = await report(); + const text = lines.join("\n"); + // The namespace graph: guarded declarations, the revealing module's + // table, and the constructor function. + expect(text).toMatch( + /graph ns-fixture\/index\.js:\d+ \{lib: \{math: \{TAU: number, half: fn\}, text: \{upper: fn\}/, + ); + // The constructor's prototype object, built by member stores at init. + expect(text).toMatch(/graph ns-fixture\/index\.js:\d+ \{constructor: \{\}, norm: fn\}/); + // A field written by a function called from a loop. + expect(text).toMatch(/near miss ns-fixture\/index\.js:12 \.hits: written-after-init/); + // A lazily filled cache. + expect(text).toMatch(/near miss ns-fixture\/index\.js:14 \.table: lazy-cache/); + // A deleted key, and an object stored into the global object. + expect(text).toMatch(/near miss ns-fixture\/index\.js:16: reflective-write \(delete/); + expect(text).toMatch(/near miss ns-fixture\/index\.js:18: escapes/); + // A factory called from two init sites: one object per call site. + expect(text).toMatch(/near miss ns-fixture\/index\.js:20: init-factory \(.*2 call sites\)/); +}); From 5e7338ad2c5ef69402c9133bf5f18e520e40adcb Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 15:56:52 -0500 Subject: [PATCH 084/273] Scan checker reply strings with many escapes in one pass scriptc_ts7_json_value_end searched for the next quote again after each backslash escape in a string, so a string with many escapes was scanned once per escape. The quote it found now stays the candidate end until an escape skips it, and each byte of a string is scanned once by each search. A test scans strings of 2,000,000 newline, backslash and quote escapes, alone and inside an object, and compares the ends with jsonValueEnd's; before the change it does not finish within 60 s. --- packages/compiler/native/ts7-process.c | 6 +- .../test/native-json-value-end.test.ts | 75 +++++++++++++------ 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/packages/compiler/native/ts7-process.c b/packages/compiler/native/ts7-process.c index 3333cf4e8..eb90f8bd3 100644 --- a/packages/compiler/native/ts7-process.c +++ b/packages/compiler/native/ts7-process.c @@ -418,9 +418,10 @@ int32_t scriptc_ts7_json_value_end(const uint8_t *bytes, size_t size, uint32_t s uint8_t byte = bytes[index]; if (byte == '"') { index++; + // The quote found stays the candidate end until an escape skips it, so + // every byte of the string is scanned once by each search. + const uint8_t *quote = index < size ? memchr(bytes + index, '"', size - index) : NULL; for (;;) { - if (index >= size) return -1; - const uint8_t *quote = memchr(bytes + index, '"', size - index); if (quote == NULL) return -1; const uint8_t *escape = memchr(bytes + index, '\\', (size_t)(quote - (bytes + index))); if (escape == NULL) { @@ -428,6 +429,7 @@ int32_t scriptc_ts7_json_value_end(const uint8_t *bytes, size_t size, uint32_t s break; } index = (size_t)(escape - bytes) + 2; + if (bytes + index > quote) quote = index < size ? memchr(bytes + index, '"', size - index) : NULL; } index++; if (depth == 0) return (int32_t)index; diff --git a/packages/compiler/test/native-json-value-end.test.ts b/packages/compiler/test/native-json-value-end.test.ts index 9d9cc3950..0818fbab6 100644 --- a/packages/compiler/test/native-json-value-end.test.ts +++ b/packages/compiler/test/native-json-value-end.test.ts @@ -76,31 +76,45 @@ function cases(): { bytes: Uint8Array; start: number }[] { return out; } +/** The native scanner's results for `all`, from a harness built in `dir`. */ +function nativeEnds( + dir: string, + all: { bytes: Uint8Array; start: number }[], + timeout?: number, +): number[] { + const harness = join(dir, "scan" + (process.platform === "win32" ? ".exe" : "")); + execFileSync("clang", [ + "-std=c11", + "-Wall", + "-Wextra", + "-Werror", + ...(process.env["SCRIPTC_SAN"] === "1" ? ["-fsanitize=address,undefined"] : []), + join(sources, "ts7-json.test.c"), + join(sources, "ts7-process.c"), + "-o", + harness, + ]); + const chunks: Buffer[] = []; + for (const { bytes, start } of all) { + const header = Buffer.alloc(8); + header.writeUInt32LE(bytes.length, 0); + header.writeUInt32LE(start, 4); + chunks.push(header, Buffer.from(bytes)); + } + const output = execFileSync(harness, { + input: Buffer.concat(chunks), + encoding: "utf8", + maxBuffer: 64 * 1024 * 1024, + ...(timeout === undefined ? {} : { timeout }), + }); + return output.trim().split(/\r?\n/).map(Number); +} + test("the native scanner finds the value ends jsonValueEnd finds", () => { const dir = mkdtempSync(join(tempRoot, "scriptc-ts7-json-")); try { - const harness = join(dir, "scan" + (process.platform === "win32" ? ".exe" : "")); - execFileSync("clang", [ - "-std=c11", - "-Wall", - "-Wextra", - "-Werror", - ...(process.env["SCRIPTC_SAN"] === "1" ? ["-fsanitize=address,undefined"] : []), - join(sources, "ts7-json.test.c"), - join(sources, "ts7-process.c"), - "-o", - harness, - ]); const all = cases(); - const chunks: Buffer[] = []; - for (const { bytes, start } of all) { - const header = Buffer.alloc(8); - header.writeUInt32LE(bytes.length, 0); - header.writeUInt32LE(start, 4); - chunks.push(header, Buffer.from(bytes)); - } - const output = execFileSync(harness, { input: Buffer.concat(chunks), encoding: "utf8" }); - const actual = output.trim().split(/\r?\n/).map(Number); + const actual = nativeEnds(dir, all); expect(actual.length).toBe(all.length); for (let k = 0; k < all.length; k++) { const { bytes, start } = all[k]!; @@ -116,3 +130,22 @@ test("the native scanner finds the value ends jsonValueEnd finds", () => { rmSync(dir, { recursive: true, force: true }); } }); + +test("the native scanner reads strings with many escapes in one pass", () => { + const dir = mkdtempSync(join(tempRoot, "scriptc-ts7-json-")); + try { + // Strings of 2,000,000 escapes each: newlines, backslashes and quotes, + // alone and as a member of an object. + const strings = ["\\n", "\\\\", '\\"'].map((escape) => `"${escape.repeat(2_000_000)}"`); + const all = [ + ...strings.map((text) => ({ bytes: Buffer.from(text, "utf8"), start: 0 })), + ...strings.map((text) => ({ bytes: Buffer.from(`{"value":${text}},1`, "utf8"), start: 0 })), + ]; + // A scan that searched for the closing quote again after each escape + // would take minutes here. + const actual = nativeEnds(dir, all, 60_000); + expect(actual).toEqual(all.map(({ bytes, start }) => expectedEnd(bytes, start))); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); From ebeec4818d680f09278c51aea8ae5a9126dda032 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 15:58:14 -0500 Subject: [PATCH 085/273] Prove 32-bit products exact only below 2^53 The exactness test for a product's 32-bit form multiplied the operands' magnitude bounds in a double and accepted a result of at most 2^53. A bound of 2^53 + 1 (3 * 3002399751580331) rounds to 2^53 there, so a product the double rounds took the integer multiply's low bits: `(3 * 3002399751580331) | 0` printed 1 where Node prints 0. The test is now strict: a bound below 2^53 is exact, and no bound of 2^53 or more rounds below it. --- .../src/backend/llvm/wrapping-sums.ts | 8 +++--- .../8195-wrapping-products-near-2-53.ts.json | 6 +++++ .../8195-wrapping-products-near-2-53.ts | 25 +++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8195-wrapping-products-near-2-53.ts.json create mode 100644 tests/corpus/8195-wrapping-products-near-2-53.ts diff --git a/packages/compiler/src/backend/llvm/wrapping-sums.ts b/packages/compiler/src/backend/llvm/wrapping-sums.ts index e93d66909..5b487d809 100644 --- a/packages/compiler/src/backend/llvm/wrapping-sums.ts +++ b/packages/compiler/src/backend/llvm/wrapping-sums.ts @@ -176,12 +176,14 @@ export function emitWrappingSum( } /** Whether every product of a value in `left` and one in `right` is an - * integer of magnitude at most 2^53, which the double product holds - * exactly. */ + * integer of magnitude below 2^53, which the double product holds exactly. + * The bound's own product is computed in a double: a bound of at least + * 2^53 never rounds below 2^53, while 2^53 + 1 (3 * 3002399751580331) + * rounds to 2^53, so the test is strict. */ function exactProduct(left: IntegerRange, right: IntegerRange): boolean { const magnitude = (range: IntegerRange): number => Math.max(Math.abs(range.min), Math.abs(range.max)); - return magnitude(left) * magnitude(right) <= 2 ** 53; + return magnitude(left) * magnitude(right) < 2 ** 53; } /** The number locals of `fn` that only hold exact integers (never -0) or NaN, with the range of diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8195-wrapping-products-near-2-53.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8195-wrapping-products-near-2-53.ts.json new file mode 100644 index 000000000..234bcebd5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8195-wrapping-products-near-2-53.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8195-wrapping-products-near-2-53.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8195-wrapping-products-near-2-53.ts b/tests/corpus/8195-wrapping-products-near-2-53.ts new file mode 100644 index 000000000..c42104290 --- /dev/null +++ b/tests/corpus/8195-wrapping-products-near-2-53.ts @@ -0,0 +1,25 @@ +// Products that reach bitwise operators with operands whose ranges are +// known, around 2^53: below it the double product is exact, and from it on +// the double product rounds, so the 32-bit result comes from the rounded +// value (3 * 3002399751580331 is 2^53 + 1, which rounds to 2^53). +const pairs: [number, number][] = [ + [3, 3002399751580331], + [-3, 3002399751580331], + [2 ** 26, 2 ** 27], + [94906265, 94906267], + [94906267, 94906267], + [7, 1286742750677284], + [65535, 137443147777], +]; +for (const [a, b] of pairs) console.log(a, b, (a * b) | 0, (a * b) >>> 0, (a * b) ^ 1, (a * b) << 3); + +// The same products with literal operands, which the compiler sees whole. +console.log((3 * 3002399751580331) | 0, (3 * 3002399751580331) >>> 0); +console.log((-3 * 3002399751580331) | 0, (2 ** 26 * 2 ** 27) | 0); +console.log((94906267 * 94906267) | 0, (94906265 * 94906267) >>> 0); + +// A counted loop whose counter multiplies a constant past 2^53. +const step = 1286742750677284; +let h = 0; +for (let k = 5; k < 9; k++) h = (h + ((k * step) | 0)) | 0; +console.log(h); From d813fdafd32dfc999ca38754585f26d40acb34ae Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:00:06 -0500 Subject: [PATCH 086/273] Compare termination signals and resolve the Node checkout path in the test/parallel runner A run that a signal ends has a null exit code, so the runner recorded a pass when Node and the executable ended by different signals, or when one ended by a signal and the other with an exit code compared as null. The verdict now compares both the exit code and the signal (verdict.mjs), and records Node's signal in nodeExit as it does for the executable. A relative --node path also made the minimal lane's fixtures symlink relative to the copied tree, where it does not resolve. The checkout path is now resolved first. tests/harness/node-parallel-runner.test.ts covers both: the termination comparison, and a relative --node path whose fixtures resolve from the minimal tree. --- tests/harness/node-parallel-runner.test.ts | 52 ++++++++++++++++++++++ tests/node-parallel/README.md | 2 +- tests/node-parallel/run.mjs | 13 +++--- tests/node-parallel/verdict.mjs | 13 ++++++ 4 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 tests/harness/node-parallel-runner.test.ts create mode 100644 tests/node-parallel/verdict.mjs diff --git a/tests/harness/node-parallel-runner.test.ts b/tests/harness/node-parallel-runner.test.ts new file mode 100644 index 000000000..a829c4d78 --- /dev/null +++ b/tests/harness/node-parallel-runner.test.ts @@ -0,0 +1,52 @@ +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, relative, resolve } from "node:path"; +import { afterAll, expect, test } from "vitest"; +// @ts-expect-error -- a plain ESM helper of the runner script +import { sameTermination, termination } from "../node-parallel/verdict.mjs"; + +const repo = resolve(import.meta.dirname, "../.."); +const scratch = mkdtempSync(join(tmpdir(), "scriptc-node-parallel-")); +afterAll(() => rmSync(scratch, { recursive: true, force: true })); + +test("runs that end by different signals, or by a signal and an exit, differ", () => { + expect(sameTermination({ code: 0, signal: null }, { code: 0, signal: null })).toBe(true); + expect( + sameTermination({ code: null, signal: "SIGABRT" }, { code: null, signal: "SIGABRT" }), + ).toBe(true); + expect( + sameTermination({ code: null, signal: "SIGABRT" }, { code: null, signal: "SIGSEGV" }), + ).toBe(false); + expect(sameTermination({ code: 1, signal: null }, { code: null, signal: "SIGKILL" })).toBe(false); + expect(sameTermination({ code: 0, signal: null }, { code: 1, signal: null })).toBe(false); + expect(termination({ code: null, signal: "SIGSEGV" })).toBe("SIGSEGV"); + expect(termination({ code: 0, signal: null })).toBe(0); +}); + +test("a relative --node path links the checkout's fixtures into the minimal tree", () => { + const checkout = join(scratch, "node"); + mkdirSync(join(checkout, "test", "parallel"), { recursive: true }); + mkdirSync(join(checkout, "test", "fixtures"), { recursive: true }); + mkdirSync(join(checkout, "test", "common"), { recursive: true }); + writeFileSync(join(checkout, "test", "fixtures", "marker.txt"), "fixture\n"); + writeFileSync(join(checkout, "test", "common", "fixtures.js"), "module.exports = {};\n"); + const out = join(scratch, "out"); + const result = spawnSync( + process.execPath, + [ + join(repo, "tests/node-parallel/run.mjs"), + "--node", + relative(scratch, checkout), + "--pattern", + "^no-such-test$", + "--out", + out, + ], + { cwd: scratch, encoding: "utf8" }, + ); + expect(result.status, result.stderr).toBe(0); + const linked = join(out, "tree", "test", "fixtures", "marker.txt"); + expect(existsSync(linked)).toBe(true); + expect(readFileSync(linked, "utf8")).toBe("fixture\n"); +}); diff --git a/tests/node-parallel/README.md b/tests/node-parallel/README.md index 35af64464..c8ac12134 100644 --- a/tests/node-parallel/README.md +++ b/tests/node-parallel/README.md @@ -1,6 +1,6 @@ # Node test/parallel files -`run.mjs` builds files from a Node.js checkout's `test/parallel` with scriptc, runs each executable in that directory, and compares its exit status with Node's. It records a verdict per file: `pass` (same exit status), `fail` (different exit status), or `refused` (the build failed), with the first error line. +`run.mjs` builds files from a Node.js checkout's `test/parallel` with scriptc, runs each executable in that directory, and compares its exit status with Node's. It records a verdict per file: `pass` (the same exit code, or the same terminating signal), `fail` (a different exit code or signal), or `refused` (the build failed), with the first error line. ```bash git clone --depth 1 --branch v24.22.0 https://github.com/nodejs/node /path/to/node diff --git a/tests/node-parallel/run.mjs b/tests/node-parallel/run.mjs index 4f2230c6b..e7a1e3cec 100644 --- a/tests/node-parallel/run.mjs +++ b/tests/node-parallel/run.mjs @@ -14,6 +14,7 @@ import { import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; +import { sameTermination, termination } from "./verdict.mjs"; const here = dirname(fileURLToPath(import.meta.url)); const repo = resolve(here, "../.."); @@ -33,9 +34,9 @@ if (!values.node || !values.pattern || !["minimal", "node"].includes(values.comm ); process.exit(2); } -const nodeTest = existsSync(join(values.node, "test", "parallel")) - ? join(values.node, "test") - : values.node; +// Absolute, so the minimal lane's fixtures symlink resolves from the tree. +const nodeRoot = resolve(values.node); +const nodeTest = existsSync(join(nodeRoot, "test", "parallel")) ? join(nodeRoot, "test") : nodeRoot; const pattern = new RegExp(values.pattern); const out = resolve(values.out); mkdirSync(out, { recursive: true }); @@ -96,15 +97,15 @@ async function one(file) { cwd: dir, timeout: 900_000, }); - const result = { file, flags: flags.join(" "), nodeExit: node.code }; + const result = { file, flags: flags.join(" "), nodeExit: termination(node) }; if (build.code !== 0) { result.verdict = "refused"; result.cause = firstCause(build.stdout + build.stderr); return result; } const native = await run(exe, [], { cwd: dir, timeout: 120_000 }); - result.exit = native.code ?? native.signal; - result.verdict = native.code === node.code ? "pass" : "fail"; + result.exit = termination(native); + result.verdict = sameTermination(node, native) ? "pass" : "fail"; if (result.verdict !== "pass") result.cause = firstCause(native.stderr + native.stdout); return result; } diff --git a/tests/node-parallel/verdict.mjs b/tests/node-parallel/verdict.mjs new file mode 100644 index 000000000..392238f44 --- /dev/null +++ b/tests/node-parallel/verdict.mjs @@ -0,0 +1,13 @@ +// How two runs of one test file compare (run.mjs). + +/** Whether Node and the executable ended the same way: the same exit code, + * or the same terminating signal (an exit code is null when a signal ended + * the process). */ +export function sameTermination(node, native) { + return node.code === native.code && node.signal === native.signal; +} + +/** An exit code, or the signal that ended the process. */ +export function termination(result) { + return result.code ?? result.signal; +} From 190fb8d58e75f0165e7be2b973bc32a802a3aac8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:01:33 -0500 Subject: [PATCH 087/273] Mix weak-collection hashes in 64 bits on 32-bit targets uintptr_t is 32 bits on wasm32, where the weak-collection hashes shifted it by 33 and 56 bits. The hashes now widen keys, owners and kinds to uint64_t before mixing; 64-bit targets compute the same values as before. --- packages/runtime/src/scr_json.c | 11 +++++---- packages/runtime/test/json.test.ts | 31 ++++++++++++++++++++++++- tests/harness/wasi-differential.test.ts | 3 +++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index afe3315de..7df866a4c 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -16157,16 +16157,19 @@ typedef struct { void *key; ScrWeakEntry *head; } ScrWeakKeySlot; static SCR_TL ScrWeakKeySlot *scr_weak_keys; static SCR_TL size_t scr_weak_keys_mask, scr_weak_keys_used; -static inline size_t scr_weak_mix(uintptr_t bits) { +/* Mixed in 64 bits on every target: uintptr_t is 32 bits on wasm32, where + * these shifts would exceed its width. */ +static inline size_t scr_weak_mix(uint64_t bits) { bits ^= bits >> 33; - bits *= 0xff51afd7ed558ccdull; + bits *= UINT64_C(0xff51afd7ed558ccd); bits ^= bits >> 29; return (size_t)bits; } static inline size_t scr_weak_entry_hash(const ScrWeakMap *owner, const void *key, unsigned kind) { - return scr_weak_mix((uintptr_t)key ^ ((uintptr_t)owner * 0x9E3779B97F4A7C15ull) ^ ((uintptr_t)kind << 56)); + return scr_weak_mix((uint64_t)(uintptr_t)key ^ ((uint64_t)(uintptr_t)owner * UINT64_C(0x9E3779B97F4A7C15)) ^ + ((uint64_t)kind << 56)); } -static inline size_t scr_weak_key_hash(const void *key) { return scr_weak_mix((uintptr_t)key); } +static inline size_t scr_weak_key_hash(const void *key) { return scr_weak_mix((uint64_t)(uintptr_t)key); } static void scr_weak_index_put(ScrWeakEntry *entry) { size_t j = scr_weak_entry_hash(entry->owner, entry->key, entry->kind) & scr_weak_index_mask; diff --git a/packages/runtime/test/json.test.ts b/packages/runtime/test/json.test.ts index 3315cb415..f83f5ad65 100644 --- a/packages/runtime/test/json.test.ts +++ b/packages/runtime/test/json.test.ts @@ -1,4 +1,4 @@ -import { execFile } from "node:child_process"; +import { execFile, execFileSync } from "node:child_process"; import { mkdir } from "node:fs/promises"; import { join } from "node:path"; import { promisify } from "node:util"; @@ -85,3 +85,32 @@ test("json runtime: parser, dyn, dynCheck failure path, stringify buffer", async ].join("\n"), ); }); + +function zigOnPath(): boolean { + try { + execFileSync("zig", ["version"], { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +// 32-bit uintptr_t: the weak-collection hashes mix in 64 bits, so no shift +// exceeds the width of its operand on wasm32. +test.skipIf(!zigOnPath())("json runtime builds for wasm32 without oversized shifts", async () => { + await execFileAsync("zig", [ + "cc", + "-target", + "wasm32-wasi", + "-std=c11", + "-O2", + "-D_GNU_SOURCE", + "-D_WASI_EMULATED_SIGNAL", + "-D_WASI_EMULATED_PROCESS_CLOCKS", + "-Werror=shift-count-overflow", + "-c", + "-o", + join(testDir, "build", "scr_json.wasm32.o"), + join(testDir, "../src/scr_json.c"), + ]); +}); diff --git a/tests/harness/wasi-differential.test.ts b/tests/harness/wasi-differential.test.ts index 5d814b384..66c2934f2 100644 --- a/tests/harness/wasi-differential.test.ts +++ b/tests/harness/wasi-differential.test.ts @@ -313,6 +313,9 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => { // ThreadSlots blocks and pages hold 32-bit pointers. "5727-thread-slots-basics.ts", "5729-thread-slots-values.ts", + // Weak-collection hashes mix 32-bit pointers in 64 bits. + "4591-weak-keys-union-records.ts", + "7742-weak-collections-many-keys.ts", ])("the LLVM target runs %s byte-identically to Node", async (fixture) => { const entry = join(repoRoot, "tests/corpus", fixture); const outDir = await mkdtemp("/tmp/scriptc-wasi-"); From 1d579ea5a2ba93fb7073e0656720cc9fe91e12e9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:05:19 -0500 Subject: [PATCH 088/273] Check the identity of the open file a runtime pack artifact is read from Staging stat'ed an artifact's path, read the path, and stat'ed it again. When the path was a symbolic link switched to another file of the same size during the read and back before the second stat, both identities were those of the verified file, so the bytes of the other file were staged without hashing. Staging now opens the artifact once and takes both identities from the open file with fstat, so they describe the file the bytes come from. A test switches the artifact's link to a same-sized file during the read and checks that the staged copy has the verified bytes. --- .../src/backend/runtime-pack-native.test.ts | 50 ++++++++++++++++++- .../src/backend/runtime-pack-native.ts | 20 ++++++-- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/backend/runtime-pack-native.test.ts b/packages/compiler/src/backend/runtime-pack-native.test.ts index 723deba39..293d814b8 100644 --- a/packages/compiler/src/backend/runtime-pack-native.test.ts +++ b/packages/compiler/src/backend/runtime-pack-native.test.ts @@ -1,5 +1,14 @@ import { createHash } from "node:crypto"; -import { mkdirSync, readFileSync, renameSync, statSync, utimesSync, writeFileSync } from "node:fs"; +import { + mkdirSync, + readFileSync, + renameSync, + statSync, + symlinkSync, + unlinkSync, + utimesSync, + writeFileSync, +} from "node:fs"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -21,6 +30,10 @@ vi.mock("node:crypto", async (original) => { const crypto = await original(); return { ...crypto, createHash: vi.fn(crypto.createHash) }; }); +vi.mock("node:fs", async (original) => { + const fs = await original(); + return { ...fs, readFileSync: vi.fn(fs.readFileSync) }; +}); const dirs: string[] = []; afterEach(async () => @@ -192,3 +205,38 @@ test("staging does not hash an unchanged artifact again", async () => { expect(hashes() - start).toBe(firstStage - 1); expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object"); }); + +test.skipIf(process.platform === "win32")( + "staging reads an artifact's bytes from the file whose identity it checks", + async () => { + const f = await packFixture(); + // The artifact path is a symbolic link to the verified file. + const real = f.object + ".real"; + renameSync(f.object, real); + symlinkSync(real, f.object); + const selection = f.select(); + f.stage(selection); + // During the next read the link names another file of the same size, + // and it names the verified file again before the read ends. + const other = f.object + ".other"; + writeFileSync(other, "x".repeat(statSync(real).size)); + const read = vi.mocked(readFileSync); + const original = read.getMockImplementation()!; + read.mockImplementation(((source: Parameters[0], ...rest: unknown[]) => { + if (typeof source === "string" && !source.endsWith("base.o")) + return (original as (...args: unknown[]) => unknown)(source, ...rest); + read.mockImplementation(original); + unlinkSync(f.object); + symlinkSync(other, f.object); + try { + return (original as (...args: unknown[]) => unknown)(source, ...rest); + } finally { + unlinkSync(f.object); + symlinkSync(real, f.object); + } + }) as typeof readFileSync); + const staged = f.stage(selection); + expect(read.getMockImplementation()).toBe(original); + expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object"); + }, +); diff --git a/packages/compiler/src/backend/runtime-pack-native.ts b/packages/compiler/src/backend/runtime-pack-native.ts index f51b24221..0fff34455 100644 --- a/packages/compiler/src/backend/runtime-pack-native.ts +++ b/packages/compiler/src/backend/runtime-pack-native.ts @@ -9,11 +9,13 @@ import { closeSync, constants, existsSync, + fstatSync, mkdirSync, openSync, readFileSync, statSync, writeSync, + type Stats, } from "node:fs"; import { dirname, join } from "node:path"; import type { NativeLinkFeatures } from "./native-link-info.js"; @@ -62,7 +64,10 @@ interface FileIdentity { } function fileIdentity(path: string): FileIdentity { - const info = statSync(path); + return statsIdentity(statSync(path)); +} + +function statsIdentity(info: Stats): FileIdentity { return { dev: info.dev, ino: info.ino, @@ -256,9 +261,16 @@ function stageArtifact( let before: FileIdentity; let after: FileIdentity; try { - before = fileIdentity(source); - bytes = readFileSync(source); - after = fileIdentity(source); + // The identities are those of the open file the bytes come from, also + // when the path is a symbolic link that changes during the read. + const input = openSync(source, constants.O_RDONLY); + try { + before = statsIdentity(fstatSync(input)); + bytes = readFileSync(input); + after = statsIdentity(fstatSync(input)); + } finally { + closeSync(input); + } } catch { throw new RuntimePackError(`runtime pack artifact is missing: ${artifact.path}`, "invalid"); } From 70f565177e1823c1a67853b9fe46da50e8572fae Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:05:36 -0500 Subject: [PATCH 089/273] Count profile-annotated functions when the annotation runs The match report counted the functions with profile counts after the whole pipeline, so an annotated function that inlining or dead code elimination later removed was not counted, while the annotation's mismatch and missing-function warnings were. A pass-instrumentation callback now counts the annotated functions right after PGOInstrumentationUse runs, in the one-module and the partitioned pipelines. A new test builds helpers that reach the annotation and are then inlined into main and deleted, and checks that the report counts them. --- native/llvm-codegen/src/emit.cpp | 23 +++- packages/compiler/test/native-profile.test.ts | 100 +++++++++++++++++- 2 files changed, 114 insertions(+), 9 deletions(-) diff --git a/native/llvm-codegen/src/emit.cpp b/native/llvm-codegen/src/emit.cpp index 35d7ce4f0..14bcd1522 100644 --- a/native/llvm-codegen/src/emit.cpp +++ b/native/llvm-codegen/src/emit.cpp @@ -296,6 +296,7 @@ std::optional inspectProfile(const EmitOptions &Options) { // Owns the analysis managers a pipeline needs for one module. struct Pipeline { + PassInstrumentationCallbacks Callbacks; LoopAnalysisManager LAM; FunctionAnalysisManager FAM; CGSCCAnalysisManager CGAM; @@ -304,13 +305,26 @@ struct Pipeline { Pipeline(TargetMachine &Machine, OptimizationLevel Level, std::optional Profile = std::nullopt) - : PB(&Machine, tuningFor(Level), std::move(Profile)) { + : PB(&Machine, tuningFor(Level), std::move(Profile), &Callbacks) { PB.registerModuleAnalyses(MAM); PB.registerCGSCCAnalyses(CGAM); PB.registerFunctionAnalyses(FAM); PB.registerLoopAnalyses(LAM); PB.crossRegisterProxies(LAM, FAM, CGAM, MAM); } + + // Adds to Count the functions the profile annotation gave counts to, as + // soon as that pass has run: inlining and dead code elimination later in + // the pipeline delete annotated functions. + void countAnnotated(std::atomic &Count) { + Callbacks.registerAfterPassCallback( + [&Count](StringRef PassID, Any IR, const PreservedAnalyses &) { + if (PassID != "PGOInstrumentationUse") + return; + if (const auto *M = llvm::any_cast(&IR)) + Count += annotatedFunctions(**M); + }); + } }; std::optional verify(Module &M, StringRef Code) { @@ -499,9 +513,9 @@ emitModule(Module &M, TargetMachine &Machine, OptimizationLevel Level, M.getContext().setDiagnosticHandler( std::make_unique(Match)); Pipeline P(Machine, Level, std::move(Profile)); - P.PB.buildPerModuleDefaultPipeline(Level).run(M, P.MAM); if (Annotating) - Match.Annotated += annotatedFunctions(M); + P.countAnnotated(Match.Annotated); + P.PB.buildPerModuleDefaultPipeline(Level).run(M, P.MAM); if (std::optional Failure = verify(M, "post_optimization_verification_failed")) return Failure; @@ -716,6 +730,8 @@ compileOptimizedPartitions(const EmitOptions &Options, // Profile instrumentation or annotation happens here, before the thin // link; the post-link optimization reads the annotated weights. Pipeline P(*Machine, Level, profileOptions(Options)); + if (Annotating) + P.countAnnotated(PartitionMatch.Annotated); ModulePassManager MPM = P.PB.buildThinLTOPreLinkDefaultPipeline(Level); MPM.addPass(BitcodeWriterPass(Stream, /*ShouldPreserveUseListOrder=*/false, /*EmitSummaryIndex=*/true, @@ -723,7 +739,6 @@ compileOptimizedPartitions(const EmitOptions &Options, MPM.run(Mod, P.MAM); } if (Annotating) { - PartitionMatch.Annotated += annotatedFunctions(Mod); Match.add(PartitionMatch); // Stored before the summary, so a reused summary finds its counts. Cache.store(Key, "match", PartitionMatch.serialize()); diff --git a/packages/compiler/test/native-profile.test.ts b/packages/compiler/test/native-profile.test.ts index 53d077017..9d04739d0 100644 --- a/packages/compiler/test/native-profile.test.ts +++ b/packages/compiler/test/native-profile.test.ts @@ -110,6 +110,65 @@ function expectedExit(argc: number, edit = 0): number { return Number(total % 251n); } +const INLINED = 6; + +/** A program whose helpers are too large for the inliner that runs before the + * profile annotation and each called twice, so they reach the annotation, + * and which the profile-guided inliner then inlines into their hot call + * sites in `main` and deletes. */ +function inlinedProgram(): string { + const lines = ["; profiled program, helpers inlined after annotation"]; + for (let index = 0; index < INLINED; index++) { + lines.push( + `define internal i64 @g${index}(i64 %n) {`, + "entry:", + " %x0 = add i64 %n, " + index, + ); + for (let step = 1; step <= 40; step++) + lines.push(` %x${step} = ${step % 2 === 0 ? "xor" : "mul"} i64 %x${step - 1}, ${step + 3}`); + lines.push(" ret i64 %x40", "}"); + } + lines.push( + "define i32 @main(i32 %argc, ptr %argv) {", + "entry:", + " %wide = sext i32 %argc to i64", + " %limit = mul i64 %wide, 1000", + " br label %loop", + "loop:", + " %i = phi i64 [ 0, %entry ], [ %next, %loop ]", + " %total = phi i64 [ 0, %entry ], [ %grown, %loop ]", + ...Array.from({ length: INLINED }, (_, index) => [ + ` %a${index} = call i64 @g${index}(i64 %i)`, + ` %b${index} = call i64 @g${index}(i64 %a${index})`, + ` %t${index} = add i64 ${index === 0 ? "%total" : `%t${index - 1}`}, %b${index}`, + ]).flat(), + ` %grown = add i64 %t${INLINED - 1}, 0`, + " %next = add i64 %i, 1", + " %again = icmp ult i64 %next, %limit", + " br i1 %again, label %loop, label %exit", + "exit:", + " %low = urem i64 %grown, 251", + " %code = trunc i64 %low to i32", + " ret i32 %code", + "}", + ); + return lines.join("\n") + "\n"; +} + +function inlinedExit(argc: number): number { + const mask = (1n << 64n) - 1n; + const g = (index: number, n: bigint) => { + let x = (n + BigInt(index)) & mask; + for (let step = 1; step <= 40; step++) + x = step % 2 === 0 ? x ^ BigInt(step + 3) : (x * BigInt(step + 3)) & mask; + return x; + }; + let total = 0n; + for (let i = 0n; i < BigInt(argc * 1000); i++) + for (let index = 0; index < INLINED; index++) total = (total + g(index, g(index, i))) & mask; + return Number(total % 251n); +} + type EmitOptions = { outputs: number; name: string; @@ -119,12 +178,17 @@ type EmitOptions = { }; async function emit(dir: string, source: string, options: EmitOptions): Promise { + return (await emitReporting(dir, source, options)).outputs; +} + +/** `emit`, also returning the helper's stdout (its profile match report). */ +async function emitReporting(dir: string, source: string, options: EmitOptions) { const input = join(dir, `${options.name}.ll`); await writeFile(input, source); const outputs = Array.from({ length: options.outputs }, (_, index) => join(dir, `${options.name}.${index}.o`), ); - await execFileAsync(helper!, [ + const { stdout } = await execFileAsync(helper!, [ "emit", "--input", input, @@ -145,7 +209,7 @@ async function emit(dir: string, source: string, options: EmitOptions): Promise< ...(options.use === undefined ? [] : ["--profile-use", options.use]), ...(options.cache === undefined ? [] : ["--cache-dir", options.cache]), ]); - return outputs; + return { outputs, stdout }; } async function link(dir: string, objects: string[], name: string, instrumented = false) { @@ -181,12 +245,18 @@ async function merge(output: string, inputs: string[]) { /** An instrumented build of `source` run with 1 and 3 arguments, its raw * profiles merged into one indexed profile. */ -async function collect(dir: string, source: string, outputs: number, name: string) { +async function collect( + dir: string, + source: string, + outputs: number, + name: string, + expected: (argc: number) => number = expectedExit, +) { const raw = join(dir, `${name}-raw`); const objects = await emit(dir, source, { outputs, name, generate: join(raw, "%p.profraw") }); const executable = await link(dir, objects, name, true); - expect(await run(executable, 1)).toBe(expectedExit(1)); - expect(await run(executable, 3)).toBe(expectedExit(3)); + expect(await run(executable, 1)).toBe(expected(1)); + expect(await run(executable, 3)).toBe(expected(3)); const profiles = (await readdir(raw)).map((file) => join(raw, file)); expect(profiles).toHaveLength(2); const profile = join(dir, `${name}.profdata`); @@ -286,6 +356,26 @@ describe.runIf(helper !== null)("LLVM profile-guided optimization", () => { expect(await run(await link(dir, objects, "edited"), 2)).toBe(expectedExit(2, 7)); }); + test("the match report counts annotated functions that inlining removes", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-profile-match-")); + dirs.push(dir); + const { profile } = await collect(dir, inlinedProgram(), 1, "train", inlinedExit); + const { outputs, stdout } = await emitReporting(dir, inlinedProgram(), { + outputs: 1, + name: "guided", + use: profile, + }); + expect(await run(await link(dir, outputs, "guided"), 2)).toBe(inlinedExit(2)); + // Every g and main received counts, though optimization then + // inlined the helpers into main and deleted them. + expect(JSON.parse(stdout.trim().split("\n").at(-1)!)).toEqual({ + ok: true, + profile: { annotated: INLINED + 1, mismatched: 0, missing: 0, unknown_partitions: 0 }, + }); + const symbols = await execFileAsync("nm", [outputs[0]!]); + expect(symbols.stdout).not.toMatch(/\bg\d\b/); + }); + test("the profile keys the partition cache", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-profile-cache-")); dirs.push(dir); From 530bd9534ffc45ca8a29db5c971b533c407d55d6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:10:00 -0500 Subject: [PATCH 090/273] Await the values of untyped sync pipeline sources An untyped pipeline source that is a sync iterable passed each value on as it came, so a promise reached the next stage as the promise: a byte-mode destination refused it and an object-mode one received it. for await over a sync iterator, which Node's pipeline pumps it with, awaits each value. The source's generator now awaits a value that is an object (a promise included) before yielding it, and a rejection closes the source, as Node's async-from-sync iterator does. --- .../src/frontend/lowering/lower-stream.ts | 44 ++++++++++++++++++- .../7504-pipeline-untyped-source-errors.mjs | 27 +++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 72bea5dd7..b519ce378 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -3947,11 +3947,13 @@ function lowerDynIterableSource( const syncId = frame.local("sync", BOOL, false); const nextId = frame.local("next", DYN, false); const stepId = frame.local("step", DYN, true); + const valueId = frame.local("value", DYN, true); const activeId = frame.local("active", BOOL, true); const closeId = frame.local("close", DYN, false); const it = (): IrExpr => varRef(itId, DYN, loc); const sync = (): IrExpr => varRef(syncId, BOOL, loc); const step = (): IrExpr => varRef(stepId, DYN, loc); + const value = (): IrExpr => varRef(valueId, DYN, loc); const get = (obj: IrExpr, name: string): IrExpr => ({ kind: "dynKeyGet", value: obj, @@ -4011,6 +4013,7 @@ function lowerDynIterableSource( }, { kind: "varDecl", localId: nextId, init: get(it(), "next"), loc }, { kind: "varDecl", localId: stepId, init: dynUndefinedExpr(loc), loc }, + { kind: "varDecl", localId: valueId, init: dynUndefinedExpr(loc), loc }, { kind: "varDecl", localId: activeId, init: boolLit(false, loc), loc }, { kind: "tryCatch", @@ -4055,9 +4058,48 @@ function lowerDynIterableSource( loc, }, setActive(true), + { kind: "assign", localId: valueId, value: get(step(), "value"), loc }, + // for await over a sync iterator awaits each value + // (CreateAsyncFromSyncIterator): a promise or a thenable + // passes on its result, and a rejection closes the iterator + { + kind: "if", + cond: { + kind: "logical", + op: "&&", + left: sync(), + right: { + kind: "logical", + op: "||", + left: { kind: "dynTest", test: "promise", value: value(), type: BOOL, loc }, + right: { + kind: "logical", + op: "&&", + left: { + kind: "dynTest", + test: "nullish", + negated: true, + value: value(), + type: BOOL, + loc, + }, + right: { kind: "dynTest", test: "object", value: value(), type: BOOL, loc }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + then: [{ kind: "assign", localId: valueId, value: awaitDyn(value()), loc }], + else_: null, + loc, + }, { kind: "exprStmt", - expr: { kind: "yieldExpr", value: get(step(), "value"), type: VOID, loc }, + expr: { kind: "yieldExpr", value: value(), type: VOID, loc }, loc, }, ], diff --git a/tests/corpus/7504-pipeline-untyped-source-errors.mjs b/tests/corpus/7504-pipeline-untyped-source-errors.mjs index 9c677c011..3ee984547 100644 --- a/tests/corpus/7504-pipeline-untyped-source-errors.mjs +++ b/tests/corpus/7504-pipeline-untyped-source-errors.mjs @@ -1,6 +1,7 @@ // pipeline() over untyped iterable sources: a destination that fails closes // the source, backpressure waits for drain, an abort signal stops the pump, -// and async generator and Map sources reach object-mode destinations. +// async generator and Map sources reach object-mode destinations, and a +// sync source's promise values are awaited. import { pipeline, Writable, PassThrough } from "node:stream"; import { pipeline as pipelineP } from "node:stream/promises"; @@ -88,3 +89,27 @@ await new Promise((resolve) => { (err) => { console.log("map done", err); resolve(undefined); }, ); }); + +// sync sources whose values are promises: each value is awaited, as +// for await over a sync iterator does, and a rejection closes the source +await pipelineP( + pick([Promise.resolve("p1"), "p2", new Promise((r) => setTimeout(() => r("p3"), 1))]), + new Writable({ write(c, _e, cb) { console.log("byte write", String(c)); cb(); } }), +); +function* promised() { + try { + yield Promise.resolve({ n: 1 }); + yield Promise.reject(new Error("value rejected")); + yield "never"; + } finally { + console.log("promised finally"); + } +} +try { + await pipelineP( + pick(promised()), + new Writable({ objectMode: true, write(c, _e, cb) { console.log("object write", JSON.stringify(c)); cb(); } }), + ); +} catch (err) { + console.log("promised rejected", err.message); +} From 317162af78f367342ff8a06b46779f9c4e2b5e9d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:13:05 -0500 Subject: [PATCH 091/273] Count repeated calls through constructors, apply and per-value callbacks The constant-graph pass treats a function whose call edges all run once at initialization as init-only. Some calls recorded no edge or a once-only one: - `new F()` of an ES5 constructor and `f.apply(thisArg, args)` bound the arguments without a call edge, so a function also called directly once was init-only even when a loop constructed or applied it. - Callbacks of Array.from, toSorted, replace and replaceAll run once per element, comparison or match, but those methods were missing from the repeating callback set. - JSON.parse revivers, JSON.stringify replacers and toJSON methods run once per value; their edges at the JSON call site now count as repeated, including toJSON calls resolved after the call's handling. Objects these functions write or allocate were reported as constant, and their reads as foldable. constant-graphs.test.ts covers each case. --- .../src/frontend/shape-inference/analyze.ts | 15 +++++++- .../compiler/test/constant-graphs.test.ts | 37 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/shape-inference/analyze.ts b/packages/compiler/src/frontend/shape-inference/analyze.ts index 98ec87099..8b1101da2 100644 --- a/packages/compiler/src/frontend/shape-inference/analyze.ts +++ b/packages/compiler/src/frontend/shape-inference/analyze.ts @@ -252,6 +252,10 @@ const LOOP_CALLBACK_METHODS: ReadonlySet = new Set([ "reduceRight", "flatMap", "sort", + "toSorted", + "from", + "replace", + "replaceAll", ]); export function analyzeShapes(program: ts.Program, checker: ts.TypeChecker): ShapeAnalysis { @@ -298,12 +302,17 @@ class Analyzer { this.siteCtx.set(site, { caller: this.currentFn, inLoop: this.loopDepth > 0 }); } + /** Builtin calls that invoke their callbacks once per value + * (JSON.parse revivers, JSON.stringify replacers and toJSON methods), + * which may run after the call's own handling. */ + private readonly repeatingSites = new Set(); + edgeFor(site: Node, callee: FunctionInfo): void { const ctx = this.siteCtx.get(site); this.calls.push({ caller: ctx?.caller ?? null, callee, - inLoop: (ctx?.inLoop ?? false) || this.forceLoop, + inLoop: (ctx?.inLoop ?? false) || this.forceLoop || this.repeatingSites.has(site), site, }); } @@ -2547,6 +2556,7 @@ class Analyzer { this.add(info.thisNode, inst); this.bindArgs(info, args, spread); this.edge(info.ret, dst); + this.edgeFor(e, info); } return; } @@ -2894,6 +2904,7 @@ class Analyzer { for (const p of info.params) if (args[1] !== undefined) this.iterateInto(args[1], p, site); this.edge(info.ret, dst); + this.edgeFor(site, info); } return; } @@ -3034,6 +3045,7 @@ class Analyzer { // result. const revived = this.newNode(); this.add(revived, this.top); + this.repeatingSites.add(site); if (args[1] !== undefined) this.on(args[1], (f) => { if (f.kind === "function") @@ -3057,6 +3069,7 @@ class Analyzer { // Their results are serialized in turn. const serialized = this.newNode(); this.childrenInto(serialized); + this.repeatingSites.add(site); this.edge(arg(0), serialized); const key = this.constNode(this.prim("string")); this.on(serialized, (v) => this.callToJSON(v, key, serialized, site)); diff --git a/packages/compiler/test/constant-graphs.test.ts b/packages/compiler/test/constant-graphs.test.ts index 258c83b08..a89276ce7 100644 --- a/packages/compiler/test/constant-graphs.test.ts +++ b/packages/compiler/test/constant-graphs.test.ts @@ -104,3 +104,40 @@ test("constant graphs: namespaces, revealing modules and prototypes, with near m // A factory called from two init sites: one object per call site. expect(text).toMatch(/near miss ns-fixture\/index\.js:20: init-factory \(.*2 call sites\)/); }); + +// Functions that one call site runs repeatedly: an ES5 constructor and an +// apply call in a loop, and builtin callbacks that run once per element, +// match or parsed value. +const repeatedSource = ` +var lib = {}; +lib.newState = { v: 0 }; +function bumpNew() { lib.newState.v++; } +bumpNew(); +lib.applyState = { v: 0 }; +function bumpApply() { lib.applyState.v++; } +bumpApply(); +lib.repeat = function () { for (var i = 0; i < 3; i++) { new bumpNew(); bumpApply.apply(null, []); } }; +lib.seen = { match: null, pair: null, mapped: null, key: null }; +lib.replaced = "a-b".replace(/\\\\w/g, function (m) { lib.seen.match = { m: m }; return m; }); +lib.sorted = [3, 1, 2].toSorted(function (a, b) { lib.seen.pair = { a: a }; return a - b; }); +lib.mapped = Array.from([1, 2], function (x) { lib.seen.mapped = { x: x }; return x; }); +lib.revived = JSON.parse('{"a":{"b":1}}', function (k, v) { lib.seen.key = { k: k }; return v; }); +module.exports = { lib }; +`; + +test("constant graphs: constructors, apply calls and callbacks that one site runs repeatedly", async () => { + await writeFile(join(dir, "node_modules", "ns-fixture", "index.js"), repeatedSource); + await writeFile( + join(dir, "main.cjs"), + 'const { lib } = require("ns-fixture");\nlib.repeat();\nconsole.log(lib.newState.v, lib.applyState.v, lib.replaced, lib.sorted, lib.mapped, lib.revived);\n', + ); + const text = (await report()).join("\n"); + // `new bumpNew()` and `bumpApply.apply(...)` in a loop write after init. + expect(text).toMatch(/near miss ns-fixture\/index\.js:3 \.v: written-after-init/); + expect(text).toMatch(/near miss ns-fixture\/index\.js:6 \.v: written-after-init/); + // Each callback runs once per match, comparison, element or parsed value. + for (const field of ["match", "pair", "mapped", "key"]) + expect(text).toMatch( + new RegExp(`near miss ns-fixture/index\\.js:10 \\.${field}: written-after-init`), + ); +}); From 8f3c2c28d625015a93bb15acbc3cdb5fb1c20d9a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:14:15 -0500 Subject: [PATCH 092/273] Count comparator calls in AArch64 and Mach-O assembly The string comparison emission test matched only x86-64 `call` instructions to unprefixed symbols, so on AArch64 and macOS hosts it counted no calls and failed its positive-count check. It now also matches AArch64 `bl` and Mach-O's leading underscore, and it runs for the host and for aarch64-linux-gnu and x86_64-macos assembly, so every host checks all three spellings. --- tests/harness/string-compare-emission.test.ts | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/tests/harness/string-compare-emission.test.ts b/tests/harness/string-compare-emission.test.ts index 2654f56ba..1a8fdddf6 100644 --- a/tests/harness/string-compare-emission.test.ts +++ b/tests/harness/string-compare-emission.test.ts @@ -11,17 +11,23 @@ import { compile } from "@scriptc/compiler"; const repoRoot = join(import.meta.dirname, "../.."); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); -async function emittedAsm(name: string, source: string): Promise { - const key = createHash("sha256").update(source).digest("hex").slice(0, 16); +async function emittedAsm(name: string, source: string, target: string): Promise { + const key = createHash("sha256").update(target).update(source).digest("hex").slice(0, 16); const outDir = join(cacheDir, `string-compare-${key}`); mkdirSync(outDir, { recursive: true }); const file = join(outDir, `${name}.ts`); writeFileSync(file, source); + const saved = process.env["SCRIPTC_TARGET"]; + if (target === "host") delete process.env["SCRIPTC_TARGET"]; + else process.env["SCRIPTC_TARGET"] = target; const result = await compile(file, { outPath: join(outDir, name), outDir, backend: "llvm", outputKind: "asm", + }).finally(() => { + if (saved === undefined) delete process.env["SCRIPTC_TARGET"]; + else process.env["SCRIPTC_TARGET"] = saved; }); if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); @@ -36,12 +42,23 @@ words.sort(order); console.log(words.join(",")); `; +// Calls to the comparison runtime: `call`/`callq` on x86-64 and `bl` on +// AArch64, with Mach-O's leading underscore on macOS. const compareCalls = (asm: string): number => - asm.match(/\bcall[a-z]*\s+(?:sc_str_cmp_u16|scr_str_cmp_u16_bytes|scr_str_cmp)\b/g)?.length ?? 0; + asm.match( + /\b(?:call[a-z]*|bl)\s+_?(?:sc_str_cmp_u16|scr_str_cmp_u16_bytes|scr_str_cmp)\b/g, + )?.length ?? 0; -test("a three-way string comparator compares its pair once", async () => { - const once = compareCalls(await emittedAsm("once", program("a < b ? -1 : 1"))); - const both = compareCalls(await emittedAsm("both", program("a < b ? -1 : a > b ? 1 : 0"))); - expect(once).toBeGreaterThan(0); - expect(both).toBe(once); -}); +// The host's assembly, and two other targets' so that every host checks the +// x86-64, AArch64 and Mach-O spellings. +test.each(["host", "aarch64-linux-gnu", "x86_64-macos"])( + "a three-way string comparator compares its pair once (%s)", + async (target) => { + const once = compareCalls(await emittedAsm("once", program("a < b ? -1 : 1"), target)); + const both = compareCalls( + await emittedAsm("both", program("a < b ? -1 : a > b ? 1 : 0"), target), + ); + expect(once).toBeGreaterThan(0); + expect(both).toBe(once); + }, +); From c98c1fe345a988113032d425cc02434ab1090f48 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:15:53 -0500 Subject: [PATCH 093/273] Test powers of two exactly before treating a product as exact The product of two exact integers was treated as exact everywhere when one of them was a constant power of two, tested with Number.isInteger(Math.log2). Math.log2 rounds to 52 for 2^52 - 1, so a product by 2^52 - 1 that passes 2^53 was compared as its integer instead of its rounded double. The test now compares 2 ** Math.round(Math.log2(value)) with the value. Corpus 7983 adds products by 2^52 - 1 compared with products by 4. --- .../compiler/src/backend/llvm/integer-values.ts | 4 +++- tests/corpus/7983-integer-compares.ts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/backend/llvm/integer-values.ts b/packages/compiler/src/backend/llvm/integer-values.ts index 497eb5b0d..5b8935036 100644 --- a/packages/compiler/src/backend/llvm/integer-values.ts +++ b/packages/compiler/src/backend/llvm/integer-values.ts @@ -143,7 +143,9 @@ export function wideArithmetic( function powerOfTwo(integer: LlInteger | null): boolean { if (integer === null || integer.range.min !== integer.range.max) return false; const value = Math.abs(integer.range.min); - return value >= 1 && value <= 2 ** 52 && Number.isInteger(Math.log2(value)); + // Math.log2 can round to an integer for a value that is not a power of + // two (2^52 - 1), so the candidate power is compared with the value. + return value >= 1 && value <= 2 ** 52 && 2 ** Math.round(Math.log2(value)) === value; } /** The i64 operands of a number compare that gives the doubles' result: diff --git a/tests/corpus/7983-integer-compares.ts b/tests/corpus/7983-integer-compares.ts index 1114df8a8..2603a39a1 100644 --- a/tests/corpus/7983-integer-compares.ts +++ b/tests/corpus/7983-integer-compares.ts @@ -123,6 +123,21 @@ function scaled(i: number, end: number): string { .join(""); } +function roundedProducts(): string { + // 3 * (2^52 - 1) is 13510798882111485, which rounds to 13510798882111484 = + // 4 * 3377699720527871: the products' doubles are equal and the integers + // are not. A product by 4 never rounds. + let equal = 0; + let below = 0; + for (let i = 1; i <= 8; i++) { + for (let j = 3377699720527869; j <= 3377699720527873; j++) { + if (i * 4503599627370495 === j * 4) equal++; + if (i * 4503599627370495 < j * 4) below++; + } + } + return `${equal} ${below}`; +} + const a = new Int32Array([5, -3, 2147483647, -2147483648, 5, 0]); const b = new Uint8Array([0, 255, 7, 128, 5, 0]); const c = new Uint32Array([4294967295, 2147483648, 2147483647, 0, 1, 4294967295]); @@ -148,3 +163,4 @@ console.log(descend(3, 100), descend(0, 1), descend(5, 0)); for (const i of [0, 1, p52 / 2 - 1, p52 / 2, p52 / 1024, p52 / 2 + 1, p52, -p52]) { console.log(scaled(i, safe), scaled(i, safe - 1), scaled(i, 2 ** 51), scaled(i, 4)); } +console.log(roundedProducts()); From 7405e93e5647e7020248608d47d4934a65105429 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:16:23 -0500 Subject: [PATCH 094/273] Convert compareArray's message only when the arrays differ --- tests/harness/test262.test.ts | 16 ++++++++++++++++ tests/test262/README.md | 2 +- tests/test262/harness.ts | 11 ++++++----- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/tests/harness/test262.test.ts b/tests/harness/test262.test.ts index fd35a08fb..678685103 100644 --- a/tests/harness/test262.test.ts +++ b/tests/harness/test262.test.ts @@ -278,6 +278,22 @@ assert.compareArray([object, fn], [object, fn]); status: "fail", source: "assert.sameValue(1, 2, false);", }, + { + name: "passing assertions do not convert their message", + status: "pass", + source: ` +const message = { toString() { throw new Error("converted"); } }; +assert(true, message); +assert.sameValue(1, 1, message); +assert.notSameValue(1, 2, message); +assert.compareArray([1, 2], [1, 2], message); +`, + }, + { + name: "array mismatch with an object message", + status: "fail", + source: 'assert.compareArray([1], [2], { toString() { return "differs"; } });', + }, ]; describe(`Test262 host assertion contract${shardSuffix()}`, () => { diff --git a/tests/test262/README.md b/tests/test262/README.md index 5474fa2c3..231e074e6 100644 --- a/tests/test262/README.md +++ b/tests/test262/README.md @@ -32,7 +32,7 @@ The current profile is `static-adapted-v6`. It adapts strict scripts to standalo The unmodified upstream assertion helper currently reaches scriptc's JavaScript function-expando refusal. Test262 explicitly permits host implementations of harness functions in its [interpretation rules](https://github.com/tc39/test262/blob/7ab7fafa0003f73fc85c1b95d88094d33f7eb8bd/INTERPRETING.md#host-defined-functions). `harness.ts` supplies `assert`, `assert.sameValue`, `assert.notSameValue`, `assert.compareArray`, `assert.throws` for selected Error constructors, `$DONE`, and `Test262Error`. Scalar SameValue and scalar array-element comparisons use the statically compiled `node:assert/strict` implementation. Contract tests compare successful and failing assertions with the original Test262 helpers under Node, including NaN, signed zero, null/undefined, differing primitive types, and premature termination. -Conversion at a native function boundary can copy references, so one object, array, or function may reach the adapter as two distinct references. A copy cannot merge two references into one. `assert.sameValue`, `assert.notSameValue`, and `assert.compareArray` elements therefore treat identical references as the same value and a reference as different from every primitive. Two distinct references terminate with a distinct harness-refusal result, which tests cannot catch and turn into a pass. Assertion messages may be any value; the adapter converts them with `String`, as the upstream helpers do. Sparse array comparisons are not yet handled faithfully across that boundary. The adapted `propertyHelper.js` admits direct `verifyProperty` calls with an inline descriptor literal or `undefined`; reference-valued comparisons follow the same identity rule, while other property helpers and descriptor expressions remain excluded. The adapted `testTypedArray.js` keeps the upstream constructor lists, argument factories and `testWith*` helpers. It omits the `TypedArray` (%TypedArray%) binding, so tests that name it are excluded. Its resizable and immutable buffer factories run only where `ArrayBuffer.prototype` provides `resize` and `transferToImmutable`, as the upstream feature checks intend. Contract tests compare its successful and failing uses with the upstream helper under Node. Assertion aliases, mutation, reflection, additional assertion methods, and other `includes` helpers remain excluded. +Conversion at a native function boundary can copy references, so one object, array, or function may reach the adapter as two distinct references. A copy cannot merge two references into one. `assert.sameValue`, `assert.notSameValue`, and `assert.compareArray` elements therefore treat identical references as the same value and a reference as different from every primitive. Two distinct references terminate with a distinct harness-refusal result, which tests cannot catch and turn into a pass. Assertion messages may be any value; the adapter converts them with `String` when an assertion fails, as the upstream `assert`, `assert.sameValue`, `assert.notSameValue` and `assert.compareArray` do. Sparse array comparisons are not yet handled faithfully across that boundary. The adapted `propertyHelper.js` admits direct `verifyProperty` calls with an inline descriptor literal or `undefined`; reference-valued comparisons follow the same identity rule, while other property helpers and descriptor expressions remain excluded. The adapted `testTypedArray.js` keeps the upstream constructor lists, argument factories and `testWith*` helpers. It omits the `TypedArray` (%TypedArray%) binding, so tests that name it are excluded. Its resizable and immutable buffer factories run only where `ArrayBuffer.prototype` provides `resize` and `transferToImmutable`, as the upstream feature checks intend. Contract tests compare its successful and failing uses with the upstream helper under Node. Assertion aliases, mutation, reflection, additional assertion methods, and other `includes` helpers remain excluded. Raw tests, module variants, agents, resolution negatives, and runtime negatives remain excluded. Parse negatives pass only when a source-matched syntax or vetted JavaScript early-error diagnostic appears; TypeScript type errors and harness-only failures never satisfy a negative expectation. `$262`, dynamic evaluation, observable script-global state, and other unsupported host requirements are excluded as well. The runtime and compiler continue to enforce their existing refusal boundaries for admitted source. diff --git a/tests/test262/harness.ts b/tests/test262/harness.ts index b478b4555..dce1be99c 100644 --- a/tests/test262/harness.ts +++ b/tests/test262/harness.ts @@ -79,18 +79,19 @@ export namespace assert { if (outcome !== "pass") throw new Test262Error(text(message, "Expected exception")); } + /** Converts `message` only when the contents differ, as the upstream helper does. */ export function compareArray(actual: unknown, expected: unknown, message?: unknown): void { - const failure = text(message, "Expected matching array contents"); if (!Array.isArray(actual) || !Array.isArray(expected)) { scalar(actual); scalar(expected); - throw new Test262Error(failure); + throw new Test262Error(text(message, "Expected matching array contents")); } - if (actual.length !== expected.length) throw new Test262Error(failure); - for (let i = 0; i < actual.length; i++) { + let matches = actual.length === expected.length; + for (let i = 0; matches && i < actual.length; i++) { const left: unknown = i in actual ? actual[i] : undefined; const right: unknown = i in expected ? expected[i] : undefined; - if (!same(left, right)) throw new Test262Error(failure); + matches = same(left, right); } + if (!matches) throw new Test262Error(text(message, "Expected matching array contents")); } } From af33be8046e3a3833d44dcf63880082cccea4257 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:16:49 -0500 Subject: [PATCH 095/273] Treat a collection exported through an export list as escaping A reference in `export { m }` resolved to the export's alias symbol, not to the local binding, so the inference did not see the export and typed the collection from its local uses. An importing module then used it as an untyped Map and failed to compile (SC9001). An unrenamed export specifier now resolves to the binding it exports, which refuses the inference as `export const` does. Corpus 7985 adds `export { listed, renamed as alias }`. --- .../src/frontend/lowering/js-collection-types.ts | 12 +++++++++++- packages/compiler/test/js-collection-types.test.ts | 2 ++ tests/corpus/7985-untyped-collection-types.mjs | 9 +++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/js-collection-types.ts b/packages/compiler/src/frontend/lowering/js-collection-types.ts index 8820ade4a..a4c182523 100644 --- a/packages/compiler/src/frontend/lowering/js-collection-types.ts +++ b/packages/compiler/src/frontend/lowering/js-collection-types.ts @@ -225,7 +225,17 @@ function referencedSymbol(lowerer: Lowerer, id: ts.Identifier): ts.Symbol | unde const parent = id.parent; if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) return lowerer.checker.getShorthandAssignmentValueSymbol(parent); - return lowerer.checker.getSymbolAtLocation(id); + const symbol = lowerer.checker.getSymbolAtLocation(id); + // `export { m }` names the export, an alias of the local binding. + if ( + symbol !== undefined && + parent !== undefined && + ts.isExportSpecifier(parent) && + parent.propertyName === undefined && + (symbol.flags & ts.SymbolFlags.Alias) !== 0 + ) + return lowerer.checker.getAliasedSymbol(symbol); + return symbol; } /** Records a reference's use, or answers false when the reference is not diff --git a/packages/compiler/test/js-collection-types.test.ts b/packages/compiler/test/js-collection-types.test.ts index 88e2006b7..6a6f65798 100644 --- a/packages/compiler/test/js-collection-types.test.ts +++ b/packages/compiler/test/js-collection-types.test.ts @@ -54,6 +54,8 @@ test("untyped JavaScript collections take the types their uses prove, and only t "spread", "unread", "exported", + "listed", + "renamed", ]) expect(typed(types.get(untyped)!), untyped).toBe(false); } finally { diff --git a/tests/corpus/7985-untyped-collection-types.mjs b/tests/corpus/7985-untyped-collection-types.mjs index a60a2ea04..de4e581c2 100644 --- a/tests/corpus/7985-untyped-collection-types.mjs +++ b/tests/corpus/7985-untyped-collection-types.mjs @@ -114,3 +114,12 @@ console.log(unread.size, unread.get("none")); export const exported = new Map(); exported.set("e", 1); console.log(exported.get("e")); +// Exported through an export list, under its own name or another: an +// importer sees an untyped Map. +const listed = new Map(); +listed.set("l", 1); +console.log(listed.get("l")); +const renamed = new Map(); +renamed.set("r", 1); +console.log(renamed.get("r")); +export { listed, renamed as alias }; From a538ff42f0ab99581f2e65a00a03510781090b19 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:19:16 -0500 Subject: [PATCH 096/273] Read only value slots in lastIndexOf with an element-typed needle lastIndexOf with a needle of the element type read every non-hole index as the element type. An undefined or foreign element written through a dynamic view (or a hole that toSorted turned into undefined) has no element-typed payload, so the read aborted with "array index N is undefined". The search now reads an index only when it holds a value, and skips undefined and foreign elements, as Node's strict equality does for a string or number needle. --- .../lowering/containers/array-methods.ts | 32 ++++++++++++------- .../6068-foreign-elements-through-views.ts | 23 +++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 4ef203edd..0e8b4fd84 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -1788,17 +1788,27 @@ function arraySearchHelper( }, loc, }; - const visitPresent: IrStmt[] = [ - { kind: "varDecl", localId: "value.0", init: value, loc }, - { - kind: "if", - cond: match, - then: [{ kind: "return", value: method === "includes" ? boolLit(true, loc) : indexRef, loc }], - else_: null, - loc, - }, - advance, - ]; + const found: IrStmt = { + kind: "if", + cond: directNeedle ? equal : match, + then: [{ kind: "return", value: method === "includes" ? boolLit(true, loc) : indexRef, loc }], + else_: null, + loc, + }; + // A direct needle reads only value slots: an undefined or foreign + // element written through a dynamic view has no element-typed payload. + const visitPresent: IrStmt[] = directNeedle + ? [ + { + kind: "if", + cond: { kind: "bin", op: "===", left: stateRef, right: numLit(1, loc), type: BOOL, loc }, + then: [{ kind: "varDecl", localId: "value.0", init: value, loc }, found], + else_: null, + loc, + }, + advance, + ] + : [{ kind: "varDecl", localId: "value.0", init: value, loc }, found, advance]; const skipHole: IrStmt[] = method === "lastIndexOf" ? [advance] diff --git a/tests/corpus/6068-foreign-elements-through-views.ts b/tests/corpus/6068-foreign-elements-through-views.ts index 5181d7fe5..1b23f6fa7 100644 --- a/tests/corpus/6068-foreign-elements-through-views.ts +++ b/tests/corpus/6068-foreign-elements-through-views.ts @@ -177,3 +177,26 @@ holderView.push({ }, }); console.log(JSON.stringify(holder), JSON.stringify(holder)); + +// toSorted keeps the full length and reads holes as undefined; indexOf, +// lastIndexOf and includes skip the undefined and foreign elements. +const searched: string[] = ["b", "a"]; +const searchedView: unknown[] = searched; +searchedView[9] = 4; +searchedView[5] = undefined; +console.log(searched.indexOf("b"), searched.lastIndexOf("b"), searched.lastIndexOf("a", 8)); +console.log(searched.includes("a"), searched.lastIndexOf("z")); +const searchedSorted = searched.toSorted(); +console.log(searchedSorted.length, searchedSorted.slice(0, 4), searchedSorted.lastIndexOf("b")); +console.log(searchedSorted.indexOf("a"), searchedSorted.includes("b")); +const counts: number[] = [3, 1]; +const countsView: unknown[] = counts; +countsView[6] = "x"; +countsView[4] = 2; +const countsSorted = counts.toSorted(); +console.log(countsSorted.length, countsSorted, countsSorted.lastIndexOf(3), counts.lastIndexOf(2)); +const far: number[] = [5]; +const farView: unknown[] = far; +farView[70000] = "z"; +const farSorted = far.toSorted(); +console.log(farSorted.length, farSorted.slice(0, 3), farSorted.slice(69999), farSorted.lastIndexOf(5)); From 49d99d776493120858fef3a2971471f344deeb1e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:20:33 -0500 Subject: [PATCH 097/273] Rewrite tests of held character locals inside other optional reads An optional read tested against a one-unit literal is replaced by a test of its code unit, built from the read's receiver and index. When that receiver or index tested a held character local, as in `src[c === "a" ? pos + 1 : pos] === "b"`, the inner test was kept as it was and still compared the local as a string|undefined union while the local's binding became a number, which failed IR validation. The same held for a held local's own initializer. The receiver, index and temps of both reads are now rewritten first. --- .../compiler/src/ir/char-unit-locals.test.ts | 26 +++++++++++++++++++ packages/compiler/src/ir/char-unit-locals.ts | 18 +++++++++++-- .../corpus/7304-string-optional-char-reads.ts | 14 ++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/ir/char-unit-locals.test.ts b/packages/compiler/src/ir/char-unit-locals.test.ts index 378a7f0df..2cce2f32b 100644 --- a/packages/compiler/src/ir/char-unit-locals.test.ts +++ b/packages/compiler/src/ir/char-unit-locals.test.ts @@ -4,6 +4,7 @@ import { join } from "node:path"; import { expect, test } from "vitest"; import { compile, deserializeModule } from "../index.js"; import { holdCharLocalsAsUnits } from "./char-unit-locals.js"; +import { validateModule } from "./validate.js"; import type { IrFunction, IrModule } from "./ir.js"; import { everyStmtList } from "./traverse.js"; @@ -166,3 +167,28 @@ console.log(kept("a b", 1), kept("ab", 0), kept("", 3), longer("ab", 0), twice(" // The local's own test still reads it as a union. expect(unionTests(fn(after, "kept"))).toContain("unionEq"); }); + +test("a held local tested inside another read's index or receiver becomes its unit", async () => { + const mod = holdCharLocalsAsUnits( + await lower(` +function count(src: string, other: string): number { + let n = 0; + for (let pos = 0; pos <= src.length; pos += 1.5) { + const c = src[pos]; + if (src[c === "a" ? pos + 1 : pos] === "b") n++; + if ((c === "x" ? other : src)[pos] === "y") n += 2; + const d = src[c === "c" ? pos - 1 : pos]; + if (d === "c") n += 10; + } + return n; +} +console.log(count("abcab", "xyz"), count("cab", "yy"), count("", "")); +`), + ); + const count = fn(mod, "count"); + expect(validateModule(mod)).toEqual([]); + expect(count.locals.find((l) => l.name === "c")!.type).toEqual({ kind: "f64" }); + expect(count.locals.find((l) => l.name === "d")!.type).toEqual({ kind: "f64" }); + // No test of either local reads it as a union. + expect(unionTests(count)).not.toContain("unionEq"); +}); diff --git a/packages/compiler/src/ir/char-unit-locals.ts b/packages/compiler/src/ir/char-unit-locals.ts index d8be86f08..03636fa87 100644 --- a/packages/compiler/src/ir/char-unit-locals.ts +++ b/packages/compiler/src/ir/char-unit-locals.ts @@ -421,18 +421,32 @@ function rewriteFunction(fn: IrFunction): IrFunction { const read = optionalCharRead(operand)!; if (read.plain || !optional.twice) { changed = true; - return optional.test(() => unitRead(read, operand.loc)); + const rewritten = rewriteRead(read); + return optional.test(() => unitRead(rewritten, operand.loc)); } } return mapExprChildren(e, rewriteExpr, rewriteStmt); }; + // A read's receiver and index may test other held locals. + const rewriteRead = (read: OptionalCharRead): OptionalCharRead => { + const [str, idx] = read.temps; + return { + ...read, + temps: [ + { ...str, init: rewriteExpr(str.init!) }, + { ...idx, init: rewriteExpr(idx.init!) }, + ], + receiver: rewriteExpr(read.receiver), + index: rewriteExpr(read.index), + }; + }; const rewriteStmt = (s: IrStmt): IrStmt => { if (s.kind === "varDecl" && held.has(s.localId) && s.init) { changed = true; const source = copyOf.get(s.localId); if (source !== undefined) return { ...s, init: unitRef(source, s.init.loc) }; const read = reads.get(s.localId); - if (read) return { ...s, init: unitRead(read, s.init.loc) }; + if (read) return { ...s, init: unitRead(rewriteRead(read), s.init.loc) }; if (s.init.kind === "strIntrinsic") return { ...s, diff --git a/tests/corpus/7304-string-optional-char-reads.ts b/tests/corpus/7304-string-optional-char-reads.ts index 13222219d..6e47abb75 100644 --- a/tests/corpus/7304-string-optional-char-reads.ts +++ b/tests/corpus/7304-string-optional-char-reads.ts @@ -119,3 +119,17 @@ for (const p of programs) console.log(JSON.stringify(p), JSON.stringify(new Lexe let text = ""; for (let k = 0; k < 30; k++) text += (k % 4 === 0 ? "==" : "w" + k) + " "; console.log(new Lexer(text).tokens().length, new Lexer(text).tokens().slice(0, 6).join(",")); + +// A character local tested inside another read's index or receiver. +function nested(src: string, other: string): number { + let n = 0; + for (let pos = 0; pos <= src.length; pos += 1.5) { + const c = src[pos]; + if (src[c === "a" ? pos + 1 : pos] === "b") n++; + if ((c === "x" ? other : src)[pos] === "y") n += 2; + const d = src[c === "c" ? pos - 1 : pos]; + if (d === "c") n += 10; + } + return n; +} +console.log(nested("abcab", "xyz"), nested("cab", "yy"), nested("xyxy", "yyyy"), nested("", "")); From 0c6ed44227b893057e75e5868d4b67d4d4df27eb Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:21:32 -0500 Subject: [PATCH 098/273] Match a nested union in full after a getter runs in a trusted build A union without recursion builds its matched arm without a visit session, so its getters and proxy traps ran without marking the conversion. A nested union then took its arm from the earlier match, even when the getter returned another value. A getter or trap that runs during a trusted build outside a session now marks it as well, and the outermost trusted build clears the mark when it ends. --- packages/runtime/src/scr_json.c | 24 ++++++++++++++++------ tests/harness/dyncheck.test.ts | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 2f9c9884e..b0f4c99c4 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -10824,9 +10824,10 @@ static SCR_TL const ScrDyn *scr_dyn_visit_held; /* Set while a union's builder builds the arm it matched in full * (scr_dyn_trust_enter). */ static SCR_TL bool scr_dyn_trusted; -/* Set once a getter or proxy trap has run during this session. Their code - * can return a value the match never saw or change one it saw, so no build - * in the rest of the session trusts an earlier match. */ +/* Set once a getter or proxy trap has run during this session, or during + * a trusted build outside any session. Their code can return a value the + * match never saw or change one it saw, so no build in the rest of the + * session, or of the outermost trusted build, trusts an earlier match. */ static SCR_TL bool scr_dyn_session_ran_code; /* Tables up to this many slots stay allocated for the next session. */ @@ -10978,7 +10979,10 @@ static void scr_dyn_visit_resume(const ScrDynVisitState *saved) { /* Calls a getter for a member read, outside any visit session. */ static ScrDyn *scr_dyn_visit_call_getter(ScrDyn *getter) { - if (scr_dyn_visit_depth == 0) return scr_dyn_call(getter, NULL, 0, "getter"); + if (scr_dyn_visit_depth == 0) { + if (scr_dyn_trusted) scr_dyn_session_ran_code = true; + return scr_dyn_call(getter, NULL, 0, "getter"); + } ScrDynVisitState saved; scr_dyn_visit_suspend(&saved); ScrDyn *result = scr_dyn_call(getter, NULL, 0, "getter"); @@ -10988,7 +10992,10 @@ static ScrDyn *scr_dyn_visit_call_getter(ScrDyn *getter) { /* A proxy's get trap for a member read, outside any visit session. */ static ScrDyn *scr_dyn_visit_proxy_get(const ScrDyn *d, ScrStr *name) { - if (scr_dyn_visit_depth == 0) return scr_dyn_proxy_get(d, name); + if (scr_dyn_visit_depth == 0) { + if (scr_dyn_trusted) scr_dyn_session_ran_code = true; + return scr_dyn_proxy_get(d, name); + } ScrDynVisitState saved; scr_dyn_visit_suspend(&saved); ScrDyn *result = scr_dyn_proxy_get(d, name); @@ -11002,7 +11009,12 @@ bool scr_dyn_trust_enter(void) { return previous; } -void scr_dyn_trust_exit(bool previous) { scr_dyn_trusted = previous; } +void scr_dyn_trust_exit(bool previous) { + scr_dyn_trusted = previous; + /* The outermost trusted build outside a session ends: later conversions + * may trust their own matches again. A session clears it on its exit. */ + if (!previous && scr_dyn_visit_depth == 0) scr_dyn_session_ran_code = false; +} bool scr_dyn_trusted_build(void) { return scr_dyn_trusted && !scr_dyn_session_ran_code; } diff --git a/tests/harness/dyncheck.test.ts b/tests/harness/dyncheck.test.ts index eb0ea1a7f..f38d8237d 100644 --- a/tests/harness/dyncheck.test.ts +++ b/tests/harness/dyncheck.test.ts @@ -650,6 +650,42 @@ console.log("getter calls", calls); ]); }); + test("nested unions: a getter's value in a non-recursive union's build is matched in full", async () => { + // A union without recursion builds without a visit session. The getter + // still runs user code during the trusted build, so the nested union + // matches its second answer in full, and the failure names that union. + const r = await compileAndRun( + "flat-discriminated-getter", + `type Inner = { tag: "x"; n: number } | { tag: "y"; s: string }; +type Outer = { kind: "a"; inner: Inner } | { kind: "b"; other: number }; +const v = JSON.parse('{"kind":"a"}') as unknown; +let calls = 0; +Object.defineProperty(v, "inner", { + enumerable: true, + get() { + calls++; + return calls === 1 ? JSON.parse('{"tag":"x","n":1}') : JSON.parse('{"tag":"x","n":"bad"}'); + }, +}); +try { + const o = v as Outer; + console.log("ok", JSON.stringify(o)); +} catch (err) { + console.log((err as Error).message); +} +console.log("getter calls", calls); +const plain = JSON.parse('{"kind":"a","inner":{"tag":"y","s":"z"}}') as Outer; +console.log(JSON.stringify(plain)); +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout.trimEnd().split("\n")).toEqual([ + "expected object | object at $.inner, got object", + "getter calls 2", + '{"kind":"a","inner":{"tag":"y","s":"z"}}', + ]); + }); + test("array where a record is expected names the mismatch at the root", async () => { const r = await compileAndRun( "root-kind", From e00d3fd9116431b8ce2ac4e52358aea7b51ccde2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:22:03 -0500 Subject: [PATCH 099/273] Check comparison operands and walk instanceof chains as Node does - The assert comparison methods read as function values (equal, strictEqual, deepEqual, partialDeepStrictEqual and their negations) throw Node's ERR_MISSING_ARGS TypeError, `The "actual" and "expected" arguments must be specified`, when called with fewer than two arguments, where `[assert.equal][0]()` passed. - Untyped `instanceof Object` starts at the value's prototype, so Object.prototype itself, whose prototype is null, is not an Object. - Untyped `instanceof Function` follows the whole prototype chain through objects and functions: an object whose prototype is a function, or a function whose prototype was set to such an object, is a Function. --- packages/runtime/src/scr_assert.c | 11 ++++ packages/runtime/src/scr_json.c | 64 ++++++++++++++++--- .../8078-instanceof-object-function.cjs | 22 +++++++ tests/corpus/8190-assert-function-values.cjs | 15 +++++ 4 files changed, 102 insertions(+), 10 deletions(-) diff --git a/packages/runtime/src/scr_assert.c b/packages/runtime/src/scr_assert.c index 738a7729c..bf5d1c875 100644 --- a/packages/runtime/src/scr_assert.c +++ b/packages/runtime/src/scr_assert.c @@ -3066,8 +3066,18 @@ static ScrDyn *scr_av_fail(ScrClosure *c, ScrDyn *const *args, size_t argc) { return scr_av_done(msg); } +/* The comparison methods take both operands: Node's ERR_MISSING_ARGS + * TypeError otherwise. False with it pending. */ +static bool scr_av_operands(size_t argc) { + if (argc >= 2) return true; + static const char text[] = "The \"actual\" and \"expected\" arguments must be specified"; + scr_throw_error_msg_code(SCR_ERR_TYPE, text, sizeof text - 1, "ERR_MISSING_ARGS"); + return false; +} + /* equal / notEqual: Node's loose comparison, NaN equal to NaN. */ static ScrDyn *scr_av_loose(ScrDyn *const *args, size_t argc, bool negated) { + if (!scr_av_operands(argc)) return NULL; const ScrDyn *a = scr_av_arg(args, argc, 0), *b = scr_av_arg(args, argc, 1); bool nan = a->kind == SCR_DYN_NUM && b->kind == SCR_DYN_NUM && isnan(a->v.num) && isnan(b->v.num); bool equal = nan || scr_dyn_abstract_eq(a, b); @@ -3097,6 +3107,7 @@ static ScrDyn *scr_av_not_equal(ScrClosure *c, ScrDyn *const *a, size_t n) { enum { SCR_AV_STRICT, SCR_AV_DEEP_STRICT, SCR_AV_DEEP, SCR_AV_PARTIAL }; static ScrDyn *scr_av_compare(ScrDyn *const *args, size_t argc, int form, bool negated) { + if (!scr_av_operands(argc)) return NULL; ScrDyn *a = (ScrDyn *)scr_av_arg(args, argc, 0), *b = (ScrDyn *)scr_av_arg(args, argc, 1); ScrStr *msg; bool has; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index ecc49e8cf..5414611b8 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4684,12 +4684,18 @@ static bool scr_dyn_instanceof_object(const ScrDyn *value) { default: break; } + /* Object.prototype is on the chain from the value's own prototype on: + * Object.prototype itself has a null prototype, so it is not an Object. */ ScrDyn *current = scr_dyn_retain((ScrDyn *)value); - for (;;) { - if (current == scr_builtin_object_prototype) { + for (bool first = true;; first = false) { + if (!first && current == scr_builtin_object_prototype) { scr_dyn_release(current); return true; } + if (current == scr_builtin_object_prototype) { + scr_dyn_release(current); + return false; + } ScrDyn *next = NULL; if (current->kind == SCR_DYN_FUNC) { /* A function's prototype is Function.prototype, whose own is @@ -4719,15 +4725,53 @@ static bool scr_dyn_instanceof_object(const ScrDyn *value) { } } -/* `value instanceof Function`: every function, unless the program set its - * prototype to null or to a non-function. */ +/* `value instanceof Function`: whether a function is on the value's + * prototype chain. A function's own prototype is Function.prototype unless + * the program set it, so the walk ends true at the first function that + * kept its prototype; it follows set prototypes through functions and + * objects alike (`Object.create(function () {})` is a Function instance), + * and ends false at null or at a value without a chain the runtime walks. + * False with the exception pending when a prototype read throws. */ static bool scr_dyn_instanceof_function(const ScrDyn *value) { - if (value->kind != SCR_DYN_FUNC) return false; - if (!scr_closure_identity(value->v.fn.clo)->props) return true; - ScrDyn *table = scr_dyn_fn_properties((ScrDyn *)value); - bool result = table->prototype ? scr_dyn_instanceof_function(table->prototype) : !table->null_proto; - scr_dyn_release(table); - return result; + ScrDyn *current = NULL; + if (value->kind == SCR_DYN_FUNC) { + if (!scr_closure_identity(value->v.fn.clo)->props) return true; + ScrDyn *table = scr_dyn_fn_properties((ScrDyn *)value); + current = table->prototype ? scr_dyn_retain(table->prototype) : NULL; + bool unset = !table->prototype && !table->null_proto; + scr_dyn_release(table); + if (unset) return true; + } else if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR) { + current = scr_dyn_get_prototype((ScrDyn *)value); + if (!current) return false; /* the read threw: pending */ + } + while (current && current->kind != SCR_DYN_NULL) { + ScrDyn *next = NULL; + if (current->kind == SCR_DYN_FUNC) { + if (!scr_closure_identity(current->v.fn.clo)->props) { + scr_dyn_release(current); + return true; + } + ScrDyn *table = scr_dyn_fn_properties(current); + bool unset = !table->prototype && !table->null_proto; + next = table->prototype ? scr_dyn_retain(table->prototype) : NULL; + scr_dyn_release(table); + if (unset) { + scr_dyn_release(current); + return true; + } + } else if (current->kind == SCR_DYN_OBJ || current->kind == SCR_DYN_ARR) { + next = scr_dyn_get_prototype(current); + if (!next) { + scr_dyn_release(current); + return false; /* the read threw: pending */ + } + } + scr_dyn_release(current); + current = next; + } + if (current) scr_dyn_release(current); + return false; } bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal) { diff --git a/tests/corpus/8078-instanceof-object-function.cjs b/tests/corpus/8078-instanceof-object-function.cjs index 9a72de00f..22f212269 100644 --- a/tests/corpus/8078-instanceof-object-function.cjs +++ b/tests/corpus/8078-instanceof-object-function.cjs @@ -53,3 +53,25 @@ function describe(value) { return value instanceof Function ? 'function' : 'object'; } console.log([nullProto, {}, describe, 0].map(describe).join(', ')); + +// Prototype chains through functions and objects, and Object.prototype +// itself, whose own prototype is null. +const base = function base() {}; +const fromFunction = {}; +Object.setPrototypeOf(fromFunction, base); +const throughObject = {}; +Object.setPrototypeOf(throughObject, fromFunction); +const nullRooted = function nullRooted() {}; +Object.setPrototypeOf(nullRooted, null); +const reparented = function reparented() {}; +Object.setPrototypeOf(reparented, fromFunction); +const chains = { + objectPrototype: Object.getPrototypeOf({}), + fromFunction, + throughObject, + nullRooted, + reparented, +}; +for (const [label, value] of Object.entries(chains)) { + console.log(`${label}: Object ${value instanceof Object}, Function ${value instanceof Function}`); +} diff --git a/tests/corpus/8190-assert-function-values.cjs b/tests/corpus/8190-assert-function-values.cjs index b91434899..f27bf22dc 100644 --- a/tests/corpus/8190-assert-function-values.cjs +++ b/tests/corpus/8190-assert-function-values.cjs @@ -28,3 +28,18 @@ const checks = [assert.ok, assert.fail, assert.strictEqual]; checks.forEach((check, i) => console.log(i, typeof check, check === checks[i], check.name)); [1, 'x', true].forEach((value) => assert.ok(value)); console.log('done'); + +// The comparison methods need both operands, as Node's ERR_MISSING_ARGS +// TypeError says, also through function values. +const comparisons = [assert.equal, assert.notEqual, assert.strictEqual, assert.notStrictEqual, + assert.deepEqual, assert.notDeepEqual, assert.deepStrictEqual, assert.notDeepStrictEqual]; +for (const compare of comparisons) { + for (const args of [[], [1]]) { + try { + compare(...args); + console.log(compare.name, args.length, 'passed'); + } catch (e) { + console.log(compare.name, args.length, e.name, e.code, e.message); + } + } +} From e7c7a458106c6bf5fb9f538eba159c0822dd510d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:24:50 -0500 Subject: [PATCH 100/273] Convert a read length once in fs.read and fs.readSync fs.read and fs.readSync with arguments the compiler could not type validated the length with `length |= 0` and then converted it again for the read. A length with a valueOf method ran valueOf twice, and the read used the second result. Node converts the length once. scr_fs_read_validate now returns the converted length, and both reads use it. --- packages/runtime/src/scr_bytes_io.c | 23 ++++++++++++------- .../8273-fs-read-write-argument-checks.cjs | 17 +++++++++++--- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index bebc2a479..daeb47c60 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -635,7 +635,8 @@ void scr_fs_cb_schedule(const ScrDyn *callback, ScrDyn *result, const ScrDyn *bu #endif } -static bool scr_fs_read_validate(const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *); +static bool scr_fs_read_validate(const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *, + double *); static bool scr_fs_encoding_chk(const ScrDyn *); static bool scr_fs_int_range_chk(const ScrDyn *, const char *, double, double, const char *); static bool scr_fs_length_int32(const ScrDyn *, double *); @@ -904,7 +905,12 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc } if (!scr_fs_cb_chk(callback, "cb")) return NULL; } - if (!strcmp(op, "read") && count >= 4 && !scr_fs_read_validate(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4))) return NULL; + /* The read length after `length |= 0`, converted once (a valueOf call + * runs once, as in Node). */ + double readLength = 0; + if (!strcmp(op, "read") && count >= 4 && + !scr_fs_read_validate(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), &readLength)) + return NULL; /* write checks its fd first, then the buffer form's callback, offset and * length (validateOffsetLengthWrite), or the string form's data and * callback. */ @@ -1121,7 +1127,7 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc double offset = ownedBuffer ? scr_fs_cb_option_number(options, "offset", 0) : scr_fs_cb_number(ARG(2), "offset", 0); double length = bytes->len - offset; if (ownedBuffer) length = scr_fs_cb_option_number(options, "length", length); - else if (readOp && count >= 4) scr_fs_length_int32(ARG(3), &length); + else if (readOp && count >= 4) length = readLength; else length = scr_fs_cb_number(ARG(3), "length", length); double position = ownedBuffer ? scr_fs_cb_option_number(options, "position", -1) : scr_fs_cb_number(ARG(4), "position", -1); if (!scr_exc_pending()) result = scr_dyn_new_num(readOp ? scr_fs_read_sync(fd, bytes, offset, length, position) : scr_fs_write_sync(fd, bytes, offset, length, position)); @@ -1973,9 +1979,10 @@ static bool scr_fs_bigint_position_chk(ScrBigInt *position, double length) { * Node's order: the fd, the buffer, the offset (an integer >= 0), the * length as `length |= 0`, the position (validatePosition, against that * length), then for a non-empty read the empty-buffer and - * validateOffsetLengthRead checks. */ + * validateOffsetLengthRead checks. The converted length goes to + * *length_out when it is not NULL, so the read does not convert again. */ static bool scr_fs_read_validate(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, - const ScrDyn *length, const ScrDyn *position) { + const ScrDyn *length, const ScrDyn *position, double *length_out) { if (fd->kind != SCR_DYN_NUM) { scr_dyn_arg_type_fail("fd", "of type number", fd); return false; @@ -1995,6 +2002,7 @@ static bool scr_fs_read_validate(const ScrDyn *fd, const ScrDyn *buffer, const S } double count = 0; if (!scr_fs_length_int32(length, &count)) return false; + if (length_out) *length_out = count; if (!scr_fs_dyn_absent(position)) { if (position->kind == SCR_DYN_BIGINT) { if (!scr_fs_bigint_position_chk(position->v.bigint, count)) return false; @@ -2034,7 +2042,7 @@ static bool scr_fs_read_validate(const ScrDyn *fd, const ScrDyn *buffer, const S void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, const ScrDyn *length, const ScrDyn *position, const ScrStr *fence) { - if (scr_fs_read_validate(fd, buffer, offset, length, position)) scr_throw_lowering_fence(fence); + if (scr_fs_read_validate(fd, buffer, offset, length, position, NULL)) scr_throw_lowering_fence(fence); } /* createReadStream/createWriteStream(path, options?): getOptions' @@ -2090,9 +2098,8 @@ double scr_fs_read_sync_dyn(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer); return 0; } - if (!scr_fs_read_validate(fd, buffer, offset, length, position)) return 0; double count = 0; - if (!scr_fs_length_int32(length, &count)) return 0; + if (!scr_fs_read_validate(fd, buffer, offset, length, position, &count)) return 0; if (count == 0) return 0; double at = scr_fs_dyn_absent(position) ? -1 : position->kind == SCR_DYN_BIGINT ? scr_bigint_to_f64(position->v.bigint) diff --git a/tests/corpus/8273-fs-read-write-argument-checks.cjs b/tests/corpus/8273-fs-read-write-argument-checks.cjs index 623d5286f..b27564837 100644 --- a/tests/corpus/8273-fs-read-write-argument-checks.cjs +++ b/tests/corpus/8273-fs-read-write-argument-checks.cjs @@ -1,7 +1,8 @@ 'use strict'; // fs.read, fs.readSync and fs.write check their arguments in Node's order // and with Node's errors: the fd, the buffer, the offset, `length |= 0`, -// number and bigint positions, and the buffer window. +// number and bigint positions, and the buffer window. A length with a +// valueOf method is converted once. const fs = require('fs'); const path = require('path'); const os = require('os'); @@ -48,6 +49,11 @@ attempt('readSync offset 4', () => fs.readSync(fd, Buffer.alloc(3), 4, 1, 0)); attempt('readSync buffer number', () => fs.readSync(fd, 3, 0, 1, 0)); attempt('readSync fd null', () => fs.readSync(null, Buffer.alloc(3), 0, 1, 0)); console.log(fs.readSync(fd, Buffer.alloc(3), 0, 0.5, 0), fs.readSync(fd, Buffer.alloc(3), 0, 2.5, 0)); +let syncCalls = 0; +const syncLength = { valueOf: () => (++syncCalls === 1 ? 2 : 3) }; +const syncTarget = Buffer.alloc(3); +const syncCount = fs.readSync(fd, syncTarget, 0, syncLength, 0); +console.log('readSync valueOf', syncCount, syncCalls, syncTarget.toString('utf8', 0, syncCount)); attempt('write fd null', () => fs.write(null, Buffer.alloc(1), 0, 1, noop)); attempt('write fd "1"', () => fs.write('1', 'x', noop)); @@ -63,7 +69,12 @@ fs.read(fd, Buffer.alloc(2), 0, 2, 2n, (error, count, buffer) => { console.log('read 2n', error, count, buffer.toString()); fs.write(fd, Buffer.from('XY'), 0, 2, 0, (error2, written) => { console.log('wrote', error2, written, fs.readFileSync(file, 'utf8')); - fs.closeSync(fd); - fs.rmSync(dir, { recursive: true }); + let calls = 0; + const length = { valueOf: () => (++calls === 1 ? 1 : 3) }; + fs.read(fd, Buffer.alloc(3), 0, length, 0, (error3, count3, buffer3) => { + console.log('read valueOf', error3, count3, calls, buffer3.toString('utf8', 0, count3)); + fs.closeSync(fd); + fs.rmSync(dir, { recursive: true }); + }); }); }); From bffd887145a81541b05cd6a1361c074c88f0f57c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:26:20 -0500 Subject: [PATCH 101/273] Keep the native receiver for private accessors and private method values A JavaScript prototype method value runs over the call-site receiver when its private members are fields (brand-checked reads and writes) or private methods it calls. A private accessor, or a private method read as a value, would read a public property of the same name, so such a method keeps its native receiver, as before. A super method call with spread arguments over the call-site receiver now spreads them into the dynamic call instead of rejecting them. --- .../frontend/lowering/class-method-values.ts | 34 ++++++++++++- .../src/frontend/lowering/lower-classes.ts | 11 ++++- .../7621-class-methods-foreign-receivers.mjs | 49 +++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts index af2ef51d5..ab9101cf7 100644 --- a/packages/compiler/src/frontend/lowering/class-method-values.ts +++ b/packages/compiler/src/frontend/lowering/class-method-values.ts @@ -383,12 +383,29 @@ function reflectedMethodValue( // base has a prototype object. Other uses of `super` keep the native // receiver. const superCalls = !staticMethod && info.base !== null && prototypeChainViewable(info.base); + // Over a dynamic receiver, private fields are brand-checked and read, and private methods + // brand-checked and called; a private accessor, or a private method read as a value, would become a + // lookup of its name. + const privateAccessors = new Set(); + const privateMethods = new Set(); + for (const other of declaration.members.nodes) + if (other.name !== undefined && ts.isPrivateIdentifier(other.name)) { + if (ts.isAccessor(other)) privateAccessors.add(other.name.text); + else if (ts.isMethodDeclaration(other)) privateMethods.add(other.name.text); + } ts.walkPreorder(member, (node) => { if (node.kind === ts.SyntaxKind.SuperKeyword && !(superCalls && isSuperMethodCallee(node))) needsNativeReceiver = true; + if (!ts.isPrivateIdentifier(node)) return; // Private members on a static method's receiver (the class value) keep - // the native receiver; prototype methods brand-check theirs. - if (staticMethod && ts.isPrivateIdentifier(node)) needsNativeReceiver = true; + // the native receiver; prototype methods brand-check theirs, except + // private accessors and private methods used other than by a call. + if ( + staticMethod || + privateAccessors.has(node.text) || + (privateMethods.has(node.text) && !isPrivateMethodCallee(node)) + ) + needsNativeReceiver = true; }); if (needsNativeReceiver) return null; // The method reads itself as a value inside its own body: it is the @@ -477,6 +494,19 @@ function prototypeChainViewable(base: ClassInfo): boolean { } /** `super` as the receiver of a called member: `super.m(...)`. */ +/** Whether `node`, a private name, is the method a call calls (`x.#m(...)`). */ +function isPrivateMethodCallee(node: ts.Node): boolean { + const access = node.parent; + return ( + access !== undefined && + ts.isPropertyAccessExpression(access) && + access.name === node && + access.parent !== undefined && + ts.isCallExpression(access.parent) && + access.parent.expression === access + ); +} + function isSuperMethodCallee(node: ts.Node): boolean { const access = node.parent; return ( diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index db8f35f95..2abbe81ca 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -9402,12 +9402,21 @@ export function lowerSuperMethodCall( type: DYN, loc, }; + // Spread arguments are spread into the dynamic call, as in a dynamic method call. + const spreads: { arg: number; what: string }[] = []; + const args = call.arguments.nodes.map((arg, index) => { + if (!ts.isSpreadElement(arg)) return lowerer.lowerExprExpecting(arg, DYN); + const spelling = ts.getTextOfNode(arg.expression); + spreads.push({ arg: index, what: spelling }); + return checkedIterableSpread(lowerer, lowerer.lowerExpr(arg.expression), spelling, loc); + }); const value: IrExpr = { kind: "dynCall", callee, receiver: lowerer.coerceToExpected(thisRef, DYN), calleeName: ts.getTextOfNode(access), - args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args, + ...(spreads.length > 0 ? { spreads } : {}), type: DYN, loc, }; diff --git a/tests/corpus/7621-class-methods-foreign-receivers.mjs b/tests/corpus/7621-class-methods-foreign-receivers.mjs index 8e4feedbd..70826a192 100644 --- a/tests/corpus/7621-class-methods-foreign-receivers.mjs +++ b/tests/corpus/7621-class-methods-foreign-receivers.mjs @@ -167,3 +167,52 @@ console.log( JSON.stringify(meshClone.geometry), typeof copyValue, ); + +// Methods that use a private accessor or return a private method keep their own receiver, as values too. +class Vault { + #v = 1; + get #value() { + return this.#v * 10; + } + set #value(x) { + this.#v = x; + } + #hidden() { + return "hidden " + this.#v; + } + read() { + return this.#value; + } + bump() { + this.#value = this.#v + 1; + return this.#v; + } + hiddenValue() { + return this.#hidden; + } +} +const vault = new Vault(); +const readVault = Vault.prototype.read; +const bumpVault = Vault.prototype.bump; +const hiddenOf = Vault.prototype.hiddenValue; +console.log(readVault.call(vault), bumpVault.call(vault), readVault.call(vault), typeof hiddenOf.call(vault)); +console.log(hiddenOf.call(vault).call(vault)); +try { + readVault.call({}); +} catch (error) { + console.log(error.constructor.name); +} + +// A method value whose body calls super with spread arguments. +class Summer { + add(...xs) { + return xs.reduce((sum, x) => sum + x, 0); + } +} +class Doubler extends Summer { + add(...xs) { + return super.add(...xs) * 2; + } +} +const addValue = Doubler.prototype.add; +console.log(addValue.call(new Doubler(), 1, 2, 3), addValue.apply(new Doubler(), [4, 5])); From 49964dc9970f39538c2d471783936f5a25dc5b57 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:26:38 -0500 Subject: [PATCH 102/273] Read prototype methods stored under computed keys on ES5 instances A member read on a `new F()` instance looked up the name only among the fields of F's prototype objects. A method stored under a computed key (`F.prototype[name] = fn`) is kept in the prototype's unknown-key field, so the read found nothing: the method's body was never visited, and the objects it writes stayed constant in the report. The read now also takes the prototype's unknown-key field, as reads of the object's own fields do. --- .../src/frontend/shape-inference/analyze.ts | 8 +++++-- .../compiler/test/constant-graphs.test.ts | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/shape-inference/analyze.ts b/packages/compiler/src/frontend/shape-inference/analyze.ts index 8b1101da2..19d3cac22 100644 --- a/packages/compiler/src/frontend/shape-inference/analyze.ts +++ b/packages/compiler/src/frontend/shape-inference/analyze.ts @@ -2190,8 +2190,12 @@ class Analyzer { this.add(protos, this.protoAtom(fn)); this.edge(this.field(fn, "prototype"), protos); this.on(protos, (proto) => { - if (proto.kind === "object") this.edge(this.field(proto, name), dst); - else if (proto.kind === "top") this.add(dst, this.top); + if (proto.kind === "object") { + // A method stored under a computed key (`F.prototype[k] = fn`) + // may be any name. + this.edge(this.field(proto, name), dst); + this.edge(this.anyField(proto), dst); + } else if (proto.kind === "top") this.add(dst, this.top); }); return; } diff --git a/packages/compiler/test/constant-graphs.test.ts b/packages/compiler/test/constant-graphs.test.ts index a89276ce7..a7cbe6118 100644 --- a/packages/compiler/test/constant-graphs.test.ts +++ b/packages/compiler/test/constant-graphs.test.ts @@ -141,3 +141,24 @@ test("constant graphs: constructors, apply calls and callbacks that one site run new RegExp(`near miss ns-fixture/index\\.js:10 \\.${field}: written-after-init`), ); }); + +// A prototype method stored under a computed key and called on instances. +const computedMethodSource = ` +var lib = {}; +lib.touched = { count: 0 }; +lib.Widget = function () {}; +var methodName = ["to", "uch"].join(""); +lib.Widget.prototype[methodName] = function () { lib.touched.count++; }; +lib.useWidget = function () { new lib.Widget().touch(); }; +module.exports = { lib }; +`; + +test("constant graphs: prototype methods stored under computed keys run on their instances", async () => { + await writeFile(join(dir, "node_modules", "ns-fixture", "index.js"), computedMethodSource); + await writeFile( + join(dir, "main.cjs"), + 'const { lib } = require("ns-fixture");\nfor (let i = 0; i < 3; i++) lib.useWidget();\nconsole.log(lib.touched.count);\n', + ); + const text = (await report()).join("\n"); + expect(text).toMatch(/near miss ns-fixture\/index\.js:3 \.count: written-after-init/); +}); From f645e8e7022e8710631cbf641b99e00d4b55f0a3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:27:15 -0500 Subject: [PATCH 103/273] Run a prototype's constructor getter on the instance An instance's `constructor` read that reaches a class prototype's own property now reads it with the instance as the receiver (Reflect.get), so an accessor's getter sees the instance as `this`, as in Node, instead of the prototype. --- .../lowering/class-dynamic-dispatch.ts | 24 +++++++++++++++---- tests/corpus/8260-class-prototype-views.mjs | 23 ++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index fd6b8c941..db1ed1c8c 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -4235,25 +4235,35 @@ export class ClassDynamicDispatch { * (prototypeReflectedFlag, set when the view initializes): then the * program deleted the property, and the read continues to the base * class's prototype and past the last one to Object.prototype, as in - * Node. */ + * Node. A prototype's own `constructor` is read with the instance + * `receiver` as `this`, so an accessor's getter runs on the instance. */ private inheritedConstructor( lowerer: Lowerer, info: ClassInfo, key: IrExpr, + receiver: IrExpr, loc: SrcLoc, ): IrExpr { const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl!), DYN); const prototype = hasClassPrototypeData(info) ? classPrototypeData(lowerer, info, loc) : null; if (!prototype) return cls; const base = info.base; + const read = (): IrExpr => ({ + kind: "libCall", + fn: "dyn.reflectGet", + args: [prototype, lowerer.coerceToExpected(key, DYN), receiver], + type: DYN, + loc, + }); + // Past the last class prototype, the read continues to Object.prototype's data property. const next: IrExpr = base && !base.builtinError && base.decl && !base.generic && !base.localClass - ? this.inheritedConstructor(lowerer, base, key, loc) + ? this.inheritedConstructor(lowerer, base, key, receiver, loc) : { kind: "dynKeyGet", value: prototype, key, type: DYN, loc }; return { kind: "ternary", cond: { kind: "libCall", fn: "dyn.hasOwn", args: [prototype, key], type: BOOL, loc }, - then: { kind: "dynKeyGet", value: prototype, key, type: DYN, loc }, + then: read(), else_: { kind: "ternary", cond: varRef(prototypeReflectedFlag(lowerer, info), BOOL, loc), @@ -6045,7 +6055,13 @@ export class ClassDynamicDispatch { type: DYN, loc, }, - else_: this.inheritedConstructor(lowerer, info, key, loc), + else_: this.inheritedConstructor( + lowerer, + info, + key, + lowerer.coerceToExpected(receiver, DYN), + loc, + ), type: DYN, loc, }, diff --git a/tests/corpus/8260-class-prototype-views.mjs b/tests/corpus/8260-class-prototype-views.mjs index 35e55f5f4..0f7b080bf 100644 --- a/tests/corpus/8260-class-prototype-views.mjs +++ b/tests/corpus/8260-class-prototype-views.mjs @@ -153,3 +153,26 @@ delete Object.getPrototypeOf(new Root()).constructor; console.log(constructorNames(), new Middle().constructor === Object); Middle.prototype.constructor = function Named() {}; console.log(constructorNames()); + +// A reflected prototype whose constructor is an accessor: an instance's read runs the getter with the +// instance as `this`, also where the instance's class inherits it. +class Facet { + constructor(tag) { + this.tag = tag; + } +} +class Tile extends Facet {} +class Brick extends Tile {} +const facets = [new Tile("s"), new Brick("c"), new Facet("p")]; +const facetTags = () => + facets.map((facet) => (typeof facet.constructor === "string" ? facet.constructor : facet.constructor.name)).join(","); +Object.defineProperty(Tile.prototype, "constructor", { + get() { + return "on " + this.tag; + }, + configurable: true, +}); +delete Object.getPrototypeOf(new Brick("x")).constructor; +console.log(facetTags()); +delete Object.getPrototypeOf(new Tile("x")).constructor; +console.log(facetTags()); From e22cdf358bcd2a67909e3e905e68312370e7928b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:28:43 -0500 Subject: [PATCH 104/273] Decode an untyped stream unshift's string chunk with its encoding `s.unshift("e9", "hex")` on a native stream held as an untyped value dropped the encoding argument and put the string's UTF-8 bytes at the front. A byte-mode stream now takes a string chunk with an encoding as the bytes Buffer.from(chunk, encoding) holds, as Node's readableAddChunkUnshift converts it, and an unknown encoding throws ERR_UNKNOWN_ENCODING. Object-mode streams keep the string as the entry. --- packages/runtime/src/scr_stream.c | 25 ++++++++++++++++---- tests/corpus/8232-native-streams-untyped.cjs | 24 +++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 33382363b..552fef146 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -8101,10 +8101,25 @@ static ScrDyn *scr_stream_dyn_push(ScrStream *stream, ScrDyn *const *args, size_ return scr_exc_pending() ? NULL : scr_dyn_new_bool(more); } -/* unshift(chunk): whether the stream takes more data afterwards, Node's - * canPushMore. */ -static ScrDyn *scr_stream_dyn_unshift(ScrStream *stream, const ScrDyn *chunk) { - scr_stream_unshift_dyn(stream, chunk); +/* unshift(chunk[, encoding]): a byte-mode stream takes a string chunk with + * an encoding as the bytes Buffer.from(chunk, encoding) holds, as Node's + * readableAddChunkUnshift converts it; an unknown encoding throws + * ERR_UNKNOWN_ENCODING. Answers whether the stream takes more data + * afterwards, Node's canPushMore. */ +static ScrDyn *scr_stream_dyn_unshift(ScrStream *stream, ScrDyn *const *args, size_t argc) { + ScrDyn *chunk = argc ? args[0] : scr_dyn_undefined(); + ScrDyn *encoding = argc > 1 ? args[1] : scr_dyn_undefined(); + if (chunk->kind == SCR_DYN_STR && encoding->kind == SCR_DYN_STR && + !(stream->st->has_r && stream->st->r.object_entries)) { + ScrStr *canonical = scr_bytes_normalize_encoding(encoding->v.str); + if (!canonical) return NULL; + ScrBytes *b = scr_stream_buffer(scr_bytes_from_str(chunk->v.str, canonical)); + scr_str_release(canonical); + scr_stream_unshift(stream, b); + scr_bytes_release(b); + } else { + scr_stream_unshift_dyn(stream, chunk); + } if (scr_exc_pending()) return NULL; ScrStreamState *st = stream->st; bool more = chunk->kind != SCR_DYN_NULL && st->has_r && !st->destroyed && !st->errored && @@ -8169,7 +8184,7 @@ static ScrDyn *scr_stream_dyn_call(ScrStream *stream, ScrDyn *self, const char * } } else if (!strcmp(method, "eventNames")) result = scr_stream_dyn_event_names(stream); else if (!strcmp(method, "push")) result = scr_stream_dyn_push(stream, args, argc); - else if (!strcmp(method, "unshift")) result = scr_stream_dyn_unshift(stream, first); + else if (!strcmp(method, "unshift")) result = scr_stream_dyn_unshift(stream, args, argc); else if (!strcmp(method, "setDefaultEncoding")) { ScrStream *returned = scr_stream_set_default_encoding(stream, first); if (returned) scr_stream_release(returned); diff --git a/tests/corpus/8232-native-streams-untyped.cjs b/tests/corpus/8232-native-streams-untyped.cjs index b70017b5a..fb1c1f0c1 100644 --- a/tests/corpus/8232-native-streams-untyped.cjs +++ b/tests/corpus/8232-native-streams-untyped.cjs @@ -54,3 +54,27 @@ const w = new Writable({ }); w.on("finish", () => console.log("finish")); drain(w); + +// unshift(string, encoding) puts the decoded bytes at the front; an +// unknown encoding throws. +const bytes = new Readable({ read() {} }); +const parts = []; +bytes.on("data", (c) => parts.push(c.toString("hex"))); +bytes.on("end", () => console.log("unshift bytes", parts.join("|"))); +(function (s) { + console.log(s.unshift("e9", "hex"), s.unshift("aGk=", "base64"), s.unshift("é", "latin1"), s.unshift("x", "utf8")); + try { + s.unshift("x", "bogus"); + } catch (e) { + console.log(e.code, e.message); + } + s.push(null); +})(bytes); +const text = new Readable({ read() {} }); +text.setEncoding("utf8"); +text.on("data", (c) => console.log("unshift text", JSON.stringify(c))); +(function (s) { + s.unshift("c3a9", "hex"); + s.unshift("41", "hex"); + s.push(null); +})(text); From d9fdd81ee92ed8b60dbceb6a311843e119207059 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:32:40 -0500 Subject: [PATCH 105/273] Construct X509Certificate bound through a require() member `const X = require("crypto").X509Certificate; new X(data)` now resolves to the builtin class, so the standard-library constructor fence reported SC2020 for it before the X509Certificate lowering ran. That lowering now runs ahead of the fence. Corpus 8231 constructs a certificate through the binding. --- .../src/frontend/lowering/lower-classes.ts | 189 +++++++++--------- .../8231-builtin-member-binding-new.cjs | 27 ++- 2 files changed, 122 insertions(+), 94 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 8bd5e57d7..fc89dc6b1 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -10122,6 +10122,102 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const agent = lowerHttpAgentNew(lowerer, expr); if (agent) return agent; } + // `new crypto.X509Certificate(data)` — the Dirent-style data record: + // the certificate's lowered members (fingerprint — the SHA-1 of the + // DER, uppercase colon-separated — plus the validFrom/validTo + // validity window in Node's ASN1_TIME_print shape) compute AT + // CONSTRUCTION, when Node parses too, so unparseable input throws + // Node's exact PEM error here (ERR_OSSL_PEM_NO_START_LINE) and the + // handle never exists. Both import forms (`crypto.X509Certificate` + // through the namespace, named `X509Certificate`); Buffer input only + // — the readFileSync idiom. mapType interns the matching record, so + // locals and the composed member reads all flow. The data argument + // feeds THREE field computations, so construction goes through an + // interned helper whose parameter evaluates it exactly once. + // It runs before the standard-library constructor fence below, which + // also sees `const X = require("crypto").X509Certificate` as the + // builtin class. + { + const callee = expr.expression; + const isX509 = + (ts.isPropertyAccessExpression(callee) && + callee.name.text === "X509Certificate" && + lowerer.builtinNamespaceModuleOf(callee.expression) === "crypto") || + (ts.isIdentifier(callee) && + (() => { + const bi = lowerer.builtinImportOf(callee); + return bi?.module === "crypto" && bi.member === "X509Certificate"; + })()); + if (isX509) { + const args: readonly ts.Expression[] = expr.arguments?.nodes ?? []; + if (args.length !== 1) { + lowerer.noLowering( + "X509Certificate with this argument shape", + expr, + "the supported form is new X509Certificate(readFileSync(path))", + ); + } + const data = lowerer.lowerExpr(args[0]!); + const isBytes = data.type.kind === "bytes" && data.type.elem === "u8"; + if (!isBytes && data.type.kind !== "string") { + lowerer.noLowering( + `X509Certificate over '${lowerer.fmt(data.type)}' data`, + args[0]!, + "pass the certificate Buffer or PEM string (an fs.readFileSync result)", + ); + } + const t = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (t?.kind !== "record") lowerer.badType(expr, lowerer.typeOf(expr)); + const key = `x509.record:${isBytes ? "bytes" : "str"}`; + let helper = lowerer.valueHelpers.get(key); + if (!helper) { + 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 }; + const field = ( + name: string, + fn: + | "crypto.x509Fingerprint" + | "crypto.x509FingerprintStr" + | "crypto.x509ValidFrom" + | "crypto.x509ValidFromStr" + | "crypto.x509ValidTo" + | "crypto.x509ValidToStr", + ): { name: string; value: IrExpr } => ({ + name, + value: { kind: "libCall", fn, args: [dRef], type: STRING, loc }, + }); + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "d.0", name: "d", type: dataT }], + returnType: t, + locals: [{ id: "d.0", name: "d", type: dataT, mutable: false }], + body: [ + { + kind: "return", + value: { + kind: "recordLit", + fields: [ + field( + "fingerprint", + isBytes ? "crypto.x509Fingerprint" : "crypto.x509FingerprintStr", + ), + field("validFrom", isBytes ? "crypto.x509ValidFrom" : "crypto.x509ValidFromStr"), + field("validTo", isBytes ? "crypto.x509ValidTo" : "crypto.x509ValidToStr"), + ], + type: t, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: helper, args: [data], type: t, loc }; + } + } if ( ts.isIdentifier(expr.expression) || (ts.isPropertyAccessExpression(expr.expression) && @@ -10907,99 +11003,6 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { ); } } - // `new crypto.X509Certificate(data)` — the Dirent-style data record: - // the certificate's lowered members (fingerprint — the SHA-1 of the - // DER, uppercase colon-separated — plus the validFrom/validTo - // validity window in Node's ASN1_TIME_print shape) compute AT - // CONSTRUCTION, when Node parses too, so unparseable input throws - // Node's exact PEM error here (ERR_OSSL_PEM_NO_START_LINE) and the - // handle never exists. Both import forms (`crypto.X509Certificate` - // through the namespace, named `X509Certificate`); Buffer input only - // — the readFileSync idiom. mapType interns the matching record, so - // locals and the composed member reads all flow. The data argument - // feeds THREE field computations, so construction goes through an - // interned helper whose parameter evaluates it exactly once. - { - const callee = expr.expression; - const isX509 = - (ts.isPropertyAccessExpression(callee) && - callee.name.text === "X509Certificate" && - lowerer.builtinNamespaceModuleOf(callee.expression) === "crypto") || - (ts.isIdentifier(callee) && - (() => { - const bi = lowerer.builtinImportOf(callee); - return bi?.module === "crypto" && bi.member === "X509Certificate"; - })()); - if (isX509) { - const args: readonly ts.Expression[] = expr.arguments?.nodes ?? []; - if (args.length !== 1) { - lowerer.noLowering( - "X509Certificate with this argument shape", - expr, - "the supported form is new X509Certificate(readFileSync(path))", - ); - } - const data = lowerer.lowerExpr(args[0]!); - const isBytes = data.type.kind === "bytes" && data.type.elem === "u8"; - if (!isBytes && data.type.kind !== "string") { - lowerer.noLowering( - `X509Certificate over '${lowerer.fmt(data.type)}' data`, - args[0]!, - "pass the certificate Buffer or PEM string (an fs.readFileSync result)", - ); - } - const t = lowerer.mapTypeOf(lowerer.typeOf(expr)); - if (t?.kind !== "record") lowerer.badType(expr, lowerer.typeOf(expr)); - const key = `x509.record:${isBytes ? "bytes" : "str"}`; - let helper = lowerer.valueHelpers.get(key); - if (!helper) { - 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 }; - const field = ( - name: string, - fn: - | "crypto.x509Fingerprint" - | "crypto.x509FingerprintStr" - | "crypto.x509ValidFrom" - | "crypto.x509ValidFromStr" - | "crypto.x509ValidTo" - | "crypto.x509ValidToStr", - ): { name: string; value: IrExpr } => ({ - name, - value: { kind: "libCall", fn, args: [dRef], type: STRING, loc }, - }); - lowerer.liftedFns.push({ - name: helper, - params: [{ localId: "d.0", name: "d", type: dataT }], - returnType: t, - locals: [{ id: "d.0", name: "d", type: dataT, mutable: false }], - body: [ - { - kind: "return", - value: { - kind: "recordLit", - fields: [ - field( - "fingerprint", - isBytes ? "crypto.x509Fingerprint" : "crypto.x509FingerprintStr", - ), - field("validFrom", isBytes ? "crypto.x509ValidFrom" : "crypto.x509ValidFromStr"), - field("validTo", isBytes ? "crypto.x509ValidTo" : "crypto.x509ValidToStr"), - ], - type: t, - loc, - }, - loc, - }, - ], - loc, - }); - } - return { kind: "call", callee: helper, args: [data], type: t, loc }; - } - } // `new X(...)` through a class VALUE (a classval-typed binding, array // element, map read, param): the newValue dispatch through the class // object's construct thunk. Arguments complete against the STATIC diff --git a/tests/corpus/8231-builtin-member-binding-new.cjs b/tests/corpus/8231-builtin-member-binding-new.cjs index 1549425f7..df1b2ebb3 100644 --- a/tests/corpus/8231-builtin-member-binding-new.cjs +++ b/tests/corpus/8231-builtin-member-binding-new.cjs @@ -1,12 +1,14 @@ // `new` of a builtin class bound through a member of a require() result // (`const SD = require("string_decoder").StringDecoder`, the spelling of // Node's own tests): the binding resolves to the class, as the -// destructured `const { StringDecoder } = require(...)` does. +// destructured `const { StringDecoder } = require(...)` does, also for the +// X509Certificate record. "use strict"; const SD = require("string_decoder").StringDecoder; const Readable = require("stream").Readable; const PassThrough = require("stream").PassThrough; const Transform = require("stream").Transform; +const X509 = require("crypto").X509Certificate; const decoder = new SD("utf8"); console.log(decoder.encoding); @@ -33,3 +35,26 @@ const upper = new Transform({ }); upper.on("data", (chunk) => console.log("transform", String(chunk))); upper.end("shout"); + +// The X509Certificate record constructs through the same binding. +const PEM = `-----BEGIN CERTIFICATE----- +MIIDEzCCAfugAwIBAgIUMsBCFtPck8TATMJzfikgW963q3AwDQYJKoZIhvcNAQEL +BQAwGDEWMBQGA1UEAwwNdHNuYXRpdmUtdGVzdDAgFw0yNjA3MTcxNzUyMTFaGA8y +MTI2MDYyMzE3NTIxMVowGDEWMBQGA1UEAwwNdHNuYXRpdmUtdGVzdDCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALRspe2XjtKvNU7Ws5JfqzmTCPmWtfw0 +1Uha271hNp/30VstOxWLbpqPrXDvIwJMN4o8CRp92Y1hcmS36m9QS5qfXJFw+n+S +SWGpFdMNvp6hro0OoNBA+anuKWoTsMyHSwe8ND0fyobQbgAXJ0+snmuK0E2ii+CM +IoUqYbK57nHeyHfISLupgrRoNGXjDHv4v+RSr3MJDag6BLA+/yVjkBOBOT6kaE4K +oyNXYYsfFxGnB/4pDSanV/UsfJ4wHXkVn18ndZHIzTsZMUkV4BecF8UaMrP+u8NI +c5wce0uWLFFEneK4q15iVvk9TaY0g1Uj69ryRT3L58W7RYYuHu7cVB0CAwEAAaNT +MFEwHQYDVR0OBBYEFLT2GqaTmxDhntPxMWaUZvA3s9V5MB8GA1UdIwQYMBaAFLT2 +GqaTmxDhntPxMWaUZvA3s9V5MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAFcTLVJIyhbIKHfOnGM5T+Kzu5IoDlqzr+Rdx/0IKFLVDqEA9ihEme07 +K/d246pOW5LBceVDY2eMikDpIOjYPEoe9C+Ftv5zrCaDw1wtXrq8a7/eS5B+3TzL +k8c1SVVDPIbmViAZNRmnkwCPBsIF9xIfMFhxz4BTJrm/Lt2Hv1GcRBQx/yRK89sj +JiGnsTna//4mPGRGYw+dRCq4tbyr8HbBKdJmbzOCb4CPKfwAMKUpd4e0Ao7jh/BU +EML7B3BEzv5Jw/BT7B39wAfDyhq32PnRnZefF+7pAmpkklHbckBtZtWn02zwQazv +5z62tszZee2i4BouJiyFnb30F6Pebs4= +-----END CERTIFICATE-----`; +const cert = new X509(PEM); +console.log(cert.fingerprint, cert.validFrom, cert.validTo); From 54f12d73dccf1b2cb62bb36ac920e7b8c4e0f5d0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:37:07 -0500 Subject: [PATCH 106/273] Use built-in typed-array species, copy slices after species creation, and read element descriptors - A built-in typed-array constructor set as a view's `constructor` is its own species, as the getter it inherits from %TypedArray% returns it: `u8.constructor = Float64Array; u8.map(f)` makes a Float64Array. An own Symbol.species on the constructor still wins. - TypedArray.prototype.slice with a species constructor creates the result first and then copies the source's current elements, up to its current length: a constructor that changes the source is seen. - Object.defineProperty on a typed-array element reads the descriptor as ToPropertyDescriptor does, through its getters and in its order, and throws a TypeError for a primitive descriptor or an accessor with a value. The generic object path shares the same reader. --- packages/runtime/src/scr_json.c | 176 ++++++++++++------ .../7753-typed-array-named-properties.js | 23 +++ 2 files changed, 140 insertions(+), 59 deletions(-) diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index be63e1436..f28160d42 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -5024,6 +5024,29 @@ static double *dyn_bytes_values(const ScrBytes *b, size_t n) { return values; } +static ScrDyn *scr_dyn_symbol_receiver(const ScrDyn *value); +static ScrDynEntry *scr_dyn_symbol_entry(const ScrDyn *receiver, const ScrDyn *key, bool own); + +/* Whether a typed-array constructor has a Symbol.species of its own, + * shadowing the one it inherits from %TypedArray%. */ +static bool scr_bytes_ctor_own_species(const ScrDyn *constructor) { + ScrDyn *table = scr_dyn_symbol_receiver(constructor); + bool own = table && scr_dyn_symbol_entry(table, scr_species_symbol, true); + scr_dyn_release(table); + return own; +} + +/* A relative index (ToIntegerOrInfinity, negatives from the end), clamped + * to [0, len], as scr_bytes_slice computes it. */ +static size_t scr_bytes_rel_index_of(double i, size_t len) { + if (i != i) return 0; + double t = trunc(i); + if (t < 0) t += (double)len; + if (t < 0) return 0; + if (t > (double)len) return len; + return (size_t)t; +} + /* SpeciesConstructor of a typed array: NULL (and no exception) for the * default constructor of its kind, else the species constructor (+1). * Only a `constructor` the program set on the view can differ. */ @@ -5044,6 +5067,13 @@ static ScrDyn *scr_bytes_species(ScrDyn *recv, bool *failed) { return NULL; } ScrDyn *species = scr_species_symbol ? scr_dyn_symbol_key_get(constructor, scr_species_symbol, true) : NULL; + if (species && species->kind == SCR_DYN_UNDEF && scr_bytes_ctor_elem(constructor) >= 0 && + !scr_bytes_ctor_own_species(constructor)) { + /* A built-in typed-array constructor inherits %TypedArray%'s species + * getter, which returns the constructor itself. */ + scr_dyn_release(species); + species = scr_dyn_retain(constructor); + } scr_dyn_release(constructor); if (scr_exc_pending()) { scr_dyn_release(species); *failed = true; return NULL; } if (!species || species->kind == SCR_DYN_UNDEF || species->kind == SCR_DYN_NULL || @@ -5096,33 +5126,40 @@ ScrDyn *scr_dyn_bytes_method(ScrDyn *recv, const char *method, ScrDyn *const *ar bool failed; ScrDyn *species = recv->buffer ? NULL : scr_bytes_species(recv, &failed); if (!recv->buffer && failed) return NULL; + if (species && !subarray) { + /* TypedArraySpeciesCreate runs first: the constructor can change the + * source, and the copy reads its elements as they are afterwards, + * up to its current length. */ + size_t from = scr_bytes_rel_index_of(startD, blen), to = scr_bytes_rel_index_of(endD, blen); + size_t count = to > from ? to - from : 0; + ScrDyn *result = scr_bytes_static_create(species, count); + scr_dyn_release(species); + if (!result) return NULL; + size_t end = to < bytes->len ? to : bytes->len; + for (size_t k = from, n = 0; k < end; k++, n++) + scr_bytes_set(result->v.bytes, (double)n, scr_bytes_get(bytes, (double)k)); + return result; + } ScrBytes *out = subarray ? scr_bytes_subarray(bytes, startD, endD) : scr_bytes_slice(bytes, startD, endD); if (!species) { ScrDyn *d = recv->buffer ? scr_dyn_new_buffer(out) : scr_dyn_new_bytes(out); scr_bytes_release(out); return d; } - ScrDyn *result; - if (subarray) { - /* The species constructor views the same buffer. */ - ScrDyn *ctor_args = scr_dyn_new_arr(); - scr_dyn_arr_push(ctor_args, scr_array_buffer_from_bytes(bytes)); - scr_dyn_arr_push(ctor_args, scr_dyn_new_num(scr_bytes_byte_offset(out))); - scr_dyn_arr_push(ctor_args, scr_dyn_new_num((double)out->len)); - ScrStr *what = scr_str_new("TypedArray species", 18); - result = scr_dyn_construct(species, ctor_args, what); - scr_str_release(what); - scr_dyn_release(ctor_args); - if (result && (result->kind != SCR_DYN_BYTES || result->v.bytes->is_data_view)) { - scr_dyn_release(result); - result = NULL; - static const char message[] = "this is not a typed array."; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); - } - } else { - result = scr_bytes_static_create(species, out->len); - for (size_t i = 0; result && i < out->len; i++) - scr_bytes_set(result->v.bytes, (double)i, scr_bytes_get(out, (double)i)); + /* A subarray's species constructor views the same buffer. */ + ScrDyn *ctor_args = scr_dyn_new_arr(); + scr_dyn_arr_push(ctor_args, scr_array_buffer_from_bytes(bytes)); + scr_dyn_arr_push(ctor_args, scr_dyn_new_num(scr_bytes_byte_offset(out))); + scr_dyn_arr_push(ctor_args, scr_dyn_new_num((double)out->len)); + ScrStr *what = scr_str_new("TypedArray species", 18); + ScrDyn *result = scr_dyn_construct(species, ctor_args, what); + scr_str_release(what); + scr_dyn_release(ctor_args); + if (result && (result->kind != SCR_DYN_BYTES || result->v.bytes->is_data_view)) { + scr_dyn_release(result); + result = NULL; + static const char message[] = "this is not a typed array."; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); } scr_dyn_release(species); scr_bytes_release(out); @@ -6382,6 +6419,45 @@ 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]); } + +/* ToPropertyDescriptor: the present fields of `descriptor` (enumerable, + * configurable, value, writable, get, set; +1 each, NULL when absent), + * read in that order through any getters. False with a TypeError pending + * for a primitive descriptor, a getter's exception, or an invalid + * combination; every field is then released. */ +static bool scr_dyn_descriptor_fields_read(const ScrDyn *descriptor, ScrDyn **fields) { + for (size_t i = 0; i < 6; i++) fields[i] = NULL; + if (descriptor->kind != SCR_DYN_OBJ) { + static const char msg[] = "Property description must be an object"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return false; + } + static const char *const names[] = {"enumerable", "configurable", "value", "writable", "get", "set"}; + static const size_t lengths[] = {10, 12, 5, 8, 3, 3}; + for (size_t i = 0; i < 6; i++) { + if (!scr_dyn_obj_get(descriptor, names[i], lengths[i])) continue; + fields[i] = scr_dyn_obj_read(descriptor, names[i], lengths[i]); + if (!fields[i]) { + scr_dyn_descriptor_fields_drop(fields); + return false; + } + } + ScrDyn *get = fields[4], *set = fields[5]; + if ((get || set) && (fields[2] || fields[3])) { + scr_dyn_descriptor_fields_drop(fields); + static const char msg[] = "Invalid property descriptor. Cannot both specify accessors and a value or writable attribute"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return false; + } + if ((get && get->kind != SCR_DYN_UNDEF && get->kind != SCR_DYN_FUNC) || + (set && set->kind != SCR_DYN_UNDEF && set->kind != SCR_DYN_FUNC)) { + scr_dyn_descriptor_fields_drop(fields); + static const char msg[] = "Getter and setter must be functions"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return false; + } + return true; +} 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); @@ -8374,12 +8450,19 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) if (numeric) { /* [[DefineOwnProperty]] of an element: a valid index and a data * descriptor that keeps it writable, enumerable and configurable. */ + /* The descriptor is read first (ToPropertyDescriptor), through its + * getters, and must be an object. */ + ScrDyn *fields[6]; + if (!scr_dyn_descriptor_fields_read(descriptor, fields)) { + scr_str_release(name); + return NULL; + } + const ScrDyn *enumerable = fields[0], *configurable = fields[1], *value = fields[2]; + const ScrDyn *writable = fields[3]; + bool accessor = fields[4] || fields[5]; bool valid = !signbit(index) && trunc(index) == index && index < (double)target->v.bytes->len; - const ScrDyn *configurable = scr_dyn_obj_get(descriptor, "configurable", 12); - const ScrDyn *enumerable = scr_dyn_obj_get(descriptor, "enumerable", 10); - const ScrDyn *writable = scr_dyn_obj_get(descriptor, "writable", 8); - bool accessor = scr_dyn_obj_get(descriptor, "get", 3) || scr_dyn_obj_get(descriptor, "set", 3); if (!valid) { + scr_dyn_descriptor_fields_drop(fields); scr_str_release(name); static const char message[] = "Invalid typed array index"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); @@ -8387,6 +8470,7 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) } if (accessor || (configurable && !scr_dyn_truthy(configurable)) || (enumerable && !scr_dyn_truthy(enumerable)) || (writable && !scr_dyn_truthy(writable))) { + scr_dyn_descriptor_fields_drop(fields); ScrJsonBuf message; scr_jb_init(&message); scr_jb_puts(&message, "Cannot redefine property: "); @@ -8396,13 +8480,15 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) return NULL; } scr_str_release(name); - const ScrDyn *value = scr_dyn_obj_get(descriptor, "value", 5); double number; + bool stored = true; if (value) { - if (!scr_dyn_number_coerce_js(value, &number)) return NULL; - scr_bytes_set(target->v.bytes, index, number); + stored = scr_dyn_number_coerce_js(value, &number); + /* The conversion can run user code; the index is checked again. */ + if (stored && index < (double)target->v.bytes->len) scr_bytes_set(target->v.bytes, index, number); } - return scr_dyn_retain(target); + scr_dyn_descriptor_fields_drop(fields); + return stored ? scr_dyn_retain(target) : NULL; } bool fenced = scr_bytes_protocol_name(name) && !scr_bytes_has_named(target, name); if (fenced) scr_bytes_protocol_fence(name->data, name->len); @@ -8420,44 +8506,16 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) } ScrStr *name = scr_dyn_property_key(key); if (!name) return NULL; - if (descriptor->kind != SCR_DYN_OBJ) { + ScrDyn *fields[6]; + if (!scr_dyn_descriptor_fields_read(descriptor, fields)) { scr_str_release(name); - static const char msg[] = "Property description must be an object"; - scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); return NULL; } - static const char *const names[] = {"enumerable", "configurable", "value", "writable", "get", "set"}; - static const size_t lengths[] = {10, 12, 5, 8, 3, 3}; - ScrDyn *fields[6] = {NULL}; - for (size_t i = 0; i < 6; i++) { - if (!scr_dyn_obj_get(descriptor, names[i], lengths[i])) continue; - fields[i] = scr_dyn_obj_read(descriptor, names[i], lengths[i]); - if (!fields[i]) { - scr_dyn_descriptor_fields_drop(fields); - scr_str_release(name); - return NULL; - } - } ScrDyn *e = fields[0], *c = fields[1], *value = fields[2]; ScrDyn *w = fields[3], *get = fields[4], *set = fields[5]; ScrDynEntry *prior = scr_dyn_entry(target, name); bool accessor_descriptor = get || set; bool data_descriptor = value || w; - if (accessor_descriptor && data_descriptor) { - scr_dyn_descriptor_fields_drop(fields); - scr_str_release(name); - static const char msg[] = "Invalid property descriptor. Cannot both specify accessors and a value or writable attribute"; - scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); - return NULL; - } - if ((get && get->kind != SCR_DYN_UNDEF && get->kind != SCR_DYN_FUNC) || - (set && set->kind != SCR_DYN_UNDEF && set->kind != SCR_DYN_FUNC)) { - scr_dyn_descriptor_fields_drop(fields); - scr_str_release(name); - static const char msg[] = "Getter and setter must be functions"; - scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); - return NULL; - } bool accessor = accessor_descriptor || (!data_descriptor && prior && prior->accessor); bool writable = w ? scr_dyn_truthy(w) : prior && !prior->accessor && !accessor ? prior->writable : false; bool enumerable = e ? scr_dyn_truthy(e) : prior ? prior->enumerable : false; diff --git a/tests/corpus/7753-typed-array-named-properties.js b/tests/corpus/7753-typed-array-named-properties.js index b49f100b5..449e78ac7 100644 --- a/tests/corpus/7753-typed-array-named-properties.js +++ b/tests/corpus/7753-typed-array-named-properties.js @@ -23,6 +23,17 @@ function expandos(TA) { try { Object.defineProperty(a, "7", { value: 1 }); } catch (e) { console.log(e.constructor.name, e.message); } try { Object.defineProperty(a, "-0", { value: 1 }); } catch (e) { console.log(e.constructor.name, e.message); } try { Object.defineProperty(a, "0", { get() { return 1; } }); } catch (e) { console.log(e.constructor.name, e.message); } + // An element's descriptor is read through its getters, and must be an object. + const got = []; + Object.defineProperty(a, "1", { get value() { got.push("value"); return 6; }, get enumerable() { got.push("enumerable"); return true; } }); + console.log(a[1], got); + for (const bad of [5, "x", undefined]) { + try { Object.defineProperty(a, "0", bad); } catch (e) { console.log(e.constructor.name); } + } + try { Object.defineProperty(a, "9", 5); } catch (e) { console.log(e.constructor.name); } + try { Object.defineProperty(a, "0", { get configurable() { throw new RangeError("read"); } }); } catch (e) { console.log(e.constructor.name, e.message); } + try { Object.defineProperty(a, "0", { value: 1, get: undefined }); } catch (e) { console.log(e.constructor.name); } + console.log(a[0], a[1]); let reads = 0; Object.defineProperty(a, "g", { get() { reads++; return this.length; }, enumerable: true }); console.log(a.g, reads, new TA(0)); @@ -56,6 +67,18 @@ function species(TA, Other) { const u = new TA([1, 2, 3]); u.constructor = { [Symbol.species]: Uint16Array }; console.log(u.filter(() => true), u.subarray(1), u.subarray(1).constructor === Uint16Array); + // A built-in constructor set as `constructor` is its own species. + const w = new TA([1, 2, 3]); + w.constructor = Float64Array; + const wm = w.map((v) => v / 2), ws = w.slice(1); + console.log(wm, wm.constructor === Float64Array, ws, ws.constructor === Float64Array, w.filter((v) => v > 1)); + w.constructor = TA; + console.log(w.map((v) => v / 2).constructor === TA); + // slice copies the source after the species constructor ran. + const x = new TA([1, 2, 3, 4]); + x.constructor = {}; + x.constructor[Symbol.species] = function (n) { x[1] = 40; x[2] = 30; return new Other(n); }; + console.log(x.slice(1, 3), x.slice(-1), x.slice(2, 1)); } function reflection(TA) { From 9b64c7388d7b675a645ae09099e955feea03fb55 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:38:12 -0500 Subject: [PATCH 107/273] Resume hoisted-call writes reached from earlier bails, and shift only by exact powers of two - A speculated write with hoisted calls resumes over its stored call results only when its own check failed. Reached in the resume copy from an earlier write's failed check, its calls never ran on that iteration, so it now runs as written: the calls happen once and the arithmetic reads their fresh results. - A literal divisor is a power of two only when it is exactly 2^k with k in [0, 52]. Math.log2 rounds 2^52 + 1 to 52, so `Math.floor(v / (2^52 + 1))` was emitted as a shift by 52. - A helper expands only when every call in its body is of another helper that expands, so an expansion never moves a call with effects ahead of an argument read. Hoisting already refused such writes; the classification now says so itself. --- packages/compiler/src/backend/llvm/emitter.ts | 37 ++++++++++----- .../backend/llvm/speculative-integers.test.ts | 30 ++++++++++++ .../src/backend/llvm/speculative-integers.ts | 29 ++++++++---- tests/corpus/7757-speculative-key-chains.ts | 47 +++++++++++++++++++ 4 files changed, 124 insertions(+), 19 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 4080084de..eb1fa4157 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1457,14 +1457,20 @@ export class LlEmitter { /** What speculative loop planning may assume here (SpeculationContext). */ speculationContext(): SpeculationContext { + const inlinable = (callee: string): InlinableFunction | undefined => { + if (!this.inlinableFunctions.has(callee)) { + // Unset while its body is checked: a recursive helper is not inlinable. + this.inlinableFunctions.set(callee, undefined); + const fn = this.fnByName.get(callee); + this.inlinableFunctions.set( + callee, + fn ? inlinableNumberFunction(fn, inlinable) : undefined, + ); + } + return this.inlinableFunctions.get(callee); + }; return { - inlinable: (callee) => { - if (!this.inlinableFunctions.has(callee)) { - const fn = this.fnByName.get(callee); - this.inlinableFunctions.set(callee, fn ? inlinableNumberFunction(fn) : undefined); - } - return this.inlinableFunctions.get(callee); - }, + inlinable, unaliased: (id) => { const local = this.currentLocals.get(id); return ( @@ -5626,15 +5632,24 @@ export class LlEmitter { if (resume && !resume.placed && !this.B.isTerminated()) { // The fast copy's failed checks re-execute this statement here. resume.placed = true; - this.B.br(resume.label); - this.B.startBlock(resume.label); - // A write with hoisted leaves re-executes its arithmetic over the - // leaves' stored doubles: each leaf ran once, in the fast copy. + // A write with hoisted leaves resumed from its own failed check + // re-executes its arithmetic over the leaves' stored doubles: each + // leaf ran once, in the fast copy. Reached from an earlier statement + // of this copy (a failed check before it), its leaves never ran, so + // it runs as written. const rewritten = this.speculationResumeValues?.get(s); if (rewritten !== undefined && rewritten.leaves.length > 0) { + const join = this.B.newLabel("spec.resume.join"); + this.emitStmt(s); + if (!this.B.isTerminated()) this.B.br(join); + this.B.startBlock(resume.label); this.emitStmt(withSpeculativeValue(s, rewritten.value)); + if (!this.B.isTerminated()) this.B.br(join); + this.B.startBlock(join); return; } + this.B.br(resume.label); + this.B.startBlock(resume.label); } if (this.speculation?.writes.has(s)) { this.emitSpeculativeWrite(s, this.speculation.resume.get(s)); diff --git a/packages/compiler/src/backend/llvm/speculative-integers.test.ts b/packages/compiler/src/backend/llvm/speculative-integers.test.ts index 86055b1b6..151fab7c4 100644 --- a/packages/compiler/src/backend/llvm/speculative-integers.test.ts +++ b/packages/compiler/src/backend/llvm/speculative-integers.test.ts @@ -339,6 +339,36 @@ test("Math.floor needs a power-of-two divisor, and expansion pure arguments", () body: [{ kind: "exprStmt", expr: call("log", []), loc }], }; expect(inlinableNumberFunction(effectful as never)).toBeUndefined(); + // A divisor next to a power of two is not one, though Math.log2 rounds it to 52. + const near = (d: number): IrExpr => libCall("math.floor", [bin("/", ref("a"), num(d))]); + expect(rewriteSpeculable(near(2 ** 52), {})).not.toBeNull(); + expect(rewriteSpeculable(near(2 ** 52 + 1), {})).toBeNull(); + expect(rewriteSpeculable(near(2 ** 53), {})).toBeNull(); + expect(rewriteSpeculable(near(0.5), {})).toBeNull(); +}); + +test("a helper's own calls must be of helpers that expand too", () => { + // `function outer(v, n) { return trim(v, n) + other(v); }` + const outer = { + ...(trim as object), + name: "outer", + body: [ + { + kind: "return", + value: bin("+", call("trim", [ref("v"), ref("n")]), call("other", [ref("v")])), + loc, + }, + ], + }; + const helpers = (callee: string) => + callee === "trim" ? inlinableNumberFunction(trim) : undefined; + expect(inlinableNumberFunction(outer as never)).toBeUndefined(); + expect(inlinableNumberFunction(outer as never, helpers)).toBeUndefined(); + const pure = { + ...(outer as object), + body: [{ kind: "return", value: bin("*", call("trim", [ref("v"), ref("n")]), num(2)), loc }], + }; + expect(inlinableNumberFunction(pure as never, helpers)).toBeDefined(); }); test("other leaves are hoisted only when no call can change the write's locals", () => { diff --git a/packages/compiler/src/backend/llvm/speculative-integers.ts b/packages/compiler/src/backend/llvm/speculative-integers.ts index 69066a5b4..1d33a71ce 100644 --- a/packages/compiler/src/backend/llvm/speculative-integers.ts +++ b/packages/compiler/src/backend/llvm/speculative-integers.ts @@ -86,8 +86,12 @@ const MAX_INLINE_NODES = 64; /** A function that only returns arithmetic over its number parameters: * `function trim(v, n) { return v - Math.floor(v / Math.pow(2, n)) * Math.pow(2, n); }`. * Its call evaluates nothing but its arguments, so a speculated write may - * expand it in place. */ -export function inlinableNumberFunction(fn: IrFunction): InlinableFunction | undefined { + * expand it in place. A call in its body must be of another such function + * (`resolve`): any other call could change what the expansion reads. */ +export function inlinableNumberFunction( + fn: IrFunction, + resolve?: (callee: string) => InlinableFunction | undefined, +): InlinableFunction | undefined { if ( fn.async || fn.generator || @@ -126,7 +130,7 @@ export function inlinableNumberFunction(fn: IrFunction): InlinableFunction | und case "libCall": return (e.fn === "math.floor" || e.fn === "math.pow") && e.args.every(pure); case "call": - return e.args.every(pure); + return resolve?.(e.callee) !== undefined && e.args.every(pure); default: return false; } @@ -175,13 +179,18 @@ function substitute(e: IrExpr, args: ReadonlyMap): IrExpr { } } +/** k when `value` is exactly 2^k with k in [0, 52], else null. + * Math.log2 rounds: 2^52 + 1 gives 52, so the power is compared back. */ +function exactPowerOfTwo(value: number): number | null { + if (!(value >= 1)) return null; + const k = Math.round(Math.log2(value)); + return k >= 0 && k <= 52 && 2 ** k === value ? k : null; +} + /** Whether `e` is a power of two speculation can shift by: * `Math.pow(2, n)` or a literal 2^k with k in [0, 52]. */ function powerOfTwo(e: IrExpr): boolean { - if (e.kind === "numLit") { - const k = Math.log2(e.value); - return Number.isInteger(k) && k >= 0 && k <= 52; - } + if (e.kind === "numLit") return exactPowerOfTwo(e.value) !== null; return ( e.kind === "libCall" && e.fn === "math.pow" && @@ -1022,7 +1031,11 @@ function exponentOf( reads: ReadonlyMap, integral: boolean, ): string { - if (e.kind === "numLit") return String(Math.log2(e.value)); + if (e.kind === "numLit") { + const k = exactPowerOfTwo(e.value); + if (k === null) throw new InternalCompilerError("llvm emitter bug: speculative power of two"); + return String(k); + } if (e.kind !== "libCall") throw new InternalCompilerError("llvm emitter bug: speculative power of two"); const n = emitSpeculativeInteger(host, e.args[1]!, bail, reads, integral); diff --git a/tests/corpus/7757-speculative-key-chains.ts b/tests/corpus/7757-speculative-key-chains.ts index bdbd12e1e..206b53827 100644 --- a/tests/corpus/7757-speculative-key-chains.ts +++ b/tests/corpus/7757-speculative-key-chains.ts @@ -54,6 +54,49 @@ function hoisted(n: number): number { return acc; } +// A check that fails before a write with a hoisted call: that write still +// makes its call on the iteration that bails. +function tally(x: number): number { + calls++; + return x * 2 + 1; +} +function chained(n: number): number { + let a = 1; + let b = 1; + for (let i = 0; i < n; i++) { + a = a * 3 + i; + b = (b * 7 + tally(i) + (a % 11)) % 1000003; + } + return b; +} + +// A divisor next to a power of two is not one: 2^52 + 1 divides as itself. +function nearPowers(start: number, steps: number): number[] { + const out: number[] = []; + let v = start; + for (let i = 0; i < steps; i++) { + v = Math.floor(v / 4503599627370497) * 4 + Math.floor(v / 4503599627370496) + i; + out.push(v); + } + return out; +} + +// A helper that calls a function with effects is not expanded: its +// argument is read before the call runs. +let level = 0; +function raise(): number { + level += 100; + return 1; +} +function lifted(x: number): number { + return raise() + x; +} +function effectfulHelper(n: number): number { + let acc = 0; + for (let i = 0; i < n; i++) acc = (acc * 3 + lifted(level)) % 1000003; + return acc; +} + const codes = new Uint8Array(5000); let seed = 7; for (let i = 0; i < codes.length; i++) { @@ -65,3 +108,7 @@ console.log(shifts(1000, 3, 6), shifts(-1000, 2, 6), shifts(5, 53, 3), shifts(7. console.log(hoisted(20), calls); const short = codes.subarray(0, 3); console.log(chain(short, 2), chain(new Uint8Array(0), 2)); +calls = 0; +console.log(chained(34), chained(60), calls); +console.log(nearPowers(4503599627370496, 3), nearPowers(9007199254740991, 2)); +console.log(effectfulHelper(10), level); From 02ec0e86f607ce56a9a2241bb13aab08e25f23e6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:54:24 -0500 Subject: [PATCH 108/273] Address review findings in the npm and CommonJS interop batch - Intl.Segmenter accepts an options object literal made only of `granularity` entries again, including the empty `{}` main accepted; with none the segmenter is a grapheme segmenter, and a repeated key takes its last value, as in JavaScript. - Namespace reads of a CommonJS module refuse a name that any module it re-exports assigns inside a function, not only the module itself: Node's namespace keeps the values from the end of evaluation. - The named-import link check treats `module.exports` as an export every CommonJS module provides, as Node 24 does, instead of throwing its missing-export SyntaxError. - The npm cases cjs-esm-named and cjs-lexer-aligned, which the batch no longer refuses at build time, are listed with the run-time SC1090 their CommonJS exports-object reads report. --- .../src/frontend/lowering/lower-classes.ts | 29 ++++++++++++------- .../compiler/src/frontend/lowering/lowerer.ts | 14 ++++++++- packages/compiler/src/frontend/program.ts | 27 +++++++++++++++-- .../main.ts | 10 ++++--- .../node_modules/text-width/index.d.ts | 1 + .../node_modules/text-width/index.js | 5 ++++ .../cjs-namespace-reexports/impl.cjs | 6 ++++ .../cjs-namespace-reexports/main.mjs | 7 +++++ .../cjs-namespace-reexports/package.json | 1 + .../cjs-namespace-reexports/wrapper.cjs | 2 ++ tests/harness/coverage.test.ts | 16 ++++++++++ tests/harness/npm.test.ts | 4 +-- 12 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 tests/coverage-fixtures/cjs-namespace-reexports/impl.cjs create mode 100644 tests/coverage-fixtures/cjs-namespace-reexports/main.mjs create mode 100644 tests/coverage-fixtures/cjs-namespace-reexports/package.json create mode 100644 tests/coverage-fixtures/cjs-namespace-reexports/wrapper.cjs diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index e50466114..80ad97090 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -9950,23 +9950,30 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const options = args[1]; let granularity = "grapheme"; if (options && !undefinedArg(options)) { - const properties = ts.isObjectLiteralExpression(options) ? options.properties.nodes : null; - const property = properties?.length === 1 ? properties[0] : undefined; - if ( - property === undefined || - !ts.isPropertyAssignment(property) || - !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) || - property.name.text !== "granularity" || - !ts.isStringLiteral(property.initializer) || - !granularities.includes(property.initializer.text) - ) { + // An object literal of `granularity` entries (none: the default; a + // repeated key: the last one, as in JavaScript). + if (!ts.isObjectLiteralExpression(options)) lowerer.noLowering( "Intl.Segmenter options", options, 'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported', ); + for (const property of options.properties.nodes) { + if ( + !ts.isPropertyAssignment(property) || + !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) || + property.name.text !== "granularity" || + !ts.isStringLiteral(property.initializer) || + !granularities.includes(property.initializer.text) + ) { + lowerer.noLowering( + "Intl.Segmenter options", + options, + 'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported', + ); + } + granularity = property.initializer.text; } - granularity = property.initializer.text; } // Explicit undefined expressions can carry effects; retain argument order. const stmts: IrStmt[] = args.map((arg) => { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 59dda3d08..b62c024f0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -270,6 +270,7 @@ import { cjsExportAssignmentOf, cjsExportDiscardReason, cjsExportsAssignedInFunctions, + cjsReexportedModules, cjsNamespaceExportNames, fallbackDtsPath, isCheckedJsSourceFile, @@ -4016,9 +4017,20 @@ export class Lowerer { } { let namespace = this.cjsNamespaces.get(dep); if (namespace === undefined) { + // A re-exported name reads the module that assigns it, so the writes + // after evaluation of every re-exported module count too. + let assignedLater: Set | "all" = new Set(); + for (const mod of cjsReexportedModules(this.program, dep)) { + const later = cjsExportsAssignedInFunctions(mod); + if (later === "all") { + assignedLater = "all"; + break; + } + for (const name of later) assignedLater.add(name); + } namespace = { names: cjsNamespaceExportNames(this.program, dep), - assignedLater: cjsExportsAssignedInFunctions(dep), + assignedLater, }; this.cjsNamespaces.set(dep, namespace); } diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index ce5f3c2bc..755eff830 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -6403,6 +6403,25 @@ function lexWithAccessorNames(sf: ts.SourceFile): ReturnType([dep]); + const order = [dep]; + for (let i = 0; i < order.length; i++) { + const mod = order[i]!; + for (const spec of lexWithAccessorNames(mod).reexports) { + const next = resolveDep(mod, spec); + if (next !== null && !seen.has(next)) { + seen.add(next); + order.push(next); + } + } + } + return order; +} + export function cjsNamespaceExportNames(program: ts.Program, dep: ts.SourceFile): Set { const names = cjsVisibleNames(dep, lexWithAccessorNames, cjsReexportDep7(program), new Map()); return new Set([...names, "default", "module.exports"]); @@ -6504,10 +6523,12 @@ function cjsNamedImportLinkCheck( }; const resolveCjsDep = cjsReexportDep7(program); const visible = (dep: ts.SourceFile, name: string): boolean => - // `default` is the module.exports binding itself — always provided. - // The program already owns native source ASTs; reuse them without - // starting another syntax session or reparsing their source text. + // `default` and `module.exports` are the module.exports binding itself + // — always provided. The program already owns native source ASTs; + // reuse them without starting another syntax session or reparsing + // their source text. name === "default" || + name === "module.exports" || cjsVisibleNames(dep, lexWithAccessorNames, resolveCjsDep, lexMemo).has(name); /** The statement's resolved LOCAL CommonJS dependency, when it is an diff --git a/tests/corpus/8217-intl-segmenter-word-construct/main.ts b/tests/corpus/8217-intl-segmenter-word-construct/main.ts index 5a2a42e08..d0c2247e7 100644 --- a/tests/corpus/8217-intl-segmenter-word-construct/main.ts +++ b/tests/corpus/8217-intl-segmenter-word-construct/main.ts @@ -1,8 +1,10 @@ // A package builds its segmenters at module load (pi-tui, grok-mermaid): -// a grapheme segmenter with a locale tag, and a word segmenter that only -// some paths use. Grapheme segmentation matches Node; the word segmenter -// constructs, and this program never segments with it. -import { columns, describe } from "text-width"; +// a grapheme segmenter with a locale tag, one with empty options, and a +// word segmenter that only some paths use. Grapheme segmentation matches +// Node; the word segmenter constructs, and this program never segments +// with it. +import { columns, describe, plainColumns } from "text-width"; console.log(describe()); console.log(columns("éa👍🏽🇫🇷"), columns("abc")); +console.log(plainColumns("a\u0301b\u{1F44D}\u{1F3FD}")); diff --git a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts index 471b4fd0a..89b16ddc0 100644 --- a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts +++ b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts @@ -1,2 +1,3 @@ export declare function columns(text: string): string; export declare function describe(): string; +export declare function plainColumns(text: string): number; diff --git a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js index d58b4ccbb..c7619bd81 100644 --- a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js +++ b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js @@ -1,11 +1,16 @@ const graphemes = new Intl.Segmenter("en", { granularity: "grapheme" }); const words = new Intl.Segmenter(undefined, { granularity: "word" }); const sentences = new Intl.Segmenter(undefined, { granularity: "sentence" }); +const plain = new Intl.Segmenter(undefined, {}); export function columns(text) { return [...graphemes.segment(text)].map((part) => part.segment).join("|"); } +export function plainColumns(text) { + return [...plain.segment(text)].length; +} + export function describe() { return `${typeof words} ${typeof sentences}`; } diff --git a/tests/coverage-fixtures/cjs-namespace-reexports/impl.cjs b/tests/coverage-fixtures/cjs-namespace-reexports/impl.cjs new file mode 100644 index 000000000..3172d6685 --- /dev/null +++ b/tests/coverage-fixtures/cjs-namespace-reexports/impl.cjs @@ -0,0 +1,6 @@ +"use strict"; +exports.version = 1; +exports.current = null; +exports.set = (value) => { + exports.current = value; +}; diff --git a/tests/coverage-fixtures/cjs-namespace-reexports/main.mjs b/tests/coverage-fixtures/cjs-namespace-reexports/main.mjs new file mode 100644 index 000000000..2ce528d48 --- /dev/null +++ b/tests/coverage-fixtures/cjs-namespace-reexports/main.mjs @@ -0,0 +1,7 @@ +// A namespace import of a CommonJS wrapper that re-exports another module: +// Node's namespace keeps impl.cjs's values from the end of its evaluation, +// so `ns.current` reads null after `ns.set(5)`. +import * as ns from "./wrapper.cjs"; + +ns.set(5); +console.log(ns.current, ns.version); diff --git a/tests/coverage-fixtures/cjs-namespace-reexports/package.json b/tests/coverage-fixtures/cjs-namespace-reexports/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/coverage-fixtures/cjs-namespace-reexports/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/coverage-fixtures/cjs-namespace-reexports/wrapper.cjs b/tests/coverage-fixtures/cjs-namespace-reexports/wrapper.cjs new file mode 100644 index 000000000..ab637bab5 --- /dev/null +++ b/tests/coverage-fixtures/cjs-namespace-reexports/wrapper.cjs @@ -0,0 +1,2 @@ +"use strict"; +module.exports = require("./impl.cjs"); diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 5b541cc54..ffb30b1ce 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -553,6 +553,22 @@ test("CommonJS var and let exports that can change after the export keep their r expect(refused).toEqual(["counter", "tab", "twice"]); }); +test("CommonJS namespace reads of exports a re-exported module assigns later keep their refusal", () => { + // wrapper.cjs re-exports impl.cjs, whose set() assigns `current` after + // evaluation; `version` never changes. + const { coverage } = analyze(fixture("cjs-namespace-reexports/main.mjs")); + const refused = [...coverage.diagnostics, ...(coverage.runtimeFences ?? [])] + .map( + (d) => + /reads of '(\w+)' through namespace imports of CommonJS modules whose functions assign exports/.exec( + d.message, + )?.[1], + ) + .filter((name) => name !== undefined) + .sort(); + expect(refused).toEqual(["current"]); +}); + test("inferred JavaScript compiles natively", async () => { // The js-gap fixture turns noImplicitAny off. Its inferred parameter // uses checked native values alongside the JSDoc-typed neighbor. diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 27b27b8e3..d1bdcf190 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -38,8 +38,8 @@ const KNOWN: Record = { "assert-shims": { run: "SC1043" }, "buffer-shims": { run: "SC2020" }, "bytes-exit": { build: "SC2004" }, - "cjs-esm-named": { build: "SC2013" }, - "cjs-lexer-aligned": { build: "SC2013" }, + "cjs-esm-named": { run: "SC1090" }, + "cjs-lexer-aligned": { run: "SC1090" }, "commander-calc": { run: "SC1090", argvs: [["--help"]] }, "commander-calc-async": { run: "SC1090", argvs: [["--help"]] }, "create-require": { run: "SC1090" }, From 3d4aeddb82d94f78692c4fe125bee413c0dec9e3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:03:27 -0500 Subject: [PATCH 109/273] Keep the reply scanner in a constant object A native build of a program that installs the C scanner but decodes no reply, such as the self-hosted source parser and CommonJS checks, failed at startup with "Cannot access 'valueEnd' before initialization": the build left the module variable uninitialized because its only other use was the assignment in useNativeJsonValueEnd, which checks it first. The scanner is now a property of a constant object that the decoders and useNativeJsonValueEnd read, so it is initialized in every program. --- .../compiler/src/frontend/ts7/semantic-json.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/frontend/ts7/semantic-json.ts b/packages/compiler/src/frontend/ts7/semantic-json.ts index 1a4d93bd6..8913007d3 100644 --- a/packages/compiler/src/frontend/ts7/semantic-json.ts +++ b/packages/compiler/src/frontend/ts7/semantic-json.ts @@ -104,15 +104,20 @@ export function jsonValueEnd(bytes: Uint8Array, start: number): number { } /** The scanner the decoders find value ends with: jsonValueEnd, or the - * native compiler's scanner (useNativeJsonValueEnd). */ -let valueEnd: (bytes: Uint8Array, start: number) => number = jsonValueEnd; + * native compiler's scanner (useNativeJsonValueEnd). It is a property of a + * constant object: a program that installs the scanner but decodes no + * reply must still initialize it, and an assigned module variable with no + * other reader would be left uninitialized by a native build. */ +const scanner: { valueEnd: (bytes: Uint8Array, start: number) => number } = { + valueEnd: jsonValueEnd, +}; /** Has the decoders find value ends with `scan`, which returns what * jsonValueEnd returns, or -1 where jsonValueEnd throws. The native compiler * installs its C scanner (native/ts7-process.c), which skips the strings that * make up most reply bytes with memchr. */ export function useNativeJsonValueEnd(scan: (bytes: Uint8Array, start: number) => number): void { - valueEnd = (bytes, start) => { + scanner.valueEnd = (bytes, start) => { const end = scan(bytes, start); if (end < 0) malformedReply(); return end; @@ -174,7 +179,7 @@ export function decodeSemanticObject( ): R | null { const start = skipJsonSpace(bytes, 0); if (start === bytes.length) return null; - const end = valueEnd(bytes, start); + const end = scanner.valueEnd(bytes, start); if (skipJsonSpace(bytes, end) !== bytes.length) malformedReply(); return replyElement(bytes, start, end, cached, build); } @@ -190,7 +195,7 @@ export function decodeSemanticObjects( let index = skipJsonSpace(bytes, 0); if (index === bytes.length) return null; if (bytes[index] !== OPEN_BRACKET) { - const end = valueEnd(bytes, index); + const end = scanner.valueEnd(bytes, index); if ( skipJsonSpace(bytes, end) !== bytes.length || parseJsonSlice(bytes, index, end) !== null @@ -205,7 +210,7 @@ export function decodeSemanticObjects( return result; } for (;;) { - const end = valueEnd(bytes, index); + const end = scanner.valueEnd(bytes, index); if (end === index) malformedReply(); result.push(replyElement(bytes, index, end, cached, build)); index = skipJsonSpace(bytes, end); From 9406ba5b892b16b1675ba55fbfbcd5c1a762e514 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:04:14 -0500 Subject: [PATCH 110/273] Run row sums two rows per pass A loop nest that sums one term per row, for (let i = start; i < end; i++) { let t = 0; for (let j = from; j < to; j++) t += term; out[i] = t; } where the term only reads numbers, the counters and typed-array elements, now runs two rows per pass when both row bounds are int32, the start is at most the end, and the stored array shares no byte with any array the term reads. Each row keeps its order of additions, so each sum has the bits of the loop as written; an odd row count runs its last row as written, and any other case runs the nest as written. Element reads that do not depend on the row are read once per column for both rows. With two independent sums in the inner loop, neither waits on the other's addition, and LLVM computes both rows' terms in one vector. bytes.disjoint compares two typed arrays' element ranges in memory. --- packages/compiler/src/backend/llvm/emitter.ts | 19 +- .../compiler/src/backend/llvm/lib-dispatch.ts | 34 +- .../compiler/src/ir/builtin-signatures.ts | 3 + packages/compiler/src/ir/row-pairs.test.ts | 147 ++++++ packages/compiler/src/ir/row-pairs.ts | 438 ++++++++++++++++++ .../tests/corpus/8194-row-pair-sums.ts.json | 6 + tests/corpus/8194-row-pair-sums.ts | 72 +++ 7 files changed, 710 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/src/ir/row-pairs.test.ts create mode 100644 packages/compiler/src/ir/row-pairs.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8194-row-pair-sums.ts.json create mode 100644 tests/corpus/8194-row-pair-sums.ts diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index f6f9ea313..0009d5229 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -81,6 +81,7 @@ import { import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js"; import { versionLoopBoundParameters } from "../../ir/parameter-versions.js"; import { inlineExpressionFunctions } from "../../ir/expression-inlining.js"; +import { pairRowSums } from "../../ir/row-pairs.js"; import { allocatedLiteral, fillAllocationLength, @@ -565,14 +566,16 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter { scalarizeNumericRecords( specializeCallbackCalls( specializeNumericCalls( - inlineExpressionFunctions( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads( - freezeInitOnlyGlobals( - holdCharLocalsAsUnits( - dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)), + pairRowSums( + inlineExpressionFunctions( + foldConstantGlobals( + copyStableCaptures( + resolveSelfCaptures( + hoistFinalFieldReads( + freezeInitOnlyGlobals( + holdCharLocalsAsUnits( + dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)), + ), ), ), ), diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index d8ca5add8..f4ce9e405 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -10,7 +10,7 @@ import { FN_ATTRS, arrNewCall, traceArg, vAdapters } from "./shapes.js"; import { InternalCompilerError } from "../../errors.js"; import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js"; -import { F64, type IrExpr, type IrType } from "../../ir/ir.js"; +import { BYTES_ELEMENT_SIZE, F64, type IrExpr, type IrType } from "../../ir/ir.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import { emitJsLibCall } from "./jsval.js"; import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js"; @@ -936,6 +936,38 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { return { name: t, type: e.type }; } if (e.fn === "bytes.absent") return host.own({ name: "null", type: e.type }); + if (e.fn === "bytes.disjoint") { + // [data, data + len * size) of each view, compared as addresses. + const B = host.B; + const range = (arg: IrExpr): { start: string; end: string } => { + const value = host.emitReadReceiver(arg); + const size = arg.type.kind === "bytes" ? BYTES_ELEMENT_SIZE[arg.type.elem] : 1; + const dataSlot = B.tmp(), + data = B.tmp(), + lenSlot = B.tmp(), + len = B.tmp(), + bytes = B.tmp(), + start = B.tmp(), + end = B.tmp(); + B.line(`${dataSlot} = getelementptr inbounds %ScrBytes, ptr ${value.name}, i64 0, i32 3`); + B.line(`${data} = load ptr, ptr ${dataSlot}`); + B.line(`${lenSlot} = getelementptr inbounds %ScrBytes, ptr ${value.name}, i64 0, i32 1`); + B.line(`${len} = load ${host.sizeType}, ptr ${lenSlot}`); + B.line(`${bytes} = mul ${host.sizeType} ${len}, ${size}`); + B.line(`${start} = ptrtoint ptr ${data} to ${host.sizeType}`); + B.line(`${end} = add ${host.sizeType} ${start}, ${bytes}`); + return { start, end }; + }; + const left = range(e.args[0]!), + right = range(e.args[1]!); + const before = B.tmp(), + after = B.tmp(), + t = B.tmp(); + B.line(`${before} = icmp ule ${host.sizeType} ${left.end}, ${right.start}`); + B.line(`${after} = icmp ule ${host.sizeType} ${right.end}, ${left.start}`); + B.line(`${t} = or i1 ${before}, ${after}`); + return { name: t, type: e.type }; + } if (e.fn === "record.isFrozen") { // The frozen bit of the record's rc word (scr_rc.h SCR_RC_FROZEN) is // its second-highest bit; an immortal word sets the highest one too. diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 1109f7f95..ef75b1b6d 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4930,6 +4930,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * and crypto callback adapters pass NULL after an error. Borrowed arg; * never throws. */ "bytes.isAbsent": { argTypes: [BYTES_U8], result: BOOL }, + /** Whether two typed arrays share no byte: their element ranges in + * memory do not overlap. Borrowed args of any element kind; never throws. */ + "bytes.disjoint": { argTypes: [null, null], 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. */ diff --git a/packages/compiler/src/ir/row-pairs.test.ts b/packages/compiler/src/ir/row-pairs.test.ts new file mode 100644 index 000000000..f9db5b89d --- /dev/null +++ b/packages/compiler/src/ir/row-pairs.test.ts @@ -0,0 +1,147 @@ +import { expect, test } from "vitest"; +import { BOOL, F64, VOID, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; +import { pairRowSums } from "./row-pairs.js"; + +const loc = { file: "row-pairs.ts", start: 0, end: 0 }; +const BYTES = { kind: "bytes", elem: "f64" } as const; +const num = (value: number): IrExpr => ({ loc, kind: "numLit", value, type: F64 }); +const ref = (localId: string): IrExpr => ({ loc, kind: "varRef", localId, type: F64 }); +const bytes = (localId: string): IrExpr & { kind: "varRef" } => ({ + loc, + kind: "varRef", + localId, + type: BYTES, +}); +const bin = (op: "+" | "/" | "<", left: IrExpr, right: IrExpr): IrExpr => ({ + loc, + kind: "bin", + op, + left, + right, + type: op === "<" ? BOOL : F64, +}); +const step = (id: string): IrStmt => ({ + loc, + kind: "assign", + localId: id, + value: bin("+", ref(id), num(1)), +}); +const counted = ( + id: string, + start: IrExpr, + end: IrExpr, + body: IrStmt[], +): IrStmt & { kind: "for" } => ({ + loc, + kind: "for", + init: { loc, kind: "varDecl", localId: id, init: start }, + cond: bin("<", ref(id), end), + update: step(id), + body, +}); +const get = (array: string, index: IrExpr): IrExpr => ({ + loc, + kind: "bytesIntrinsic", + method: "get", + receiver: bytes(array), + args: [index], + type: F64, + invalidNaN: true, +}); + +/** for (i = start; i < end; i++) { t = 0; for (j = 0; j < n; j++) t += u[j] / (i + j + 1); v[i] = t } */ +function rowSum( + term: IrExpr = bin("/", get("u", ref("j")), bin("+", bin("+", ref("i"), ref("j")), num(1))), +) { + return counted("i", ref("start"), ref("end"), [ + { loc, kind: "varDecl", localId: "t", init: num(0) }, + counted("j", num(0), ref("n"), [ + { loc, kind: "assign", localId: "t", value: bin("+", ref("t"), term) }, + ]), + { loc, kind: "bytesSet", arr: bytes("v"), index: ref("i"), value: ref("t") }, + ]); +} + +const local = (id: string, type: IrExpr["type"] = F64) => ({ id, name: id, type, mutable: true }); +function mod(body: IrStmt[]): IrModule { + const fn: IrFunction = { + loc, + name: "rows", + params: [], + locals: [ + ...["i", "j", "t", "start", "end", "n"].map((id) => local(id)), + local("u", BYTES), + local("v", BYTES), + ], + returnType: VOID, + body, + }; + return { irVersion: 17, sourceFile: "row-pairs.ts", entry: "rows", functions: [fn] }; +} + +test("a row-sum nest runs two rows per pass behind its bounds and overlap test", () => { + const result = pairRowSums(mod([rowSum()])); + const top = result.functions[0]!.body[0] as IrStmt & { kind: "block" }; + expect(top.kind).toBe("block"); + const [go, from, branch, rest] = top.body as [ + IrStmt & { kind: "varDecl" }, + IrStmt & { kind: "varDecl" }, + IrStmt & { kind: "if" }, + IrStmt & { kind: "for" }, + ]; + // The test: int32 bounds, start <= end, and the stored array disjoint + // from the array the term reads. + expect(JSON.stringify(go.init)).toContain('"fn":"bytes.disjoint"'); + expect(branch.else_).toBeNull(); + const pairs = branch.then[0] as IrStmt & { kind: "for" }; + expect(pairs.body.filter((s) => s.kind === "bytesSet")).toHaveLength(2); + const inner = pairs.body[2] as IrStmt & { kind: "for" }; + // One read of u[j] for both rows, then the two sums. + expect(inner.body).toHaveLength(3); + expect(inner.body[0]).toMatchObject({ kind: "varDecl", init: { kind: "bytesIntrinsic" } }); + // The second row's term reads `row + 1`. + const row = pairs.init?.kind === "varDecl" ? pairs.init.localId : ""; + expect((inner.body[2] as IrStmt & { kind: "assign" }).value).toMatchObject({ + op: "+", + right: { + op: "/", + left: { kind: "varRef" }, + right: { + op: "+", + left: { + op: "+", + left: { op: "+", left: { kind: "varRef", localId: row }, right: { value: 1 } }, + }, + }, + }, + }); + // The loop as written runs from `end - ((end - start) & 1)` after the + // pairs, or from the start when the test failed: the nest's one other copy. + expect(from.init).toMatchObject({ + kind: "ternary", + then: { op: "-" }, + else_: { localId: "start" }, + }); + expect(rest.init).toMatchObject({ + kind: "varDecl", + localId: "i", + init: { localId: from.localId }, + }); + expect(top.body).toHaveLength(4); + expect( + result.functions[0]!.locals.map((l) => l.id).filter((id) => id.startsWith("%pair.")), + ).toHaveLength(7); +}); + +test("nests whose term calls, writes or reads the sum keep their loops", () => { + const calls: IrExpr = { loc, kind: "call", callee: "f", args: [ref("j")], type: F64 }; + const readsSum = bin("+", ref("t"), get("u", ref("j"))); + for (const term of [calls, readsSum]) { + const m = mod([rowSum(term)]); + expect(pairRowSums(m)).toBe(m); + } + const stored = rowSum(); + stored.body[2] = { loc, kind: "bytesSet", arr: bytes("v"), index: ref("j"), value: ref("t") }; + const m = mod([stored]); + expect(pairRowSums(m)).toBe(m); +}); diff --git a/packages/compiler/src/ir/row-pairs.ts b/packages/compiler/src/ir/row-pairs.ts new file mode 100644 index 000000000..ed71166a8 --- /dev/null +++ b/packages/compiler/src/ir/row-pairs.ts @@ -0,0 +1,438 @@ +import { + BOOL, + F64, + type IrExpr, + type IrFunction, + type IrLocal, + type IrModule, + type IrStmt, +} from "./ir.js"; +import { everyExpr, everyExprChild, mapExprChildren, transformStmtList } from "./traverse.js"; + +/** Inner-loop expressions above this many nodes keep the loop as written. */ +const MAX_TERM_NODES = 64; + +/** The number operators a term may use: they run no other code and cannot + * throw. */ +const PURE_BINARY = new Set([ + "+", + "-", + "*", + "/", + "%", + "**", + "&", + "|", + "^", + "<<", + ">>", + ">>>", +]); + +/** A loop nest that sums one term per row: + * + * for (let i = start; i < end; i++) { + * let t = ; + * for (let j = from; j < to; j++) t = t + ; + * out[i] = t; + * } + */ +interface RowSum { + row: string; + start: IrExpr; + end: IrExpr; + sum: string; + init: IrExpr & { kind: "numLit" }; + inner: IrStmt & { kind: "for" }; + column: string; + term: IrExpr; + out: IrExpr & { kind: "varRef" }; + /** The typed arrays the term reads. */ + reads: (IrExpr & { kind: "varRef" })[]; +} + +function isRef(e: IrExpr | null | undefined, id: string): boolean { + return e?.kind === "varRef" && e.localId === id; +} + +/** `id = id + 1`, or `id++` as a statement. */ +function unitStep(update: IrStmt | null, id: string): boolean { + if (update?.kind === "exprStmt") + return update.expr.kind === "incDec" && update.expr.localId === id && update.expr.op === "+"; + return ( + update?.kind === "assign" && + update.localId === id && + update.value.kind === "bin" && + update.value.op === "+" && + isRef(update.value.left, id) && + update.value.right.kind === "numLit" && + update.value.right.value === 1 + ); +} + +/** A plain number local: unboxed and outside the temporal dead zone. */ +function plainNumber(locals: ReadonlyMap, id: string): boolean { + const local = locals.get(id); + return local !== undefined && local.type.kind === "f64" && !local.boxed && !local.tdz; +} + +function readsLocal(e: IrExpr, id: string): boolean { + return !everyExpr(e, { stmt: () => true, expr: (x) => !isRef(x, id) }); +} + +/** The row-sum shape of a `for` statement, or null. Bounds and receivers + * must be literals or locals the nest never writes. */ +function matchRowSum( + s: IrStmt & { kind: "for" }, + locals: ReadonlyMap, +): RowSum | null { + if (s.labels?.length || s.init?.kind !== "varDecl" || !s.init.init) return null; + const row = s.init.localId; + if (!plainNumber(locals, row) || !unitStep(s.update, row)) return null; + const cond = s.cond; + if (cond?.kind !== "bin" || cond.op !== "<" || !isRef(cond.left, row)) return null; + const start = s.init.init, + end = cond.right; + if (s.body.length !== 3) return null; + const [decl, inner, store] = s.body as [IrStmt, IrStmt, IrStmt]; + if (decl.kind !== "varDecl" || decl.init?.kind !== "numLit") return null; + const sum = decl.localId; + if (!plainNumber(locals, sum)) return null; + if (inner.kind !== "for" || inner.labels?.length) return null; + if (inner.init?.kind !== "varDecl" || !inner.init.init) return null; + const column = inner.init.localId; + if (!plainNumber(locals, column) || !unitStep(inner.update, column)) return null; + const innerCond = inner.cond; + if (innerCond?.kind !== "bin" || innerCond.op !== "<" || !isRef(innerCond.left, column)) + return null; + if (inner.body.length !== 1) return null; + const add = inner.body[0]!; + if ( + add.kind !== "assign" || + add.localId !== sum || + add.value.kind !== "bin" || + add.value.op !== "+" || + !isRef(add.value.left, sum) + ) + return null; + const term = add.value.right; + if ( + store.kind !== "bytesSet" || + store.arr.kind !== "varRef" || + store.arr.type.kind !== "bytes" || + !isRef(store.index, row) || + !isRef(store.value, sum) + ) + return null; + const out = store.arr; + // Values the nest reads but never writes: the row counter, the column + // counter and the sum are its only writes. + const written = new Set([row, sum, column]); + // A typed array's length is fixed, so a stable receiver's length is + // stable too. + const stable = (e: IrExpr): boolean => + e.kind === "numLit" || + (e.kind === "varRef" && + !written.has(e.localId) && + locals.get(e.localId) !== undefined && + !locals.get(e.localId)!.boxed && + !locals.get(e.localId)!.tdz) || + (e.kind === "bytesIntrinsic" && + e.method === "length" && + e.args.length === 0 && + e.receiver.kind === "varRef" && + e.receiver.type.kind === "bytes" && + stable(e.receiver)); + if (!stable(start) || !stable(end) || !stable(out)) return null; + if (!stable(inner.init.init) || !stable(innerCond.right)) return null; + const reads: (IrExpr & { kind: "varRef" })[] = []; + let nodes = 0; + const pure = (e: IrExpr): boolean => { + if (++nodes > MAX_TERM_NODES) return false; + switch (e.kind) { + case "numLit": + return e.type.kind === "f64"; + case "varRef": + return e.type.kind === "f64" && (e.localId === row || e.localId === column || stable(e)); + case "bin": + return e.type.kind === "f64" && PURE_BINARY.has(e.op) && pure(e.left) && pure(e.right); + case "unary": + return e.type.kind === "f64" && (e.op === "-" || e.op === "~") && pure(e.operand); + case "bytesIntrinsic": + if (stable(e)) return true; + if ( + e.method !== "get" || + e.args.length !== 1 || + e.receiver.kind !== "varRef" || + e.receiver.type.kind !== "bytes" || + !stable(e.receiver) + ) + return false; + reads.push(e.receiver); + return pure(e.args[0]!); + default: + return false; + } + }; + if (!pure(term) || readsLocal(term, sum)) return null; + return { row, start, end, sum, init: decl.init, inner, column, term, out, reads }; +} + +/** A fresh copy of `e` with `substitute` applied to its local reads and + * `replace` to the nodes it answers for. */ +function copy( + e: IrExpr, + substitute: (ref: IrExpr & { kind: "varRef" }) => IrExpr, + replace: (node: IrExpr) => IrExpr | null = () => null, +): IrExpr { + const replaced = replace(e); + if (replaced !== null) return replaced; + if (e.kind === "varRef") return substitute(e); + return mapExprChildren( + { ...e }, + (child) => copy(child, substitute, replace), + (stmt) => stmt, + ); +} + +const bin = ( + op: "+" | "-" | "&" | "|", + left: IrExpr, + right: IrExpr, + loc: IrExpr["loc"], +): IrExpr => ({ + kind: "bin", + op, + left, + right, + type: F64, + loc, +}); + +/** Runs the row sums of `match` two rows per pass when the test holds and + * as written otherwise. */ +function pairRows( + s: IrStmt & { kind: "for" }, + match: RowSum, + fresh: (name: string) => string, + done: Set, +): IrStmt { + const loc = s.loc; + const ref = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: F64, loc }); + const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); + const again = (e: IrExpr): IrExpr => copy(e, (r) => ({ ...r })); + const row = fresh("row"), + sum = fresh("sum"), + next = fresh("next"), + column = fresh("column"); + const atColumn = (r: IrExpr & { kind: "varRef" }): IrExpr => + r.localId === match.column ? { ...r, localId: column } : { ...r }; + // Element reads that do not depend on the row are read once per column + // and shared by both rows. + const shared = new Map(); + const elements: IrStmt[] = []; + const collect = (e: IrExpr): boolean => { + if (e.kind === "bytesIntrinsic" && e.method === "get" && !readsLocal(e, match.row)) { + const id = fresh("element"); + shared.set(e, id); + elements.push({ kind: "varDecl", localId: id, init: copy(e, atColumn), loc }); + return true; + } + return everyExprChild(e, collect, () => true); + }; + collect(match.term); + // The second row's term reads `row + 1`, so its counter sums stay visible + // to the counted-loop analysis. + const termAt = (rowExpr: () => IrExpr): IrExpr => + copy( + match.term, + (r) => (r.localId === match.row ? rowExpr() : atColumn(r)), + (node) => { + const id = shared.get(node); + return id === undefined ? null : { kind: "varRef", localId: id, type: node.type, loc }; + }, + ); + const innerCond = match.inner.cond as IrExpr & { kind: "bin" }; + const pairs: IrStmt = { + kind: "for", + init: { kind: "varDecl", localId: row, init: again(match.start), loc }, + cond: { + kind: "bin", + op: "<", + left: ref(row), + right: bin("-", again(match.end), num(1), loc), + type: BOOL, + loc, + }, + update: { kind: "assign", localId: row, value: bin("+", ref(row), num(2), loc), loc }, + body: [ + { kind: "varDecl", localId: sum, init: { ...match.init }, loc }, + { kind: "varDecl", localId: next, init: { ...match.init }, loc }, + { + kind: "for", + init: { + kind: "varDecl", + localId: column, + init: again((match.inner.init as IrStmt & { kind: "varDecl" }).init!), + loc, + }, + cond: { ...innerCond, left: ref(column), right: again(innerCond.right) }, + update: { kind: "assign", localId: column, value: bin("+", ref(column), num(1), loc), loc }, + body: [ + ...elements, + { + kind: "assign", + localId: sum, + value: bin( + "+", + ref(sum), + termAt(() => ref(row)), + loc, + ), + loc, + }, + { + kind: "assign", + localId: next, + value: bin( + "+", + ref(next), + termAt(() => bin("+", ref(row), num(1), loc)), + loc, + ), + loc, + }, + ], + loc, + }, + { kind: "bytesSet", arr: { ...match.out }, index: ref(row), value: ref(sum), loc }, + { + kind: "bytesSet", + arr: { ...match.out }, + index: bin("+", ref(row), num(1), loc), + value: ref(next), + loc, + }, + ], + loc, + }; + // Both bounds are int32, so `row < end - 1` is `row + 1 < end`, and the + // start is at most the end. Rows are independent when the written array + // shares no byte with an array the term reads. + const int32 = (e: IrExpr): IrExpr => ({ + kind: "bin", + op: "===", + left: bin("|", again(e), num(0), loc), + right: again(e), + type: BOOL, + loc, + }); + const tests: IrExpr[] = [ + int32(match.start), + int32(match.end), + { kind: "bin", op: "<=", left: again(match.start), right: again(match.end), type: BOOL, loc }, + ...match.reads.map((read): IrExpr => ({ + kind: "libCall", + fn: "bytes.disjoint", + args: [{ ...match.out }, { ...read }], + type: BOOL, + loc, + })), + ]; + const test = tests.reduce((left, right) => ({ + kind: "logical", + op: "&&", + left, + right, + type: BOOL, + loc, + })); + // The loop as written then runs the rows the pairs left: the last one when + // the count is odd (from `end - ((end - start) & 1)`), or all of them when + // the test failed. It is the nest's only other copy. + const go = fresh("go"), + from = fresh("from"); + const paired: IrExpr = { kind: "bin", op: "===", left: ref(go), right: num(1), type: BOOL, loc }; + const rest: IrStmt = { + ...s, + init: { kind: "varDecl", localId: match.row, init: ref(from), loc }, + }; + done.add(rest); + return { + kind: "block", + body: [ + { + kind: "varDecl", + localId: go, + init: { kind: "ternary", cond: test, then: num(1), else_: num(0), type: F64, loc }, + loc, + }, + { + kind: "varDecl", + localId: from, + init: { + kind: "ternary", + cond: paired, + then: bin( + "-", + again(match.end), + bin("&", bin("-", again(match.end), again(match.start), loc), num(1), loc), + loc, + ), + else_: again(match.start), + type: F64, + loc, + }, + loc, + }, + { kind: "if", cond: { ...paired }, then: [pairs], else_: null, loc }, + rest, + ], + loc, + }; +} + +/** Runs loop nests that sum one term per row (spectral-norm's `au`) two rows + * per pass. + * + * Each row's sum keeps its order of additions, so every sum has the bits + * the nest computes as written: the second row's term is the same + * expression of `row + 1`. The term only reads numbers, the counters and + * typed-array elements, so it runs no other code and cannot throw. The + * nest runs paired when both row bounds are int32 (so `row < end - 1` is + * `row + 1 < end`), the start is at most the end, and the array the rows + * store into shares no byte with any array the term reads, so no row reads + * another row's result. An odd row count runs its last row as written. + * Otherwise the nest runs as written. Two independent sums take turns in + * the inner loop, so neither waits on the other's addition. */ +export function pairRowSums(mod: IrModule): IrModule { + let changed = false; + const functions = mod.functions.map((fn): IrFunction => { + if (fn.async || fn.generator) return fn; + const locals = new Map(fn.locals.map((local) => [local.id, local])); + const added: IrLocal[] = []; + const ids = new Set(fn.locals.map((local) => local.id)); + const fresh = (name: string): string => { + let id = `%pair.${name}.${ids.size}`; + while (ids.has(id)) id += "_"; + ids.add(id); + added.push({ id, name: `%pair.${name}`, type: F64, mutable: true }); + return id; + }; + const paired = new Set(); + const body = transformStmtList(fn.body, { + expr: (e) => e, + stmt: (s) => { + if (s.kind !== "for" || paired.has(s)) return s; + const match = matchRowSum(s, locals); + if (match === null) return s; + paired.add(s); + return pairRows(s, match, fresh, paired); + }, + }); + if (added.length === 0) return fn; + changed = true; + return { ...fn, locals: [...fn.locals, ...added], body }; + }); + return changed ? { ...mod, functions } : mod; +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8194-row-pair-sums.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8194-row-pair-sums.ts.json new file mode 100644 index 000000000..afb53a915 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8194-row-pair-sums.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8194-row-pair-sums.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8194-row-pair-sums.ts b/tests/corpus/8194-row-pair-sums.ts new file mode 100644 index 000000000..9aa22b9bd --- /dev/null +++ b/tests/corpus/8194-row-pair-sums.ts @@ -0,0 +1,72 @@ +// Row sums (`for i { t = 0; for j t += term; out[i] = t }`) over typed +// arrays, run with even and odd row counts, an empty range, fractional, +// negative-zero and out-of-int32 bounds, and output arrays that are the +// input array or overlap it in one buffer, so a row may read an earlier +// row's result. +function rows(u: Float64Array, v: Float64Array, start: number, end: number, n: number): void { + for (let i = start; i < end; i++) { + let t = 0; + for (let j = 0; j < n; j++) t += u[j]! / ((((i + j) * (i + j + 1)) >>> 1) + i + 1); + v[i] = t; + } +} + +function weighted(u: Float32Array, w: Int32Array, v: Float64Array, start: number, end: number): void { + for (let i = start; i < end; i++) { + let t = 0.5; + for (let j = 0; j < u.length; j++) t += u[j]! * w[(i + j) % w.length]! - i * 0.25; + v[i] = t; + } +} + +const show = (label: string, v: Float64Array): void => { + const parts: string[] = []; + for (const x of v) parts.push(Object.is(x, -0) ? "-0" : x.toPrecision(17)); + console.log(label, parts.join(" ")); +}; + +const n = Number(process.argv[2] ?? 7); +const u = new Float64Array(n).map((_, k) => k + 1); +for (const [start, end] of [ + [0, n], + [0, n - 1], + [1, n], + [2, 2], + [3, 1], + [-0, 3], + [0.5, 4], + [0, 4.5], + [2 ** 31 - 3, 2 ** 31 + 1], + [NaN, 3], +]) { + const v = new Float64Array(n + 2); + rows(u, v, start!, end!, n); + show(`rows ${start} ${end}`, v); +} + +// The output is the input: each row reads the rows stored before it. +const same = new Float64Array(n).map((_, k) => k + 1); +rows(same, same, 0, n, n); +show("same", same); + +// Overlapping views of one buffer, offset by one element. +const buffer = new ArrayBuffer(8 * (n + 1)); +const lo = new Float64Array(buffer, 0, n).map((_, k) => k + 1); +const hi = new Float64Array(buffer, 8, n); +rows(lo, hi, 0, n, n); +show("overlap", new Float64Array(buffer)); + +// Disjoint views of one buffer. +const shared = new ArrayBuffer(16 * n); +const a = new Float64Array(shared, 0, n).map((_, k) => n - k); +const b = new Float64Array(shared, 8 * n, n); +rows(a, b, 0, n, n); +show("disjoint", b); + +const f = new Float32Array(n).map((_, k) => k / 3); +const w = new Int32Array([3, -1, 4, 1, -5]); +const out = new Float64Array(n); +weighted(f, w, out, 0, n); +show("weighted", out); +weighted(f, w, out, 1, n - 1); +show("weighted inner", out); From 01e6d55fc56e00e9ba91b55f29e021029d3289f3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:05:18 -0500 Subject: [PATCH 111/273] Keep optional-element arrays through nested conversions, union arms, rest packing and receiver reassignment - Checked conversions from dynamic values key their builders and matchers by the type with `?` after every `(T | undefined)[]`, at any depth. `number[][]` and `(number | undefined)[][]` share a typeKey, so the second conversion reused the first one's builder and rejected undefined elements. - The dynamic matcher of a `(T | undefined)[]` takes undefined elements, so a union with such an arm matches arrays that hold undefined. - An optional-element rest parameter packs tuple-spread values and plain arguments together as present undefined elements, each evaluated once in argument order, instead of converting each value to T. - The element-state scan counts an argument in a rest position that may be undefined, and a spread tuple with undefined elements, as undefined elements of the rest array, so reads in the callee check element states. - A union index read keeps the receiver in a temporary when the key can assign it, so `a[(a = other, 0)]` reads the array `a` held before the key. --- packages/compiler/src/backend/llvm/dyn.ts | 34 +++++++++++-- .../src/frontend/lowering/call-arguments.ts | 26 +++++++++- .../lowering/expressions/element-access.ts | 15 ++++-- .../compiler/src/frontend/lowering/lowerer.ts | 24 ++++++++- .../lowering/runtime-optional-elements.ts | 19 ++++++- .../8004-optional-element-conversions.ts.json | 6 +++ .../8004-optional-element-conversions.ts | 49 +++++++++++++++++++ .../8004-optional-element-conversions.ts.json | 1 + 8 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8004-optional-element-conversions.ts.json create mode 100644 tests/corpus/8004-optional-element-conversions.ts create mode 100644 tests/harness/corpus-costs/8004-optional-element-conversions.ts.json diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 57c8b5177..4ec710b28 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -66,6 +66,25 @@ import { recordMayKeepSource, recordSourceFieldOrder } from "./record-sources.js import type { WalkerHost } from "./walkers.js"; import type { LlStreamTypedRefAdapter } from "./expr-context.js"; +/** typeKey with `?` after every `(T | undefined)[]` (optionalElements), + * at any depth of arrays, Maps, Sets and promises: checked conversions to + * `number[][]` and `(number | undefined)[][]` need separate helpers, though + * the two types share a typeKey. */ +function checkedConversionKey(t: IrType): string { + switch (t.kind) { + case "array": + return `array<${checkedConversionKey(t.elem)}>${t.optionalElements ? "?" : ""}`; + case "map": + return `map<${checkedConversionKey(t.key)},${checkedConversionKey(t.value)}>`; + case "set": + return `set<${checkedConversionKey(t.elem)}>`; + case "promise": + return `promise<${checkedConversionKey(t.inner)}>`; + default: + return typeKey(t); + } +} + /** Whether to-dyn of a record shape keeps the record itself: an empty shape * and an event-listener record (a `handleEvent` function field) box their * identity. Every other shape, tuples included, copies its fields into a @@ -1060,7 +1079,7 @@ export class LlDyn { /** `sc_dm_(ptr d) -> i1` — does this dyn fit T? Never throws. */ dynMatchHelper(t: IrType, preserveRefs = false): string { - const key = typeKey(t); + const key = checkedConversionKey(t); const matcherKey = preserveRefs ? `identity:${key}` : key; const existing = this.dynMatchers.get(matcherKey); if (existing) return existing; @@ -1375,7 +1394,7 @@ export class LlDyn { B.startBlock(l0); const n = this.lenOf(B, "%d"); const items = this.itemsOf(B, "%d"); - this.i64Loop(B, "dm.el", n, (i, brNext) => { + this.i64Loop(B, "dm.el", n, (i, brNext, next) => { this.host.declare(`declare zeroext i1 @scr_dyn_arr_has_index(ptr, ${this.S})`); const present = B.tmp(); B.line(`${present} = call zeroext i1 @scr_dyn_arr_has_index(ptr %d, ${this.S} ${i})`); @@ -1386,6 +1405,15 @@ export class LlDyn { brNext(); B.startBlock(valueLabel); const e = this.itemAt(B, items, i); + if (t.optionalElements) { + // A `(T | undefined)[]` stored as T elements takes undefined + // elements as well. + const undef = B.tmp(); + const checkLabel = B.newLabel("dm.check"); + B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`); + B.condBr(undef, next, checkLabel); + B.startBlock(checkLabel); + } const ok = B.tmp(); B.line(`${ok} = call zeroext i1 @${m}(ptr ${e})`); const lOk = B.newLabel("dm.eo"); @@ -1798,7 +1826,7 @@ export class LlDyn { * TypeError and return a dummy with the pending flag set. */ dynCheckHelper(t: IrType, preserveRefs = false): string { // A `(T | undefined)[]` stored as T elements also takes undefined ones. - const key = t.kind === "array" && t.optionalElements ? `${typeKey(t)}?` : typeKey(t); + const key = checkedConversionKey(t); const builderKey = preserveRefs ? `identity:${key}` : key; const existing = this.dynBuilders.get(builderKey); if (existing) return existing; diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index 6da0cb337..b02d0b85c 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -28,7 +28,7 @@ import { checkedIterableSpread } from "./checked-iterable-spread.js"; import { type ParamShape } from "./call-signatures.js"; import { arrayValueRead, arrayValueType } from "./array-values.js"; import { buildArrayConversion } from "./coercions/builders.js"; -import { lowerArraySpreadItems } from "./containers/array-construction.js"; +import { lowerArraySpreadItems, lowerArrayValueItems } from "./containers/array-construction.js"; import { restTupleElementTypes, typeKey } from "../type-mapper.js"; function fixedTupleSpreadInfo( @@ -413,6 +413,30 @@ export function completeArgs( ); return out; } + // Tuple-spread values (pre-lowered) and plain arguments pack the same + // way. Each value evaluates once, in argument order. + if ( + restType.optionalElements === true && + !restNodes.some((a) => !isIr(a) && ts.isSpreadElement(a)) + ) { + const stmts: IrStmt[] = []; + const values = restNodes.map((a): IrExpr => { + if (isIr(a)) { + if (a.ir.kind !== "seqExpr") return a.ir; + stmts.push(...a.ir.stmts); + return a.ir.result; + } + const value = lowerer.lowerExpr(a as ts.Expression); + const saved = lowerer.declareHiddenLocal("%callArg", value.type); + stmts.push({ kind: "varDecl", localId: saved.id, init: value, loc: value.loc }); + return { kind: "varRef", localId: saved.id, type: value.type, loc: value.loc }; + }); + const items = lowerArrayValueItems(lowerer, values, restType.elem, restType, loc); + out.push( + stmts.length === 0 ? items : { kind: "seqExpr", stmts, result: items, type: restType, loc }, + ); + return out; + } // The rest pack is a fresh array per call; surplus SPREADS copy // their elements in (JS-exact — `f(a, ...xs, b, ...ys)` packs in // order, sources untouched). diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 70909ca7b..a2f2ebdb6 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -2471,14 +2471,21 @@ function optionalArmIndexRead( return null; if (unionArrayValueRead(lowerer, value, key, loc) === null) return null; const stmts: IrStmt[] = []; - const stable = (e: IrExpr, name: string): IrExpr => { - if (e.kind === "varRef" || e.kind === "numLit") return e; + const stable = (e: IrExpr, name: string, keep: boolean): IrExpr => { + if (keep) return e; const local = lowerer.declareHiddenLocal(name, e.type); stmts.push({ kind: "varDecl", localId: local.id, init: e, loc }); return varRef(local.id, e.type, loc); }; - const receiver = stable(value, "%indexReceiver"); - const index = stable(key, "%indexKey"); + // A receiver local stays a direct read only when the key cannot assign + // it (`a[(a = other, 0)]` reads the array `a` held before the key). + const plainKey = key.kind === "varRef" || key.kind === "numLit"; + const receiver = stable( + value, + "%indexReceiver", + value.kind === "numLit" || (value.kind === "varRef" && plainKey), + ); + const index = stable(key, "%indexKey", plainKey); const read = unionArrayValueRead(lowerer, receiver, index, loc)!; return stmts.length === 0 ? read : { kind: "seqExpr", stmts, result: read, type: read.type, loc }; } diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 87825d030..28288a5f1 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -6546,8 +6546,20 @@ export class Lowerer { }, iterationMayYieldUndefined: (node) => { const type = typeOrNull(node); - if (type === null || this.checker.isArrayType(type) || this.checker.isTupleType(type)) - return false; + if (type === null || this.checker.isArrayType(type)) return false; + // A tuple yields its elements, in order. + if (this.checker.isTupleType(type)) + return panicSafe( + () => + this.checker + .getTypeArguments(type as ts.TypeReference) + .some((element) => + (element.isUnionType() ? ts.constituentTypes(element) : [element]).some( + (part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0, + ), + ), + true, + ); // The first type argument of Set, Map and Set iterators, // generators and iterables is the value they yield. const element = @@ -6601,6 +6613,14 @@ export class Lowerer { const rest = sig.params.find((shape) => shape.mode === "rest"); return rest?.type.kind === "array" ? [typeKey(rest.type.elem)] : []; }, + restStart: (call) => { + const callee = peel(call.expression); + if (!ts.isIdentifier(callee) && !ts.isPropertyAccessExpression(callee)) return null; + const symbol = callableSymbolOf(callee); + const sig = symbol ? signatureBySymbol.get(symbol) : undefined; + const at = sig ? sig.params.findIndex((shape) => shape.mode === "rest") : -1; + return at >= 0 ? at : null; + }, markIterationBinding: (node, known) => { const symbol = symbolOf(node); if (!symbol) return false; diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts index fffe79a51..7561f4a65 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts @@ -31,6 +31,9 @@ export interface ElementStateHost { /** Element keys of the rest parameter that the spread arguments of * `call` fill: `[]` when the callee has none, null when it is unknown. */ restKeys(call: ts.CallExpression | ts.NewExpression): readonly string[] | null; + /** The argument index where the callee's rest parameter starts, or null + * when the callee has none or is unknown. */ + restStart?(call: ts.CallExpression | ts.NewExpression): number | null; /** Record that iterating `node`'s binding can observe `undefined`; * `known` when the iterated array's own facts (not only those shared by * its element ABI) say so. Returns true when the binding's record grew. */ @@ -518,14 +521,26 @@ export class ArrayElementStates { private scanCall(node: ts.CallExpression | ts.NewExpression): boolean { let changed = false; const args = node.arguments?.nodes ?? []; - // Spreading a sparse array into a rest parameter fills the holes. + // Spreading a sparse array, or a tuple with undefined elements, into a + // rest parameter fills the holes. if (args.some((arg) => ts.isSpreadElement(arg))) { for (const arg of args) { - if (!ts.isSpreadElement(arg) || !this.mayYieldUndefined(arg.expression)) continue; + if (!ts.isSpreadElement(arg) || !this.valueMayBeUndefined(arg)) continue; const rest = this.host.restKeys(node) ?? this.host.arrayKeys(arg.expression); if (this.addUndefined(rest)) changed = true; } } + // An argument in a rest position that may be undefined is a present + // undefined element of the rest array. + const restStart = this.host.restStart?.(node) ?? null; + if (restStart !== null) + for (const arg of args.slice(restStart)) + if ( + !ts.isSpreadElement(arg) && + this.host.mayBeUndefined(arg) && + this.addUndefined(this.host.restKeys(node)) + ) + changed = true; if (this.record(node)) changed = true; const callee = peel(node.expression); if (!ts.isPropertyAccessExpression(callee)) return changed; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8004-optional-element-conversions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8004-optional-element-conversions.ts.json new file mode 100644 index 000000000..2a1bc7c66 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8004-optional-element-conversions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8004-optional-element-conversions.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8004-optional-element-conversions.ts b/tests/corpus/8004-optional-element-conversions.ts new file mode 100644 index 000000000..2a394c331 --- /dev/null +++ b/tests/corpus/8004-optional-element-conversions.ts @@ -0,0 +1,49 @@ +// `(T | undefined)[]` values built by checked conversions, rest parameters +// and union index reads: nested arrays converted beside their non-optional +// form, a union arm that is an optional-element array, undefined arguments +// and tuple spreads packed into a rest parameter, and an index expression +// that reassigns the receiver. + +const plainRows = JSON.parse("[[1, 2], [3]]") as number[][]; +console.log(plainRows); +const optionalSource: unknown = (JSON.parse("[[1, null], [null, 4]]") as unknown[][]).map((row) => + row.map((v) => (v === null ? undefined : v)), +); +const optionalRows = optionalSource as (number | undefined)[][]; +console.log(optionalRows, optionalRows[0]![1], optionalRows[1]!.length); + +const armSource: unknown = [1, undefined, 3]; +const arm = armSource as (number | undefined)[] | string; +console.log(typeof arm === "string" ? arm : arm.length, arm); +const stringArm = ["a", undefined] as unknown as (string | undefined)[] | number; +console.log(stringArm); +const textArm = "text" as unknown as (string | undefined)[] | string; +console.log(textArm); + +function flags(...values: (boolean | undefined)[]): string { + let text = ""; + for (const value of values) text += value === undefined ? "u" : value ? "t" : "f"; + return `${values.length}:${text}:${String(values[1])}`; +} +console.log(flags(true, undefined)); +const flagPair: [boolean, boolean | undefined] = [false, undefined]; +console.log(flags(...flagPair), flags(true, ...flagPair), flags(...flagPair, true)); +function label(first: string, ...rest: (string | undefined)[]): string { + return `${first}/${rest.length}/${rest.map((v) => v ?? "-").join("")}`; +} +const words: [string, string | undefined, string | undefined] = ["a", undefined, "c"]; +console.log(label(...words), label("b", undefined)); + +function pick(start: (number | undefined)[] | string[], other: (number | undefined)[] | string[]): string { + let a = start; + const read = a[((a = other), 0)]; + return `${String(read)} ${String(a[0])}`; +} +console.log(pick([1, undefined, 3], ["x", "y"]), pick([undefined, 2], ["z"])); +function counted(start: (number | undefined)[] | string[]): string { + let b = start; + let i = 1; + const read = b[((b = ["q"]), i--)]; + return `${String(read)} ${b.length} ${i}`; +} +console.log(counted([4, undefined]), counted(["s", "t"])); diff --git a/tests/harness/corpus-costs/8004-optional-element-conversions.ts.json b/tests/harness/corpus-costs/8004-optional-element-conversions.ts.json new file mode 100644 index 000000000..9f8eb9e05 --- /dev/null +++ b/tests/harness/corpus-costs/8004-optional-element-conversions.ts.json @@ -0,0 +1 @@ +{ "plain": 4882, "san": 6379 } From 40adc2d1619c6598bc8600d79c93fb5455cbdef4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:06:14 -0500 Subject: [PATCH 112/273] Hoist a call only when its arguments write no binding A hoisted call runs before the rest of its write. An argument such as `i++` in `acc = acc * 3 + i + tally(i++)` would then move that write ahead of the write's earlier read of `i`, so such a call stays where it is and the write is not speculated. --- .../src/backend/llvm/speculative-integers.test.ts | 5 +++++ .../src/backend/llvm/speculative-integers.ts | 15 ++++++++++++++- tests/corpus/7757-speculative-key-chains.ts | 10 ++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/backend/llvm/speculative-integers.test.ts b/packages/compiler/src/backend/llvm/speculative-integers.test.ts index 151fab7c4..1b82768d5 100644 --- a/packages/compiler/src/backend/llvm/speculative-integers.test.ts +++ b/packages/compiler/src/backend/llvm/speculative-integers.test.ts @@ -382,6 +382,11 @@ test("other leaves are hoisted only when no call can change the write's locals", const hoisted = rewriteSpeculable(value, { unaliased: () => true, leafId })!; expect(hoisted.leaves.map((leaf) => leaf.expr)).toEqual([call("noisy", [ref("i")])]); expect(rewriteSpeculable(value, { unaliased: (id) => id !== "acc", leafId })).toBeNull(); + // A call whose argument writes a local stays where it is. + const bump: IrExpr = { kind: "incDec", localId: "i", op: "+", prefix: false, type: F64, loc }; + expect( + rewriteSpeculable(bin("+", ref("i"), call("noisy", [bump])), { unaliased: () => true, leafId }), + ).toBeNull(); // Without leaf ids nothing is hoisted. expect(rewriteSpeculable(value, { unaliased: () => true })).toBeNull(); const write = assign("acc", value); diff --git a/packages/compiler/src/backend/llvm/speculative-integers.ts b/packages/compiler/src/backend/llvm/speculative-integers.ts index 1d33a71ce..07e5e3721 100644 --- a/packages/compiler/src/backend/llvm/speculative-integers.ts +++ b/packages/compiler/src/backend/llvm/speculative-integers.ts @@ -1,5 +1,5 @@ import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js"; -import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; +import { everyExpr, everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { InternalCompilerError } from "../../errors.js"; import { f64Lit } from "./common.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; @@ -219,6 +219,15 @@ function isSpeculativeByteRead(e: IrExpr): boolean { ); } +/** Whether evaluating `e` assigns no binding: no `++`/`--`, no assignment + * expression, and no statement inside it. */ +function writesNoLocal(e: IrExpr): boolean { + return everyExpr(e, { + expr: (node) => node.kind !== "incDec" && node.kind !== "assignExpr", + stmt: () => false, + }); +} + /** Rewrite a write's value that isSpeculableValue refuses into one it can * speculate (SpeculativeValue), or null. Expansion and the new operations * (`Math.pow(2, n)`, `Math.floor(a / 2^n)`, byte reads) evaluate nothing @@ -295,6 +304,10 @@ export function rewriteSpeculable( // length, a Map read) are cheap where they stand, and would turn every // loop that adds one into a second copy. if (context.leafId === undefined || e.kind !== "call") return null; + // Hoisting runs the arguments before the rest of the write: one that + // assigns a local would move that write ahead of the write's earlier + // reads of it. + if (!e.args.every(writesNoLocal)) return null; const ref: IrExpr = { kind: "varRef", localId: context.leafId(), type: e.type, loc: e.loc }; leaves.push({ localId: (ref as { localId: string }).localId, expr: e, ref }); return ref; diff --git a/tests/corpus/7757-speculative-key-chains.ts b/tests/corpus/7757-speculative-key-chains.ts index 206b53827..1ef08a942 100644 --- a/tests/corpus/7757-speculative-key-chains.ts +++ b/tests/corpus/7757-speculative-key-chains.ts @@ -97,6 +97,15 @@ function effectfulHelper(n: number): number { return acc; } +// A call whose argument assigns a local the write also reads stays where it +// is: `i` is read before `i++` runs. +function stepped(n: number): number { + let acc = 1; + let i = 0; + while (i < n) acc = (acc * 3 + i + tally(i++)) % 1000003; + return acc; +} + const codes = new Uint8Array(5000); let seed = 7; for (let i = 0; i < codes.length; i++) { @@ -112,3 +121,4 @@ calls = 0; console.log(chained(34), chained(60), calls); console.log(nearPowers(4503599627370496, 3), nearPowers(9007199254740991, 2)); console.log(effectfulHelper(10), level); +console.log(stepped(40)); From 34ada100c47f24865125092cfaeb9bab7142a12b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:07:05 -0500 Subject: [PATCH 113/273] Format the string comparison emission test --- tests/harness/string-compare-emission.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/harness/string-compare-emission.test.ts b/tests/harness/string-compare-emission.test.ts index 1a8fdddf6..b4219853e 100644 --- a/tests/harness/string-compare-emission.test.ts +++ b/tests/harness/string-compare-emission.test.ts @@ -45,9 +45,8 @@ console.log(words.join(",")); // Calls to the comparison runtime: `call`/`callq` on x86-64 and `bl` on // AArch64, with Mach-O's leading underscore on macOS. const compareCalls = (asm: string): number => - asm.match( - /\b(?:call[a-z]*|bl)\s+_?(?:sc_str_cmp_u16|scr_str_cmp_u16_bytes|scr_str_cmp)\b/g, - )?.length ?? 0; + asm.match(/\b(?:call[a-z]*|bl)\s+_?(?:sc_str_cmp_u16|scr_str_cmp_u16_bytes|scr_str_cmp)\b/g) + ?.length ?? 0; // The host's assembly, and two other targets' so that every host checks the // x86-64, AArch64 and Mach-O spellings. From 055c9ffa257d0f9ef63af5673d088c4804c997f4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:07:44 -0500 Subject: [PATCH 114/273] Evaluate the arguments of deferred require.resolve calls before they throw Preflight admits a createRequire loader's require.resolve call with a computed request inside a try block, but lowering refused the call, so in JavaScript source the whole statement became a runtime fence. The fence threw before the request expression ran, and before anything else in the statement: `try { where = load.resolve(candidate(name)); }` skipped candidate()'s side effects, so the catch clause saw a different state than in Node. Such calls (a computed request, another argument shape, or runtime-computed options, on require.resolve and require.resolve.paths) now evaluate their arguments in order and then throw the fence, as a computed require call does. The addon-probe fixture checks that a computed candidate path runs before each failed lookup. --- docs/content/limitations/native-addons.mdx | 5 +- .../lowering/builtins/module-resolution.ts | 81 ++++++++++++++----- tests/fixtures/npm-static/addon-probe-cli.ts | 3 +- .../node_modules/addon-probe/index.d.ts | 1 + .../node_modules/addon-probe/index.js | 24 ++++++ tests/harness/npm-static.test.ts | 6 +- 6 files changed, 95 insertions(+), 25 deletions(-) diff --git a/docs/content/limitations/native-addons.mdx b/docs/content/limitations/native-addons.mdx index 5fff3d7e8..8fa7f94bf 100644 --- a/docs/content/limitations/native-addons.mdx +++ b/docs/content/limitations/native-addons.mdx @@ -3,8 +3,9 @@ Direct `createRequire` calls to local addons are rejected at compile time. A package that requires an addon needs a JavaScript implementation that compiles, or a replacement through native FFI. A package that looks for an optional addon at run time compiles with `--npm-static` when the lookup is inside a `try` block. -There, a `createRequire` require call with a computed specifier throws an error where it runs, and its `require.resolve` read as a value is a function that throws when called. -Coverage lists both as deferred to runtime. +There, a `createRequire` require call with a computed specifier, and a `require.resolve` or `require.resolve.paths` call with a computed request, evaluate their arguments and then throw an error. +Its `require.resolve` read as a value is a function that throws when called. +Coverage lists these as deferred to runtime. The package's `catch` clause then runs as it does in Node.js when the addon is missing. Outside a `try` block, the same uses keep the package refused. diff --git a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts index 42ecfdd50..107e1f793 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts @@ -193,6 +193,39 @@ export function createRequireResolveFenceValue( }; } +/** A createRequire use deferred to run time in JavaScript source: as in + * Node, the call's arguments are evaluated in order, then the call throws + * (here, the fence). Code before the call in the same statement runs, and + * the expression diverges, so nothing after it does. */ +function deferredCreateRequireThrow( + lowerer: Lowerer, + call: ts.CallExpression, + surface: string, + hint: string, + loc: SrcLoc, +): IrExpr { + const stmts: IrStmt[] = call.arguments.nodes.map((arg) => ({ + kind: "exprStmt", + expr: lowerer.lowerExpr(arg), + loc: locOf(arg), + })); + const type = lowerer.irTypeOf(call); + const slot = lowerer.declareHiddenLocal("%never", type); + slot.mutable = true; + stmts.push( + { kind: "varDecl", localId: slot.id, init: null, loc }, + createRequireFence(lowerer, surface, call, hint), + ); + return { + kind: "seqExpr", + stmts, + result: varRef(slot.id, type, loc), + diverges: true, + type, + loc, + }; +} + export function lowerCreateRequireCall( lowerer: Lowerer, call: ts.CallExpression, @@ -205,27 +238,7 @@ export function lowerCreateRequireCall( const hint = 'the compiled module graph is fixed at build time — the one lowered form is require(""); native addons are not loaded'; if (!isJsSourceFile(ts.getSourceFileOfNode(call))) lowerer.noLowering(surface, call, hint); - // Node evaluates the arguments, then the require throws. - const stmts: IrStmt[] = call.arguments.nodes.map((arg) => ({ - kind: "exprStmt", - expr: lowerer.lowerExpr(arg), - loc: locOf(arg), - })); - const type = lowerer.irTypeOf(call); - const slot = lowerer.declareHiddenLocal("%never", type); - slot.mutable = true; - stmts.push( - { kind: "varDecl", localId: slot.id, init: null, loc }, - createRequireFence(lowerer, surface, call, hint), - ); - return { - kind: "seqExpr", - stmts, - result: varRef(slot.id, type, loc), - diverges: true, - type, - loc, - }; + return deferredCreateRequireThrow(lowerer, call, surface, hint, loc); } const spec = cr.spec; if (spec === "node:ffi") { @@ -446,11 +459,30 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio } const baseFile = requireResolverBaseFile(lowerer, receiver); if (baseFile === null) return null; + const surface = pathsCall ? "require.resolve.paths" : "require.resolve"; + // A createRequire loader's query that resolves only at run time, in + // JavaScript source: preflight admits it inside a try block + // (createRequireUse7 in program.ts). Its arguments run, then it throws. + const deferred = (what: string, hint: string): IrExpr | null => + call.questionDotToken === undefined && + createRequireCalleeFileOf(lowerer, receiver) !== null && + isJsSourceFile(ts.getSourceFileOfNode(call)) + ? deferredCreateRequireThrow( + lowerer, + call, + `createRequire's ${surface} ${what}`, + hint, + locOf(call), + ) + : null; + const resolveHint = `call it with a string literal (${surface}("")), which resolves at build time`; if ( call.questionDotToken !== undefined || call.arguments.nodes.length < 1 || call.arguments.nodes.length > (pathsCall ? 1 : 2) ) { + const fenced = deferred("with this argument shape", resolveHint); + if (fenced) return fenced; lowerer.noLowering( pathsCall ? "require.resolve.paths with this argument shape" @@ -460,6 +492,8 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio } const specifier = staticString(call.arguments.nodes[0]); if (specifier === null) { + const fenced = deferred("with a computed request", resolveHint); + if (fenced) return fenced; lowerer.noLowering( `${pathsCall ? "require.resolve.paths" : "require.resolve"} with a runtime-computed request`, call.arguments.nodes[0]!, @@ -495,6 +529,11 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio } const paths = staticResolveOptionPaths(call.arguments.nodes[1]); if (paths === null) { + const fenced = deferred( + "with runtime-computed options", + 'omit options or pass { paths: ["", ...] }', + ); + if (fenced) return fenced; lowerer.noLowering( "require.resolve with runtime-computed options", call.arguments.nodes[1]!, diff --git a/tests/fixtures/npm-static/addon-probe-cli.ts b/tests/fixtures/npm-static/addon-probe-cli.ts index 4515fb5bf..aa21df05d 100644 --- a/tests/fixtures/npm-static/addon-probe-cli.ts +++ b/tests/fixtures/npm-static/addon-probe-cli.ts @@ -1,5 +1,6 @@ -import { probe, resolveHelper } from "addon-probe"; +import { locate, probe, resolveHelper } from "addon-probe"; import { join } from "node:path"; console.log(join("a", "b"), [probe("clipboard"), probe("keys"), resolveHelper("addon-probe-helper")].join(",")); console.log(resolveHelper("other", (id) => `custom:${id}`)); +console.log(locate("helper")); diff --git a/tests/fixtures/npm-static/node_modules/addon-probe/index.d.ts b/tests/fixtures/npm-static/node_modules/addon-probe/index.d.ts index cfacc4764..a9987d787 100644 --- a/tests/fixtures/npm-static/node_modules/addon-probe/index.d.ts +++ b/tests/fixtures/npm-static/node_modules/addon-probe/index.d.ts @@ -1,2 +1,3 @@ export declare function probe(name: string): string; export declare function resolveHelper(name: string, resolvePackage?: (id: string) => string): string; +export declare function locate(name: string): string; diff --git a/tests/fixtures/npm-static/node_modules/addon-probe/index.js b/tests/fixtures/npm-static/node_modules/addon-probe/index.js index e65b1ad0e..2cc423359 100644 --- a/tests/fixtures/npm-static/node_modules/addon-probe/index.js +++ b/tests/fixtures/npm-static/node_modules/addon-probe/index.js @@ -26,3 +26,27 @@ export function resolveHelper(name, resolvePackage) { return "unresolved"; } } + +let probes = 0; +function candidate(name) { + probes++; + return `./${name}-${probes}.node`; +} + +// Resolves computed candidate paths: the arguments run before the lookup +// fails, as an assignment and as a statement of their own. +export function locate(name) { + let where = "none"; + try { + where = load.resolve(candidate(name)); + } catch { + where = `missing after ${probes}`; + } + try { + load.resolve(candidate(name)); + where += ",found"; + } catch { + where += `,missing after ${probes}`; + } + return where; +} diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 026da8f4e..4ddb6fe15 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -435,6 +435,8 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { ).toEqual([ expect.stringContaining("'createRequire's require with a computed specifier"), expect.stringContaining("'createRequire's require.resolve as a value'"), + expect.stringContaining("'createRequire's require.resolve with a computed request'"), + expect.stringContaining("'createRequire's require.resolve with a computed request'"), ]); const outDir = join(cacheDir, "addon-probe", sanitize ? "san" : "plain", backend); const result = await compile(entry, { @@ -450,7 +452,9 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { runBinary(process.execPath, [entry]), runBinary(result.binaryPath, []), ]); - expect(nativeRes.stdout.toString()).toBe("a/b absent,absent,unresolved\ncustom:other\n"); + expect(nativeRes.stdout.toString()).toBe( + "a/b absent,absent,unresolved\ncustom:other\nmissing after 1,missing after 2\n", + ); expect(nativeRes.stdout).toEqual(nodeRes.stdout); expect(comparableStderr(nativeRes.stderr)).toEqual(nodeRes.stderr); expect(nativeRes.exitCode).toBe(nodeRes.exitCode); From adbd9cd1e9b85fbe6b997e6ba2170b2f1d5a1858 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:18:30 -0500 Subject: [PATCH 115/273] Lower each closure argument of an implicit-any call once To key an implicit-any function's instance, storedImplicitArgumentType lowers a JavaScript arrow or function-expression argument to read its function type, then discards the value; the call's argument list then lowers the closure again. Every such closure was lowered twice, and a closure nested in another one four times, and each lowering lifted its own copy of the body. The first lowering now keeps its value (Lowerer.speculatedClosures), and the argument's own lowering in the same function and with the same type bindings takes it instead of lowering the closure again. A lowering that reported diagnostics is not kept. --- .../frontend/lowering/generic-functions.ts | 17 ++++++++++-- .../compiler/src/frontend/lowering/lowerer.ts | 26 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 7896da578..0f9e41723 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -2082,6 +2082,10 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp isJsSourceFile(ts.getSourceFileOfNode(arg)) && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) ) { + // A closure lowered here before (another call site's instance key, or + // a repeated demand) keeps its value for the argument's own lowering. + const known = lowerer.speculatedClosure(arg); + if (known !== null) return known.type; const saved = lowerer.diagSink; const diagnostics: typeof lowerer.diags = []; lowerer.diagSink = diagnostics; @@ -2092,8 +2096,17 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp lowerer.diagSink = saved; } // Direct inferred helpers can receive native callbacks even when their - // result is an opaque handle that cannot cross checked storage. - if (diagnostics.length === 0 && value?.type.kind === "func") return value.type; + // result is an opaque handle that cannot cross checked storage. The + // argument's own lowering then takes this value instead of lowering + // the closure a second time. + if (diagnostics.length === 0 && value?.type.kind === "func") { + lowerer.speculatedClosures.set(arg, { + ctx: lowerer.ctx, + bindings: lowerer.typeParamBindings, + value, + }); + return value.type; + } } if (arg.kind === ts.SyntaxKind.ThisKeyword) return lowerer.resolveThis()?.type ?? null; if (ts.isAwaitExpression(arg)) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 6fd6d863a..dd07d0075 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3174,6 +3174,14 @@ export class Lowerer { * (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(); + /** Closure arguments an implicit-any call already lowered to learn their + * type (storedImplicitArgumentType), by node: the argument's own lowering + * in the same function and with the same type bindings takes that value + * once instead of lowering the closure again. */ + readonly speculatedClosures = new Map< + ts.Expression, + { ctx: FnCtx; bindings: Map | null; value: IrExpr } + >(); /** What each fs path argument of a URL-path call lowers to (the plans * of builtins/fs-paths.ts). */ readonly fsPathPlans = new Map(); @@ -12938,12 +12946,30 @@ export class Lowerer { /* ── expressions ──────────────────────────────────────────────────── */ lowerExpr(expr: ts.Expression): IrExpr { + if (this.speculatedClosures.size > 0) { + const speculated = this.speculatedClosure(expr); + if (speculated !== null) { + this.speculatedClosures.delete(expr); + return speculated; + } + } const streamValue = this.valueStreamReceivers.get(expr); if (streamValue) return valueStreamOf(varRef(streamValue.localId, streamValue.type, locOf(expr))); return this.stagedArgument(expr) ?? lowerExpr(this, expr); } + /** The value storedImplicitArgumentType lowered for `expr` in the active + * function and type bindings (speculatedClosures), or null. */ + speculatedClosure(expr: ts.Expression): IrExpr | null { + const entry = this.speculatedClosures.get(expr); + return entry !== undefined && + entry.ctx === this.ctx && + entry.bindings === this.typeParamBindings + ? entry.value + : null; + } + /** 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 { From cc6b1144681da6c0d29649d44ee203703e9db25d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:23:08 -0500 Subject: [PATCH 116/273] Take the argument count before parameters bind A function that reads arguments.length and is called through a value reads the count its adapter stored, then clears the slot. That read ran after the parameter prologue, so a parameter binding that threw would leave the slot set for the next direct call. The count is now taken, and the slot cleared, before any parameter binds. --- .../src/frontend/lowering/lower-functions.ts | 7 ++++++- ...3-argument-count-destructured-parameter.ts.json | 6 ++++++ .../8223-argument-count-destructured-parameter.ts | 14 ++++++++++++++ ...3-argument-count-destructured-parameter.ts.json | 1 + 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8223-argument-count-destructured-parameter.ts.json create mode 100644 tests/corpus/8223-argument-count-destructured-parameter.ts create mode 100644 tests/harness/corpus-costs/8223-argument-count-destructured-parameter.ts.json diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index c1682f7c8..bd9b5f55d 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -535,12 +535,18 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I const diagsBefore = lowerer.diags.length; lowerer.fnStack.push(ctx); try { + // The count a call through a value stored is taken (and its slot + // cleared) before the parameters bind: a destructuring default that + // throws must not leave the slot set for the next direct call. + const argumentCount: IrStmt[] = []; + declareArgumentCount(lowerer, decl, argumentCount); const receiver = decl.parameters.nodes.some(isThisParameter) || isJsSourceFile(ts.getSourceFileOfNode(decl)) ? declareCallReceiver(lowerer, decl, sig.generator !== undefined) : null; const { params, prologue } = lowerer.declareParams(decl.parameters.nodes, sig.params); if (receiver) prologue.unshift(receiver); + prologue.unshift(...argumentCount); declareFunctionArguments( lowerer, decl, @@ -552,7 +558,6 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I (sig.params[sig.params.length - 1]!.mode === "dynRest" || sig.params[sig.params.length - 1]!.mode === "arguments"), ); - declareArgumentCount(lowerer, decl, prologue); const bodyBlock = blockBodyOf(decl); if (!bodyBlock) { lowerer.unsupported( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8223-argument-count-destructured-parameter.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8223-argument-count-destructured-parameter.ts.json new file mode 100644 index 000000000..6cae202d3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8223-argument-count-destructured-parameter.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8223-argument-count-destructured-parameter.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8223-argument-count-destructured-parameter.ts b/tests/corpus/8223-argument-count-destructured-parameter.ts new file mode 100644 index 000000000..def8c5ba8 --- /dev/null +++ b/tests/corpus/8223-argument-count-destructured-parameter.ts @@ -0,0 +1,14 @@ +// A function with a destructured parameter that reads arguments.length, +// called through a value (map passes three arguments) and directly. The +// count is taken before the parameters bind, so each call sees its own. + +function probe({ v, w }: { v: number; w: string }): string { + return `${arguments.length}:${v}${w}`; +} + +console.log([{ v: 1, w: "a" }, { v: 2, w: "b" }].map(probe)); +console.log(probe({ v: 3, w: "c" })); +const call = (f: (item: { v: number; w: string }, index: number) => string): string => + f({ v: 4, w: "d" }, 0); +console.log(call(probe)); +console.log(probe({ v: 5, w: "e" })); diff --git a/tests/harness/corpus-costs/8223-argument-count-destructured-parameter.ts.json b/tests/harness/corpus-costs/8223-argument-count-destructured-parameter.ts.json new file mode 100644 index 000000000..664b49416 --- /dev/null +++ b/tests/harness/corpus-costs/8223-argument-count-destructured-parameter.ts.json @@ -0,0 +1 @@ +{ "plain": 4700, "san": 4200 } From 7fb950deaf04b0eb48943694c213a7b652f5903b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:29:34 -0500 Subject: [PATCH 117/273] Keep rejectUnauthorized on unless false, and certificates independent of raw A server with requestCert read a runtime rejectUnauthorized through JavaScript truthiness, so null, 0 or "" let clients without a valid certificate through, and tls.connect read it the same way. Both now treat only false as off, as Node's options.rejectUnauthorized !== false does; a standalone TLSSocket keeps Node's truthiness test. An X509Certificate's methods read the record's raw Buffer, so writing into the Buffer that certificate.raw answers changed later checks, the PEM and the public key. The record now keeps a hidden copy of the DER that the methods read, and raw stays the same Buffer at every read, as in Node. --- .../src/frontend/lowering/builtins/x509.ts | 17 +++--- .../src/frontend/lowering/lower-classes.ts | 3 + packages/compiler/src/frontend/type-mapper.ts | 9 ++- ...79-tls-reject-unauthorized-values.mjs.json | 6 ++ .../tests/corpus/8181-x509-raw-writes.ts.json | 6 ++ packages/runtime/src/scr_tls.c | 14 +++-- .../8179-tls-reject-unauthorized-values.mjs | 55 +++++++++++++++++++ tests/corpus/8181-x509-raw-writes.ts | 20 +++++++ ...79-tls-reject-unauthorized-values.mjs.json | 1 + .../corpus-costs/8181-x509-raw-writes.ts.json | 1 + 10 files changed, 119 insertions(+), 13 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8179-tls-reject-unauthorized-values.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8181-x509-raw-writes.ts.json create mode 100644 tests/corpus/8179-tls-reject-unauthorized-values.mjs create mode 100644 tests/corpus/8181-x509-raw-writes.ts create mode 100644 tests/harness/corpus-costs/8179-tls-reject-unauthorized-values.mjs.json create mode 100644 tests/harness/corpus-costs/8181-x509-raw-writes.ts.json diff --git a/packages/compiler/src/frontend/lowering/builtins/x509.ts b/packages/compiler/src/frontend/lowering/builtins/x509.ts index bc0092369..5d81070eb 100644 --- a/packages/compiler/src/frontend/lowering/builtins/x509.ts +++ b/packages/compiler/src/frontend/lowering/builtins/x509.ts @@ -16,13 +16,14 @@ import { } from "../../../ir/ir.js"; import { everyStmtList } from "../../../ir/traverse.js"; import { unsupportedDiag } from "../../../diagnostics/diagnostic.js"; -import { X509_PUBLIC_KEY_FIELD } from "../../type-mapper.js"; +import { X509_DER_FIELD, X509_PUBLIC_KEY_FIELD } from "../../type-mapper.js"; import { keyedHelperName } from "../stable-names.js"; import { x509RecordCall } from "../lower-classes.js"; /* node:crypto's X509Certificate members beyond its fields: the methods * and the publicKey getter. A certificate is the Dirent-style record - * (type-mapper.ts), and each member reads its raw DER. checkHost, + * (type-mapper.ts), and each member reads the record's hidden copy of its + * DER, which writes into the `raw` Buffer leave alone, as in Node. checkHost, * checkEmail, checkIP, checkIssued, toLegacyObject and the PEM of * toString/toJSON run in scr_x509.c; * publicKey, verify and checkPrivateKey read keys in scr_pki.c. The first @@ -110,7 +111,7 @@ export function refuseX509DynValues(lowerer: Lowerer, functions: readonly IrFunc /** String(x509), `${x509}` and x509 + "": the certificate's toString(), * its PEM. */ export function x509ToString(record: IrExpr, loc: SrcLoc): IrExpr { - return { kind: "libCall", fn: "crypto.x509Pem", args: [rawOf(record, loc)], type: STRING, loc }; + return { kind: "libCall", fn: "crypto.x509Pem", args: [derOf(record, loc)], type: STRING, loc }; } /** Node keeps a certificate's fields as prototype getters, so own-key @@ -131,13 +132,13 @@ export function fenceX509KeyWalk( ); } -function rawOf(record: IrExpr, loc: SrcLoc): IrExpr { +function derOf(record: IrExpr, loc: SrcLoc): IrExpr { if (record.type.kind !== "record") return record; return { kind: "recordGet", obj: record, shapeId: record.type.shapeId, - field: "raw", + field: X509_DER_FIELD, type: BYTES_U8, loc, }; @@ -254,7 +255,7 @@ export function lowerX509PublicKey( value: { kind: "libCall", fn: "crypto.x509PublicKey", - args: [rawOf(c, loc)], + args: [derOf(c, loc)], type: DYN, loc, }, @@ -310,7 +311,7 @@ export function lowerX509Call(lowerer: Lowerer, call: ts.CallExpression): IrExpr `the supported form is ${method.form}`, ); } - const raw = rawOf(lowerer.lowerExpr(access.expression), loc); + const raw = derOf(lowerer.lowerExpr(access.expression), loc); if (method.fn === "crypto.x509LegacyObject") { return { kind: "libCall", fn: method.fn, args: [raw], type: DYN, loc }; } @@ -337,7 +338,7 @@ export function lowerX509Call(lowerer: Lowerer, call: ts.CallExpression): IrExpr loc, }; } - const other = rawOf(lowerer.lowerExpr(args[0]!), loc); + const other = derOf(lowerer.lowerExpr(args[0]!), loc); return { kind: "libCall", fn: method.fn, args: [raw, other], type: BOOL, loc }; } const first = lowerer.lowerExprExpecting(args[0]!, DYN); diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 9035869d9..23dac58a2 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -100,6 +100,7 @@ import { jsOpenObjectType, typeKey, withUnitArm, + X509_DER_FIELD, X509_PUBLIC_KEY_FIELD, } from "../type-mapper.js"; import { @@ -11313,6 +11314,8 @@ function x509RecordHelper( type: DYN, loc, }), + // A copy: writes into `raw` must not reach the methods. + [X509_DER_FIELD]: () => call("crypto.x509RawDer", BYTES_U8), }; /** A string | undefined field: the runtime's text, or undefined for "". */ function optionalText(local: string, fn: IrLibFn, type: IrType): IrExpr { diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index f51efd5f1..9a8ae46ba 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -6715,8 +6715,14 @@ function boundVoidReturnType(p: ts.Type, resolveTypeParam?: TypeParamResolver): * filled on the first read (builtins/x509.ts). */ export const X509_PUBLIC_KEY_FIELD = "%publicKey"; +/** The X509Certificate record's hidden copy of its DER, which its methods + * read: a program can write into the Buffer `raw` answers, and in Node + * that changes neither the certificate's checks nor its PEM. */ +export const X509_DER_FIELD = "%der"; + /** The X509Certificate record's fields in @types/node's declaration order - * ("string?" is string | undefined), then the hidden publicKey slot. */ + * ("string?" is string | undefined), then the hidden publicKey and DER + * slots. */ export const X509_FIELDS: readonly (readonly [string, IrType | "string?"])[] = [ ["ca", BOOL], ["fingerprint", STRING], @@ -6735,6 +6741,7 @@ export const X509_FIELDS: readonly (readonly [string, IrType | "string?"])[] = [ ["validTo", STRING], ["validToDate", DATE_T], [X509_PUBLIC_KEY_FIELD, DYN], + [X509_DER_FIELD, { kind: "bytes", elem: "u8" }], ]; export function withUndefinedArm(t: IrType, unions: UnionRegistry): IrType | null { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8179-tls-reject-unauthorized-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8179-tls-reject-unauthorized-values.mjs.json new file mode 100644 index 000000000..ef98570e2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8179-tls-reject-unauthorized-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8179-tls-reject-unauthorized-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8181-x509-raw-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8181-x509-raw-writes.ts.json new file mode 100644 index 000000000..d92d8f8fd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8181-x509-raw-writes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8181-x509-raw-writes.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_tls.c b/packages/runtime/src/scr_tls.c index e764926b2..547fef27a 100644 --- a/packages/runtime/src/scr_tls.c +++ b/packages/runtime/src/scr_tls.c @@ -2232,8 +2232,8 @@ static void scr_tls_opt_fence(const char *api, const char *key, const char *hint scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b)); } -/* JS truthiness over the dyn kinds — Node reads requestCert / - * rejectUnauthorized through `if (options.x)`. */ +/* JS truthiness over the dyn kinds — Node reads requestCert (and a + * standalone TLSSocket's rejectUnauthorized) through `if (options.x)`. */ static bool scr_tls_dyn_truthy(const ScrDyn *v) { switch (v->kind) { case SCR_DYN_UNDEF: @@ -2245,6 +2245,12 @@ static bool scr_tls_dyn_truthy(const ScrDyn *v) { } } +/* tls.Server and tls.connect read rejectUnauthorized as + * `options.rejectUnauthorized !== false`: null, 0 and "" keep it on. */ +static bool scr_tls_dyn_not_false(const ScrDyn *v) { + return v->kind != SCR_DYN_BOOL || v->v.b; +} + /* PEM material from a runtime option value: a string, a Buffer/Uint8Array, * or an array of those. `concat` (the client `ca` bundle) joins array * entries; cert/key arrays accept exactly one entry (one identity per @@ -2630,7 +2636,7 @@ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrByt continue; } if (strcmp(e->key, "rejectUnauthorized") == 0) { - if (auth != NULL) auth->reject = scr_tls_dyn_truthy(v); + if (auth != NULL) auth->reject = scr_tls_dyn_not_false(v); continue; /* without client certificates it has no effect */ } if (server_ok && (strcmp(e->key, "ALPNProtocols") == 0 || strcmp(e->key, "minVersion") == 0 || @@ -3300,7 +3306,7 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, continue; } if (strcmp(e->key, "rejectUnauthorized") == 0) { - reject = scr_tls_dyn_truthy(v); + reject = scr_tls_dyn_not_false(v); continue; } if (strcmp(e->key, "ca") == 0) { diff --git a/tests/corpus/8179-tls-reject-unauthorized-values.mjs b/tests/corpus/8179-tls-reject-unauthorized-values.mjs new file mode 100644 index 000000000..d0275ab4a --- /dev/null +++ b/tests/corpus/8179-tls-reject-unauthorized-values.mjs @@ -0,0 +1,55 @@ +// rejectUnauthorized is off only when it is false, as in Node: on a server +// with requestCert, null, 0, "" and undefined keep rejecting clients that +// send no certificate, and in tls.connect they keep rejecting a server +// certificate that does not verify. +import { readFileSync } from "node:fs"; +import * as tls from "node:tls"; + +const D = "tests/fixtures/server/certs"; +const r = (f) => readFileSync(`${D}/${f}`); +const ca = r("ca.pem"), cert = r("localhost.pem"), key = r("localhost-key.pem"); +const self = r("selfsigned.pem"), selfKey = r("selfsigned-key.pem"); + +const values = [ + ["false", false], + ["true", true], + ["null", null], + ["0", 0], + ['""', ""], + ["undefined", undefined], +]; + +for (const [label, value] of values) { + const events = []; + const server = tls.createServer({ cert, key, requestCert: true, rejectUnauthorized: value }, (s) => { + events.push(`server authorized=${s.authorized} error=${s.authorizationError}`); + s.end("ok"); + }); + const port = await new Promise((res) => server.listen(0, () => res(server.address().port))); + await new Promise((done) => { + const c = tls.connect({ port, ca, servername: "localhost" }); + c.on("secureConnect", () => events.push("client secureConnect")); + c.on("data", (d) => events.push(`client data ${d}`)); + c.on("error", (e) => events.push(`client error ${e.code}`)); + c.on("close", () => setTimeout(done, 50)); + }); + console.log("rejectUnauthorized", label, "|", events.join(" ; ")); + await new Promise((res) => server.close(() => res())); +} + +{ + const server = tls.createServer({ cert: self, key: selfKey }, (s) => s.end("ok")); + const port = await new Promise((res) => server.listen(0, () => res(server.address().port))); + for (const [label, value] of values) { + const events = []; + await new Promise((done) => { + const c = tls.connect({ port, servername: "localhost", rejectUnauthorized: value }); + c.on("secureConnect", () => events.push(`secureConnect authorized=${c.authorized} error=${c.authorizationError}`)); + c.on("data", (d) => events.push(`data ${d}`)); + c.on("error", (e) => events.push(`error ${e.code}`)); + c.on("close", () => done()); + }); + console.log("tls.connect rejectUnauthorized", label, "|", events.join(" ; ")); + } + server.close(); +} diff --git a/tests/corpus/8181-x509-raw-writes.ts b/tests/corpus/8181-x509-raw-writes.ts new file mode 100644 index 000000000..17704096d --- /dev/null +++ b/tests/corpus/8181-x509-raw-writes.ts @@ -0,0 +1,20 @@ +// A certificate's raw Buffer is the same object at every read, and writing +// into it changes neither the certificate's fields, checks and PEM nor its +// public key, as in Node: the methods read the certificate itself. +import { X509Certificate } from "node:crypto"; +import { readFileSync } from "node:fs"; + +const cert = new X509Certificate(readFileSync("tests/fixtures/server/certs/localhost.pem")); +const ca = new X509Certificate(readFileSync("tests/fixtures/server/certs/ca.pem")); +const raw = cert.raw; +console.log("same Buffer", raw === cert.raw); +const report = (): string => + `${cert.fingerprint256}\n${cert.subject}\n${cert.checkHost("localhost")}\n` + + `${cert.checkIssued(ca)} ${ca.checkIssued(cert)} ${cert.verify(ca.publicKey)}\n` + + `${cert.toString()}\n${cert.publicKey.export({ format: "pem", type: "spki" })}`; +const before = report(); +raw.fill(0); +console.log("unchanged", report() === before); +console.log("raw now", cert.raw.every((b) => b === 0), cert.raw.length); +const other = new X509Certificate(readFileSync("tests/fixtures/server/certs/localhost.pem")); +console.log("fresh certificate raw", other.raw[0], other.raw.equals(cert.raw)); diff --git a/tests/harness/corpus-costs/8179-tls-reject-unauthorized-values.mjs.json b/tests/harness/corpus-costs/8179-tls-reject-unauthorized-values.mjs.json new file mode 100644 index 000000000..ef441a0df --- /dev/null +++ b/tests/harness/corpus-costs/8179-tls-reject-unauthorized-values.mjs.json @@ -0,0 +1 @@ +{ "plain": 5582, "san": 65742 } diff --git a/tests/harness/corpus-costs/8181-x509-raw-writes.ts.json b/tests/harness/corpus-costs/8181-x509-raw-writes.ts.json new file mode 100644 index 000000000..df264cf6e --- /dev/null +++ b/tests/harness/corpus-costs/8181-x509-raw-writes.ts.json @@ -0,0 +1 @@ +{ "plain": 3996, "san": 8383 } From 2edebe7e2ff7df84903a9236aa147472950b3002 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:30:32 -0500 Subject: [PATCH 118/273] Read spread tuple fields before later rest arguments run The packing path for `(T | undefined)[]` rest parameters saved plain arguments to temporaries but read tuple fields only when it built the pack, after every later argument had run. A later argument that wrote the tuple changed values already spread into the call. Each tuple field is now saved where its spread appears, in argument order. --- .../src/frontend/lowering/call-arguments.ts | 14 +++++++++----- .../corpus/8004-optional-element-conversions.ts | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index b02d0b85c..a139bfc64 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -421,12 +421,16 @@ export function completeArgs( ) { const stmts: IrStmt[] = []; const values = restNodes.map((a): IrExpr => { + // A tuple field is read where its spread appears: a later + // argument may write the tuple. + let value: IrExpr; if (isIr(a)) { - if (a.ir.kind !== "seqExpr") return a.ir; - stmts.push(...a.ir.stmts); - return a.ir.result; - } - const value = lowerer.lowerExpr(a as ts.Expression); + value = a.ir; + if (value.kind === "seqExpr") { + stmts.push(...value.stmts); + value = value.result; + } + } else value = lowerer.lowerExpr(a as ts.Expression); const saved = lowerer.declareHiddenLocal("%callArg", value.type); stmts.push({ kind: "varDecl", localId: saved.id, init: value, loc: value.loc }); return { kind: "varRef", localId: saved.id, type: value.type, loc: value.loc }; diff --git a/tests/corpus/8004-optional-element-conversions.ts b/tests/corpus/8004-optional-element-conversions.ts index 2a394c331..07178e9b0 100644 --- a/tests/corpus/8004-optional-element-conversions.ts +++ b/tests/corpus/8004-optional-element-conversions.ts @@ -33,6 +33,22 @@ function label(first: string, ...rest: (string | undefined)[]): string { } const words: [string, string | undefined, string | undefined] = ["a", undefined, "c"]; console.log(label(...words), label("b", undefined)); +// Spread tuple fields are read before later arguments run. +const changing: [boolean, boolean | undefined] = [true, undefined]; +function flip(pair: [boolean, boolean | undefined], second: boolean | undefined): boolean { + pair[0] = !pair[0]; + pair[1] = second; + return pair[0]; +} +console.log(flags(...changing, flip(changing, true))); +console.log(flags(...changing, flip(changing, undefined), ...changing)); +const swapped: [string, string | undefined] = ["x", "y"]; +function swap(pair: [string, string | undefined]): string { + pair[0] = "w"; + pair[1] = undefined; + return "v"; +} +console.log(label("z", ...swapped, swap(swapped), ...swapped)); function pick(start: (number | undefined)[] | string[], other: (number | undefined)[] | string[]): string { let a = start; From 51557931556f79b49f4b8a00fe80c6a59aeaaf05 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:33:30 -0500 Subject: [PATCH 119/273] Decode passphrases with the key's encoding, and refuse RSA-PSS keys restricted to other digests createPrivateKey decoded a string passphrase as UTF-8 whatever the options' encoding, so a correct passphrase given in that encoding failed to decrypt, and an encrypted export read its passphrase the same way. Both now decode it with the encoding, as Node does. An RSA-PSS key whose parameters name a digest the runtime does not sign with, such as sha512-224 or sha512-256, was imported without its digest, MGF1 and salt restrictions, so it allowed operations the key forbids. It now throws a coded SC2020 error that names the digest. Parameters that are malformed still leave the key unrestricted, as OpenSSL reads them. --- .../8183-key-passphrase-encoding.cjs.json | 6 ++ packages/runtime/src/scr_pki.c | 78 +++++++++++++++---- tests/corpus/8183-key-passphrase-encoding.cjs | 28 +++++++ .../8183-key-passphrase-encoding.cjs.json | 1 + tests/harness/errors.test.ts | 32 ++++++++ 5 files changed, 128 insertions(+), 17 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8183-key-passphrase-encoding.cjs.json create mode 100644 tests/corpus/8183-key-passphrase-encoding.cjs create mode 100644 tests/harness/corpus-costs/8183-key-passphrase-encoding.cjs.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8183-key-passphrase-encoding.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8183-key-passphrase-encoding.cjs.json new file mode 100644 index 000000000..ee89a2add --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8183-key-passphrase-encoding.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8183-key-passphrase-encoding.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index 908129243..9877ac6c9 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -1635,48 +1635,79 @@ static SpKey *sp_ed_from_der(const unsigned char *der, size_t len, bool private_ return k; } -/* RSASSA-PSS-params at *p (RFC 4055): true with the restrictions set on - * `k`, false when they are malformed or unsupported, which leaves the key - * without restrictions, as OpenSSL reads a negative or oversized salt. */ -static bool sp_pss_params_read(unsigned char **p, const unsigned char *end, SpKey *k) { +/* The name of a digest OID that RSASSA-PSS-params can hold and this + * runtime does not sign with, for its refusal. */ +static const char *sp_pss_other_digest(const mbedtls_asn1_buf *oid) { + /* 2.16.840.1.101.3.4.2.x, the NIST hash arc */ + static const unsigned char nist[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02}; + if (oid->len == sizeof nist + 1 && memcmp(oid->p, nist, sizeof nist) == 0) { + switch (oid->p[sizeof nist]) { + case 0x05: return "sha512-224"; + case 0x06: return "sha512-256"; + case 0x07: return "sha3-224"; + case 0x08: return "sha3-256"; + case 0x09: return "sha3-384"; + case 0x0a: return "sha3-512"; + } + } + mbedtls_md_type_t md; + if (mbedtls_oid_get_md_alg(oid, &md) == 0) { + if (md == MBEDTLS_MD_MD5) return "md5"; + if (md == MBEDTLS_MD_RIPEMD160) return "ripemd160"; + } + return "this digest"; +} + +/* RSASSA-PSS-params at *p (RFC 4055): 1 with the restrictions set on `k`; + * 0 when they are malformed, which leaves the key without restrictions, as + * OpenSSL reads a negative or oversized salt; -1 with *digest named when a + * hash or MGF1 digest is one this runtime does not sign with, so that the + * key is refused rather than imported without the restrictions it carries. */ +static int sp_pss_params_read(unsigned char **p, const unsigned char *end, SpKey *k, const char **digest) { 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 0; const unsigned char *params_end = *p + n; mbedtls_md_type_t md = MBEDTLS_MD_SHA1, mgf1 = MBEDTLS_MD_SHA1; int salt = 20; if (*p < params_end && **p == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0)) { mbedtls_asn1_buf oid, params; if (mbedtls_asn1_get_tag(p, params_end, &n, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0) != 0 || - mbedtls_asn1_get_alg(p, *p + n, &oid, ¶ms) != 0 || mbedtls_oid_get_md_alg(&oid, &md) != 0 || - !sp_pss_md_name(md)) - return false; + mbedtls_asn1_get_alg(p, *p + n, &oid, ¶ms) != 0) + return 0; + if (mbedtls_oid_get_md_alg(&oid, &md) != 0 || !sp_pss_md_name(md)) { + *digest = sp_pss_other_digest(&oid); + return -1; + } } if (*p < params_end && **p == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 1)) { mbedtls_asn1_buf oid, params; if (mbedtls_asn1_get_tag(p, params_end, &n, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 1) != 0 || mbedtls_asn1_get_alg(p, *p + n, &oid, ¶ms) != 0 || oid.len != MBEDTLS_OID_SIZE(MBEDTLS_OID_MGF1) || memcmp(oid.p, MBEDTLS_OID_MGF1, oid.len) != 0) - return false; + return 0; /* MGF1's parameters are the hash's AlgorithmIdentifier, whose * content starts with the hash's OID. */ - if (params.tag != (MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) return false; + if (params.tag != (MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) return 0; unsigned char *alg = params.p; mbedtls_asn1_buf hash_oid = {.tag = MBEDTLS_ASN1_OID}; - if (mbedtls_asn1_get_tag(&alg, params.p + params.len, &hash_oid.len, MBEDTLS_ASN1_OID) != 0) return false; + if (mbedtls_asn1_get_tag(&alg, params.p + params.len, &hash_oid.len, MBEDTLS_ASN1_OID) != 0) return 0; hash_oid.p = alg; - if (mbedtls_oid_get_md_alg(&hash_oid, &mgf1) != 0 || !sp_pss_md_name(mgf1)) return false; + if (mbedtls_oid_get_md_alg(&hash_oid, &mgf1) != 0 || !sp_pss_md_name(mgf1)) { + *digest = sp_pss_other_digest(&hash_oid); + return -1; + } } if (*p < params_end && **p == (MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 2)) { if (mbedtls_asn1_get_tag(p, params_end, &n, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 2) != 0 || mbedtls_asn1_get_int(p, *p + n, &salt) != 0 || salt < 0) - return false; + return 0; } *p = (unsigned char *)params_end; k->pss_params = true; k->pss_md = md; k->pss_mgf1 = mgf1; k->pss_salt = salt; - return true; + return 1; } /* An RSA-PSS key from PKCS#8 or SubjectPublicKeyInfo DER whose algorithm @@ -1706,7 +1737,19 @@ static SpKey *sp_pss_from_der(const unsigned char *der, size_t len, bool private p += oid_len; SpKey *k = sp_key_alloc(pkcs8); k->rsa_pss = true; - if (p < alg_end && !sp_pss_params_read(&p, alg_end, k)) k->pss_params = false; + const char *digest = NULL; + if (p < alg_end) { + int read = sp_pss_params_read(&p, alg_end, k, &digest); + if (read < 0) { + sp_key_release(k); + *failed = true; + char what[96]; + snprintf(what, sizeof what, "RSA-PSS keys restricted to %s", digest); + sp_not_supported(what); + return NULL; + } + if (read == 0) k->pss_params = false; + } p = (unsigned char *)alg_end; int r; if (pkcs8) { @@ -1967,7 +2010,8 @@ static ScrDyn *sp_create_key(ScrDyn *input, bool private_key) { } ScrBytes *pass = NULL; if (passphrase && !sp_absent(passphrase)) { - pass = sp_source_bytes(passphrase, NULL); + /* Node decodes a string passphrase with the key's encoding. */ + pass = sp_source_bytes(passphrase, encoding); if (!pass) { scr_bytes_release(bytes); return NULL; @@ -2292,7 +2336,7 @@ static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { /* OpenSSL's error, raised once the key is encoded. */ } else if (ci) { const unsigned char *start = der + SP_DER_MAX - n; - ScrBytes *pass = sp_source_bytes(passphrase, NULL); + ScrBytes *pass = sp_source_bytes(passphrase, sp_opt(options, "encoding")); const unsigned char *pass_data = pass ? pass->data : (const unsigned char *)""; size_t pass_len = pass ? (size_t)scr_bytes_byte_len(pass) : 0; if (pkcs8) { diff --git a/tests/corpus/8183-key-passphrase-encoding.cjs b/tests/corpus/8183-key-passphrase-encoding.cjs new file mode 100644 index 000000000..e52fa6c1c --- /dev/null +++ b/tests/corpus/8183-key-passphrase-encoding.cjs @@ -0,0 +1,28 @@ +// A string passphrase is decoded with the key's encoding, as in Node: +// createPrivateKey's { encoding } and the encoding option of an encrypted +// export both apply to it. +const c = require("node:crypto"); + +const { privateKey } = c.generateKeyPairSync("ec", { namedCurve: "P-256" }); +const pass = "pässwörd"; +const hex = Buffer.from(pass).toString("hex"); +const pem = privateKey.export({ format: "pem", type: "pkcs8", cipher: "aes-256-cbc", passphrase: hex, encoding: "hex" }); +const der = privateKey.export({ format: "der", type: "pkcs8", cipher: "aes-128-cbc", passphrase: pass }); + +function attempt(label, options) { + try { + const key = c.createPrivateKey(options); + console.log(label, key.asymmetricKeyType, key.equals(privateKey)); + } catch (e) { + console.log(label, "throws", e.code); + } +} + +attempt("utf8 passphrase", { key: pem, passphrase: pass }); +attempt("hex passphrase, encoding hex", { key: Buffer.from(pem), passphrase: hex, encoding: "hex" }); +attempt("base64 passphrase, encoding base64", { key: Buffer.from(pem), passphrase: Buffer.from(pass).toString("base64"), encoding: "base64" }); +attempt("latin1 passphrase, encoding latin1", { key: Buffer.from(pem), passphrase: Buffer.from(pass).toString("latin1"), encoding: "latin1" }); +attempt("hex passphrase, no encoding", { key: pem, passphrase: hex }); +attempt("Buffer passphrase, encoding hex", { key: Buffer.from(pem), passphrase: Buffer.from(pass), encoding: "hex" }); +attempt("der, hex passphrase, encoding hex", { key: der, format: "der", type: "pkcs8", passphrase: hex, encoding: "hex" }); +attempt("hex key string, encoding hex", { key: Buffer.from(pem).toString("hex"), passphrase: hex, encoding: "hex" }); diff --git a/tests/harness/corpus-costs/8183-key-passphrase-encoding.cjs.json b/tests/harness/corpus-costs/8183-key-passphrase-encoding.cjs.json new file mode 100644 index 000000000..0d507484f --- /dev/null +++ b/tests/harness/corpus-costs/8183-key-passphrase-encoding.cjs.json @@ -0,0 +1 @@ +{ "plain": 3665, "san": 67315 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index c8849a10f..663806942 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -870,6 +870,38 @@ console.log(\`\${register(7)}\`); }); }); +// An RSA-PSS key whose parameters restrict it to a digest the runtime does +// not sign with (sha512-224 and sha512-256, which OpenSSL reads) throws a +// coded SC2020 error naming the digest, instead of importing the key +// without the restrictions it carries. +describe("RSA-PSS keys restricted to unimplemented digests", () => { + test("importing one throws SC2020 naming the digest", async () => { + const r = await compileAndRun( + "pss-restricted-digest", + `import { createPublicKey } from "node:crypto"; +const keys = [ + "-----BEGIN PUBLIC KEY-----\\nMIGQMEEGCSqGSIb3DQEBCjA0oA8wDQYJYIZIAWUDBAIFBQChHDAaBgkqhkiG9w0B\\nAQgwDQYJYIZIAWUDBAIFBQCiAwIBCANLADBIAkEArMDQESqjeWDLqFa1E5SzsMl4\\n6tyUsEqcDKSwRUvPilv1AUudf/w6qaXlPnAxBmFjZYihdQSZlk0czRLOX3o7gwID\\nAQAB\\n-----END PUBLIC KEY-----\\n", + "-----BEGIN PUBLIC KEY-----\\nMIGQMEEGCSqGSIb3DQEBCjA0oA8wDQYJYIZIAWUDBAIBBQChHDAaBgkqhkiG9w0B\\nAQgwDQYJYIZIAWUDBAIGBQCiAwIBCANLADBIAkEAuOa9NkzXzMDelHCIEkf0mnKe\\nnQdalNUm9vZtIMwpDJcn5U0Vbhhd6MTfRPhSo5nW71Oh5eU7v5M3PMYOHS7cMQID\\nAQAB\\n-----END PUBLIC KEY-----\\n", +]; +for (const key of keys) { + try { + console.log(createPublicKey(key).asymmetricKeyType); + } catch (error) { + const e = error as Error & { code?: string }; + console.log(e.name, e.code, e.message); + } +} +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + "Error SC2020 'RSA-PSS keys restricted to sha512-224' is part of the standard library types but has no scriptc lowering yet\n" + + "Error SC2020 'RSA-PSS keys restricted to sha512-256' is part of the standard library types but has no scriptc lowering yet\n", + ); + expect(r.stderr).toBe(""); + }); +}); + // A cipher name Node lists in crypto.getCiphers() but the runtime does not // implement throws a coded SC2020 error naming the cipher when the name is // computed; literal names are refused at compile time (node-types-fenced). From 6e4bfb3c913bf82bd9119243495f9074bced4ea0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:34:40 -0500 Subject: [PATCH 120/273] Await the final value of an untyped sync pipeline source for await over a sync iterator awaits the value of the result that ends it too, so a sync iterator that returns a rejected promise rejects the pump. The source's generator skipped that value and reported success. It now awaits a final value that is an object (a promise included) before it stops, without closing the finished iterator. --- .../src/frontend/lowering/lower-stream.ts | 80 +++++++++++-------- .../7504-pipeline-untyped-source-errors.mjs | 24 +++++- 2 files changed, 70 insertions(+), 34 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index b519ce378..d3883427a 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -3954,6 +3954,38 @@ function lowerDynIterableSource( const sync = (): IrExpr => varRef(syncId, BOOL, loc); const step = (): IrExpr => varRef(stepId, DYN, loc); const value = (): IrExpr => varRef(valueId, DYN, loc); + // for await over a sync iterator awaits each value, the final one + // included (CreateAsyncFromSyncIterator): an object (a promise or a + // thenable) passes on its result + const awaitsValue = (): IrExpr => ({ + kind: "logical", + op: "&&", + left: sync(), + right: { + kind: "logical", + op: "||", + left: { kind: "dynTest", test: "promise", value: value(), type: BOOL, loc }, + right: { + kind: "logical", + op: "&&", + left: { + kind: "dynTest", + test: "nullish", + negated: true, + value: value(), + type: BOOL, + loc, + }, + right: { kind: "dynTest", test: "object", value: value(), type: BOOL, loc }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }); const get = (obj: IrExpr, name: string): IrExpr => ({ kind: "dynKeyGet", value: obj, @@ -4053,46 +4085,28 @@ function lowerDynIterableSource( type: BOOL, loc, }, - then: [{ kind: "break", loc }], + // the final value is awaited too: a rejection rejects the + // pump, and the finished iterator is not closed + then: [ + { kind: "assign", localId: valueId, value: get(step(), "value"), loc }, + { + kind: "if", + cond: awaitsValue(), + then: [{ kind: "exprStmt", expr: awaitDyn(value()), loc }], + else_: null, + loc, + }, + { kind: "break", loc }, + ], else_: null, loc, }, setActive(true), { kind: "assign", localId: valueId, value: get(step(), "value"), loc }, - // for await over a sync iterator awaits each value - // (CreateAsyncFromSyncIterator): a promise or a thenable - // passes on its result, and a rejection closes the iterator + // a rejection here closes the iterator { kind: "if", - cond: { - kind: "logical", - op: "&&", - left: sync(), - right: { - kind: "logical", - op: "||", - left: { kind: "dynTest", test: "promise", value: value(), type: BOOL, loc }, - right: { - kind: "logical", - op: "&&", - left: { - kind: "dynTest", - test: "nullish", - negated: true, - value: value(), - type: BOOL, - loc, - }, - right: { kind: "dynTest", test: "object", value: value(), type: BOOL, loc }, - type: BOOL, - loc, - }, - type: BOOL, - loc, - }, - type: BOOL, - loc, - }, + cond: awaitsValue(), then: [{ kind: "assign", localId: valueId, value: awaitDyn(value()), loc }], else_: null, loc, diff --git a/tests/corpus/7504-pipeline-untyped-source-errors.mjs b/tests/corpus/7504-pipeline-untyped-source-errors.mjs index 3ee984547..5d6faded1 100644 --- a/tests/corpus/7504-pipeline-untyped-source-errors.mjs +++ b/tests/corpus/7504-pipeline-untyped-source-errors.mjs @@ -1,7 +1,7 @@ // pipeline() over untyped iterable sources: a destination that fails closes // the source, backpressure waits for drain, an abort signal stops the pump, // async generator and Map sources reach object-mode destinations, and a -// sync source's promise values are awaited. +// sync source's promise values, its final value included, are awaited. import { pipeline, Writable, PassThrough } from "node:stream"; import { pipeline as pipelineP } from "node:stream/promises"; @@ -113,3 +113,25 @@ try { } catch (err) { console.log("promised rejected", err.message); } + +// a sync source's final value is awaited too: a rejected one rejects the +// pipeline, and a resolved one is not written +function* finalRejects() { + yield "f1"; + return Promise.reject(new Error("final value rejected")); +} +function* finalResolves() { + yield "r1"; + return Promise.resolve("not written"); +} +for (const [label, source] of [["finalRejects", finalRejects], ["finalResolves", finalResolves]]) { + try { + await pipelineP( + pick(source()), + new Writable({ objectMode: true, write(c, _e, cb) { console.log(label, "write", c); cb(); } }), + ); + console.log(label, "resolved"); + } catch (err) { + console.log(label, "rejected", err.message); + } +} From 43ff8a11222f131b8eaed0bcb40e1e945f49982f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:35:25 -0500 Subject: [PATCH 121/273] Inline destructured literal results and read ASCII slice ranges inline - A function whose whole body is `return [a, b]` or `return { a, b }`, with number, boolean or string fields, is now inlined where its result is only destructured (`const [head, tail] = split(text, at)`). The parameters are declared from the arguments, then each field straight into its destructured binding, in the literal's order; an unread field still evaluates into a binding of its own. No record is built, and a slice field read only through its length or char codes becomes a scalar slice that copies nothing. Arguments may own their value, but as for numeric records nothing inside an argument may hold a temporary of its own. - A scalar slice of a proven-ASCII string computes its range inline: the bounds clamp over the byte length exactly as scr_str_slice_range does, and no boundary can split a surrogate pair. Other strings keep the runtime call, which gains the same ASCII fast path. - value-records' tuples section, `[head, tail] = split(...)` read only through lengths, goes from two heap slices per step to none. --- packages/compiler/src/backend/llvm/emitter.ts | 28 +- .../src/backend/llvm/stack-objects.test.ts | 5 +- .../src/backend/llvm/string-slices.ts | 100 +++++- .../compiler/src/ir/scalar-records.test.ts | 160 ++++++++- packages/compiler/src/ir/scalar-records.ts | 321 ++++++++++++++++++ .../corpus/8241-literal-result-slices.ts.json | 6 + packages/runtime/src/scr_string.c | 11 + tests/corpus/8241-literal-result-slices.ts | 81 +++++ .../8241-literal-result-slices.ts.json | 1 + 9 files changed, 696 insertions(+), 17 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8241-literal-result-slices.ts.json create mode 100644 tests/corpus/8241-literal-result-slices.ts create mode 100644 tests/harness/corpus-costs/8241-literal-result-slices.ts.json diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 0243763f1..7c4a8aed1 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -68,7 +68,7 @@ import { type StoredSplitSnapshot, type StringSplit, } from "./split-loops.js"; -import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; +import { scalarizeLiteralResults, scalarizeNumericRecords } from "../../ir/scalar-records.js"; import { addRecordResultEntries, resultEntryName } from "./record-results.js"; import { specializeCallbackCalls } from "../../ir/callback-specialization.js"; import { @@ -578,27 +578,29 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter { ? selectInt32Arrays( versionParameters( addRecordResultEntries( - scalarizeNumericRecords( - specializeCallbackCalls( - specializeNumericCalls( - inlineExpressionFunctions( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads( - freezeInitOnlyGlobals( - holdCharLocalsAsUnits( - dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)), + scalarizeLiteralResults( + scalarizeNumericRecords( + specializeCallbackCalls( + specializeNumericCalls( + inlineExpressionFunctions( + foldConstantGlobals( + copyStableCaptures( + resolveSelfCaptures( + hoistFinalFieldReads( + freezeInitOnlyGlobals( + holdCharLocalsAsUnits( + dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)), + ), ), ), ), ), ), ), + mangleFunction, ), mangleFunction, ), - mangleFunction, ), ), ), diff --git a/packages/compiler/src/backend/llvm/stack-objects.test.ts b/packages/compiler/src/backend/llvm/stack-objects.test.ts index a74878080..c4f2bedc4 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.test.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.test.ts @@ -286,8 +286,9 @@ function bump(s: State): State { return { ...s, count: s.count + 1 }; } export function tokens(n: number, w: string): number { const t = { kind: "id", text: w, pos: n }; const u = make("num", w, n); - const [q, r] = divmod(n, 7); - return t.pos + width(u) + width({ kind: w, text: w, pos: 0 }) + q + r; + // Read by index: a destructured literal result is inlined instead. + const qr = divmod(n, 7); + return t.pos + width(u) + width({ kind: w, text: w, pos: 0 }) + qr[0] + qr[1]; } export function states(n: number, w: string): number { let s: State = { name: w, count: 0 }; diff --git a/packages/compiler/src/backend/llvm/string-slices.ts b/packages/compiler/src/backend/llvm/string-slices.ts index d00c681d6..35abaec14 100644 --- a/packages/compiler/src/backend/llvm/string-slices.ts +++ b/packages/compiler/src/backend/llvm/string-slices.ts @@ -8,6 +8,7 @@ import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse import { F64_INF, f64Lit, sizeToDouble } from "./common.js"; import type { CallLifetimes } from "./call-lifetimes.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; +import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js"; type Slice = IrExpr & { kind: "strIntrinsic" }; @@ -66,6 +67,58 @@ export function findScalarStringSlices( return slices; } +/** scr_slice_unit: a slice()/substring() bound `v` over `length` units. A + * non-negative bound clamps to the length; a negative one counts from the + * end for slice (truncated toward zero) and is 0 for substring, as is NaN. */ +function emitSliceUnit( + host: LlvmEmitterContext, + v: string, + length: string, + substring: boolean, +): string { + const B = host.B; + const size = host.sizeType; + const lengthNumber = B.tmp(), + nonNegative = B.tmp(), + past = B.tmp(), + whole = B.tmp(), + clamped = B.tmp(), + result = B.tmp(); + B.line(`${lengthNumber} = ${sizeToDouble(size)} ${size} ${length} to double`); + B.line(`${nonNegative} = fcmp oge double ${v}, 0.0`); + B.line(`${past} = fcmp oge double ${v}, ${lengthNumber}`); + // Frozen: the conversion is only used where the bound is in range. + const raw = B.tmp(); + B.line(`${raw} = fptosi double ${v} to i64`); + B.line(`${whole} = freeze i64 ${raw}`); + let unit = whole; + if (size !== "i64") { + unit = B.tmp(); + B.line(`${unit} = trunc i64 ${whole} to ${size}`); + } + B.line(`${clamped} = select i1 ${past}, ${size} ${length}, ${size} ${unit}`); + if (substring) { + B.line(`${result} = select i1 ${nonNegative}, ${size} ${clamped}, ${size} 0`); + return result; + } + // Negative: (-length, -1] counts back from the end; (-1, 0) truncates to + // 0, and at or below -length, and NaN, give 0. + const negLength = B.tmp(), + inside = B.tmp(), + zero = B.tmp(), + back = B.tmp(), + counted = B.tmp(), + negative = B.tmp(); + B.line(`${negLength} = fneg double ${lengthNumber}`); + B.line(`${inside} = fcmp ogt double ${v}, ${negLength}`); + B.line(`${zero} = icmp eq ${size} ${unit}, 0`); + B.line(`${back} = add ${size} ${length}, ${unit}`); + B.line(`${counted} = select i1 ${zero}, ${size} 0, ${size} ${back}`); + B.line(`${negative} = select i1 ${inside}, ${size} ${counted}, ${size} 0`); + B.line(`${result} = select i1 ${nonNegative}, ${size} ${clamped}, ${size} ${negative}`); + return result; +} + export interface StringSliceSnapshot { source: string; range: string; @@ -87,10 +140,55 @@ export function emitStringSliceSnapshot( `${range} = alloca { ${host.sizeType}, ${host.sizeType}, i32 }`, ); B.line(`store ptr ${source.name}, ptr ${sourceSlot}`); + const start = args[0]?.name ?? f64Lit(0), + end = args[1]?.name ?? F64_INF; + const substring = slice.method === "substring"; + // A proven-ASCII receiver's units are its bytes, so the range is the + // runtime's clamped bounds over its byte length, and no boundary splits a + // surrogate pair (scr_str_slice_range); other strings ask the runtime. + const ascii = B.newLabel("slice.range.ascii"), + runtime = B.newLabel("slice.range.runtime"), + done = B.newLabel("slice.range.done"); + B.condBr(emitStringAsciiFlag(host, source.name), ascii, runtime); + B.startBlock(ascii); + const size = host.sizeType; + const length = emitStringByteLength(host, source.name); + let from = emitSliceUnit(host, start, length, substring), + to = emitSliceUnit(host, end, length, substring); + if (substring) { + const swap = B.tmp(), + low = B.tmp(), + high = B.tmp(); + B.line(`${swap} = icmp ugt ${size} ${from}, ${to}`); + B.line(`${low} = select i1 ${swap}, ${size} ${to}, ${size} ${from}`); + B.line(`${high} = select i1 ${swap}, ${size} ${from}, ${size} ${to}`); + from = low; + to = high; + } + const nonEmpty = B.tmp(), + difference = B.tmp(), + units = B.tmp(), + startPtr = B.tmp(), + lengthPtr = B.tmp(), + splitPtr = B.tmp(); + B.line(`${nonEmpty} = icmp ugt ${size} ${to}, ${from}`); + B.line(`${difference} = sub ${size} ${to}, ${from}`); + B.line(`${units} = select i1 ${nonEmpty}, ${size} ${difference}, ${size} 0`); + const rangeType = `{ ${size}, ${size}, i32 }`; + B.line(`${startPtr} = getelementptr inbounds ${rangeType}, ptr ${range}, i32 0, i32 0`); + B.line(`store ${size} ${from}, ptr ${startPtr}`); + B.line(`${lengthPtr} = getelementptr inbounds ${rangeType}, ptr ${range}, i32 0, i32 1`); + B.line(`store ${size} ${units}, ptr ${lengthPtr}`); + B.line(`${splitPtr} = getelementptr inbounds ${rangeType}, ptr ${range}, i32 0, i32 2`); + B.line(`store i32 0, ptr ${splitPtr}`); + B.br(done); + B.startBlock(runtime); host.declare("declare void @scr_str_slice_range(ptr, double, double, i1 zeroext, ptr)"); B.line( - `call void @scr_str_slice_range(ptr ${source.name}, double ${args[0]?.name ?? f64Lit(0)}, double ${args[1]?.name ?? F64_INF}, i1 ${slice.method === "substring" ? "true" : "false"}, ptr ${range})`, + `call void @scr_str_slice_range(ptr ${source.name}, double ${start}, double ${end}, i1 ${substring ? "true" : "false"}, ptr ${range})`, ); + B.br(done); + B.startBlock(done); host.toFrame(source); return { source: sourceSlot, range }; } diff --git a/packages/compiler/src/ir/scalar-records.test.ts b/packages/compiler/src/ir/scalar-records.test.ts index e3132088d..7b355ee26 100644 --- a/packages/compiler/src/ir/scalar-records.test.ts +++ b/packages/compiler/src/ir/scalar-records.test.ts @@ -10,7 +10,7 @@ import { type IrStmt, type IrType, } from "./ir.js"; -import { scalarizeNumericRecords } from "./scalar-records.js"; +import { scalarizeLiteralResults, scalarizeNumericRecords } from "./scalar-records.js"; import { validateModule } from "./validate.js"; const loc = { file: "scalar-records.ts", start: 0, end: 0 }; @@ -284,3 +284,161 @@ test("bounds expansion in callers with many eligible calls", () => { expect(remaining).toBeGreaterThan(0); expect(remaining).toBeLessThan(300); }); + +const tuple: IrType = { kind: "record", shapeId: "t0" }; +const strRef = (localId: string): IrExpr => ref(localId, STRING); +const element = (localId: string, field: string, type: IrType): IrStmt => ({ + kind: "varDecl", + localId, + init: { kind: "recordGet", obj: ref("result", tuple), shapeId: "t0", field, type, loc }, + loc, +}); + +/** `split(text, at)` returning `[text.slice(0, at), text.slice(at), at > 0]`, + * and a `main` destructuring it. */ +function literalFixture(after: IrStmt[]): IrModule { + const slice = (args: IrExpr[]): IrExpr => ({ + kind: "strIntrinsic", + method: "slice", + receiver: strRef("text"), + args, + type: STRING, + loc, + }); + return { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + records: [ + { + id: "t0", + fields: [ + { name: "0", type: STRING }, + { name: "1", type: STRING }, + { name: "2", type: BOOL }, + ], + tuple: true, + }, + ], + functions: [ + { + name: "split", + params: [ + { localId: "text", name: "text", type: STRING }, + { localId: "at", name: "at", type: F64 }, + ], + returnType: tuple, + locals: [ + { id: "text", name: "text", type: STRING, mutable: false }, + { id: "at", name: "at", type: F64, mutable: false }, + ], + body: [ + { + kind: "return", + value: { + kind: "recordLit", + type: tuple, + fields: [ + { name: "0", value: slice([num(0), ref("at")]) }, + { name: "1", value: slice([ref("at")]) }, + { + name: "2", + value: { kind: "bin", op: ">", left: ref("at"), right: num(0), type: BOOL, loc }, + }, + ], + loc, + }, + loc, + }, + ], + loc, + }, + { + name: "main", + params: [{ localId: "source", name: "source", type: STRING }], + returnType: VOID, + locals: [ + { id: "source", name: "source", type: STRING, mutable: false }, + { id: "result", name: "result", type: tuple, mutable: false }, + { id: "head", name: "head", type: STRING, mutable: false }, + { id: "tail", name: "tail", type: STRING, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "result", + init: call("split", [strRef("source"), call("effect", [num(1)])], tuple), + loc, + }, + ...after, + ], + loc, + }, + { + name: "effect", + params: [{ localId: "x", name: "x", type: F64 }], + returnType: F64, + locals: [{ id: "x", name: "x", type: F64, mutable: true }], + body: [{ kind: "return", value: ref("x"), loc }], + loc, + }, + ], + }; +} + +function literalTransformed(mod: IrModule): IrModule { + expect(validateModule(mod)).toEqual([]); + const out = scalarizeLiteralResults(mod); + expect(validateModule(out)).toEqual([]); + return out; +} + +test("declares destructured elements from a literal producer's fields in evaluation order", () => { + const mod = literalFixture([element("head", "0", STRING), element("tail", "1", STRING)]); + const snapshot = structuredClone(mod); + const out = literalTransformed(mod); + expect(mod).toEqual(snapshot); + const caller = main(out); + expect(nodes(caller.body, "recordGet")).toEqual([]); + expect(nodes(caller.body, "call").map((c) => c["callee"])).toEqual(["effect"]); + const declared = caller.body.map((s) => (s.kind === "varDecl" ? s.localId : s.kind)); + // Parameters, then every field in the literal's order: the unread third + // field still evaluates, into a binding of its own. + expect(declared.slice(2)).toEqual(["head", "tail", declared[4]]); + expect(caller.locals.find((l) => l.id === declared[4])?.type).toEqual(BOOL); + expect(caller.locals.some((l) => l.id === "result")).toBe(false); + // Slices stay slices of the renamed parameter. + expect(nodes(caller.body, "strIntrinsic").map((n) => (n["receiver"] as IrExpr).kind)).toEqual([ + "varRef", + "varRef", + ]); +}); + +test("keeps the call when the result is read other than by adjacent element declarations", () => { + const escaped = literalFixture([ + element("head", "0", STRING), + { kind: "exprStmt", expr: ref("result", tuple), loc }, + ]); + expect(literalTransformed(escaped)).toBe(escaped); + const apart = literalFixture([ + element("head", "0", STRING), + { kind: "exprStmt", expr: num(0), loc }, + element("tail", "1", STRING), + ]); + expect(literalTransformed(apart)).toBe(apart); +}); + +test("keeps the call when an argument holds a counted temporary of its own", () => { + const mod = literalFixture([element("head", "0", STRING)]); + const decl = main(mod).body[0] as IrStmt & { kind: "varDecl" }; + const length: IrExpr = { + kind: "strIntrinsic", + method: "length", + receiver: { kind: "strLit", value: "abc", type: STRING, loc }, + args: [], + type: F64, + loc, + }; + (decl.init as IrExpr & { kind: "call" }).args[1] = length; + expect(literalTransformed(mod)).toBe(mod); +}); diff --git a/packages/compiler/src/ir/scalar-records.ts b/packages/compiler/src/ir/scalar-records.ts index 0a5b0c80f..f75e522bf 100644 --- a/packages/compiler/src/ir/scalar-records.ts +++ b/packages/compiler/src/ir/scalar-records.ts @@ -10,6 +10,7 @@ import { } from "./ir.js"; import { everyExpr, + transformExpr, everyExprChild, everyStmtChild, everyStmtList, @@ -362,3 +363,323 @@ export function scalarizeNumericRecords(mod: IrModule): IrModule { }); return changed ? { ...mod, functions } : mod; } + +/** A producer whose whole body is `return `, with number, boolean + * or string fields, inlined where its result is destructured. */ +interface LiteralProducer { + fn: IrFunction; + shape: IrRecordShape; + literal: IrExpr & { kind: "recordLit" }; + size: number; +} + +/** Whether `value` holds no counted temporary of its own: every counted + * node in it only views a binding, so evaluating it in its own statement + * shortens no lifetime. A message argument of a throw counts as nothing: it + * lives until the throw, which ends the evaluation anyway. */ +function viewsOnly(value: IrExpr): boolean { + if (!isRefCounted(value.type)) return everyExprChild(value, viewsOnly, () => false); + switch (value.kind) { + case "varRef": + return true; + case "unionNarrow": + return viewsOnly(value.value); + case "ternary": + return viewsOnly(value.cond) && viewsOnly(value.then) && viewsOnly(value.else_); + case "libCall": + return value.fn === "error.nodeThrow"; + default: + return false; + } +} + +/** The node's own value may be counted (it moves into the binding that + * takes it); everything under it must hold no temporary (viewsOnly), as + * scalarTemporaries requires for the numeric pass. */ +function ownedTemporaries(value: IrExpr): boolean { + return everyExprChild(value, viewsOnly, () => false); +} + +const LITERAL_FIELD_KINDS: ReadonlySet = new Set(["f64", "bool", "string"]); + +function literalProducer( + fn: IrFunction, + shapes: ReadonlyMap, +): LiteralProducer | null { + if ( + fn.async || + fn.generator || + fn.captures?.length || + fn.classCaptures?.length || + fn.returnType.kind !== "record" || + fn.body.length !== 1 + ) + return null; + const shape = shapes.get(fn.returnType.shapeId); + if ( + !shape || + shape.indexValue || + shape.fields.length === 0 || + shape.fields.length > MAX_FIELDS || + shape.fields.some((f) => !LITERAL_FIELD_KINDS.has(f.type.kind)) + ) + return null; + // Every local is a parameter: the body declares nothing. + const params = new Set(fn.params.map((p) => p.localId)); + if (fn.locals.some((l) => l.boxed || l.tdz || !params.has(l.id))) return null; + const ret = fn.body[0]!; + const literal = ret.kind === "return" ? ret.value : null; + if ( + literal?.kind !== "recordLit" || + literal.type.kind !== "record" || + literal.type.shapeId !== shape.id || + literal.fields.length !== shape.fields.length || + !literal.fields.every( + (f) => + !f.overflow && + shape.fields.some((sf) => sf.name === f.name && sf.type.kind === f.value.type.kind) && + ownedTemporaries(f.value), + ) + ) + return null; + let size = 0; + const small = everyExpr(literal, { + expr: (node) => + ++size <= MAX_CALLEE_NODES && + node.kind !== "closure" && + node.kind !== "selfRef" && + node.kind !== "awaitExpr" && + node.kind !== "awaitUnionExpr" && + node.kind !== "yieldExpr", + stmt: () => false, + }); + return small ? { fn, shape, literal, size } : null; +} + +/** Rewrite every statement list under `body`, innermost first. */ +function mapStatementLists(body: IrStmt[], f: (list: IrStmt[]) => IrStmt[]): IrStmt[] { + const lists = (s: IrStmt): IrStmt => { + switch (s.kind) { + case "if": + return { + ...s, + then: mapStatementLists(s.then, f), + else_: s.else_ === null ? null : mapStatementLists(s.else_, f), + }; + case "block": + case "while": + case "doWhile": + case "for": + case "forOf": + return { ...s, body: mapStatementLists(s.body, f) }; + case "switch": + return { + ...s, + cases: s.cases.map((c) => ({ + ...c, + body: mapStatementLists(c.body, f), + })), + }; + case "tryCatch": + return { + ...s, + tryBody: mapStatementLists(s.tryBody, f), + catchBody: s.catchBody === null ? null : mapStatementLists(s.catchBody, f), + finallyBody: s.finallyBody === null ? null : mapStatementLists(s.finallyBody, f), + }; + default: + return s; + } + }; + return f(body.map(lists)); +} + +/** How often each local is read, anywhere in `fn`. */ +function localReads(fn: IrFunction): Map { + const reads = new Map(); + const count = (id: string): void => { + reads.set(id, (reads.get(id) ?? 0) + 1); + }; + everyStmtList(fn.body, { + expr: (node) => { + switch (node.kind) { + case "varRef": + case "incDec": + case "assignExpr": + count(node.localId); + break; + case "closure": + case "classRef": + for (const id of node.captures ?? []) count(id); + break; + } + return true; + }, + stmt: (node) => { + if (node.kind === "assign" || node.kind === "forOf" || node.kind === "rethrow") + count(node.localId); + return true; + }, + }); + return reads; +} + +/** Inline small literal producers where the result is only destructured. + * + * `const [head, tail] = split(text, at)`, where `split` is + * `return [text.slice(0, at), text.slice(at)]`, lowers to a result binding + * and one declaration per element read from it, right after the call. Each + * such declaration now takes its field's value directly: the parameters are + * declared from the arguments in order, then the fields in the literal's + * order (unread fields go to fresh bindings), then the destructured + * bindings, so evaluation order is unchanged and no record is built. A + * slice element read only through its length then needs no copy at all + * (findScalarStringSlices). The declarations stay in the caller's own + * statement list, so every binding keeps its scope; parameters live to the + * end of it, which only lengthens the arguments' lifetimes. Every other use + * of the result binding keeps the call. */ +export function scalarizeLiteralResults(mod: IrModule): IrModule { + const shapes = new Map((mod.records ?? []).map((s) => [s.id, s])); + const producers = new Map(); + for (const fn of mod.functions) { + const p = literalProducer(fn, shapes); + if (p) producers.set(fn.name, p); + } + if (producers.size === 0) return mod; + let changed = false; + const functions = mod.functions.map((fn): IrFunction => { + if (fn.async || fn.generator) return fn; + const locals = new Map(fn.locals.map((l) => [l.id, l])); + const used = new Set([...locals.keys(), ...(mod.globals ?? []).map((g) => g.id)]); + collectLabels(fn.body, used); + let next = 0; + const fresh = (): string => { + let id: string; + do { + id = `%literal.${next++}`; + } while (used.has(id)); + used.add(id); + return id; + }; + let reads: Map | null = null; + const added: IrLocal[] = []; + const removed = new Set(); + let budget = MAX_INLINE_NODES; + const rewrite = (list: IrStmt[]): IrStmt[] => { + const out: IrStmt[] = []; + for (let i = 0; i < list.length; i++) { + const decl = list[i]!; + const call = decl.kind === "varDecl" ? decl.init : null; + const p = call?.kind === "call" ? producers.get(call.callee) : undefined; + const local = decl.kind === "varDecl" ? locals.get(decl.localId) : undefined; + if ( + decl.kind !== "varDecl" || + call?.kind !== "call" || + !p || + !local || + p.fn.name === fn.name || + p.size > budget || + local.mutable || + local.boxed || + local.tdz || + local.type.kind !== "record" || + local.type.shapeId !== p.shape.id || + call.args.length !== p.fn.params.length || + !call.args.every(ownedTemporaries) + ) { + out.push(decl); + continue; + } + // The element declarations right after the call, one per field. + const elements = new Map(); + let j = i + 1; + for (; j < list.length; j++) { + const element = list[j]!; + const get = element.kind === "varDecl" ? element.init : null; + const target = element.kind === "varDecl" ? locals.get(element.localId) : undefined; + if ( + element.kind !== "varDecl" || + get?.kind !== "recordGet" || + get.obj.kind !== "varRef" || + get.obj.localId !== local.id || + get.shapeId !== p.shape.id || + elements.has(get.field) || + !target || + target.boxed || + target.tdz || + target.type.kind !== get.type.kind + ) + break; + elements.set(get.field, element); + } + reads ??= localReads(fn); + if (elements.size === 0 || reads.get(local.id) !== elements.size) { + out.push(decl); + continue; + } + budget -= p.size; + changed = true; + removed.add(local.id); + const renamed = new Map(p.fn.params.map((param) => [param.localId, fresh()])); + for (const l of p.fn.locals) { + // Copied field by field: the native compiler does not spread a + // record whose type has a union. + const copy: IrLocal = { + id: renamed.get(l.id)!, + name: l.name, + type: l.type, + mutable: false, + }; + if (l.source !== undefined) copy.source = l.source; + added.push(copy); + } + p.fn.params.forEach((param, index) => { + out.push({ + kind: "varDecl", + localId: renamed.get(param.localId)!, + init: call.args[index]!, + loc: decl.loc, + }); + }); + const rename = (value: IrExpr): IrExpr => + transformExpr(value, { + expr: (node) => + node.kind === "varRef" && renamed.has(node.localId) + ? { ...node, localId: renamed.get(node.localId)! } + : node, + stmt: (node) => node, + }); + for (const field of p.literal.fields) { + const element = elements.get(field.name); + let id = element?.localId; + if (id === undefined) { + id = fresh(); + const type = p.shape.fields.find((f) => f.name === field.name)!.type; + added.push({ + id, + name: `${local.name}.${field.name}`, + type, + mutable: false, + }); + } + out.push({ + kind: "varDecl", + localId: id, + init: rename(field.value), + loc: element?.loc ?? decl.loc, + }); + } + i = j - 1; + } + return out; + }; + const body = mapStatementLists(fn.body, rewrite); + if (!changed || removed.size === 0) return fn; + return { + ...fn, + locals: [...fn.locals.filter((l) => !removed.has(l.id)), ...added], + body, + }; + }); + return changed ? { ...mod, functions } : mod; +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8241-literal-result-slices.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8241-literal-result-slices.ts.json new file mode 100644 index 000000000..9cd832dca --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8241-literal-result-slices.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8241-literal-result-slices.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 37b92e48c..2d7f46d85 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -2210,6 +2210,17 @@ static void scr_str_slice_bounds(ScrStr *s, ScrSidx *e, double start, double end void scr_str_slice_range(ScrStr *s, double start, double end, bool substring, ScrStringSlice *out) { + if (__builtin_expect(s->ascii, 1)) { + /* Units are bytes: no index entry, and no boundary splits a pair. */ + size_t length = s->len; + size_t from = scr_slice_unit(start, length, substring); + size_t to = scr_slice_unit(end, length, substring); + if (substring && from > to) { size_t swap = from; from = to; to = swap; } + out->start = from; + out->length = to > from ? to - from : 0; + out->split = 0; + return; + } ScrSidx *e = scr_sidx_for(s); size_t from, to; scr_str_slice_bounds(s, e, start, end, substring, &from, &to); diff --git a/tests/corpus/8241-literal-result-slices.ts b/tests/corpus/8241-literal-result-slices.ts new file mode 100644 index 000000000..11de82819 --- /dev/null +++ b/tests/corpus/8241-literal-result-slices.ts @@ -0,0 +1,81 @@ +// Destructured results of small literal producers: string slices, numbers +// and booleans, with evaluation order, unread fields, throws and every +// slice() and substring() bound kind. +let trace = ""; +function mark(tag: string, value: T): T { + trace += tag; + return value; +} + +function split(text: string, at: number): [string, string] { + return [text.slice(0, at), text.slice(at)]; +} +function cut(text: string, from: number, to: number): [string, number, boolean] { + return [text.substring(from, to), text.length, from > to]; +} +function divmod(a: number, b: number): [number, number] { + return [Math.floor(a / b), a % b]; +} +function pair(text: string): { head: string; size: number } { + return { size: mark("s", text.length), head: mark("h", text.slice(1, -1)) }; +} + +const bounds = [0, 1, 2, 3, -1, -2, -3, -10, 10, 0.5, -0.5, 1.9, -1.9, -0, NaN, Infinity, -Infinity]; +const texts = ["", "a", "ab", "abcdef", "héllo", "東京ok", "a\u{1F600}b"]; +let lengths = 0; +let units = 0; +for (const text of texts) { + for (const at of bounds) { + const [head, tail] = split(text, at); + lengths = lengths * 7 + head.length * 3 + tail.length; + lengths %= 1000000007; + if (head.length > 0) units += head.charCodeAt(head.length - 1); + if (tail.length > 0) units += tail.charCodeAt(0); + for (const to of bounds) { + const [part, size, swapped] = cut(text, at, to); + lengths = (lengths * 5 + part.length + size + (swapped ? 1 : 0)) % 1000000007; + } + } +} +console.log("lengths", lengths, "units", units); + +// The pieces themselves, as strings. +const [left, right] = split("scriptc", 3); +console.log(left, right, `${left}|${right}`); + +// Arguments, then fields in the literal's order, then the bindings. +trace = ""; +const { head, size } = pair(mark("a", "[inner]")); +console.log("order", trace, head.length, size); + +// Unread fields still run. +trace = ""; +const [onlyHead] = split(mark("x", "unread"), mark("y", 2)); +const { size: onlySize } = pair("<>"); +console.log("unread", trace, onlyHead, onlySize); + +// A mutable destructured binding. +let [first, rest] = split("mutable", 1); +first += "!"; +console.log("let", first, rest.length); + +// Numbers and a throw from an argument. +let sum = 0; +for (let i = 0; i < 50; i++) { + const [q, r] = divmod(i * 31, 17); + sum += q * 3 + r; +} +console.log("divmod", sum); +const list: (string | undefined)[] = ["xy", undefined]; +for (const item of list) { + try { + const [a, b] = split(item!, 1); + console.log("ok", a.length, b.length); + } catch (error) { + console.log("threw", error instanceof TypeError); + } +} + +// The producer keeps working for every other caller. +const whole = split("kept", 2); +console.log("kept", whole[0], whole[1], whole.length); diff --git a/tests/harness/corpus-costs/8241-literal-result-slices.ts.json b/tests/harness/corpus-costs/8241-literal-result-slices.ts.json new file mode 100644 index 000000000..be69de5f7 --- /dev/null +++ b/tests/harness/corpus-costs/8241-literal-result-slices.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 2900 } From a711d74ecf3171189109b576fec9378bc5f49709 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:35:51 -0500 Subject: [PATCH 122/273] Keep copied array callbacks exact with local classes, switches and contracts - A binding that a local class captures counts as written: the class's methods may assign it while the loop runs, so the call keeps its helper. - Copied bodies rename the labels of switch and for-of statements and the binding of for-of loops. - The known-union and record-temporary cleanups rewrite switch cases and for-of bodies, where they left declarations of removed locals. - Copied locals keep their source bindings. In programs that write contract types, a parameter with a source binding keeps its own local, so an Int32 parameter is checked against the elements the loop passes. --- .../lowering/containers/array-pipelines.ts | 104 +++++++++++++++--- .../test/array-pipeline-fusion.test.ts | 59 ++++++++++ tests/diagnostics/contract-int32.ts | 4 +- .../__snapshots__/contract-int32.ts.txt | 13 ++- 4 files changed, 164 insertions(+), 16 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index c83a9c447..7c2e07b1c 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -1,3 +1,5 @@ +import * as ts from "../../ts7/adapter.js"; +import { CONTRACT_TYPES_MODULE } from "../../contract-types.js"; import { keyedHelperName } from "../stable-names.js"; import { writtenLocals } from "../../../ir/callback-specialization.js"; import { AmbientReceiverReaders } from "../../../ir/receiver-readers.js"; @@ -6,6 +8,7 @@ import { BOOL, F64, VOID, + type IrBindingSource, type IrExpr, type IrFunction, type IrGlobal, @@ -667,6 +670,10 @@ class PipelineFusion { /** Bindings each function, or a closure it creates, writes after their * declaration. */ private readonly written: Map>; + /** classCaptured's answers by function name. */ + private readonly classCaptures = new Map>(); + /** Whether a module imports or re-exports the contract types. */ + private readonly contractTypes: boolean; /** Bindings of the function being fused that inlined callbacks captured. */ private inlinedCaptures = new Set(); /** The helper calls each callback with an undefined receiver; a copied @@ -680,6 +687,15 @@ class PipelineFusion { this.effects = new CallbackEffects(functions, lowerer.globalsList); this.written = writtenLocals({ functions }); this.receivers = new AmbientReceiverReaders(new Map(functions.map((fn) => [fn.name, fn]))); + this.contractTypes = lowerer.moduleOrder.some((sf) => + sf.statements.nodes.some( + (st) => + (ts.isImportDeclaration(st) || ts.isExportDeclaration(st)) && + st.moduleSpecifier !== undefined && + ts.isStringLiteral(st.moduleSpecifier) && + st.moduleSpecifier.text === CONTRACT_TYPES_MODULE, + ), + ); } run(fn: IrFunction): void { @@ -933,19 +949,22 @@ class PipelineFusion { /** inlineTarget for a callback created in `fn` whose captured bindings * nothing assigns after their declaration: `fn`'s own locals and * parameters, written neither by `fn` nor by any closure that shares - * them, and not by the callback body. The callback must not observe the - * ambient receiver (AmbientReceiverReaders). */ + * them, and not by the callback body. A binding a local class captures + * counts as written, since its methods may assign it. The callback must + * not observe the ambient receiver (AmbientReceiverReaders). */ private stableTarget(fn: IrFunction): InlineTarget { const locals = new Map(fn.locals.map((l) => [l.id, l])); const inherited = new Set((fn.captures ?? []).map((c) => c.localId)); const written = this.written.get(fn.name); + const classCaptured = this.classCaptured(fn); return (arg) => { const target = this.closureTarget(arg); if (!target || !written || arg.kind !== "closure") return null; if (!this.receivers.ignores(target)) return null; for (const id of arg.captures) { const local = locals.get(id); - if (!local || local.tdz || inherited.has(id) || written.has(id)) return null; + if (!local || local.tdz || inherited.has(id) || written.has(id) || classCaptured.has(id)) + return null; } const captures = new Set((target.captures ?? []).map((c) => c.localId)); const assigns = !everyStmtList(target.body, { @@ -957,6 +976,22 @@ class PipelineFusion { }; } + /** The bindings of `fn` that a local class it creates captures. */ + private classCaptured(fn: IrFunction): ReadonlySet { + const known = this.classCaptures.get(fn.name); + if (known) return known; + const ids = new Set(); + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "classRef") for (const id of e.captures ?? []) ids.add(id); + return true; + }, + stmt: () => true, + }); + this.classCaptures.set(fn.name, ids); + return ids; + } + /** The function behind a closure-literal callback the loop can inline. */ private closureTarget(arg: IrExpr): IrFunction | null { if (arg.kind !== "closure" || arg.adapts) return null; @@ -967,7 +1002,12 @@ class PipelineFusion { } private emit(plan: Plan): IrExpr | null { - const built = new FusedLoop(this.lowerer, plan.chain, plan.inline ?? this.inlineTarget).build(); + const built = new FusedLoop( + this.lowerer, + plan.chain, + plan.inline ?? this.inlineTarget, + this.contractTypes, + ).build(); if (!built) return null; for (const id of built.inlinedCaptures) this.inlinedCaptures.add(id); let name = this.helpers.get(built.key); @@ -1153,10 +1193,13 @@ class FusedLoop { private next = 0; private failed = false; + /** `contractTypes`: the program writes contract types, which the + * contract checker binds to source bindings. */ constructor( private readonly lowerer: Lowerer, private readonly chain: Chain, private readonly inlineTarget: InlineTarget, + private readonly contractTypes: boolean, ) { this.loc = chain.loc; } @@ -1165,9 +1208,9 @@ class FusedLoop { return `${name}.${this.next++}`; } - private local(name: string, type: IrType, mutable = false): string { + private local(name: string, type: IrType, mutable = false, source?: IrBindingSource): string { const id = this.fresh(name); - this.locals.push({ id, name, type, mutable }); + this.locals.push(source ? { id, name, type, mutable, source } : { id, name, type, mutable }); return id; } @@ -1234,17 +1277,27 @@ class FusedLoop { return true; }, }); + // Copies keep their source bindings, so contract types written on + // them are checked against what the loop stores. In a program that + // writes contract types, a parameter with a source binding keeps its + // own local for the same reason. + const sources = new Map(fn.locals.map((l) => [l.id, l.source])); const aliased = new Set(); fn.params.forEach((p, index) => { const arg = args[index]!; - if (arg.kind === "varRef" && !assigned.has(p.localId) && typeEquals(arg.type, p.type)) { + if ( + arg.kind === "varRef" && + !assigned.has(p.localId) && + typeEquals(arg.type, p.type) && + !(this.contractTypes && sources.get(p.localId) !== undefined) + ) { renamed.set(p.localId, arg.localId); aliased.add(p.localId); } }); for (const l of fn.locals) { if (renamed.has(l.id)) continue; - const id = this.local(l.name, l.type, true); + const id = this.local(l.name, l.type, true, l.source); renamed.set(l.id, id); } const rename = (id: string): string => renamed.get(id) ?? id; @@ -1258,11 +1311,15 @@ class FusedLoop { return e; } }; - const bind: IrStmt[] = fn.params.flatMap((p, index): IrStmt[] => - aliased.has(p.localId) - ? [] - : [{ kind: "varDecl", localId: rename(p.localId), init: args[index]!, loc }], - ); + // A bound parameter's value is an element or index with no source text + // of its own; it is located at the parameter, without a span. + const bind: IrStmt[] = fn.params.flatMap((p, index): IrStmt[] => { + if (aliased.has(p.localId)) return []; + const source = sources.get(p.localId); + const arg = args[index]!; + const init = source ? { ...arg, loc: { ...source.loc, end: source.loc.start } } : arg; + return [{ kind: "varDecl", localId: rename(p.localId), init, loc }]; + }); const labels = new Map(); const label = (name: string): string => { let id = labels.get(name); @@ -1281,9 +1338,14 @@ class FusedLoop { case "break": case "continue": return st.label ? { ...st, label: label(st.label) } : st; + case "forOf": { + const renamedFor = { ...st, localId: rename(st.localId) }; + return st.labels ? { ...renamedFor, labels: st.labels.map(label) } : renamedFor; + } case "while": case "doWhile": case "for": + case "switch": case "block": return st.labels ? { ...st, labels: st.labels.map(label) } : st; default: @@ -1946,8 +2008,11 @@ function foldKnownUnions(fn: Omit): void { case "for": case "while": case "doWhile": + case "forOf": case "block": return { ...s, body: prune(s.body) }; + case "switch": + return { ...s, cases: s.cases.map((c) => ({ ...c, body: prune(c.body) })) }; default: return s; } @@ -2135,6 +2200,19 @@ function scalarizeRecordTemporaries(lowerer: Lowerer, fn: Omit ({ + test: c.test === null ? null : rewriteExpr(c.test), + body: rewriteList(c.body), + })), + }, + ]; case "block": return [{ ...s, body: rewriteList(s.body) }]; default: { diff --git a/packages/compiler/test/array-pipeline-fusion.test.ts b/packages/compiler/test/array-pipeline-fusion.test.ts index 0ff456a2c..ba814105f 100644 --- a/packages/compiler/test/array-pipeline-fusion.test.ts +++ b/packages/compiler/test/array-pipeline-fusion.test.ts @@ -154,3 +154,62 @@ console.log(a.length, b.length, limit); ${postlude}`); expect(helperCalls(init(mod))).toEqual(["%arr.filter", "%arr.map"]); }); + +test("a lone call keeps its helper when a local class's method can assign a capture", async () => { + const mod = await lowerSource(`${prelude} +let k = 1; +class Bump { + go(): void { k = 10; } +} +const b = new Bump(); +const out = rows.map((r) => { if (r.id === 2) b.go(); return r.id * k; }); +console.log(out.join(",")); +${postlude}`); + expect(helperCalls(init(mod))).toEqual(["%arr.map"]); +}); + +test("a copied callback keeps its switch and for-of labels and bindings", async () => { + const mod = await lowerSource(`${prelude} +const out = rows.map((r) => { + let n = 0; + outer: switch (r.id % 3) { + case 0: + n = 10; + break outer; + case 1: + inner: for (const v of [1, 2, 3]) { + n += v; + if (v === 2) break outer; + if (v === 3) continue inner; + } + break; + default: + n = r.value; + } + return n; +}); +console.log(out.join(",")); +${postlude}`); + expect(helperCalls(init(mod))).toEqual(["%arr.loop"]); +}); + +test("a copied callback's switch-local records and known unions are rewritten in its cases", async () => { + const mod = await lowerSource(`${prelude} +type Shape = { kind: "sq"; side: number } | { kind: "rect"; w: number; h: number }; +const out = rows.map((r) => { + switch (r.id % 3) { + case 0: + const p = { a: r.id, b: 2 }; + return p.a + p.b; + case 1: + const sq = { kind: "sq" as const, side: r.value }; + const s: Shape = sq; + return s.kind === "sq" ? s.side * 10 : 0; + default: + return r.value; + } +}); +console.log(out.join(",")); +${postlude}`); + expect(helperCalls(init(mod))).toEqual(["%arr.loop"]); +}); diff --git a/tests/diagnostics/contract-int32.ts b/tests/diagnostics/contract-int32.ts index f6e900626..20231c535 100644 --- a/tests/diagnostics/contract-int32.ts +++ b/tests/diagnostics/contract-int32.ts @@ -29,8 +29,10 @@ function bump(): void { const sizes = [1, 2, 3]; const len: Int32 = sizes.length; +const weights = [1.5, 2]; +const shifted = weights.map((w: Int32) => w + 1); const span = new Span(1.5); span.advance(); bump(); -console.log(width(span), half(7), count, len); +console.log(width(span), half(7), count, len, shifted); diff --git a/tests/harness/__snapshots__/contract-int32.ts.txt b/tests/harness/__snapshots__/contract-int32.ts.txt index 2f3a65c1d..90f046973 100644 --- a/tests/harness/__snapshots__/contract-int32.ts.txt +++ b/tests/harness/__snapshots__/contract-int32.ts.txt @@ -48,6 +48,15 @@ contract-int32.ts:31:20 - error SC7002: the value stored into 'len' (Int32) is n 30 | const sizes = [1, 2, 3]; 31 | const len: Int32 = sizes.length; | ^~~~~~~~~~~~ - 32 | + 32 | const weights = [1.5, 2]; - hint: make the int32 conversion explicit, for example 'sizes.length | 0', so Node computes the same value \ No newline at end of file + hint: make the int32 conversion explicit, for example 'sizes.length | 0', so Node computes the same value + +contract-int32.ts:33:30 - error SC7002: the value stored into 'w' (Int32) is not provably an int32 + + 32 | const weights = [1.5, 2]; + 33 | const shifted = weights.map((w: Int32) => w + 1); + | ^ + 34 | + + hint: make the int32 conversion explicit with '| 0', so Node computes the same value \ No newline at end of file From 01f5a3455a215634771f94ab4e813803013253f9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:38:44 -0500 Subject: [PATCH 123/273] Test row-pair overlaps only for nests of two or more rows The paired copy's test read the stored and read arrays' headers whatever the row range, so a nest whose arrays the program assigns only for a nonempty range dereferenced their NULL headers on an empty range, where the loop as written never reads them. The overlap tests now run only when the nest has at least two rows, and an array that is not there answers that it is not disjoint, so the loop runs as written. --- .../compiler/src/backend/llvm/lib-dispatch.ts | 35 ++++++++++++++----- packages/compiler/src/ir/row-pairs.ts | 17 ++++++--- tests/corpus/8194-row-pair-sums.ts | 18 ++++++++++ 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index f4ce9e405..95cee3860 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -937,10 +937,24 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { } if (e.fn === "bytes.absent") return host.own({ name: "null", type: e.type }); if (e.fn === "bytes.disjoint") { - // [data, data + len * size) of each view, compared as addresses. + // [data, data + len * size) of each view, compared as addresses. A view + // that is not there (a binding the nest never assigned, NULL) answers + // false without reading its header, so the caller keeps its loop. const B = host.B; - const range = (arg: IrExpr): { start: string; end: string } => { - const value = host.emitReadReceiver(arg); + const values = e.args.map((arg) => ({ arg, value: host.emitReadReceiver(arg) })); + const present = values.map(({ value }) => { + const t = B.tmp(); + B.line(`${t} = icmp ne ptr ${value.name}, null`); + return t; + }); + const both = B.tmp(); + B.line(`${both} = and i1 ${present[0]}, ${present[1]}`); + const compare = B.newLabel("disjoint.compare"), + done = B.newLabel("disjoint.done"); + const from = B.currentLabel(); + B.condBr(both, compare, done); + B.startBlock(compare); + const range = (arg: IrExpr, pointer: string): { start: string; end: string } => { const size = arg.type.kind === "bytes" ? BYTES_ELEMENT_SIZE[arg.type.elem] : 1; const dataSlot = B.tmp(), data = B.tmp(), @@ -949,23 +963,28 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { bytes = B.tmp(), start = B.tmp(), end = B.tmp(); - B.line(`${dataSlot} = getelementptr inbounds %ScrBytes, ptr ${value.name}, i64 0, i32 3`); + B.line(`${dataSlot} = getelementptr inbounds %ScrBytes, ptr ${pointer}, i64 0, i32 3`); B.line(`${data} = load ptr, ptr ${dataSlot}`); - B.line(`${lenSlot} = getelementptr inbounds %ScrBytes, ptr ${value.name}, i64 0, i32 1`); + B.line(`${lenSlot} = getelementptr inbounds %ScrBytes, ptr ${pointer}, i64 0, i32 1`); B.line(`${len} = load ${host.sizeType}, ptr ${lenSlot}`); B.line(`${bytes} = mul ${host.sizeType} ${len}, ${size}`); B.line(`${start} = ptrtoint ptr ${data} to ${host.sizeType}`); B.line(`${end} = add ${host.sizeType} ${start}, ${bytes}`); return { start, end }; }; - const left = range(e.args[0]!), - right = range(e.args[1]!); + const left = range(values[0]!.arg, values[0]!.value.name), + right = range(values[1]!.arg, values[1]!.value.name); const before = B.tmp(), after = B.tmp(), + apart = B.tmp(), t = B.tmp(); B.line(`${before} = icmp ule ${host.sizeType} ${left.end}, ${right.start}`); B.line(`${after} = icmp ule ${host.sizeType} ${right.end}, ${left.start}`); - B.line(`${t} = or i1 ${before}, ${after}`); + B.line(`${apart} = or i1 ${before}, ${after}`); + const compared = B.currentLabel(); + B.br(done); + B.startBlock(done); + B.line(`${t} = phi i1 [ false, %${from} ], [ ${apart}, %${compared} ]`); return { name: t, type: e.type }; } if (e.fn === "record.isFrozen") { diff --git a/packages/compiler/src/ir/row-pairs.ts b/packages/compiler/src/ir/row-pairs.ts index ed71166a8..2e189f42e 100644 --- a/packages/compiler/src/ir/row-pairs.ts +++ b/packages/compiler/src/ir/row-pairs.ts @@ -317,8 +317,10 @@ function pairRows( loc, }; // Both bounds are int32, so `row < end - 1` is `row + 1 < end`, and the - // start is at most the end. Rows are independent when the written array - // shares no byte with an array the term reads. + // nest has at least two rows. The overlap tests run only then, so they never + // read an array the program assigns only when there are rows. Rows are + // independent when the written array shares no byte with an array the + // term reads; an array that is not there answers false. const int32 = (e: IrExpr): IrExpr => ({ kind: "bin", op: "===", @@ -330,7 +332,14 @@ function pairRows( const tests: IrExpr[] = [ int32(match.start), int32(match.end), - { kind: "bin", op: "<=", left: again(match.start), right: again(match.end), type: BOOL, loc }, + { + kind: "bin", + op: "<", + left: bin("+", again(match.start), num(1), loc), + right: again(match.end), + type: BOOL, + loc, + }, ...match.reads.map((read): IrExpr => ({ kind: "libCall", fn: "bytes.disjoint", @@ -400,7 +409,7 @@ function pairRows( * expression of `row + 1`. The term only reads numbers, the counters and * typed-array elements, so it runs no other code and cannot throw. The * nest runs paired when both row bounds are int32 (so `row < end - 1` is - * `row + 1 < end`), the start is at most the end, and the array the rows + * `row + 1 < end`), there are at least two rows, and the array the rows * store into shares no byte with any array the term reads, so no row reads * another row's result. An odd row count runs its last row as written. * Otherwise the nest runs as written. Two independent sums take turns in diff --git a/tests/corpus/8194-row-pair-sums.ts b/tests/corpus/8194-row-pair-sums.ts index 9aa22b9bd..f28242ff5 100644 --- a/tests/corpus/8194-row-pair-sums.ts +++ b/tests/corpus/8194-row-pair-sums.ts @@ -70,3 +70,21 @@ weighted(f, w, out, 0, n); show("weighted", out); weighted(f, w, out, 1, n - 1); show("weighted inner", out); + +// Arrays a program assigns only when the row range is nonempty: an empty or +// one-row range never reads them, and the paired copy does not either. +function lazyRows(n: number, start: number, end: number): number { + let u!: Float64Array; + let v!: Float64Array; + if (end > start) { + u = new Float64Array(n).fill(1); + v = new Float64Array(end); + } + for (let i = start; i < end; i++) { + let t = 0; + for (let j = 0; j < n; j++) t += u[j]! / (i + j + 1); + v[i] = t; + } + return end > start ? v[end - 1]! : -1; +} +console.log("lazy", lazyRows(4, 3, 3), lazyRows(4, 2, 3), lazyRows(4, 0, 5), lazyRows(0, 5, 5)); From e25d871a3444c28124d9581768fba2b02040d5ba Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:45:59 -0500 Subject: [PATCH 124/273] Allocate stream ticks from the small-block allocator, and emit stream events by immortal names Each deferred stream step (afterWrite, emitReadable, resume and the others) queued a ScrStreamTick and a process.nextTick entry, both from libc calloc and free. They now come from the runtime's small-block allocator, as do the nextTick entries scheduled through scr_next_tick and scr_process_write_callback. The stream unit emitted each event with a newly allocated name string. The names it emits ("data", "end", "drain", "finish" and the others) are now immortal static strings, so an emit allocates no name. --- packages/runtime/src/scr_async.c | 12 +++---- packages/runtime/src/scr_stream.c | 54 +++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 0ae774df7..f9c6944c2 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -1275,7 +1275,7 @@ static ScrClosure *scr_als_bind(ScrClosure *cb /*moves*/); /* A tick runs with the AsyncLocalStorage context active when it was * queued, as Node's process.nextTick does. */ void scr_next_tick(ScrClosure *cb /*moves*/) { - ScrNtick *t = calloc(1, sizeof *t); + ScrNtick *t = scr_mem_calloc(sizeof *t); if (!t) scr_oom(); t->cb = scr_als_bind(cb); if (scr_nt_tail) scr_nt_tail->next = t; @@ -1288,7 +1288,7 @@ void scr_next_tick(ScrClosure *cb /*moves*/) { * adapter ABI also used by fs.rename: emitted adapters construct the * program's Error | null union, and NULL selects its null arm. */ void scr_process_write_callback(ScrClosure *cb /*moves*/, ScrFsRenameFn fn) { - ScrNtick *t = calloc(1, sizeof *t); + ScrNtick *t = scr_mem_calloc(sizeof *t); if (!t) scr_oom(); t->cb = cb; t->error_cb = fn; @@ -1303,7 +1303,7 @@ void scr_process_write_callback(ScrClosure *cb /*moves*/, ScrFsRenameFn fn) { * endReadableNT, ...). The hook dispatches exactly one stream tick; * teardown just drops markers (the stream queue owns its entries). */ void scr_next_tick_raw(void (*fn)(void)) { - ScrNtick *t = calloc(1, sizeof *t); + ScrNtick *t = scr_mem_calloc(sizeof *t); if (!t) scr_oom(); t->raw = fn; if (scr_nt_tail) scr_nt_tail->next = t; @@ -1314,7 +1314,7 @@ void scr_next_tick_raw(void (*fn)(void)) { /* A raw tick at the front of the queue: it runs before the ticks already * queued. */ static void scr_next_tick_raw_first(void (*fn)(void)) { - ScrNtick *t = calloc(1, sizeof *t); + ScrNtick *t = scr_mem_calloc(sizeof *t); if (!t) scr_oom(); t->raw = fn; t->next = scr_nt_head; @@ -1327,7 +1327,7 @@ void scr_nticks_teardown(void) { ScrNtick *t = scr_nt_head; scr_nt_head = t->next; if (t->cb) scr_closure_release(t->cb); - free(t); + scr_mem_free(t); } scr_nt_tail = NULL; } @@ -4435,7 +4435,7 @@ static bool scr_loop_run_ticks(void) { ScrClosure *cb = t->cb; ScrFsRenameFn error_cb = t->error_cb; void (*raw)(void) = t->raw; - free(t); + scr_mem_free(t); if (cb) { if (error_cb) error_cb(cb, NULL); else ((void (*)(ScrClosure *))cb->fn)(cb); diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 552fef146..406821961 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -557,7 +557,7 @@ static void scr_stream_dispatch_one(void); static void scr_st_tick(ScrStream *s, ScrStreamTickOp op, ScrError *err /*moves*/, ScrClosure *cb /*moves*/) { - ScrStreamTick *t = calloc(1, sizeof *t); + ScrStreamTick *t = scr_mem_calloc(sizeof *t); if (!t) scr_stream_oom(); t->s = scr_stream_retain(s); t->op = op; @@ -580,15 +580,57 @@ static bool scr_stream_ticks_pending(void) { return scr_st_head != NULL; } /* ── emit helpers ─────────────────────────────────────────────────────── */ +/* The event names the stream unit emits, as immortal strings (the layout + * of an emitted string literal), so an emit allocates no name. */ +#define SCR_STREAM_EVENT_NAME(name) \ + static const struct { \ + size_t rc; \ + size_t len; \ + size_t cap; \ + char data[sizeof #name]; \ + } scr_stream_lit_##name = {SIZE_MAX, sizeof #name - 1, \ + SCR_STR_CAP_WORD(sizeof #name - 1, 0, 1), #name} +SCR_STREAM_EVENT_NAME(data); +SCR_STREAM_EVENT_NAME(error); +SCR_STREAM_EVENT_NAME(close); +SCR_STREAM_EVENT_NAME(drain); +SCR_STREAM_EVENT_NAME(end); +SCR_STREAM_EVENT_NAME(finish); +SCR_STREAM_EVENT_NAME(pause); +SCR_STREAM_EVENT_NAME(prefinish); +SCR_STREAM_EVENT_NAME(readable); +SCR_STREAM_EVENT_NAME(resume); +SCR_STREAM_EVENT_NAME(pipe); +SCR_STREAM_EVENT_NAME(unpipe); +#undef SCR_STREAM_EVENT_NAME + +static ScrStr *const scr_stream_event_names[] = { + (ScrStr *)&scr_stream_lit_data, (ScrStr *)&scr_stream_lit_error, + (ScrStr *)&scr_stream_lit_close, (ScrStr *)&scr_stream_lit_drain, + (ScrStr *)&scr_stream_lit_end, (ScrStr *)&scr_stream_lit_finish, + (ScrStr *)&scr_stream_lit_pause, (ScrStr *)&scr_stream_lit_prefinish, + (ScrStr *)&scr_stream_lit_readable, (ScrStr *)&scr_stream_lit_resume, + (ScrStr *)&scr_stream_lit_pipe, (ScrStr *)&scr_stream_lit_unpipe, +}; + +/* The event name as a string: an immortal one for the names above, else a + * new string (+1 either way; releasing an immortal string does nothing). */ +static ScrStr *scr_stream_event_name(const char *name) { + for (size_t i = 0; i < sizeof scr_stream_event_names / sizeof *scr_stream_event_names; i++) { + if (strcmp(scr_stream_event_names[i]->data, name) == 0) return scr_stream_event_names[i]; + } + return scr_str_new(name, strlen(name)); +} + static bool scr_stream_emit0(ScrStream *s, const char *name) { - ScrStr *n = scr_str_new(name, strlen(name)); + ScrStr *n = scr_stream_event_name(name); bool had = scr_emitter_emit((ScrEmitter *)s, n); scr_str_release(n); return had; } static void scr_stream_emit_stream(ScrStream *s, const char *name, ScrStream *payload) { - ScrStr *n = scr_str_new(name, strlen(name)); + ScrStr *n = scr_stream_event_name(name); scr_emitter_emit((ScrEmitter *)s, n, payload); scr_str_release(n); } @@ -596,7 +638,7 @@ static void scr_stream_emit_stream(ScrStream *s, const char *name, ScrStream *pa /* 'error': unhandled throws the payload (Node's crash), via the emitter's * error contract. Borrows err. */ static void scr_stream_emit_error(ScrStream *s, ScrError *err) { - ScrStr *n = scr_str_new("error", 5); + ScrStr *n = (ScrStr *)&scr_stream_lit_error; scr_emitter_emit_error((ScrEmitter *)s, n, err); scr_str_release(n); } @@ -765,7 +807,7 @@ static void scr_stream_emit_data(ScrStream *s, void *chunk, ScrEntryKind kind) { } else { b = chunk; } - ScrStr *n = scr_str_new("data", 4); + ScrStr *n = (ScrStr *)&scr_stream_lit_data; scr_emitter_emit((ScrEmitter *)s, n, b, str, value); scr_str_release(n); if (scr_exc_pending()) return; @@ -7858,7 +7900,7 @@ static void scr_stream_tick_free(ScrStreamTick *t) { if (t->err) scr_error_release(t->err); if (t->cb) scr_closure_release(t->cb); scr_als_ctx_release(t->ctx); - free(t); + scr_mem_free(t); } /* One tick-marker's dispatch: the queue's FIFO head (markers and entries From c0a4496900d41522c1011d29999afa4cf86f9793 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:50:45 -0500 Subject: [PATCH 125/273] Check with a tsc-ts server shared by later builds (opt-in) With SCRIPTC_CHECKER_SERVER=1 on Linux and macOS, the native compiler's checker connections go to a shared `tsc-ts --api --listen` server, which keeps the files it parsed for the next build. When the variable is unset, nothing changes. checkerServerName names the server by a digest of the tsc-ts executable's identity, the working directory, the file system callbacks, the timing flag and the TSC_TS_* environment. scriptc_ts7_attach takes the server's lock file with a non-blocking flock, connects to its socket in this user's private server directory ($XDG_RUNTIME_DIR/scriptc or /tmp/scriptc-), and starts the server when none answers: in its own process group, with its standard streams on /dev/null and no other descriptor of the build, with --idle-exit 300 and TSC_TS_LISTEN_MEMORY=2048 unless the environment sets it. A build that finds the server in use, cannot start or reach it, or gets no answer to a ping checks with its own tsc-ts process, as without the variable; so does a tsc-ts without --listen, which exits as it starts. The syntax-only source parser, which reads no libraries, always uses its own process and leaves the server to the program's checker. --- docs/content/docs/cli.mdx | 3 + .../compiler/native/ts7-process-child.test.c | 52 ++++- packages/compiler/native/ts7-process.c | 190 +++++++++++++++++- packages/compiler/native/ts7-process.ffi.json | 1 + packages/compiler/native/ts7-process.h | 13 ++ packages/compiler/native/ts7-process.test.c | 88 ++++++++ packages/compiler/src/frontend/services.ts | 4 +- .../src/frontend/ts7/checker-server.test.ts | 47 +++++ .../src/frontend/ts7/checker-server.ts | 50 +++++ .../compiler/src/frontend/ts7/native-api.ts | 21 +- .../src/frontend/ts7/native-process.ts | 39 +++- .../compiler/src/frontend/ts7/program-host.ts | 3 + .../self-hosting/ts7-native-process.ts | 37 +++- .../harness/self-hosting-ts7-process.test.ts | 30 ++- 14 files changed, 569 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/src/frontend/ts7/checker-server.test.ts create mode 100644 packages/compiler/src/frontend/ts7/checker-server.ts diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index 9fa5e9447..050d076e3 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -210,6 +210,9 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
SCRIPTC_CHECKER
The TypeScript 7 API server that checks the program: tsc-ts (the default), which scriptc includes, or tsgo, from the typescript package scriptc also includes. Both give the same diagnostics and generated code.
+ +
SCRIPTC_CHECKER_SERVER
+
Set to 1 on Linux and macOS to check with a tsc-ts server that later builds share. A build connects to the server for its working directory, starting it when none answers, and the server keeps the files it parsed, so the next build of the same program loads it faster. A build that finds the server in use by another build, or cannot start or reach it, checks with its own tsc-ts process. The server exits after 5 minutes without a build, or after a build when its resident memory is above 2048 MB (TSC_TS_LISTEN_MEMORY sets another limit in megabytes). Diagnostics and generated code are the same with and without it. It applies to the default checker, tsc-ts.
```console diff --git a/packages/compiler/native/ts7-process-child.test.c b/packages/compiler/native/ts7-process-child.test.c index 7202cab51..386a0a7c3 100644 --- a/packages/compiler/native/ts7-process-child.test.c +++ b/packages/compiler/native/ts7-process-child.test.c @@ -12,7 +12,49 @@ #include #include #else +#include +#include +#include #include + +/* `--listen PATH --idle-exit SECONDS`: a stand-in for a shared server. In + * mode "listen-exit" it exits as an executable without --listen does. In + * mode "listen" it answers each client, in order, with its client number, + * its listen arguments and TSC_TS_LISTEN_MEMORY, then echoes until the + * client closes, and exits after SECONDS (at most 5) without a client. */ +static int listen_main(const char *mode, char **argv) { + if (strcmp(mode, "listen-exit") == 0) return 2; + if (strcmp(mode, "listen") != 0) return 73; + struct sockaddr_un address; + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + if (strlen(argv[0]) >= sizeof(address.sun_path)) return 77; + strcpy(address.sun_path, argv[0]); + int listener = socket(AF_UNIX, SOCK_STREAM, 0); + if (listener < 0 || bind(listener, (struct sockaddr *)&address, sizeof(address)) != 0 || + listen(listener, 4) != 0) + return 78; + const char *memory = getenv("TSC_TS_LISTEN_MEMORY"); + int idle = atoi(argv[2]); + if (idle > 5) idle = 5; + for (int client = 1;; client++) { + struct pollfd entry = {listener, POLLIN, 0}; + if (poll(&entry, 1, idle * 1000) <= 0) break; + int fd = accept(listener, NULL, NULL); + if (fd < 0) return 79; + char line[512]; + int size = snprintf(line, sizeof(line), "client %d\n%s %s\n%s\n", client, argv[0], argv[2], + memory == NULL ? "unset" : memory); + if (size <= 0 || write(fd, line, (size_t)size) != size) return 80; + uint8_t bytes[17]; + ssize_t count; + while ((count = read(fd, bytes, sizeof(bytes))) > 0) + if (write(fd, bytes, (size_t)count) != count) break; + close(fd); + } + unlink(argv[0]); + return 0; +} #endif static int child_main(int argc, char **argv) { @@ -20,9 +62,15 @@ static int child_main(int argc, char **argv) { _setmode(_fileno(stdin), _O_BINARY); _setmode(_fileno(stdout), _O_BINARY); #endif - if (argc < 5 || argc > 6 || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 || - strncmp(argv[4], "--callbacks=", 12) != 0 || (argc == 6 && strcmp(argv[5], "--timing") != 0)) return 71; + int listening = argc >= 9 && strcmp(argv[5], "--listen") == 0 && strcmp(argv[7], "--idle-exit") == 0; + int fixed = listening ? 9 : 5; + if (argc < fixed || argc > fixed + 1 || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 || + strncmp(argv[4], "--callbacks=", 12) != 0 || (argc == fixed + 1 && strcmp(argv[fixed], "--timing") != 0)) + return 71; const char *mode = argv[3]; +#ifndef _WIN32 + if (listening) return listen_main(mode, argv + 6); +#endif if (strncmp(mode, "argv:", 5) == 0) { fwrite(mode + 5, 1, strlen(mode + 5), stdout); fputc('\n', stdout); diff --git a/packages/compiler/native/ts7-process.c b/packages/compiler/native/ts7-process.c index ea451345d..dda3bac46 100644 --- a/packages/compiler/native/ts7-process.c +++ b/packages/compiler/native/ts7-process.c @@ -1,5 +1,8 @@ #ifndef _WIN32 #define _POSIX_C_SOURCE 200809L +/* flock(2), for the checker server's lock file, and syscall(2). */ +#define _DEFAULT_SOURCE +#define _DARWIN_C_SOURCE #endif #include "ts7-process.h" #include @@ -16,7 +19,13 @@ #include #include #include +#include #include +#include +#ifdef __linux__ +#include +#endif +#include #include #include #include @@ -38,6 +47,9 @@ typedef struct Ts7Process { HANDLE output; #else int socket; + /* The checker server's lock file, held while connected to the server + * (scriptc_ts7_attach); -1 for a server this process started. */ + int lock; #endif } Ts7Process; @@ -283,7 +295,7 @@ static int start_process(Ts7Process *p, char **args, size_t argc) { if (error == 0) error = posix_spawn(&pid, args[0], &actions, NULL, args, environment); posix_spawn_file_actions_destroy(&actions); if (error == 0) { - p->socket = sockets[0]; p->pid = (uint32_t)pid; + p->socket = sockets[0]; p->pid = (uint32_t)pid; p->lock = -1; } done: close(sockets[1]); @@ -293,6 +305,9 @@ done: static void stop_process(Ts7Process *p) { close(p->socket); + // A shared checker server keeps running; closing the connection ends this + // build's session, and closing the lock file lets another build connect. + if (p->lock >= 0) { close(p->lock); return; } int status; pid_t pid = (pid_t)p->pid; for (int attempt = 0; attempt < 5; attempt++) { @@ -338,6 +353,179 @@ uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size, return p->id; } +#ifdef _WIN32 +uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size, + const uint8_t *executable, size_t executable_size, + const uint8_t *cwd, size_t cwd_size, + const uint8_t *callbacks, size_t callbacks_size, + uint8_t timing, uint32_t idle_exit, uint32_t memory) { + (void)name; (void)name_size; (void)executable; (void)executable_size; (void)cwd; (void)cwd_size; + (void)callbacks; (void)callbacks_size; (void)timing; (void)idle_exit; (void)memory; + error_text("a shared checker server needs Unix sockets"); + return 0; +} +#else +/* This user's checker server directory: $XDG_RUNTIME_DIR/scriptc, or + * /tmp/scriptc-. It must be a directory this user owns with no access + * for others; one that exists otherwise is not used. */ +static int server_directory(char *path, size_t size) { + const char *runtime = getenv("XDG_RUNTIME_DIR"); + int count = runtime != NULL && runtime[0] == '/' + ? snprintf(path, size, "%s/scriptc", runtime) + : snprintf(path, size, "/tmp/scriptc-%lu", (unsigned long)geteuid()); + if (count < 0 || (size_t)count >= size) { error_text("checker server directory path is too long"); return 0; } + if (mkdir(path, 0700) != 0 && errno != EEXIST) { system_error(errno); return 0; } + struct stat info; + if (lstat(path, &info) != 0) { system_error(errno); return 0; } + if (!S_ISDIR(info.st_mode) || info.st_uid != geteuid() || (info.st_mode & 077) != 0) { + error_text("checker server directory is not private to this user"); + return 0; + } + return 1; +} + +/* A connection to the server listening at `path`, or -1. */ +static int connect_server(const char *path) { + struct sockaddr_un address; + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + memcpy(address.sun_path, path, strlen(path) + 1); + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) return -1; + if (fcntl(fd, F_SETFD, FD_CLOEXEC) < 0) { close(fd); return -1; } +#ifdef SO_NOSIGPIPE + int no_sigpipe = 1; + if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &no_sigpipe, sizeof(no_sigpipe)) != 0) { close(fd); return -1; } +#endif + int status; + do { status = connect(fd, (const struct sockaddr *)&address, sizeof(address)); } while (status != 0 && errno == EINTR); + if (status != 0) { close(fd); return -1; } + return fd; +} + +/* Starts the server in its own process group, so that a terminal's signals + * to the build do not reach it, with its standard streams on /dev/null and + * no other descriptor of this process, so that it holds no pipe a caller of + * the build waits on. `limit` (TSC_TS_LISTEN_MEMORY=...) is added to the + * environment unless it sets TSC_TS_LISTEN_MEMORY. */ +static pid_t start_server(char **args, char *limit) { +#ifdef __APPLE__ + char **inherited = *_NSGetEnviron(); +#else + char **inherited = environ; +#endif + size_t count = 0; + int has_limit = 0; + for (; inherited[count] != NULL; count++) + if (strncmp(inherited[count], "TSC_TS_LISTEN_MEMORY=", 21) == 0) has_limit = 1; + char **environment = malloc((count + 2) * sizeof(char *)); + if (environment == NULL) { error_text("out of memory"); return -1; } + memcpy(environment, inherited, count * sizeof(char *)); + environment[count] = has_limit ? NULL : limit; + environment[count + 1] = NULL; + long descriptors = sysconf(_SC_OPEN_MAX); + if (descriptors < 0 || descriptors > 65536) descriptors = 65536; + sigset_t signals; + sigemptyset(&signals); + pid_t pid = fork(); + if (pid == 0) { + // Only async-signal-safe calls until exec. + int null = open("/dev/null", O_RDWR); + if (setpgid(0, 0) != 0 || null < 0 || dup2(null, STDIN_FILENO) < 0 || dup2(null, STDOUT_FILENO) < 0 || + dup2(null, STDERR_FILENO) < 0) + _exit(127); +#if defined(__linux__) && defined(SYS_close_range) + if (syscall(SYS_close_range, 3U, ~0U, 0U) != 0) +#endif + for (long fd = 3; fd < descriptors; fd++) close((int)fd); + sigprocmask(SIG_SETMASK, &signals, NULL); + execve(args[0], args, environment); + _exit(127); + } + int error = errno; + free(environment); + if (pid < 0) { system_error(error); return -1; } + return pid; +} + +uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size, + const uint8_t *executable, size_t executable_size, + const uint8_t *cwd, size_t cwd_size, + const uint8_t *callbacks, size_t callbacks_size, + uint8_t timing, uint32_t idle_exit, uint32_t memory) { + if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; } + char *server = copy_argument(name, name_size, ""); + if (server == NULL) return 0; + char directory[256], socket_path[sizeof(((struct sockaddr_un *)0)->sun_path)], lock_path[512]; + int named = strchr(server, '/') == NULL; + if (!named) error_text("checker server names cannot contain '/'"); + if (named) named = server_directory(directory, sizeof(directory)); + if (named) { + int count = snprintf(socket_path, sizeof(socket_path), "%s/%s.sock", directory, server); + named = count > 0 && (size_t)count < sizeof(socket_path); + if (!named) error_text("checker server socket path is too long"); + else (void)snprintf(lock_path, sizeof(lock_path), "%s.lock", socket_path); + } + free(server); + if (!named) return 0; + // One build at a time uses the server; a build that finds it in use + // starts its own server instead of waiting. + int lock = open(lock_path, O_RDWR | O_CREAT | O_CLOEXEC | O_NOFOLLOW, 0600); + if (lock < 0) { system_error(errno); return 0; } + if (flock(lock, LOCK_EX | LOCK_NB) != 0) { + if (errno == EWOULDBLOCK) error_text("the checker server is in use"); + else system_error(errno); + close(lock); + return 0; + } + int fd = connect_server(socket_path); + if (fd < 0) { + char *exe = copy_argument(executable, executable_size, ""); + char *dir = exe == NULL ? NULL : copy_argument(cwd, cwd_size, ""); + char *callback_flag = dir == NULL ? NULL : copy_argument(callbacks, callbacks_size, "--callbacks="); + char idle[16], limit[48]; + (void)snprintf(idle, sizeof(idle), "%lu", (unsigned long)idle_exit); + (void)snprintf(limit, sizeof(limit), "TSC_TS_LISTEN_MEMORY=%lu", (unsigned long)memory); + pid_t pid = -1; + if (callback_flag != NULL) { + char *args[] = {exe, "--api", "--cwd", dir, callback_flag, "--listen", socket_path, "--idle-exit", idle, + timing ? "--timing" : NULL, NULL}; + pid = start_server(args, limit); + } + free(exe); free(dir); free(callback_flag); + // The server listens as it starts. One that exits first, such as a + // tsc-ts without --listen, or one that is not listening within 2 s, + // leaves this build to start its own server. + for (int attempt = 0; pid > 0; attempt++) { + fd = connect_server(socket_path); + if (fd >= 0) break; + int status; + pid_t result; + do { result = waitpid(pid, &status, WNOHANG); } while (result < 0 && errno == EINTR); + if (result != 0) { error_text("the checker server exited as it started"); break; } + if (attempt == 1000) { error_text("the checker server did not start listening"); break; } + struct timespec delay = {0, 2000000}; + while (nanosleep(&delay, &delay) != 0 && errno == EINTR) {} + } + } + if (fd < 0) { close(lock); return 0; } + Ts7Process *p = calloc(1, sizeof(*p)); + if (p == NULL) { error_text("out of memory"); close(fd); close(lock); return 0; } + if (!exit_registered) { + if (atexit(scriptc_ts7_close_all) != 0) { + error_text("could not register server cleanup"); free(p); close(fd); close(lock); return 0; + } + exit_registered = 1; + } + p->socket = fd; + p->lock = lock; + p->id = next_id++; + p->next = processes; + processes = p; + return p->id; +} +#endif + uint32_t scriptc_ts7_pid(uint32_t handle) { Ts7Process *p = find_process(handle); return p != NULL ? p->pid : 0; diff --git a/packages/compiler/native/ts7-process.ffi.json b/packages/compiler/native/ts7-process.ffi.json index 86f3cb4d2..5e3bcbc5e 100644 --- a/packages/compiler/native/ts7-process.ffi.json +++ b/packages/compiler/native/ts7-process.ffi.json @@ -2,6 +2,7 @@ "ffi_format": 6, "functions": [ { "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool"], "returns": "u32" }, + { "name": "ts7NativeAttach", "symbol": "scriptc_ts7_attach", "params": ["string", "string", "string", "string", "bool", "u32", "u32"], "returns": "u32" }, { "name": "ts7NativeRead", "symbol": "scriptc_ts7_read", "params": ["u32", "mutable-bytes", "u32", "u32"], "returns": "i32" }, { "name": "ts7NativeWrite", "symbol": "scriptc_ts7_write", "params": ["u32", "bytes", "u32", "u32"], "returns": "i32" }, { "name": "ts7NativePid", "symbol": "scriptc_ts7_pid", "params": ["u32"], "returns": "u32" }, diff --git a/packages/compiler/native/ts7-process.h b/packages/compiler/native/ts7-process.h index 3d2a0ba05..cb5c72097 100644 --- a/packages/compiler/native/ts7-process.h +++ b/packages/compiler/native/ts7-process.h @@ -13,6 +13,19 @@ uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size, const uint8_t *cwd, size_t cwd_size, const uint8_t *callbacks, size_t callbacks_size, uint8_t timing); +/* Connects to the shared checker server `name` (`--api --listen`) in this + * user's private server directory, starting it in its own process group + * with `--idle-exit idle_exit` and a limit of `memory` megabytes + * (TSC_TS_LISTEN_MEMORY, unless the environment sets it) when none answers. Returns 0 with the error + * text when another build is connected to it, it cannot be started or + * reached, or the platform has no Unix sockets; the caller then starts its + * own server with scriptc_ts7_open. Closing the handle ends the connection + * and leaves the server running. */ +uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size, + const uint8_t *executable, size_t executable_size, + const uint8_t *cwd, size_t cwd_size, + const uint8_t *callbacks, size_t callbacks_size, + uint8_t timing, uint32_t idle_exit, uint32_t memory); int32_t scriptc_ts7_read(uint32_t handle, uint8_t *bytes, size_t size, uint32_t offset, uint32_t length); int32_t scriptc_ts7_write(uint32_t handle, const uint8_t *bytes, size_t size, diff --git a/packages/compiler/native/ts7-process.test.c b/packages/compiler/native/ts7-process.test.c index e17c367c7..dc0c67ced 100644 --- a/packages/compiler/native/ts7-process.test.c +++ b/packages/compiler/native/ts7-process.test.c @@ -11,7 +11,9 @@ #include #else #include +#include #include +#include #include #include #endif @@ -171,6 +173,89 @@ static void failure_cases(void) { scriptc_ts7_close(UINT32_MAX); } +#ifndef _WIN32 +static uint32_t attach(const char *name, const char *mode, uint32_t memory) { + return scriptc_ts7_attach((const uint8_t *)name, strlen(name), (const uint8_t *)executable, strlen(executable), + (const uint8_t *)mode, strlen(mode), (const uint8_t *)callbacks, strlen(callbacks), + 0, 1, memory); +} + +static void expect_line(uint32_t id, const char *expected) { + char line[512]; + size_t at = 0; + do { + assert(at + 1 < sizeof(line)); + read_exact(id, (uint8_t *)line + at, 1); + } while (line[at++] != '\n'); + line[at] = '\0'; + assert(strcmp(line, expected) == 0); +} + +static void attach_cases(void) { + char runtime[] = "/tmp/scriptc-ts7-attach-XXXXXX"; + assert(mkdtemp(runtime) != NULL); + assert(setenv("XDG_RUNTIME_DIR", runtime, 1) == 0); + assert(unsetenv("TSC_TS_LISTEN_MEMORY") == 0); + char directory[256], socket_path[512], expected[600]; + uint8_t bytes[4]; + snprintf(directory, sizeof(directory), "%s/scriptc", runtime); + snprintf(socket_path, sizeof(socket_path), "%s/server.sock", directory); + // The first build starts the server with its idle time and memory limit. + // The server holds none of the build's descriptors, such as an output + // pipe its caller reads to the end. + int output[2]; + assert(pipe(output) == 0); + uint32_t first = attach("server", "listen", 123); + assert(first != 0 && scriptc_ts7_pid(first) == 0); + close(output[1]); + struct pollfd ended = {output[0], POLLIN, 0}; + assert(poll(&ended, 1, 5000) == 1); + assert(read(output[0], bytes, 1) == 0); + close(output[0]); + expect_line(first, "client 1\n"); + snprintf(expected, sizeof(expected), "%s 1\n", socket_path); + expect_line(first, expected); + expect_line(first, "123\n"); + write_exact(first, (const uint8_t *)"ping", 4, 0, 4); + read_exact(first, bytes, 4); + assert(memcmp(bytes, "ping", 4) == 0); + // A build that finds the server in use gets no handle. + for (int i = 0; i < 3; i++) { assert(attach("server", "listen", 123) == 0); fail_message(); } + scriptc_ts7_close(first); + scriptc_ts7_close(first); + // A later build connects to the same server. + uint32_t second = attach("server", "listen", 123); + assert(second != 0); + expect_line(second, "client 2\n"); + scriptc_ts7_close(second); + // An executable without --listen exits as it starts. + assert(attach("old", "listen-exit", 0) == 0); + fail_message(); + // A server directory others can access is not used. + assert(chmod(directory, 0755) == 0); + assert(attach("server", "listen", 123) == 0); + fail_message(); + assert(chmod(directory, 0700) == 0); + // A name that would leave the directory, or make the path too long. + assert(attach("../server", "listen", 123) == 0); + fail_message(); + char long_name[200]; + memset(long_name, 'x', sizeof(long_name) - 1); + long_name[sizeof(long_name) - 1] = '\0'; + assert(attach(long_name, "listen", 123) == 0); + fail_message(); + // The server exits after its idle second and removes its socket. + unlink(socket_path); + snprintf(expected, sizeof(expected), "%s.lock", socket_path); + unlink(expected); + snprintf(expected, sizeof(expected), "%s/old.sock.lock", directory); + unlink(expected); + rmdir(directory); + rmdir(runtime); + assert(unsetenv("XDG_RUNTIME_DIR") == 0); +} +#endif + static int process_main(int argc, char **argv) { assert(argc >= 2); executable = argv[1]; @@ -196,6 +281,9 @@ static int process_main(int argc, char **argv) { argument_cases(); exchange_cases(); failure_cases(); +#ifndef _WIN32 + attach_cases(); +#endif assert(resource_count() == before); #ifndef _WIN32 if (saved_input >= 0) { assert(dup2(saved_input, STDIN_FILENO) >= 0); close(saved_input); } diff --git a/packages/compiler/src/frontend/services.ts b/packages/compiler/src/frontend/services.ts index a0acb5818..182611526 100644 --- a/packages/compiler/src/frontend/services.ts +++ b/packages/compiler/src/frontend/services.ts @@ -61,7 +61,9 @@ export class FrontendServices { // Called as a plain function: the instance is not its receiver. const createApi = this.createApi; return (this.parser ??= new Ts7SourceParser( - (options) => createApi({ ...options, collectTiming: false }), + // The parser reads no libraries, so it gains nothing from the shared + // checker server, and leaves it to the program's checker. + (options) => createApi({ ...options, collectTiming: false, shared: false }), this.cwd, )); } diff --git a/packages/compiler/src/frontend/ts7/checker-server.test.ts b/packages/compiler/src/frontend/ts7/checker-server.test.ts new file mode 100644 index 000000000..9a0371fe6 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/checker-server.test.ts @@ -0,0 +1,47 @@ +import { mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, expect, test } from "vitest"; +import { checkerServerName } from "./checker-server.js"; + +const dir = mkdtempSync(join(tmpdir(), "scriptc-checker-server-")); +const executable = join(dir, "tsc-ts"); +writeFileSync(executable, "server"); +const enabled = { SCRIPTC_CHECKER_SERVER: "1" }; +const posix = process.platform === "linux" || process.platform === "darwin"; + +afterAll(() => rmSync(dir, { recursive: true, force: true })); + +test("builds use the shared checker server only when they opt in, on Linux and macOS, with tsc-ts", () => { + expect(checkerServerName(executable, dir, false, {})).toBeNull(); + expect(checkerServerName(executable, dir, false, { SCRIPTC_CHECKER_SERVER: "0" })).toBeNull(); + expect( + checkerServerName(executable, dir, false, { ...enabled, SCRIPTC_CHECKER: "tsgo" }), + ).toBeNull(); + const name = checkerServerName(executable, dir, false, enabled); + if (!posix) expect(name).toBeNull(); + else expect(name).toMatch(/^[0-9a-f]{32}$/); +}); + +test.skipIf(!posix)( + "the server's name changes with what its answers depend on, and with nothing else", + () => { + const name = checkerServerName(executable, dir, false, enabled); + expect( + checkerServerName(executable, dir, false, { ...enabled, SCRIPTC_CHECKER: "tsc-ts" }), + ).toBe(name); + expect(checkerServerName(executable, dir, false, { ...enabled, PATH: "/elsewhere" })).toBe( + name, + ); + const others = [ + checkerServerName(executable, join(dir, "other"), false, enabled), + checkerServerName(executable, dir, true, enabled), + checkerServerName(executable, dir, false, { ...enabled, TSC_TS_LIB_DIR: dir }), + checkerServerName(executable, dir, false, { ...enabled, TSC_TS_LISTEN_MEMORY: "512" }), + ]; + for (const other of others) expect(other).not.toBe(name); + // An updated tsc-ts executable gets its own server. + utimesSync(executable, new Date(0), new Date(0)); + expect(checkerServerName(executable, dir, false, enabled)).not.toBe(name); + }, +); diff --git a/packages/compiler/src/frontend/ts7/checker-server.ts b/packages/compiler/src/frontend/ts7/checker-server.ts new file mode 100644 index 000000000..6d80e2599 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/checker-server.ts @@ -0,0 +1,50 @@ +import { createHash } from "node:crypto"; +import { checkerBackend } from "./checker-backend.js"; +import { checkerExecutableIdentity } from "./lib-diagnostics.js"; +import { TS7_FILE_SYSTEM_CALLBACKS } from "./rpc-filesystem.js"; + +/** How long a shared checker server waits for the next build before it + * exits, in seconds. */ +export const CHECKER_SERVER_IDLE_EXIT = 300; + +/** The resident memory, in megabytes, above which a shared checker server + * exits after a build. tsc-ts never frees a program's syntax trees, types and + * symbols, so its memory grows with each build it serves. */ +export const CHECKER_SERVER_MEMORY = 2048; + +/** + * With `SCRIPTC_CHECKER_SERVER=1` on Linux and macOS, the native compiler + * checks with a tsc-ts server shared by the builds that follow + * (`tsc-ts --api --listen`): a build connects to it, starting it when none + * answers, and the server keeps the files it parsed for the next build. + * Returns the server's name, a digest of everything a server's answers + * depend on besides the files it reads through the build: the tsc-ts + * executable, the working directory, the file system callbacks, timing, and + * the `TSC_TS_*` environment. Returns null when the shared server is not used. + */ +export function checkerServerName( + executable: string, + cwd: string, + timing: boolean, + env: NodeJS.ProcessEnv = process.env, +): string | null { + if (env["SCRIPTC_CHECKER_SERVER"] !== "1") return null; + if (process.platform !== "linux" && process.platform !== "darwin") return null; + if (checkerBackend(env) !== "tsc-ts") return null; + const tscTsEnv = Object.keys(env) + .filter((name) => name.startsWith("TSC_TS_")) + .sort() + .map((name) => [name, env[name] ?? ""]); + return createHash("sha256") + .update( + JSON.stringify([ + checkerExecutableIdentity(executable), + cwd, + TS7_FILE_SYSTEM_CALLBACKS, + timing, + tscTsEnv, + ]), + ) + .digest("hex") + .slice(0, 32); +} diff --git a/packages/compiler/src/frontend/ts7/native-api.ts b/packages/compiler/src/frontend/ts7/native-api.ts index 259529541..f8e10efc5 100644 --- a/packages/compiler/src/frontend/ts7/native-api.ts +++ b/packages/compiler/src/frontend/ts7/native-api.ts @@ -1,7 +1,9 @@ import { Ts7Api } from "./api.js"; -import { spawnNativeTs7Wire } from "./native-process.js"; +import { checkerServerName } from "./checker-server.js"; +import { attachNativeTs7Process, spawnNativeTs7Wire } from "./native-process.js"; import { Ts7RpcClient } from "./rpc-client.js"; import type { Ts7FileSystem } from "./rpc-filesystem.js"; +import { Ts7Wire } from "./rpc-wire.js"; /** The native driver supplies its installed TypeScript executable path. * Package discovery is separate from starting and owning a server. */ @@ -10,8 +12,25 @@ export function createNativeTs7Api(options: { cwd: string; fs: Ts7FileSystem; collectTiming?: boolean; + shared?: boolean; }): Ts7Api { const timing = options.collectTiming ?? false; + const server = + options.shared === false ? null : checkerServerName(options.executable, options.cwd, timing); + const shared = + server === null + ? null + : attachNativeTs7Process(server, options.executable, options.cwd, timing); + if (shared !== null) { + const rpc = new Ts7RpcClient(new Ts7Wire(shared)); + try { + // A server that ended the connection leaves this build to start its own. + rpc.requestText("ping", "null"); + return new Ts7Api(rpc, options.fs, timing); + } catch { + rpc.close(); + } + } return new Ts7Api( new Ts7RpcClient(spawnNativeTs7Wire(options.executable, options.cwd, timing)), options.fs, diff --git a/packages/compiler/src/frontend/ts7/native-process.ts b/packages/compiler/src/frontend/ts7/native-process.ts index 7bb523d74..aa38cf4b4 100644 --- a/packages/compiler/src/frontend/ts7/native-process.ts +++ b/packages/compiler/src/frontend/ts7/native-process.ts @@ -1,5 +1,6 @@ import { Ts7Wire, type Ts7WireIo } from "./rpc-wire.js"; import { TS7_FILE_SYSTEM_CALLBACKS } from "./rpc-filesystem.js"; +import { CHECKER_SERVER_IDLE_EXIT, CHECKER_SERVER_MEMORY } from "./checker-server.js"; // Bound by native/ts7-process.ffi.json. This module is a native entry point; // the Node host continues to use rpc-process.ts's public spawn boundary. @@ -9,6 +10,15 @@ declare function ts7NativeOpen( callbacks: string, timing: boolean, ): number; +declare function ts7NativeAttach( + name: string, + executable: string, + cwd: string, + callbacks: string, + timing: boolean, + idleExit: number, + memory: number, +): number; declare function ts7NativeRead( handle: number, bytes: Uint8Array, @@ -47,8 +57,12 @@ export class NativeTs7Process implements Ts7WireIo { private handle: number; readonly pid: number; - constructor(executable: string, cwd: string, collectTiming = false) { - this.handle = ts7NativeOpen(executable, cwd, TS7_FILE_SYSTEM_CALLBACKS, collectTiming); + /** With `handle`, a connection to a shared checker server (attachNativeTs7Process). */ + constructor(executable: string, cwd: string, collectTiming = false, handle = 0) { + this.handle = + handle !== 0 + ? handle + : ts7NativeOpen(executable, cwd, TS7_FILE_SYSTEM_CALLBACKS, collectTiming); if (this.handle === 0) throw nativeError("startup"); this.pid = ts7NativePid(this.handle); } @@ -97,6 +111,27 @@ export class NativeTs7Process implements Ts7WireIo { }; } +/** A connection to the shared checker server `name`, started when none + * answers (`checkerServerName`), or null when this build cannot use it, such + * as while another build is connected to it. */ +export function attachNativeTs7Process( + name: string, + executable: string, + cwd: string, + collectTiming = false, +): NativeTs7Process | null { + const handle = ts7NativeAttach( + name, + executable, + cwd, + TS7_FILE_SYSTEM_CALLBACKS, + collectTiming, + CHECKER_SERVER_IDLE_EXIT, + CHECKER_SERVER_MEMORY, + ); + return handle === 0 ? null : new NativeTs7Process(executable, cwd, collectTiming, handle); +} + export function spawnNativeTs7Wire( executable: string, cwd: string, diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index ec143e401..e66d598d9 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -122,6 +122,9 @@ export interface Ts7ApiOptions { cwd: string; fs: Ts7FileSystem; collectTiming: boolean; + /** False for a connection that should not use the shared checker server + * (`SCRIPTC_CHECKER_SERVER`), such as the syntax-only source parser's. */ + shared?: boolean; } export type Ts7ApiFactory = (options: Ts7ApiOptions) => Ts7Api; diff --git a/tests/fixtures/self-hosting/ts7-native-process.ts b/tests/fixtures/self-hosting/ts7-native-process.ts index 2fdbb9e79..3b04a784c 100644 --- a/tests/fixtures/self-hosting/ts7-native-process.ts +++ b/tests/fixtures/self-hosting/ts7-native-process.ts @@ -1,6 +1,9 @@ import { writeFileSync } from "node:fs"; import { join } from "node:path"; -import { NativeTs7Process } from "../../../packages/compiler/src/frontend/ts7/native-process.js"; +import { + attachNativeTs7Process, + NativeTs7Process, +} from "../../../packages/compiler/src/frontend/ts7/native-process.js"; import { createNativeTs7Api } from "../../../packages/compiler/src/frontend/ts7/native-api.js"; import { Ts7RpcClient } from "../../../packages/compiler/src/frontend/ts7/rpc-client.js"; import { Ts7Wire } from "../../../packages/compiler/src/frontend/ts7/rpc-wire.js"; @@ -95,12 +98,44 @@ function checkTransport(executable: string): void { console.log("native transport passed"); } +function readLine(connection: NativeTs7Process): string { + const byte = new Uint8Array(1); + let line = ""; + for (;;) { + check(connection.read(byte, 0, 1) === 1, "shared server line"); + if (byte[0] === 10) return line; + line += String.fromCharCode(byte[0]!); + } +} + +// The stand-in server (ts7-process-child.test.c, mode "listen") answers each +// client with its number, its socket and idle time, and its memory limit. +function checkShared(executable: string): void { + const first = attachNativeTs7Process("server", executable, "listen"); + check(first !== null, "shared server started"); + check(readLine(first!) === "client 1", "first client"); + check(readLine(first!).endsWith("/scriptc/server.sock 300"), "listen arguments"); + check(readLine(first!) === "2048", "memory limit"); + check(attachNativeTs7Process("server", executable, "listen") === null, "shared server in use"); + first!.close(); + first!.close(); + const second = attachNativeTs7Process("server", executable, "listen"); + check(second !== null && readLine(second) === "client 2", "shared server reused"); + second!.close(); + check(attachNativeTs7Process("old", executable, "listen-exit") === null, "server without --listen"); + console.log("native shared server passed"); +} + const executable = process.argv[2]!; const directory = process.argv[3]!; const report = process.argv[4]!; const mode = process.argv[5] ?? "client"; if (mode === "transport") { checkTransport(executable); +} else if (mode === "shared") { + checkShared(executable); +} else if (mode === "api") { + checkApi(executable, directory); } else if (mode === "exit" || mode === "normal-exit") { const child = new NativeTs7Process(executable, "stall"); const byte = new Uint8Array(1); diff --git a/tests/harness/self-hosting-ts7-process.test.ts b/tests/harness/self-hosting-ts7-process.test.ts index d49a5c092..150a8860d 100644 --- a/tests/harness/self-hosting-ts7-process.test.ts +++ b/tests/harness/self-hosting-ts7-process.test.ts @@ -1,5 +1,5 @@ import { execFileSync, spawnSync } from "node:child_process"; -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { analyze, compile } from "@scriptc/compiler"; @@ -72,6 +72,20 @@ for (const backend of ["llvm"] as const) { expect(transport.status, transport.stderr).toBe(0); expect(transport.stdout).toBe("native transport passed\n"); expect(transport.stderr).toBe(""); + if (process.platform !== "win32") { + // A shared server in a private server directory, which the stand-in + // leaves after its idle time. + const runtime = join(dir, "run"); + mkdirSync(runtime, { mode: 0o700 }); + const shared = spawnSync(built.binaryPath, [child, dir, "unused", "shared"], { + encoding: "utf8", + timeout: 15_000, + env: { ...process.env, XDG_RUNTIME_DIR: runtime, TSC_TS_LISTEN_MEMORY: undefined }, + }); + expect(shared.status, shared.stderr).toBe(0); + expect(shared.stdout).toBe("native shared server passed\n"); + expect(shared.stderr).toBe(""); + } for (const mode of ["normal-exit", "exit"]) { const pidFile = join(dir, mode + ".pid"); const run = spawnSync(built.binaryPath, [child, dir, pidFile, mode], { @@ -94,6 +108,20 @@ for (const backend of ["llvm"] as const) { dir, join(dir, "oracle.json"), ); + if (process.platform !== "win32") { + // With the shared server requested, a checker without --listen exits + // as it starts and the build checks with its own process. + const runtime = join(dir, "api-run"); + mkdirSync(runtime, { mode: 0o700 }); + const api = spawnSync(built.binaryPath, [tsgoExecutable(), dir, "unused", "api"], { + encoding: "utf8", + timeout: 45_000, + env: { ...process.env, XDG_RUNTIME_DIR: runtime, SCRIPTC_CHECKER_SERVER: "1" }, + }); + expect(api.status, api.stderr).toBe(0); + expect(api.stdout).toBe(""); + expect(api.stderr).toBe(""); + } const report = join(dir, "native.json"); // Only ordinary stdio is inherited. The native executable starts the // actual tsgo process; the test runner never creates its RPC pipes. As in From 05bc5299b0846c4c4d6c0296498cff45696fb3e3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:54:15 -0500 Subject: [PATCH 126/273] Reap the checker process within a millisecond of closing it Closing a checker process checked for its exit once and then slept 10 ms before checking again, although the process exits a fraction of a millisecond after its input closes. The checks now start 0.1 ms apart and double, so the process is reaped as soon as it exits. The total wait before the kill stays at about 50 ms (51.1 ms). --- packages/compiler/native/ts7-process.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/compiler/native/ts7-process.c b/packages/compiler/native/ts7-process.c index ea451345d..b2b7c5062 100644 --- a/packages/compiler/native/ts7-process.c +++ b/packages/compiler/native/ts7-process.c @@ -295,13 +295,16 @@ static void stop_process(Ts7Process *p) { close(p->socket); int status; pid_t pid = (pid_t)p->pid; - for (int attempt = 0; attempt < 5; attempt++) { + // A server exits within a fraction of a millisecond of reading EOF, so the + // checks start 0.1 ms apart and double: 51.1 ms in all before the kill. + for (int attempt = 0; attempt < 10; attempt++) { pid_t result; do { result = waitpid(pid, &status, WNOHANG); } while (result < 0 && errno == EINTR); // ECHILD also means the PID no longer belongs to us. Never signal a // potentially recycled PID after somebody else has reaped the child. if (result == pid || (result < 0 && errno == ECHILD)) return; - struct timespec delay = {0, 10000000}; + if (attempt == 9) break; + struct timespec delay = {0, 100000L << attempt}; while (nanosleep(&delay, &delay) != 0 && errno == EINTR) {} } (void)kill(pid, SIGKILL); From fcc81c034cbef0e3fbcea1835a131d43359e8692 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 17:59:56 -0500 Subject: [PATCH 127/273] Read the final result's value only for sync pipeline sources for await reads the value of a done result only through the async-from-sync wrapper; an async iterator's done result is not read. The source's generator read it for both, so an async iterator whose done result has a throwing value getter rejected the pipeline. It now reads and awaits the final value only when the iterator is sync. --- .../src/frontend/lowering/lower-stream.ts | 19 ++++++++--- .../7504-pipeline-untyped-source-errors.mjs | 34 +++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index d3883427a..d67cf257e 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -4085,14 +4085,23 @@ function lowerDynIterableSource( type: BOOL, loc, }, - // the final value is awaited too: a rejection rejects the - // pump, and the finished iterator is not closed + // a sync iterator's final value is awaited too: a rejection + // rejects the pump, and the finished iterator is not closed. + // An async iterator's final value is not read. then: [ - { kind: "assign", localId: valueId, value: get(step(), "value"), loc }, { kind: "if", - cond: awaitsValue(), - then: [{ kind: "exprStmt", expr: awaitDyn(value()), loc }], + cond: sync(), + then: [ + { kind: "assign", localId: valueId, value: get(step(), "value"), loc }, + { + kind: "if", + cond: awaitsValue(), + then: [{ kind: "exprStmt", expr: awaitDyn(value()), loc }], + else_: null, + loc, + }, + ], else_: null, loc, }, diff --git a/tests/corpus/7504-pipeline-untyped-source-errors.mjs b/tests/corpus/7504-pipeline-untyped-source-errors.mjs index 5d6faded1..9dd671606 100644 --- a/tests/corpus/7504-pipeline-untyped-source-errors.mjs +++ b/tests/corpus/7504-pipeline-untyped-source-errors.mjs @@ -135,3 +135,37 @@ for (const [label, source] of [["finalRejects", finalRejects], ["finalResolves", console.log(label, "rejected", err.message); } } + +// the final result's value is read for a sync iterator only: an async +// iterator's done result is not read, so a throwing value getter there is +// never called +function finalResult(label, i) { + if (i === 1) return { value: `${label}1`, done: false }; + return { + done: true, + get value() { + throw new Error(`${label} final value read`); + }, + }; +} +function asyncFinal() { + let i = 0; + return { [Symbol.asyncIterator]() { return { async next() { i++; return finalResult("async", i); } }; } }; +} +function syncFinal() { + let i = 0; + return { [Symbol.iterator]() { return { next() { i++; return finalResult("sync", i); } }; } }; +} +async function runFinal(label, source) { + try { + await pipelineP( + pick(source), + new Writable({ objectMode: true, write(c, _e, cb) { console.log(label, "write", c); cb(); } }), + ); + console.log(label, "resolved"); + } catch (err) { + console.log(label, "rejected", err.message); + } +} +await runFinal("async final", asyncFinal()); +await runFinal("sync final", syncFinal()); From 8da01d3ab3778c8c1073a9624fb0aa7f2faf6240 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 18:02:25 -0500 Subject: [PATCH 128/273] State that SCRIPTC_CHECKER_SERVER needs a tsc-ts that serves --listen The tsc-ts 0.0.9 that scriptc includes rejects --listen, so with it every build that sets SCRIPTC_CHECKER_SERVER=1 checks with its own tsc-ts process. The variable's description now says so. --- docs/content/docs/cli.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index 050d076e3..4c1b7e7c6 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -212,7 +212,7 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
The TypeScript 7 API server that checks the program: tsc-ts (the default), which scriptc includes, or tsgo, from the typescript package scriptc also includes. Both give the same diagnostics and generated code.
SCRIPTC_CHECKER_SERVER
-
Set to 1 on Linux and macOS to check with a tsc-ts server that later builds share. A build connects to the server for its working directory, starting it when none answers, and the server keeps the files it parsed, so the next build of the same program loads it faster. A build that finds the server in use by another build, or cannot start or reach it, checks with its own tsc-ts process. The server exits after 5 minutes without a build, or after a build when its resident memory is above 2048 MB (TSC_TS_LISTEN_MEMORY sets another limit in megabytes). Diagnostics and generated code are the same with and without it. It applies to the default checker, tsc-ts.
+
Set to 1 on Linux and macOS to check with a tsc-ts server that later builds share. It takes effect with a tsc-ts release that serves tsc-ts --api --listen. The tsc-ts 0.0.9 that this version of scriptc includes does not, so each build checks with its own tsc-ts process, as without the variable. A build connects to the server for its working directory, starting it when none answers, and the server keeps the files it parsed, so the next build of the same program loads it faster. A build that finds the server in use by another build, or cannot start or reach it, checks with its own tsc-ts process. The server exits after 5 minutes without a build, or after a build when its resident memory is above 2048 MB (TSC_TS_LISTEN_MEMORY sets another limit in megabytes). Diagnostics and generated code are the same with and without it. It applies to the default checker, tsc-ts.
```console From 5dc3a051b73383f10f0b824fa8a2e3250f3fb872 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 18:06:45 -0500 Subject: [PATCH 129/273] Prove typed array reads in while loops bounded by the length in bounds findBytesBounds proved element reads only inside for loops, and only for `counter + c` indexes. A while loop such as while (i < len) { c = line[right - i]!; // right = len - 1 i++; if (c === 10) { if (i === len) break; c = line[right - i]!; i++; } ... } kept a bounds check on every read. The out-of-bounds path also produced a NaN that LLVM threaded into a following switch, which then stayed an indirect jump instead of becoming a lookup table. - A while condition that compares an integer counter with a typed array's length (`i < len`, `i <= right`, `len > i`) starts a bound. The body may write the counter only by statements that add a positive literal, and may not write the receiver. Calls in the body are allowed: they cannot write either binding (both are uncaptured locals), and a typed array's length is fixed at construction. - The body is followed in order. An increment raises the bound and the counter's minimum by its step. An if condition that compares the counter with the length narrows the bound in its branches (`if (i === len) break` restores `i < len` after an increment). Branches join to the weaker bound, and a statement that advances the counter inside a nested loop or labeled block leaves the counter unbounded after it. - Indexes of the form `L + d - counter` (`len - 1 - i`, `right - i`) are proven for both for and while bounds, from the counter's upper bound and its minimum. --- .../compiler/src/backend/llvm/bytes-bounds.ts | 199 +++++++++++++++++- .../8003-while-reversed-byte-reads.ts.json | 6 + .../test/while-bytes-bounds-emission.test.ts | 136 ++++++++++++ .../corpus/8003-while-reversed-byte-reads.ts | 168 +++++++++++++++ .../8003-while-reversed-byte-reads.ts.json | 1 + 5 files changed, 508 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8003-while-reversed-byte-reads.ts.json create mode 100644 packages/compiler/test/while-bytes-bounds-emission.test.ts create mode 100644 tests/corpus/8003-while-reversed-byte-reads.ts create mode 100644 tests/harness/corpus-costs/8003-while-reversed-byte-reads.ts.json diff --git a/packages/compiler/src/backend/llvm/bytes-bounds.ts b/packages/compiler/src/backend/llvm/bytes-bounds.ts index 86a1d7ac8..b35a949ff 100644 --- a/packages/compiler/src/backend/llvm/bytes-bounds.ts +++ b/packages/compiler/src/backend/llvm/bytes-bounds.ts @@ -14,11 +14,25 @@ interface Bound extends Extent { minimum: number; } type Node = IrExpr | IrStmt; +const FLIPPED: Readonly> = { + "<": ">", + "<=": ">=", + ">": "<", + ">=": "<=", + "===": "===", + "==": "==", + "!==": "!==", + "!=": "!=", +}; /** Relational proofs for fixed typed-array lengths. Cached lengths retain * their receiver identity only when that binding is never reassigned. * Loops may use offsets in either direction, but both index integrality - * and receiver stability are established separately from the relation. */ + * and receiver stability are established separately from the relation. + * For loops hold one bound over the body. While loops start from the + * condition's bound and follow the body in order: increments raise it and + * comparisons with the length in if conditions narrow it. Indexes are + * `counter + c` or `L + d - counter`, where L is the receiver's length. */ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): ReadonlySet { const proved = new Map(); if (fn.async || fn.generator || !fn.locals.some((local) => local.type.kind === "bytes")) @@ -96,6 +110,20 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly const result = left + (e.op === "+" ? e.right.value : -e.right.value); return Number.isSafeInteger(result) ? result : null; } + /** For an index of the form `L + d - counter` (`len - 1 - i`, `right - i` + * with `right = len - 1`), where L is the receiver's length: d. */ + function reversed(e: IrExpr, bound: Bound): number | null { + if (e.kind !== "bin" || (e.op !== "+" && e.op !== "-")) return null; + if (e.op === "-" && e.right.kind === "varRef" && e.right.localId === bound.counter) { + const length = extent(e.left, false); + return length && length.receiver === bound.receiver ? length.offset : null; + } + if (e.right.kind !== "numLit" || !Number.isSafeInteger(e.right.value)) return null; + const left = reversed(e.left, bound); + if (left === null) return null; + const result = left + (e.op === "+" ? e.right.value : -e.right.value); + return Number.isSafeInteger(result) ? result : null; + } function affineRange(e: IrExpr, bound: Bound): IntegerRange | null { if (e.kind === "varRef" && e.localId === bound.counter) { const max = Number.MAX_SAFE_INTEGER + bound.offset - 1; @@ -159,7 +187,7 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly if (s.cond.left.kind !== "varRef" || s.cond.left.localId !== counter) { if (right.kind !== "varRef" || right.localId !== counter) return null; right = s.cond.left; - op = ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as Record)[op] ?? ""; + op = FLIPPED[op] ?? ""; } const update = s.update; let step = 0; @@ -212,6 +240,90 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly } return result && stable(s.body, result.receiver, counter) ? result : null; } + /** A comparison of the counter with the receiver's length: the operator + * with the counter on the left, and the length's offset. */ + function lengthComparison(e: IrExpr, bound: Bound): { op: string; offset: number } | null { + if (e.kind !== "bin") return null; + let other = e.right, + op: string = e.op; + if (e.left.kind !== "varRef" || e.left.localId !== bound.counter) { + if (other.kind !== "varRef" || other.localId !== bound.counter) return null; + other = e.left; + op = FLIPPED[op] ?? op; + } + const length = extent(other, false); + return length && length.receiver === bound.receiver ? { op, offset: length.offset } : null; + } + /** The step of `counter = counter + c` with a positive literal c, or null. */ + function increment(s: IrStmt, counter: string): number | null { + return s.kind === "assign" && + s.localId === counter && + s.value.kind === "bin" && + s.value.op === "+" && + s.value.left.kind === "varRef" && + s.value.left.localId === counter && + s.value.right.kind === "numLit" && + Number.isSafeInteger(s.value.right.value) && + s.value.right.value > 0 + ? s.value.right.value + : null; + } + function writes(s: IrStmt, id: string): boolean { + return !everyStmtList([s], { + expr: (e) => (e.kind !== "assignExpr" && e.kind !== "incDec") || e.localId !== id, + stmt: (child) => + !( + ((child.kind === "assign" || child.kind === "forOf" || child.kind === "varDecl") && + child.localId === id) || + (child.kind === "tryCatch" && child.catchLocalId === id) + ), + }); + } + /** A while loop whose condition bounds an integer counter by a typed + * array's length. The body may write the counter only by statements + * that add a positive literal, and may not write the receiver. Calls + * cannot write either binding (both are uncaptured locals), and a typed + * array's length is fixed at construction, so the body may hold calls. */ + function whileBound(s: IrStmt & { kind: "while" }): Bound | null { + const cond = s.cond; + if (cond.kind !== "bin") return null; + const counterRef = [cond.left, cond.right].find( + (side, i) => + side.kind === "varRef" && + eligible(side.localId) && + extent(i === 0 ? cond.right : cond.left, false) !== null, + ); + const minimum = counterRef ? ranges.get(counterRef)?.min : undefined; + if (counterRef?.kind !== "varRef" || minimum === undefined) return null; + const counter = counterRef.localId; + const length = extent(counterRef === cond.left ? cond.right : cond.left, false)!; + const receiver = length.receiver; + const comparison = lengthComparison(cond, { receiver, counter, offset: 0, minimum }); + if (comparison?.op !== "<" && comparison?.op !== "<=") return null; + const writesOk = everyStmtList(s.body, { + expr: (e) => + (e.kind !== "assignExpr" && e.kind !== "incDec") || + (e.localId !== counter && e.localId !== receiver), + stmt: (child) => { + if (child.kind === "assign" && child.localId === counter) + return increment(child, counter) !== null; + if (child.kind === "assign" || child.kind === "forOf" || child.kind === "varDecl") + return child.localId !== counter && child.localId !== receiver; + return ( + child.kind !== "tryCatch" || + (child.catchLocalId !== counter && child.catchLocalId !== receiver) + ); + }, + }); + return writesOk + ? { + receiver, + counter, + offset: comparison.offset + (comparison.op === "<=" ? 1 : 0), + minimum, + } + : null; + } function access(node: Node, array: IrExpr, index: IrExpr, active: Bound[], width: number): void { const integer = ranges.get(index); let safe = false; @@ -228,6 +340,13 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly safe = true; break; } + // counter < L + bound.offset and counter >= bound.minimum put + // L + d - counter in [0, L - width]. + const d = integer ? reversed(index, bound) : null; + if (d !== null && bound.offset <= d + 1 && bound.minimum >= d + width) { + safe = true; + break; + } } } proved.set(node, (proved.get(node) ?? true) && safe); @@ -244,8 +363,84 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly (child) => stmt(child, active), ); } + /** The bounds where `cond` is true and where it is false, from the + * bound before it. */ + function narrow(cond: IrExpr, bound: Bound): [Bound, Bound] { + const comparison = lengthComparison(cond, bound); + if (!comparison) return [bound, bound]; + const j = comparison.offset, + offset = bound.offset; + // counter < L + j, counter <= L + j, and counter !== L + j. + const below = { ...bound, offset: Math.min(offset, j) }, + atMost = { ...bound, offset: Math.min(offset, j + 1) }, + unequal = offset === j + 1 ? { ...bound, offset: j } : bound; + switch (comparison.op) { + case "<": + return [below, bound]; + case "<=": + return [atMost, bound]; + case ">": + return [bound, atMost]; + case ">=": + return [bound, below]; + case "===": + case "==": + return [atMost, unequal]; + case "!==": + case "!=": + return [unequal, atMost]; + default: + return [bound, bound]; + } + } + /** Visits a while body in order, from the bound that holds on entry. + * Returns the bound at the end of the list. Its offset is -Infinity (and + * its minimum Infinity) when the end is unreachable, and Infinity when + * nothing bounds the counter there. Every write of the counter adds a + * positive literal (whileBound), so the minimum survives any write. */ + function flow(list: IrStmt[], active: Bound[], bound: Bound): Bound { + for (const s of list) { + const current = Number.isFinite(bound.offset) ? [...active, bound] : active; + const step = increment(s, bound.counter); + if (step !== null) { + bound = { ...bound, offset: bound.offset + step, minimum: bound.minimum + step }; + } else if (s.kind === "if") { + expr(s.cond, current); + const [then, else_] = narrow(s.cond, bound); + const left = flow(s.then, active, then), + right = s.else_ ? flow(s.else_, active, else_) : else_; + bound = { + ...bound, + offset: Math.max(left.offset, right.offset), + minimum: Math.min(left.minimum, right.minimum), + }; + } else if (s.kind === "block" && !s.labels?.length) { + bound = flow(s.body, active, bound); + } else if (writes(s, bound.counter)) { + stmt(s, active); + bound = { ...bound, offset: Infinity }; + } else { + stmt(s, current); + if ( + s.kind === "break" || + s.kind === "continue" || + s.kind === "return" || + s.kind === "throw" + ) + bound = { ...bound, offset: -Infinity, minimum: Infinity }; + } + } + return bound; + } function stmt(s: IrStmt, active: Bound[]): boolean { if (s.kind === "bytesSet") access(s, s.arr, s.index, active, 1); + if (s.kind === "while") { + expr(s.cond, active); + const bound = whileBound(s); + if (!bound) return s.body.every((child) => stmt(child, active)); + flow(s.body, active, bound); + return true; + } if (s.kind === "for") { if (s.init) stmt(s.init, active); if (s.cond) expr(s.cond, active); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8003-while-reversed-byte-reads.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8003-while-reversed-byte-reads.ts.json new file mode 100644 index 000000000..26031ac1f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8003-while-reversed-byte-reads.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8003-while-reversed-byte-reads.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/while-bytes-bounds-emission.test.ts b/packages/compiler/test/while-bytes-bounds-emission.test.ts new file mode 100644 index 000000000..2b6dc1d3c --- /dev/null +++ b/packages/compiler/test/while-bytes-bounds-emission.test.ts @@ -0,0 +1,136 @@ +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-while-bounds-")); + 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 }); + } +} + +/** The number of bounds-checked element reads and writes in a function's + * statement body (the borrowed-argument body when there is one). */ +function checks(ll: string, name: string): number { + const definition = (prefix: string): number => + ll.search(new RegExp(`define [^\\n]*@${prefix}_${name}\\(`)); + const start = definition("sc_bf") >= 0 ? definition("sc_bf") : definition("sc_f"); + expect(start).toBeGreaterThanOrEqual(0); + const fn = ll.slice(start, ll.indexOf("\n}\n", start)); + return fn.split("label %bytes.index.valid").length - 1; +} + +test("reads from the end of a typed array in a while loop are proven in bounds", async () => { + const ll = await emitLlvm(` +function code(c: number): number { + switch (c) { + case 65: return 84; + case 84: return 65; + } + return 0; +} +function reversed(line: Uint8Array): number { + const len = line.length; + const right = len - 1; + let i = 0; + let total = 0; + while (i < len) { + let c = line[right - i]!; + i++; + if (c === 10) { + if (i === len) break; + c = line[right - i]!; + i++; + } + total += code(c); + } + return total; +} +function pairs(values: Uint16Array): number { + let i = 0; + let total = 0; + while (values.length > i) { + i += 2; + total += values[i - 2]!; + if (i < values.length) total += values[values.length - 1 - i]!; + } + return total; +} +console.log(reversed(new Uint8Array([65, 10, 84])), pairs(new Uint16Array([1, 2, 3]))); +`); + expect(checks(ll, "reversed")).toBe(0); + expect(checks(ll, "pairs")).toBe(0); +}); + +test("reads a while loop does not keep in bounds keep their checks", async () => { + const ll = await emitLlvm(` +function inclusive(line: Uint8Array): number { + const len = line.length; + let i = 0; + let total = 0; + while (i <= len) { + total += line[len - i]!; + i++; + } + return total; +} +function unguarded(line: Uint8Array): number { + const right = line.length - 1; + let i = 0; + let total = 0; + while (i < line.length) { + total += line[right - i]!; + i++; + total += line[right - i]!; + } + return total; +} +function ahead(values: Uint16Array): number { + let i = 0; + let total = 0; + while (i < values.length) { + i += 2; + total += values[i - 1]!; + } + return total; +} +function nested(line: Uint8Array): number { + const len = line.length; + let i = 0; + let total = 0; + while (i < len) { + const c = line[len - 1 - i]!; + i++; + while (i < len && line[len - 1 - i]! === c) i++; + total += line[len - 1 - i]!; + } + return total; +} +console.log( + inclusive(new Uint8Array(2)), + unguarded(new Uint8Array(2)), + ahead(new Uint16Array(3)), + nested(new Uint8Array(3)), +); +`); + expect(checks(ll, "inclusive")).toBe(1); + // The read before the increment is in bounds; the one after it is not. + expect(checks(ll, "unguarded")).toBe(1); + expect(checks(ll, "ahead")).toBe(1); + // The read before the nested loop is in bounds. The nested loop's + // condition is not a bound, and the read after the loop can be at -1. + expect(checks(ll, "nested")).toBe(2); +}); diff --git a/tests/corpus/8003-while-reversed-byte-reads.ts b/tests/corpus/8003-while-reversed-byte-reads.ts new file mode 100644 index 000000000..9b2efd4f0 --- /dev/null +++ b/tests/corpus/8003-while-reversed-byte-reads.ts @@ -0,0 +1,168 @@ +// While loops that read a typed array or Buffer backwards from its length +// (`line[len - 1 - i]`, `line[right - i]`), with increments, guards that +// exit at the length, calls in the body, reads at or past either end, and +// counters advanced by nested loops. + +function complement(code: number): number { + switch (code) { + case 65: + return 84; + case 67: + return 71; + case 71: + return 67; + case 84: + return 65; + } + return 63; +} + +function reverseComplement(line: Uint8Array, target: Uint8Array): string { + const len = line.length; + const right = len - 1; + let ileft = 0; + let iright = 0; + let c: number; + while (iright < len) { + c = line[right - iright]!; + iright++; + if (c === 10) { + if (iright === len) break; + c = line[right - iright]!; + iright++; + } + target[ileft] = complement(c); + ileft++; + } + return Buffer.from(target.subarray(0, ileft)).toString("latin1"); +} + +const out = new Uint8Array(32); +console.log(reverseComplement(Buffer.from("ACGT\nTTGA\n"), out)); +console.log(reverseComplement(Buffer.from("\nGATTACA"), out)); +console.log(reverseComplement(Buffer.from("\n"), out)); +console.log(reverseComplement(Buffer.from("A\n\nC"), out)); +console.log(JSON.stringify(reverseComplement(new Uint8Array(0), out))); + +// Reads one past the end: the first pass reads line[len], which is undefined. +function inclusive(line: Uint8Array): string { + const len = line.length; + let i = 0; + const seen: (number | undefined)[] = []; + while (i <= len) { + seen.push(line[len - i]); + i++; + } + return JSON.stringify(seen.map((v) => (v === undefined ? "u" : v))); +} +console.log(inclusive(new Uint8Array([1, 2, 3])), inclusive(new Uint8Array(0))); + +// A read after an increment with no guard reads line[-1] on the last pass. +function unguarded(line: Uint8Array): string { + const right = line.length - 1; + let i = 0; + let total = 0; + let missing = 0; + while (i < line.length) { + total += line[right - i]!; + i++; + const next = line[right - i]; + if (next === undefined) missing++; + else total += next * 100; + } + return `${total} ${missing}`; +} +console.log(unguarded(new Uint8Array([1, 2, 3])), unguarded(new Uint8Array([9]))); + +// Guards in other forms: >=, !==, and a guard in an else branch. +function guards(line: Uint16Array): number[] { + const len = line.length; + const result: number[] = []; + let i = 0; + while (i < len) { + i += 1; + if (i >= len) { + result.push(-1); + break; + } + result.push(line[len - 1 - i]!); + } + let j = 0; + while (len > j) { + j++; + if (j !== len) result.push(line[len - 1 - j]! * 10); + else result.push(-2); + } + let k = 0; + while (k < len) { + k++; + if (line[0] === 0) { + result.push(-3); + } else if (k === len) { + break; + } else { + result.push(line[len - k - 1]! * 100); + } + } + return result; +} +console.log(guards(new Uint16Array([5, 6, 7]))); +console.log(guards(new Uint16Array([0, 1]))); +console.log(guards(new Uint16Array(0))); + +// A nested loop advances the counter, so the read after it can fall +// outside the array. +function skipRuns(line: Uint8Array): string { + const len = line.length; + let i = 0; + const parts: string[] = []; + while (i < len) { + const c = line[len - 1 - i]!; + i++; + while (i < len && line[len - 1 - i] === c) i++; + const after = line[len - 1 - i]; + parts.push(`${c}:${after === undefined ? "end" : after}`); + } + return parts.join(" "); +} +console.log(skipRuns(Buffer.from("aabbbc")), skipRuns(Buffer.from("zz"))); + +// A condition with <= against the last index, a labeled block, and +// counters that start past zero or past the end. +function labeled(line: Int8Array, start: number): number[] { + const len = line.length; + const right = len - 1; + const result: number[] = []; + let i = start; + while (i <= right) { + found: { + if (line[len - 1 - i]! < 0) break found; + result.push(line[len - 1 - i]!); + } + i += 2; + } + result.push(i); + return result; +} +console.log(labeled(new Int8Array([1, -2, 3, -4, 5]), 0)); +console.log(labeled(new Int8Array([1, -2, 3, -4, 5]), 1)); +console.log(labeled(new Int8Array([1, -2, 3]), 5)); + +// An exception thrown from a call in the body. +function throwing(line: Uint8Array): string { + const len = line.length; + let i = 0; + let total = 0; + try { + while (i < len) { + const c = line[len - 1 - i]!; + i++; + if (c === 0) throw new Error(`zero after ${total}`); + total += c; + } + } catch (error) { + return (error as Error).message; + } + return String(total); +} +console.log(throwing(new Uint8Array([4, 0, 2, 3])), throwing(new Uint8Array([1, 2]))); diff --git a/tests/harness/corpus-costs/8003-while-reversed-byte-reads.ts.json b/tests/harness/corpus-costs/8003-while-reversed-byte-reads.ts.json new file mode 100644 index 000000000..6c521804e --- /dev/null +++ b/tests/harness/corpus-costs/8003-while-reversed-byte-reads.ts.json @@ -0,0 +1 @@ +{ "plain": 2359, "san": 4025 } From c8ee28de80b24f5b35f7f99eb811d6ba9525e1e1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 18:23:17 -0500 Subject: [PATCH 130/273] Write slices stored in records that are only read into stack buffers A slice whose every reader only borrows it is written into a stack buffer instead of a heap allocation. A slice stored in a record field counted as kept, so a tokenizer's `const text = source.slice(start, i); const t = token(kind, text, start); sum += weight(t);` allocated and freed a string per token even though `t` is read only for `t.text.length`. A slice local stored directly in a field of a stable record local, built by a record literal or by a factory that only returns a record literal of its parameters, is now lent when the record is only projected: its field reads are lending reads, and it is passed only to record parameters whose bodies read that field only in lending positions (a fixpoint, like the one for lent string parameters). A record that is returned, stored, captured or passed anywhere else keeps the slice on the heap. includes, indexOf and lastIndexOf on string arrays now lend their search argument. --- .../src/backend/llvm/emit-function.ts | 2 +- packages/compiler/src/backend/llvm/emitter.ts | 19 +- .../src/backend/llvm/string-slices.test.ts | 184 ++++++++++- .../src/backend/llvm/string-slices.ts | 288 +++++++++++++++++- .../corpus/8114-record-field-slices.ts.json | 6 + tests/corpus/8114-record-field-slices.ts | 122 ++++++++ 6 files changed, 597 insertions(+), 24 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8114-record-field-slices.ts.json create mode 100644 tests/corpus/8114-record-field-slices.ts diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index a3ebbe0e8..ca45a6624 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -191,7 +191,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { fn, emitter.callLifetimes, emitter.scalarStringSlices, - emitter.lentStringParameters, + emitter.lendingFacts, ) : new Set(); emitter.mapReadLifetimes = findMapReadLifetimes( diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 586517356..ce698be55 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -200,8 +200,10 @@ import { StackCallbacks } from "./stack-callbacks.js"; import { findAppendedArrayLiterals } from "./appended-array-literals.js"; import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callbacks.js"; import { + findLentRecordFields, findLentStringParameters, - type LentStringParameters, + findRecordFieldProducers, + type LendingFacts, type StringSliceSnapshot, } from "./string-slices.js"; import { LazyCaptures } from "./lazy-captures.js"; @@ -792,8 +794,9 @@ export class LlEmitter { readonly stringSlices = new Map(); /** Slices written into stack buffers (findScratchStringSlices). */ scratchStringSlices: ReadonlySet = new Set(); - /** String parameters a caller may lend scratch slices to. */ - lentStringParameters: LentStringParameters = new Map(); + /** String parameters, record fields and record producers a caller may + * lend scratch slices to. */ + lendingFacts: LendingFacts = { strings: new Map(), records: new Map(), producers: new Map() }; localStackUnions = new Map(); localStackObjects = new Map(); reassignedStackObjects = new Set(); @@ -1893,8 +1896,14 @@ export class LlEmitter { }, (wrap) => this.borrowableNullableWrap(wrap) === "ref", ); - this.lentStringParameters = - this.debug === null ? findLentStringParameters(this.fnByName, this.callLifetimes) : new Map(); + if (this.debug === null) { + const strings = findLentStringParameters(this.fnByName, this.callLifetimes); + this.lendingFacts = { + strings, + records: findLentRecordFields(this.fnByName, this.callLifetimes, strings), + producers: findRecordFieldProducers(this.fnByName), + }; + } this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes, this.nodeIndex); // Integer clones signal a stack overflow through their result, which // needs the single-threaded guard; worker programs also poll for diff --git a/packages/compiler/src/backend/llvm/string-slices.test.ts b/packages/compiler/src/backend/llvm/string-slices.test.ts index b2656e560..43bec955a 100644 --- a/packages/compiler/src/backend/llvm/string-slices.test.ts +++ b/packages/compiler/src/backend/llvm/string-slices.test.ts @@ -1,8 +1,19 @@ import { expect, test } from "vitest"; -import { BOOL, F64, STRING, VOID, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js"; +import { + BOOL, + F64, + STRING, + VOID, + type IrExpr, + type IrFunction, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; import { analyzeCallLifetimes } from "./call-lifetimes.js"; import { + findLentRecordFields, findLentStringParameters, + findRecordFieldProducers, findScalarStringSlices, findScratchStringSlices, } from "./string-slices.js"; @@ -142,12 +153,12 @@ function lending(body: IrStmt[]): IrFunction { function scratch(fn: IrFunction, extra: IrFunction[] = []): Set { const functions = new Map([fn, ...extra].map((f) => [f.name, f])); const lifetimes = analyzeCallLifetimes(functions); - return findScratchStringSlices( - fn, - lifetimes, - findScalarStringSlices(fn, lifetimes), - findLentStringParameters(functions, lifetimes), - ); + const strings = findLentStringParameters(functions, lifetimes); + return findScratchStringSlices(fn, lifetimes, findScalarStringSlices(fn, lifetimes), { + strings, + records: findLentRecordFields(functions, lifetimes, strings), + producers: findRecordFieldProducers(functions), + }); } test("slices read only by lending consumers use a stack buffer", () => { const inline = slice("source", 0, 3); @@ -242,3 +253,162 @@ test("helper parameters read only by lending consumers accept stack slices", () const all = new Map([fn, ...functions].map((f) => [f.name, f])); expect([...findLentStringParameters(all, analyzeCallLifetimes(all)).keys()]).toEqual(["isGet"]); }); + +const TOKEN: IrType = { kind: "record", shapeId: "token" }; +const record = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: TOKEN, loc }); +const textOf = (obj: IrExpr): IrExpr => ({ + kind: "recordGet", + obj, + shapeId: "token", + field: "text", + type: STRING, + loc, +}); +const lengthOf = (receiver: IrExpr): IrExpr => ({ + kind: "strIntrinsic", + method: "length", + receiver, + args: [], + type: F64, + loc, +}); +const tokenLit = (text: IrExpr): IrExpr => ({ + kind: "recordLit", + fields: [ + { name: "text", value: text }, + { name: "pos", value: { kind: "numLit", value: 1, type: F64, loc } }, + ], + type: TOKEN, + loc, +}); +const keepIn = (value: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "arrayLit", elems: [value], type: { kind: "array", elem: STRING }, loc }, + loc, +}); +/** A function of one record parameter `t`. */ +function recordHelper(name: string, body: IrStmt[]): IrFunction { + return { + name, + params: [{ localId: "t", name: "t", type: TOKEN }], + locals: [{ id: "t", name: "t", type: TOKEN, mutable: false }], + returnType: F64, + loc, + body, + }; +} +/** `make(text)` returns `{ text, pos: 1 }`; `twice` also reads its parameter elsewhere. */ +function producerHelper(name: string, extra: IrStmt[] = []): IrFunction { + return { + name, + params: [{ localId: "text", name: "text", type: STRING }], + locals: [{ id: "text", name: "text", type: STRING, mutable: false }], + returnType: TOKEN, + loc, + body: [...extra, { kind: "return", value: tokenLit(ref("text")), loc }], + }; +} +function containing(init: IrExpr, declare: IrExpr, uses: IrStmt[]): IrFunction { + const fn = lending([ + { kind: "varDecl", localId: "word", init, loc }, + { kind: "varDecl", localId: "tok", init: declare, loc }, + ...uses, + ]); + fn.locals.push({ id: "tok", name: "tok", type: TOKEN, mutable: false }); + return fn; +} +const reads = recordHelper("reads", [ + { kind: "return", value: lengthOf(textOf(record("t"))), loc }, +]); +const keepsText = recordHelper("keepsText", [ + keepIn(textOf(record("t"))), + { kind: "return", value: { kind: "numLit", value: 0, type: F64, loc }, loc }, +]); +const escapes = recordHelper("escapes", [ + { + kind: "exprStmt", + expr: { kind: "arrayLit", elems: [record("t")], type: { kind: "array", elem: TOKEN }, loc }, + loc, + }, + { kind: "return", value: { kind: "numLit", value: 0, type: F64, loc }, loc }, +]); +const passCall = (callee: string, arg: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "call", callee, args: [arg], type: F64, loc }, + loc, +}); + +test("a slice stored in a record that is only projected uses a stack buffer", () => { + const helpers = [reads, keepsText, escapes, producerHelper("make")]; + for (const declare of [tokenLit(ref("word")), callOf("make", ref("word"))]) { + if (declare.kind === "call") declare.type = TOKEN; + const init = slice("source", 1, 5); + const fn = containing(init, declare, [ + { kind: "exprStmt", expr: equals(textOf(record("tok"))), loc }, + passCall("reads", record("tok")), + ]); + expect(scratch(fn, helpers).has(init)).toBe(true); + } + const all = new Map(helpers.map((f) => [f.name, f])); + const lifetimes = analyzeCallLifetimes(all); + const lent = findLentRecordFields(all, lifetimes, findLentStringParameters(all, lifetimes)); + expect(lent.get("reads")?.get(0)).toEqual(new Set()); + expect(lent.get("keepsText")?.get(0)).toEqual(new Set(["text"])); + expect(lent.has("escapes")).toBe(false); + expect(findRecordFieldProducers(all).get("make")).toEqual(new Map([[0, "text"]])); +}); + +test.each([ + "kept-field", + "returned", + "kept-by-callee", + "escapes-callee", + "captured", + "producer-reuse", +])("a slice stored in a record that may be kept (%s) stays owned", (reason) => { + const helpers = [ + reads, + keepsText, + escapes, + producerHelper("make"), + producerHelper("twice", [keepIn(ref("text"))]), + ]; + const init = slice("source", 1, 5); + const declare = + reason === "producer-reuse" ? callOf("twice", ref("word")) : tokenLit(ref("word")); + if (declare.kind === "call") declare.type = TOKEN; + const uses: IrStmt[] = [{ kind: "exprStmt", expr: equals(textOf(record("tok"))), loc }]; + if (reason === "kept-field") uses.push(keepIn(textOf(record("tok")))); + if (reason === "returned") uses.push({ kind: "return", value: record("tok"), loc }); + if (reason === "kept-by-callee") uses.push(passCall("keepsText", record("tok"))); + if (reason === "escapes-callee") uses.push(passCall("escapes", record("tok"))); + const fn = containing(init, declare, uses); + if (reason === "captured") fn.locals.find((l) => l.id === "tok")!.boxed = true; + expect(scratch(fn, helpers).has(init)).toBe(false); +}); + +test("searches of string arrays lend their needle", () => { + const keywords: IrExpr = { + kind: "varRef", + localId: "keywords", + type: { kind: "array", elem: STRING }, + loc, + }; + const classify = helper("classify", [ + { + kind: "return", + value: { + kind: "arrIntrinsic", + method: "includes", + receiver: keywords, + args: [ref("text")], + type: BOOL, + loc, + }, + loc, + }, + ]); + const init = slice("source", 0, 3); + const fn = lending([{ kind: "exprStmt", expr: callOf("classify", init), loc }]); + expect(scratch(fn, [classify]).has(init)).toBe(true); +}); diff --git a/packages/compiler/src/backend/llvm/string-slices.ts b/packages/compiler/src/backend/llvm/string-slices.ts index d00c681d6..81a4a5a3e 100644 --- a/packages/compiler/src/backend/llvm/string-slices.ts +++ b/packages/compiler/src/backend/llvm/string-slices.ts @@ -145,6 +145,10 @@ const LENDING_LIB_FNS: ReadonlySet = new Set([ "num.parseFloat", ]); +/** Searches of string arrays compare the needle with the elements during + * the call. */ +const LENDING_ARRAY_SEARCHES: ReadonlySet = new Set(["includes", "indexOf", "lastIndexOf"]); + /** Typed string-keyed collection operations: reads probe with the key, and * an insertion stores scr_str_scratch_keep's copy (scr_map_append). */ const LENDING_KEY_METHODS: ReadonlySet = new Set(["get", "has", "set", "add", "delete"]); @@ -176,6 +180,12 @@ function lentOperands(value: IrExpr, parameters: LentStringParameters): readonly return LENDING_KEY_METHODS.has(value.method) && stringKeyed(value) ? value.args.slice(0, 1) : []; + case "arrIntrinsic": + return LENDING_ARRAY_SEARCHES.has(value.method) && + value.receiver.type.kind === "array" && + value.receiver.type.elem.kind === "string" + ? value.args.slice(0, 1) + : []; case "call": { const lent = parameters.get(value.callee); return lent ? value.args.filter((_, index) => lent.has(index)) : []; @@ -185,41 +195,123 @@ function lentOperands(value: IrExpr, parameters: LentStringParameters): readonly } } +/** Record parameters, by function and index, that the body only projects: + * it reads their fields and passes them only to such parameters. The set + * lists the string fields some read keeps (reads outside a lending + * position); every other string field is lent (findLentRecordFields). */ +export type LentRecordFields = ReadonlyMap>>; + +/** Functions whose body only returns a record literal that stores some + * parameters, each read once, directly as fields: by function, parameter + * index to field (findRecordFieldProducers). A call stores those arguments + * in the fields of its result and keeps them nowhere else. */ +export type RecordFieldProducers = ReadonlyMap>; + +/** What lends a string: lent string parameters, lent record fields and + * record field producers. */ +export interface LendingFacts { + strings: LentStringParameters; + records: LentRecordFields; + producers: RecordFieldProducers; +} + +const NO_LENDING_FACTS: LendingFacts = { + strings: new Map(), + records: new Map(), + producers: new Map(), +}; + interface LendingScan { /** Slice operands of lending consumers, excluding any node also reached * in an owning position. */ scratch: Set; /** Candidates read anywhere other than a lending position. */ invalid: Set; - /** Direct callees, for the parameter fixpoint. */ + /** Record candidates read other than by a field read or a lent record + * argument. */ + invalidRecords: Set; + /** String fields of record candidates read outside a lending position. */ + keptFields: Map>; + /** Direct callees, for the parameter fixpoints. */ callees: Set; } /** One pass over `fn`: which candidate bindings only lending consumers read * (a slice initializer of a candidate local is not itself a read), and * which slice operands those consumers lend. A switch discriminant is only - * compared with the case tests. */ + * compared with the case tests. Record candidates (`records`) may only be + * projected: their field reads are checked as reads of the field, and they + * may be passed to lent record parameters. A record `container` + * declaration stores candidates in fields of a record candidate; those + * stores are not reads. */ function scanLending( fn: IrFunction, candidates: ReadonlySet, initializers: ReadonlyMap, - parameters: LentStringParameters, + facts: LendingFacts, + records: ReadonlySet = new Set(), + containers: ReadonlyMap = new Map(), ): LendingScan { - const scan: LendingScan = { scratch: new Set(), invalid: new Set(), callees: new Set() }; + const scan: LendingScan = { + scratch: new Set(), + invalid: new Set(), + invalidRecords: new Set(), + keptFields: new Map(), + callees: new Set(), + }; const owned = new Set(); + const keep = (record: string, field: string): void => { + let fields = scan.keptFields.get(record); + if (!fields) scan.keptFields.set(record, (fields = new Set())); + fields.add(field); + }; + const projected = (value: IrExpr): value is IrExpr & { kind: "recordGet" } => + value.kind === "recordGet" && value.obj.kind === "varRef" && records.has(value.obj.localId); function lent(value: IrExpr): boolean { if (value.kind === "varRef" && candidates.has(value.localId)) return true; + if (projected(value)) return true; if (!isSlice(value)) return expr(value); scan.scratch.add(value); return everyExprChild(value, expr, stmt); } + /** A call's arguments: lent string parameters lend, lent record + * parameters project their record, and `stored` arguments (a producer's + * fields in a container declaration) are not reads. */ + function callArgs(value: IrExpr & { kind: "call" }, stored: ReadonlySet | null): void { + scan.callees.add(value.callee); + const strings = facts.strings.get(value.callee); + const recordParams = facts.records.get(value.callee); + value.args.forEach((arg, index) => { + if (stored !== null && stored.has(index)) return; + const kept = recordParams?.get(index); + if (kept && arg.kind === "varRef" && records.has(arg.localId)) { + for (const field of kept) keep(arg.localId, field); + return; + } + if (strings?.has(index)) lent(arg); + else expr(arg); + }); + } function expr(value: IrExpr): boolean { if (isSlice(value)) owned.add(value); + if (projected(value)) { + if (value.type.kind === "string") + keep((value.obj as { localId: string }).localId, value.field); + return true; + } if (value.kind === "varRef" && candidates.has(value.localId)) scan.invalid.add(value.localId); + if (value.kind === "varRef" && records.has(value.localId)) + scan.invalidRecords.add(value.localId); if (value.kind === "closure" || value.kind === "classRef") - for (const id of value.captures ?? []) scan.invalid.add(id); - if (value.kind === "call") scan.callees.add(value.callee); - const operands = lentOperands(value, parameters); + for (const id of value.captures ?? []) { + scan.invalid.add(id); + scan.invalidRecords.add(id); + } + if (value.kind === "call") { + callArgs(value, null); + return true; + } + const operands = lentOperands(value, facts.strings); if (operands.length === 0) return everyExprChild(value, expr, stmt); return everyExprChild( value, @@ -231,6 +323,20 @@ function scanLending( // A candidate's single declaration initializes it with its slice. if (value.kind === "varDecl" && value.init !== null && initializers.has(value.localId)) return everyExprChild(value.init, expr, stmt); + const container = value.kind === "varDecl" ? containers.get(value.localId) : undefined; + if (container && value.kind === "varDecl" && value.init !== null) { + const init = value.init; + if (init.kind === "recordLit") + for (const field of init.fields) { + if (!container.stored.has(field.value)) expr(field.value); + } + else if (init.kind === "call") + callArgs( + init, + new Set(init.args.flatMap((arg, i) => (container.stored.has(arg) ? [i] : []))), + ); + return true; + } if (value.kind === "switch" && value.disc.type.kind === "string") { lent(value.disc); for (const entry of value.cases) { @@ -276,7 +382,7 @@ export function findLentStringParameters( const fn = functions.get(name); if (!indexes || !fn) continue; const ids = new Set([...indexes].map((index) => fn.params[index]!.localId)); - const scan = scanLending(fn, ids, new Map(), lent); + const scan = scanLending(fn, ids, new Map(), { ...NO_LENDING_FACTS, strings: lent }); for (const callee of scan.callees) { let set = callers.get(callee); if (!set) callers.set(callee, (set = new Set())); @@ -296,6 +402,148 @@ export function findLentStringParameters( return lent; } +/** Functions whose whole body is `return { ... }`, a record literal storing + * some parameters, each read nowhere else, directly as fields (a factory + * such as `token(kind, text, pos)`). A synchronous body only: the result + * then holds those arguments, and nothing else does. */ +export function findRecordFieldProducers( + functions: ReadonlyMap, +): Map> { + const producers = new Map>(); + for (const fn of functions.values()) { + if (fn.async || fn.generator || fn.body.length !== 1) continue; + const only = fn.body[0]!; + if (only.kind !== "return" || only.value?.kind !== "recordLit") continue; + const literal = only.value; + const reads = new Map(); + everyStmtList(fn.body, { + expr: (value) => { + if (value.kind === "varRef") reads.set(value.localId, (reads.get(value.localId) ?? 0) + 1); + return true; + }, + stmt: () => true, + }); + const fields = new Map(); + fn.params.forEach((param, index) => { + if (param.type.kind !== "string" || reads.get(param.localId) !== 1) return; + const field = literal.fields.find( + (entry) => + !entry.overflow && entry.value.kind === "varRef" && entry.value.localId === param.localId, + ); + if (field) fields.set(index, field.name); + }); + if (fields.size > 0) producers.set(fn.name, fields); + } + return producers; +} + +/** Record parameters that every body only projects (LentRecordFields), with + * the string fields some read keeps. Start from every borrowed record + * parameter of a synchronous function, keeping no field, and grow the kept + * fields and drop escaping parameters until nothing changes; a record passed + * to another lent record parameter keeps what that parameter keeps. */ +export function findLentRecordFields( + functions: ReadonlyMap, + lifetimes: CallLifetimes, + strings: LentStringParameters, +): Map>> { + const lent = new Map>>(); + for (const fn of functions.values()) { + if (fn.async || fn.generator) continue; + const stable = lifetimes.borrowed.get(fn.name); + const indexes = new Map>(); + fn.params.forEach((param, index) => { + if (param.type.kind === "record" && stable?.has(index)) indexes.set(index, new Set()); + }); + if (indexes.size > 0) lent.set(fn.name, indexes); + } + const callers = new Map>(); + const pending = new Set(lent.keys()); + while (pending.size > 0) { + const name = pending.values().next().value as string; + pending.delete(name); + const indexes = lent.get(name); + const fn = functions.get(name); + if (!indexes || !fn) continue; + const ids = new Set([...indexes.keys()].map((index) => fn.params[index]!.localId)); + const scan = scanLending( + fn, + new Set(), + new Map(), + { strings, records: lent, producers: new Map() }, + ids, + ); + for (const callee of scan.callees) { + let set = callers.get(callee); + if (!set) callers.set(callee, (set = new Set())); + set.add(name); + } + let changed = false; + for (const [index, kept] of [...indexes]) { + const id = fn.params[index]!.localId; + if (scan.invalidRecords.has(id)) { + indexes.delete(index); + changed = true; + continue; + } + const fields = scan.keptFields.get(id); + if (fields) + for (const field of fields) { + if (kept.has(field)) continue; + kept.add(field); + changed = true; + } + } + if (!changed) continue; + if (indexes.size === 0) lent.delete(name); + const affected = callers.get(name); + if (affected) for (const caller of affected) if (lent.has(caller)) pending.add(caller); + } + return lent; +} + +/** A record local whose declaration stores slice locals directly in some of + * its fields: a record literal, or a call to a record field producer. */ +interface RecordContainer { + /** The stored candidate reads of the declaration, by field. */ + stored: Map; +} + +/** Record locals declared with slice locals in their fields. */ +function findRecordContainers( + fn: IrFunction, + bindings: ReadonlySet | undefined, + slices: ReadonlySet, + producers: RecordFieldProducers, +): Map { + const containers = new Map(); + everyStmtList(fn.body, { + expr: () => true, + stmt: (stmt) => { + if (stmt.kind !== "varDecl" || stmt.init === null || !bindings?.has(stmt.localId)) + return true; + if (stmt.init.type.kind !== "record") return true; + const stored = new Map(); + const init = stmt.init; + if (init.kind === "recordLit") { + for (const field of init.fields) + if (!field.overflow && field.value.kind === "varRef" && slices.has(field.value.localId)) + stored.set(field.value, { field: field.name, slice: field.value.localId }); + } else if (init.kind === "call") { + const fields = producers.get(init.callee); + init.args.forEach((arg, index) => { + const field = fields?.get(index); + if (field !== undefined && arg.kind === "varRef" && slices.has(arg.localId)) + stored.set(arg, { field, slice: arg.localId }); + }); + } + if (stored.size > 0) containers.set(stmt.localId, { stored }); + return true; + }, + }); + return containers; +} + /** Slices whose every reader borrows them are written into a stack buffer * instead of a heap allocation (scr_str_slice_scratch): a slice operand of * a lending consumer, and the initializer of a stable local read only by @@ -304,12 +552,18 @@ export function findLentStringParameters( * non-ASCII results keep the ordinary owned string. Each site owns its * buffer: a site runs again only after its previous value is dead, since * nothing can keep it. Coroutine frames do not keep stack buffers across a - * suspension, so they are left alone. */ + * suspension, so they are left alone. + * + * A slice local may also be stored in a field of a stable record local + * declared with it (a record literal, or a record field producer's result): + * the record is then only projected, and every read of that field lends. + * The record is declared after the slice in its scope and is never kept, so + * it is dead before the slice's site runs again. */ export function findScratchStringSlices( fn: IrFunction, lifetimes: CallLifetimes, scalar: ReadonlyMap, - parameters: LentStringParameters = new Map(), + facts: LendingFacts = NO_LENDING_FACTS, ): Set { if (fn.async || fn.generator) return new Set(); const bindings = lifetimes.bindings.get(fn.name); @@ -328,7 +582,19 @@ export function findScratchStringSlices( return true; }, }); - const scan = scanLending(fn, new Set(locals.keys()), locals, parameters); + const containers = findRecordContainers(fn, bindings, new Set(locals.keys()), facts.producers); + const scan = scanLending( + fn, + new Set(locals.keys()), + locals, + facts, + new Set(containers.keys()), + containers, + ); + for (const [record, container] of containers) + for (const { field, slice } of container.stored.values()) + if (scan.invalidRecords.has(record) || scan.keptFields.get(record)?.has(field)) + scan.invalid.add(slice); for (const [id, init] of locals) if (!scan.invalid.has(id)) scan.scratch.add(init); return scan.scratch; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8114-record-field-slices.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8114-record-field-slices.ts.json new file mode 100644 index 000000000..58ce71d85 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8114-record-field-slices.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8114-record-field-slices.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8114-record-field-slices.ts b/tests/corpus/8114-record-field-slices.ts new file mode 100644 index 000000000..69d41a1c8 --- /dev/null +++ b/tests/corpus/8114-record-field-slices.ts @@ -0,0 +1,122 @@ +// Slices stored in record fields: records built from a literal or a factory +// whose fields are only read for their length, comparisons and keyword +// searches, next to records that are kept after the iteration that built +// them (pushed into an array, held in an outer binding, captured by a +// closure, read into a kept array, returned from a helper). Slices of +// non-ASCII text and slices longer than a short token are included. +interface Token { + kind: string; + text: string; + pos: number; +} + +const KEYWORDS = ["let", "const", "if", "else", "return"]; + +function token(kind: string, text: string, pos: number): Token { + return { kind, text, pos }; +} + +function classify(text: string): string { + return KEYWORDS.includes(text) ? "keyword" : "ident"; +} + +function weight(t: Token): number { + return t.kind === "keyword" ? t.text.length * 3 : t.text.length + (t.pos & 7); +} + +function isReturn(t: Token): boolean { + return t.text === "return"; +} + +function textOf(t: Token): string { + return t.text; +} + +/** Calls `visit` with the start and end of every run of word characters. */ +function words(source: string, visit: (start: number, end: number) => void): void { + let start = -1; + for (let i = 0; i <= source.length; i++) { + const c = i < source.length ? source.charCodeAt(i) : 32; + const word = c > 32 && c !== 44 && c !== 59; + if (word) { + if (start < 0) start = i; + } else if (start >= 0) { + visit(start, i); + start = -1; + } + } +} + +function lentTokens(source: string): string { + let sum = 0; + let returns = 0; + let longest = 0; + for (let i = 0, start = -1; i <= source.length; i++) { + const c = i < source.length ? source.charCodeAt(i) : 32; + if (c > 32 && c !== 44 && c !== 59) { + if (start < 0) start = i; + continue; + } + if (start < 0) continue; + const text = source.slice(start, i); + const t = token(classify(text), text, start); + sum += weight(t); + if (isReturn(t)) returns++; + const literal = { text, pos: start }; + if (literal.text.length > longest) longest = literal.text.length; + start = -1; + } + return `lent ${sum} ${returns} ${longest}`; +} + +function keptTokens(source: string): string { + const kept: Token[] = []; + const texts: string[] = []; + const later: (() => string)[] = []; + const returned: string[] = []; + let last: Token | null = null; + for (let i = 0, start = -1; i <= source.length; i++) { + const c = i < source.length ? source.charCodeAt(i) : 32; + if (c > 32 && c !== 44 && c !== 59) { + if (start < 0) start = i; + continue; + } + if (start < 0) continue; + const text = source.slice(start, i); + const pushed = token(classify(text), text, start); + kept.push(pushed); + const held = { kind: "held", text, pos: start }; + last = held; + const read = token("read", source.slice(start, i), start); + texts.push(read.text); + const captured = { kind: "captured", text: source.slice(start, i), pos: start }; + later.push(() => captured.text); + const viaHelper = token("helper", source.slice(start, i), start); + returned.push(textOf(viaHelper)); + start = -1; + } + return [ + `kept ${kept.map((t) => `${t.kind}:${t.text}`).join(" ")}`, + `last ${last === null ? "none" : last.text}`, + `texts ${texts.join("|")}`, + `later ${later.map((f) => f()).join("|")}`, + `returned ${returned.join("|")}`, + ].join("\n"); +} + +const ascii = "let total = count; if (total) return total; else return 0;"; +const mixed = "lét naïve = café; return 東京, x"; +const long = `const ${"identifier".repeat(12)} = ${"v".repeat(150)};`; + +for (const source of [ascii, mixed, long]) { + console.log(lentTokens(source)); + console.log(keptTokens(source)); +} + +let count = 0; +words(ascii, (start, end) => { + const text = ascii.slice(start, end); + const t = token(classify(text), text, start); + count += weight(t); +}); +console.log("callback", count); From 61c32fbc4803a91cf3769c42603fdbfca200c47b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 18:24:13 -0500 Subject: [PATCH 131/273] Keep Map.get results owned when a copy or a temporary map can outlive the entry A borrowed Map.get local holds the entry's own reference, so every use must run before anything that can release the entry. Two cases escaped that proof: - A local declared from the borrowed local (`const copy = held`) is a stable alias of it, with no reference of its own. Its reads after a write to the map were not counted as uses. Reference locals declared from the local, at any depth of the statement that declares them, now count as its uses. - A read from a map that no local holds (`fresh().get(k)`) borrowed an entry of a map released at the end of the declaration. The map must now be a local or a field chain rooted at one, whose changes the proof sees as edge changes. Both keep the owned read. Corpus 8181 covers the two cases, and the emission test checks that their reads retain the value. --- .../src/backend/llvm/map-read-lifetimes.ts | 57 +++++++++++++++---- .../test/map-get-borrow-emission.test.ts | 26 ++++++++- tests/corpus/8181-map-get-borrows.ts | 27 ++++++++- 3 files changed, 96 insertions(+), 14 deletions(-) diff --git a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts index 0f22f9e05..3371a7f70 100644 --- a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts @@ -166,6 +166,17 @@ export function findMapReadLifetimes( return result; } +/** Whether a map read's receiver keeps the map past its own statement: a + * local or a field of one. Changing either is an edge change the borrow + * proof sees. A temporary map (a call's result, `new Map()`) is released at + * the end of the declaration, and its entries with it. */ +function heldReceiver(expr: IrExpr): boolean { + if (expr.kind === "varRef") return true; + if (expr.kind === "fieldGet" || expr.kind === "recordGet") return heldReceiver(expr.obj); + if (expr.kind === "unionNarrow" || expr.kind === "downcast") return heldReceiver(expr.value); + return false; +} + /** The locals, declared by statements `candidate` accepts, that can hold a * map entry's own reference instead of a +1 copy. The statements after the * declaration run in order, and every one that names the local must @@ -174,7 +185,9 @@ export function findMapReadLifetimes( * while the local is read. An `if` passes its condition's state to both * branches and joins them; any other statement that names the local is * judged whole. A later pass of an enclosing loop declares the local again, - * so only the rest of its own statement list matters. An update such as + * so only the rest of its own statement list matters. The map must be held + * by a local or a field (heldReceiver), and locals declared from the local + * count as its uses. An update such as * `const c = m.get(k); if (c === undefined) m.set(k, v); else c.n++` keeps * the borrow: the write runs only on the branch that does not read `c`. */ export function findMapReadBorrows( @@ -184,12 +197,14 @@ export function findMapReadBorrows( ): Set { const result = new Set(); if (fn.async || fn.generator) return result; - const names = (node: IrStmt | IrExpr, id: string, isStmt: boolean): boolean => { + // `ids` holds the local and the locals declared from it: a stable alias + // (`const d = c`) reads the same entry without a reference of its own. + const names = (node: IrStmt | IrExpr, ids: ReadonlySet, isStmt: boolean): boolean => { let found = false; const visitor = { stmt: () => !found, expr: (expr: IrExpr) => { - if (expr.kind === "varRef" && expr.localId === id) found = true; + if (expr.kind === "varRef" && ids.has(expr.localId)) found = true; return !found; }, }; @@ -197,28 +212,48 @@ export function findMapReadBorrows( else everyExpr(node as IrExpr, visitor); return found; }; + // Adds the reference locals that `stmt` declares from one in `ids`, at any + // depth: a `var` inside a loop or a try stays in scope after it. + const alias = (stmt: IrStmt, ids: Set): void => { + everyStmt(stmt, { + stmt: (node) => { + if ( + node.kind === "varDecl" && + node.init !== null && + isRefCounted(node.init.type) && + names(node.init, ids, false) + ) + ids.add(node.localId); + return true; + }, + expr: () => true, + }); + }; // Whether an edge may have changed after `list`, or null when a statement // names the local after such a change (or changes edges itself). - const walk = (list: readonly IrStmt[], id: string, changed: boolean): boolean | null => { + const walk = (list: readonly IrStmt[], ids: Set, changed: boolean): boolean | null => { for (const stmt of list) { if (stmt.kind === "if") { const preserving = effects.preserves(stmt.cond); - if (names(stmt.cond, id, false) && (changed || !preserving)) return null; + if (names(stmt.cond, ids, false) && (changed || !preserving)) return null; const condition = changed || !preserving; - const then = walk(stmt.then, id, condition); - const otherwise = stmt.else_ === null ? condition : walk(stmt.else_, id, condition); + const then = walk(stmt.then, ids, condition); + const otherwise = stmt.else_ === null ? condition : walk(stmt.else_, ids, condition); if (then === null || otherwise === null) return null; changed = then || otherwise; continue; } if (stmt.kind === "block") { - const inner = walk(stmt.body, id, changed); + const inner = walk(stmt.body, ids, changed); if (inner === null) return null; changed = inner; continue; } const preserving = effects.preservesScope([stmt]); - if (names(stmt, id, true) && (changed || !preserving)) return null; + if (names(stmt, ids, true)) { + if (changed || !preserving) return null; + alias(stmt, ids); + } changed ||= !preserving; } return changed; @@ -227,8 +262,10 @@ export function findMapReadBorrows( list.forEach((stmt, index) => { if ( stmt.kind === "varDecl" && + stmt.init?.kind === "mapIntrinsic" && + heldReceiver(stmt.init.receiver) && candidate(stmt) && - walk(list.slice(index + 1), stmt.localId, false) !== null + walk(list.slice(index + 1), new Set([stmt.localId]), false) !== null ) result.add(stmt.localId); for (const nested of nestedStmtLists(stmt)) visit(nested); diff --git a/packages/compiler/test/map-get-borrow-emission.test.ts b/packages/compiler/test/map-get-borrow-emission.test.ts index 950fe35c2..eba430453 100644 --- a/packages/compiler/test/map-get-borrow-emission.test.ts +++ b/packages/compiler/test/map-get-borrow-emission.test.ts @@ -32,9 +32,26 @@ function replaced(m: Map): number { m.set(1, new Counter()); return old === undefined ? 0 : old.num; } +function aliased(m: Map): number { + const held = m.get(1); + const copy = held; + m.set(1, new Counter()); + return copy === undefined ? 0 : copy.num; +} +function fresh(): Map { + return new Map([[1, new Counter()]]); +} +function temporary(keys: number[]): number { + let total = 0; + for (const key of keys) { + const held = fresh().get(key); + if (held !== undefined) total += held.num; + } + return total; +} const numbers = new Map(); count(numbers, [1, 2, 1]); -console.log(named(new Map([["a", new Counter()]]), "a"), replaced(numbers)); +console.log(named(new Map([["a", new Counter()]]), "a"), replaced(numbers), aliased(numbers), temporary([1, 2])); `, ); const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); @@ -50,8 +67,11 @@ console.log(named(new Map([["a", new Counter()]]), "a"), replaced(numbers)); expect(fn(name)).not.toContain("@sc_retain_Counter"); expect(fn(name)).not.toContain("@sc_release_Counter"); } - // A write between the read and its use keeps the read's own reference. - expect(fn("replaced")).toContain("@sc_retain_Counter"); + // A write between the read and its use keeps the read's own reference, + // also when the use goes through a local declared from the read, and so + // does a read from a map that is released after the declaration. + for (const name of ["replaced", "aliased", "temporary"]) + expect(fn(name)).toContain("@sc_retain_Counter"); } finally { await rm(dir, { recursive: true, force: true }); } diff --git a/tests/corpus/8181-map-get-borrows.ts b/tests/corpus/8181-map-get-borrows.ts index be546cf73..5df7ad226 100644 --- a/tests/corpus/8181-map-get-borrows.ts +++ b/tests/corpus/8181-map-get-borrows.ts @@ -1,7 +1,8 @@ // Map.get results held in locals across the operations that can and cannot // release the entry's value: an update on the absent branch, a write, delete // or clear before a later read, a call that may change the map, loops, -// string keys, and values that are stored, returned or compared. +// string keys, values that are stored, returned or compared, a local declared +// from the read, and a read from a temporary map. class Counter { num: number; label: string; @@ -128,3 +129,27 @@ function loopWrites(m: Map): number { return total; } console.log("loop", loopWrites(new Map([[0, new Counter(1, "a")], [1, new Counter(2, "b")]]))); + +// A local declared from the read is read after a write that replaces the +// entry; it reads the value the map held when the local was declared. +function aliasThenWrite(m: Map): string { + const held = m.get(1); + const copy = held; + m.set(1, new Counter(10, "ten")); + m.delete(1); + const filler = [new Counter(11, "filler"), new Counter(12, "filler")]; + return copy === undefined ? "none" : `${copy.num}:${copy.label}:${filler.length}`; +} +console.log("alias", aliasThenWrite(new Map([[1, new Counter(1, "one")]]))); + +// A read from a map that no local holds: the map is released after the +// declaration, and the value stays readable. +function freshMap(): Map { + return new Map([[1, new Counter(5, "five")]]); +} +function fromTemporary(): string { + const held = freshMap().get(1); + const filler = [new Counter(6, "filler"), new Counter(7, "filler")]; + return held === undefined ? "none" : `${held.num}:${held.label}:${filler.length}`; +} +console.log("temporary", fromTemporary()); From ecd103d1fb904cdb5f8fe298261aacda03aee265 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 18:32:42 -0500 Subject: [PATCH 132/273] Keep the while-loop bound off counters written inside expressions flow follows a while body's statements and applies each increment it meets as a statement. An increment inside an expression, such as an if condition's comma expression `if ((i++, i > 0))`, lowers to a statement nested in the expression; whileBound accepted it as an increment, but flow never applied it, so a read after it kept the pre-increment bound and lost its bounds check. whileBound now gives no bound when any expression in the body holds a write of the counter. --- .../compiler/src/backend/llvm/bytes-bounds.ts | 12 ++++++++++-- .../test/while-bytes-bounds-emission.test.ts | 14 ++++++++++++++ tests/corpus/8003-while-reversed-byte-reads.ts | 15 +++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/backend/llvm/bytes-bounds.ts b/packages/compiler/src/backend/llvm/bytes-bounds.ts index b35a949ff..29b1c8e15 100644 --- a/packages/compiler/src/backend/llvm/bytes-bounds.ts +++ b/packages/compiler/src/backend/llvm/bytes-bounds.ts @@ -300,10 +300,18 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly const receiver = length.receiver; const comparison = lengthComparison(cond, { receiver, counter, offset: 0, minimum }); if (comparison?.op !== "<" && comparison?.op !== "<=") return null; + // An increment is a statement of the body: one inside an expression + // (a comma expression's statements) would run where flow does not + // follow it. const writesOk = everyStmtList(s.body, { expr: (e) => - (e.kind !== "assignExpr" && e.kind !== "incDec") || - (e.localId !== counter && e.localId !== receiver), + ((e.kind !== "assignExpr" && e.kind !== "incDec") || + (e.localId !== counter && e.localId !== receiver)) && + everyExprChild( + e, + () => true, + (child) => !writes(child, counter), + ), stmt: (child) => { if (child.kind === "assign" && child.localId === counter) return increment(child, counter) !== null; diff --git a/packages/compiler/test/while-bytes-bounds-emission.test.ts b/packages/compiler/test/while-bytes-bounds-emission.test.ts index 2b6dc1d3c..a2604eed1 100644 --- a/packages/compiler/test/while-bytes-bounds-emission.test.ts +++ b/packages/compiler/test/while-bytes-bounds-emission.test.ts @@ -119,13 +119,27 @@ function nested(line: Uint8Array): number { } return total; } +function comma(line: Uint8Array): number { + const len = line.length; + let i = 0; + let total = 0; + while (i < len) { + if ((i++, i > 0)) total += line[len - 1 - i]!; + total += line[len - 1 - i]!; + } + return total; +} console.log( + comma(new Uint8Array(2)), inclusive(new Uint8Array(2)), unguarded(new Uint8Array(2)), ahead(new Uint16Array(3)), nested(new Uint8Array(3)), ); `); + // The increment in the comma expression is not a statement of the + // body, so the loop has no bound and both reads keep their checks. + expect(checks(ll, "comma")).toBe(2); expect(checks(ll, "inclusive")).toBe(1); // The read before the increment is in bounds; the one after it is not. expect(checks(ll, "unguarded")).toBe(1); diff --git a/tests/corpus/8003-while-reversed-byte-reads.ts b/tests/corpus/8003-while-reversed-byte-reads.ts index 9b2efd4f0..1407b9324 100644 --- a/tests/corpus/8003-while-reversed-byte-reads.ts +++ b/tests/corpus/8003-while-reversed-byte-reads.ts @@ -166,3 +166,18 @@ function throwing(line: Uint8Array): string { return String(total); } console.log(throwing(new Uint8Array([4, 0, 2, 3])), throwing(new Uint8Array([1, 2]))); + +// An increment inside an if condition's comma expression: both reads are +// at index -1 on the last pass. +function commaIncrement(line: Uint8Array): string { + const len = line.length; + let i = 0; + const out: (number | undefined)[] = []; + while (i < len) { + if ((i++, i > 0)) out.push(line[len - 1 - i]); + const v = line[len - 1 - i]!; + out.push(v); + } + return JSON.stringify(out); +} +console.log(commaIncrement(new Uint8Array([1, 2, 3])), commaIncrement(new Uint8Array(0))); From 383b69cf2df230e4d9f8c529337612163a569bd0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 18:39:02 -0500 Subject: [PATCH 133/273] Represent record-only unions used as Map keys and Set elements as record pointers --- benchmarks/runtime/README.md | 1 + .../runtime/workloads/union-node-sets.json | 6 + .../runtime/workloads/union-node-sets.ts | 141 +++++++++++++++++ packages/compiler/native/cli.profdata.gz | Bin 1751358 -> 1806722 bytes .../compiler/src/backend/llvm/emit-module.ts | 6 +- .../src/backend/llvm/expr-containers.ts | 4 +- .../compiler/src/backend/llvm/expr-control.ts | 86 ++++++++++ .../src/backend/llvm/nullable-unions.test.ts | 32 +++- .../src/backend/llvm/nullable-unions.ts | 31 +++- .../backend/llvm/record-union-keys.test.ts | 148 ++++++++++++++++++ packages/compiler/src/backend/llvm/shapes.ts | 98 ++++++++---- .../src/backend/llvm/test-boxed-unions.ts | 33 ++-- 12 files changed, 540 insertions(+), 46 deletions(-) create mode 100644 benchmarks/runtime/workloads/union-node-sets.json create mode 100644 benchmarks/runtime/workloads/union-node-sets.ts create mode 100644 packages/compiler/src/backend/llvm/record-union-keys.test.ts diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index c69ec43d9..83cfcb831 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -36,6 +36,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `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, a row-major `Float32Array` matrix-vector product, 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 | +| `union-node-sets` | cpu | syntax-tree passes over a discriminated union of object literals: a `Set` of visited nodes, a `Map` from node to value, conversions to a sub-union of leaves | | `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains | | `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates | | `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns | diff --git a/benchmarks/runtime/workloads/union-node-sets.json b/benchmarks/runtime/workloads/union-node-sets.json new file mode 100644 index 000000000..98e9fe2e5 --- /dev/null +++ b/benchmarks/runtime/workloads/union-node-sets.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/union-node-sets.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/union-node-sets.ts b/benchmarks/runtime/workloads/union-node-sets.ts new file mode 100644 index 000000000..16d6f4dfe --- /dev/null +++ b/benchmarks/runtime/workloads/union-node-sets.ts @@ -0,0 +1,141 @@ +// Syntax-tree passes over a discriminated union of object literals, the way +// compilers walk their IR: a DAG of shared subexpressions, a `Set` of +// visited nodes, a `Map` from node to its value, and conversions between +// the whole union and the sub-unions passes narrow it to. +type Num = { kind: "num"; value: number }; +type Var = { kind: "var"; slot: number }; +type Neg = { kind: "neg"; operand: Node }; +type Add = { kind: "add"; left: Node; right: Node }; +type Mul = { kind: "mul"; left: Node; right: Node }; +type Pick = { kind: "pick"; cond: Node; then: Node; else_: Node }; +type Leaf = Num | Var; +type Node = Leaf | Neg | Add | Mul | Pick; + +function children(node: Node, visit: (child: Node) => void): void { + switch (node.kind) { + case "num": + case "var": + return; + case "neg": + visit(node.operand); + return; + case "add": + case "mul": + visit(node.left); + visit(node.right); + return; + case "pick": + visit(node.cond); + visit(node.then); + visit(node.else_); + return; + } +} + +function leafValue(leaf: Leaf, env: number[]): number { + return leaf.kind === "num" ? leaf.value : env[leaf.slot]!; +} + +function evaluate(node: Node, env: number[], memo: Map): number { + const cached = memo.get(node); + if (cached !== undefined) return cached; + let result: number; + switch (node.kind) { + case "num": + case "var": + result = leafValue(node, env); + break; + case "neg": + result = -evaluate(node.operand, env, memo); + break; + case "add": + result = (evaluate(node.left, env, memo) + evaluate(node.right, env, memo)) % 1000003; + break; + case "mul": + result = (evaluate(node.left, env, memo) * evaluate(node.right, env, memo)) % 1000003; + break; + case "pick": + result = + evaluate(node.cond, env, memo) % 2 === 0 + ? evaluate(node.then, env, memo) + : evaluate(node.else_, env, memo); + break; + } + memo.set(node, result); + return result; +} + +function reachable(root: Node): number { + const seen = new Set(); + const stack: Node[] = [root]; + while (stack.length > 0) { + const node = stack.pop()!; + if (seen.has(node)) continue; + seen.add(node); + children(node, (child) => { + if (!seen.has(child)) stack.push(child); + }); + } + return seen.size; +} + +function leaves(root: Node): Leaf[] { + const out: Leaf[] = []; + const seen = new Set(); + const walk = (node: Node): void => { + if (seen.has(node)) return; + seen.add(node); + if (node.kind === "num" || node.kind === "var") out.push(node); + else children(node, walk); + }; + walk(root); + return out; +} + +let seed = 2463534242; +function next(): number { + seed ^= seed << 13; + seed ^= seed >>> 17; + seed ^= seed << 5; + return seed >>> 0; +} + +function build(size: number): Node { + const pool: Node[] = []; + for (let i = 0; i < 64; i++) + pool.push(i % 2 === 0 ? { kind: "num", value: i * 7 + 1 } : { kind: "var", slot: i % 16 }); + // Operands come from the most recent nodes, so most of the DAG is + // reachable from the root and many nodes are shared. + const recent = (): Node => pool[pool.length - 1 - (next() % 48)]!; + for (let i = 0; i < size; i++) { + const a = recent(); + const b = recent(); + const r = next() % 10; + let node: Node; + if (r < 1) node = { kind: "neg", operand: a }; + else if (r < 5) node = { kind: "add", left: a, right: b }; + else if (r < 8) node = { kind: "mul", left: a, right: b }; + else node = { kind: "pick", cond: a, then: b, else_: recent() }; + pool.push(node); + } + return pool[pool.length - 1]!; +} + +const scale = Number(process.argv[2] ?? "1"); +const rounds = Math.max(1, Math.round(40 * scale)); +let checksum = 0; +let nodes = 0; +let leafCount = 0; +for (let round = 0; round < rounds; round++) { + const root = build(20_000); + nodes += reachable(root); + const found = leaves(root); + leafCount += found.length; + for (let pass = 0; pass < 4; pass++) { + const env: number[] = []; + for (let i = 0; i < 16; i++) env.push((round * 31 + pass * 7 + i) % 97); + checksum = (checksum + evaluate(root, env, new Map())) % 1000003; + for (const leaf of found) checksum = (checksum + leafValue(leaf, env)) % 1000003; + } +} +console.log("rounds", rounds, "nodes", nodes, "leaves", leafCount, "checksum", checksum); diff --git a/packages/compiler/native/cli.profdata.gz b/packages/compiler/native/cli.profdata.gz index 31423fa043e0a12aba496a4ae1604d79da4773ef..aa9398b5febacdfdc156d2c385c07312e12909f3 100644 GIT binary patch literal 1806722 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≠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
  • w_bYHksWL?#OZ@ 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Ξ!-T!N7k_2P+D{C3lJ-ubx;yT_iq9Z>?6P|8SLovMfs8;6i@SbA>E#VTi z2-lAGG9QcdN1GK6=8vt+LoY}?YYliI`jyQtJ^Ab0;ApkMI%3(n5mr5+kH)K}j=m?EISURix< z^f4Ny_xfoM?SJ1_^@4b+;2VokFv36g(@gG1t&2hrCYK;k)!%T+__H(2nMH=mCS#dG zbrgKpTrm)YzZbsblCPB@Rso$n6EO}^`!7YAIAI_1`jG{wdLcP3mq%jl7pVR0Lj#}? ziz*z9Q6KkNz23U9xzTRBQM};&I0xmz5XD3pmMW?ms6mUGOK2Xsr0q^L`}NK^u=!;@ zB0x6E#^3l<4!j+5UMv1cO|;b1^#M_wdFBRU;K{hlp)8*yix;R1X{biFySiOCdi*|{ z@Zyd1a}@|-FgfSTozlOXw$=OEsrblDmcOQJmV&}%T)nu-3L466c&b#(8C{>q9?
    4K(A&w;e%wG)z4g4TMzVj*UnuFqL^s^48R!%vM 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]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%0ATB!i@_r|K@81IwN@I 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%0D(sOE}2aB<<*K2Q_Vx-C9 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%0DZN zV3K>rRhYZ(aTV#=BxgxkxhxqD3M(oxEY&73oxuy?d*sofy=cbmE^fiP0;ru3MZRh9 zM%zM;R{>ZRJkv1w6?aJeF@H7*fJh#}o6JxSB~-!zswcktlkcexpeL&G>A|8Cv+*ao zN3q^LiBNq1a((-0POV=e2J6a^S3wn>=l={RF4ZaoomzyPIZpSQq?6rS(??Bd+^u$R z+1$E-L>Uu;+o~*3=nvvB)Un73*FzM)=Fl)%9z$ z8|^U2>>BecC84FY4+{yBJWU!&=FM#ONL_;Q!+JRm3N2QO517~D$bo}%zmNKo4&XWv zH1;xEsAZoC9gNoqLt3xi%Q%3d>aM^lZ>@^i^Ud(bsN~}8MTcwujEX$rRRN%>Pz$K& zAj*-pLl*f-tMOh|o5{zQ^LK2M_F8g)|-)c7=v*>gdD>donMO5Ip(is zy+ukG;X|>opok$=epEg(`ue)GZVxQ-lkGgcPk!!oIhMAXZT;~Af2A+G&F2QjyYk39 zq8hON;Ts`6;d%&}f9}+vujblpBi|wZ_kF3w!U>LC7GtZb84(J6dVqu zFEyuD3O31_-vJs2r$tEOG3Bm74F6O<+wmgbUaT#vP2&t;Og!w@hvp;C$rppXA4=E7?9zSI4U;kM*MUs9`C$#` zNSwAseDhBZY&mg~kswe8SnCyfm2t<`3>NuIJ?|-p-w*~X{?e*J za`uo@6ntyoEjlTnu=GFf>afpm@1TRw{I^UlWI;8KGx$9*qaiWV|Vym5NV5FFEdIOMi4Nap|)#$ z_sVw9SB9iuOr>QOAw$t}cFDU_XI0a|2ZoS}+viVuf|hc_~D|qtpady^R#14?m(9`cdM)*R(81!P_e%+i>zw} zQ|@UN!5Ul|>6u&32&?YDn^cQq*7!)9Ubkx`N8A^)whn3Q?U+C{-&_p%a#Vtg-@`K; z`H6km%x8}+zn_CwR*n`=aNWkkg$aY6asA77l03rTlg+d156^cLo;oyd^yV>&g#H9+ z4m#3|8OO{|=KCGAqfds+VeQ?Q8JEJda|) zJFy>pf3dmMIrpWqz%cvH%MM^#Y^@3{WiFv~X|@JbqcGzaT>^2i1xSCVE{xjM3$-5I zW~fj7ihp-jZ!(DhCIp3$E5-`uY7IOYoyFkLjq#Bn;*`SO+TTu+*72kdDAzAV$)D(q z&9@mxJKq;O?&Sr0S2527jc#T+PS3m&iFNhd5j95_Mf+2Ota6G9Ld;@7>ghM>k_&5= zJBb_QH_ZGnH7srRU|E>5^yHpPV*AyC0sstrgM`SMSNBiU+MhmkNLKG;VSU%m zj-bf_+sWw}eu7NJc(p&!V0~)mL5*HHSbHZr+NUnO)L>yM|LC(iHAW{3Iz#9W7~2MZ zf#yh@mgh`NwN+)&yGd%f&DVY%+`9l~yS7Dcp&6J$tG$YR`FOGsCL3HDJjuOODL|Gz zUZ3o}SmlT48$1qEY8`+OgUortp?DWDN%qbz@bkf}DYg97u5xal#rQFFcmtp1+~}X! z)%H$yNU4kufpoC% zoq~|Brwp8HxI>@Yl@L^+6Z`-cvmB^g7j0!bTnGv6OOCF3XuEZ)ILEO0hHsi)XFcfLy5h1cgULdqQ=KI?m9zBkKRkqf6=r4uX$ND zP~$ZcWNTfD@XxGf0BrZ?gjMB7N87EyY@-2E!?K%xv*Qkx!L>K7t9o2wU7_%Fn74!f z7wFm_rrSg&v(iPBbe<^PS39qo7v9edJDI@5I7_P!SV!$+ZAy|$+oF&hqPz0FC=(iec~bwqj^&m zS!85IH~VucCY!GBS+wbHq{hi#0E^+RpK>)z)~#&8j4u?jN-AaGb&=4q{T3DSeYb@a z@$72G=*7B8gd#TBdPuJ&jk9;{Ew(9W5v%u}%PGhE${~UBGeud5eL$3oxV?ZvV62ZA#o;SM1*>?lnKh?qd z-LWxq*A_2dnt9bKS;tpRlr_e~o*DZ9zN*Q~yq=)9CE|G4a>WykTGNC-=DD0W83HRdA(n+=M(oLx&-n(k$o#Se<+iSo1crzpw@ zM^R=1pE(xTzx{O|I5SXKK;W;m-em?YmR#^T>o&gvrD*$!lD4dqZ!9gB4yL+(`uE>u zASX{O{(PZTvSali{zxC@v&PdXy8+e^AeQUW(Og<(vK`a2i5F_DNM_eDA@e_|k$1IG ztNuwj4xlD&xZ;jacS(}!Thmvuq($%7?jFu2VF1UOqc$&!#&A2_l9X^Eo>US0hRz2UW*Fe8-J{Pb-T#%a=d9Ic)CTC!!L zXMCsybryyTkv7;=DK_F?8I=i+lKl*a_!*WdF`)*PGiqN3-RHJyI~|G9{deBdIe*U> zumZq1lrcMBl7fw!!UnKiL~Nq9(DlsjH&8nzlYgnx%YrI=9h!8nPmD}yA|Nj1K1jZ) zc^CvFOOxhGh2BF=cc9ha$W_T#tq-RTp#6t?=>_^^16Wy7SEh_z(3d00P}TmzBxuw} zgDyS$Tv)J1#-R8hqN+4K`ICK!x#3~BU!nEB>%x|gY9k%bCB83Y@++78l<)61`;@sN z)JfD>9g*c4FDEs94?3G%O1rb(g{G+!YgBR=>{466GC_DnkoVX+fVv(4&6ua+xHBCQ z>GQ+-(1lV5GmZ^6vCxoAHPBnncFAz6*+zb=3uv|}=MQf%>ctgE{EoB^WYK*;3c*>I z>cv?F(#_g8wq$W@5}Z*mI#+XQBVFC!UIAu1Xtf zna`>WXDCT-uXL7`3%mPkvwTM<*YRHXt4V_BoAiOh##OqI)h20kZwz(dbw@%7-dg!@1Z*9Qe}2=%f;T8tHJJWS5jxKHqRxt#GXf8$y+q+-;MjDj`V&q zLOWG7+tVSKRX;wUQT!(xh^l{uJ4LcH+-&tFCP;O^sbVzPa~f8aJv0?KE@k6D?BK;k zo1_Mn5VAo~uQ8a^5z-hFu>%4!-kHc!3AcK`e>Dr=11Y(yDZaM9y8UT6VfF@7>gfl( zvs%R-7?MY{`vJ-7BK@dpS_!!n0y;V8Z2b2y$0jr^fUM&;sOlhBac0Ssk?iTJm|TN+ zZRE!K!Zd=keCj6!yEGx}E4=WLyRaKlkab)(5NOt- zkx(Q(L7*;cyXLxbJ7A!H!V4X_u*?Q~lwcJzqXJ!v0oZ!}ceF2g`+#y4G}5kZF$e6< zAxEj)ky~WV4+f`e3G5a{=9FRZenJnm>z7)d zBbp`7zj+SK%CX?HR!CP1-RtAvaZiVMh(=p?+Z5m`Vtxw!*+VzV5pzX#Cy>3;m&M}z{28vo90*4g7eW;~SDHYC# zdrQuOyuZEHQ@srxPX^W7iS|DQNCg#Q>kg?NQ|Pd9wPN|}Tp)h?BBxn#7Kv<7N2RQq zZg~heZcu^WG0}TkET?vxT|Ev>J+4l=f5YW518O8!yat4=5ww3`O`Ty1M@m-^Q}kSL zD+Mb8>iGjN)$v1q6C-AEF0$akyg4a-q?A%|=FU2^ki0!KL$41^G)Zv(uUXk2lHFGa|{g;Q&&BS<&uV#0Rzxwa0)dhhMNXXP`CZwv9 z@)`Sez`E}|r>T)v4rM!}(5m&moGu!1A=ysH>s6w*;PdoRAWI7*_c+;QT7ARJIpr7A z$qn9CxEOtcukK;iMKtV6gYeZWgC?7QP6!3k<&%*MWA8~$n5BaLgOW^&HSTxC!jrm9 zbC6`1a9jv-c#Evt1Oi=qLy&Vedhiil7StRk67_cRU=qG}djwE}ryeSNiEa-6Q~EcC zoUi1Yq;vxM{X1tn?ua3;?H>6(_kiLiX;WSFjRJY3)$dk z^!j7`7d@5jXMx+_6>yc|jU<6}f6WH9EbFo)DR;57NUs1ZNu_i>zl~wE5gfZ2%Csp$ zr$}dh++^xqHK5d!?!RI0d)Ij@|CCsg4)Lv`t>TDL{b*W$WSzfvCwIGW){9wyTTww~oE&^ZBI0*3drPb3#NZ2#TY#`7i*oD-TGEb?)C!;58A3#HMaC0gDPy zn*Zy?qr;Kti+nZ6?=az~ATrlcM-BDsW?@nN#ZmldtBkgHnVxdH+A*t)5U~^0Qlpcc zBRJ1!0DOsA-w!_S=XS?%G9p{}_$n59dw&47@jNuq2ePScsP>QWeVcs| z*|xldpm^lV(m~{rq9c^DmjvjE8~WmK8#Rx=J&DR?GRC}g=CIttB*pV?D3;y3X{pTi zIjgK@&@KP&1rBivcA<;U6NU|EEL#Y#32OxEBN{z#9m~SN3Z(OvRMPMzJi=38SX9%G zDSdr`mn%Li?WOtuwebLLGg@jwEV>rf?|h% zN1As9R2ZRV2F#f?4*w|K((Ef=9cy4f)-6Tbgy+pU!P>=GoutwSDW&V&<&8aLM$9*1 z&}vn61U9s~fb>#gh*JaC=gnp`0_+TH{&KktD%Y=@U8u7$dDVxd>tvVopgLKRz+4k< z9sGd)%7OeX3eB*V0$e<>0LzKo!DQ2^dp_fcV{ zZFv>w(^a(B__p7u%=))MQx~S!M;l~YGIeugl3JULriwL?F>M}+Qrp}Kx!KPm6m}bl zXFrYDUr90%-#fib4}0XcY&Ch1iUy;O3-qZeX7D;X!c26J@*tuCzKkeK`kqDp4Dthd zf76RishFsTkMkE`JmY$~PufW6}0js+Fx=ln+K9dDV}I^aHR#xTJ8%az1H(?SYBJSEukGx&%esaQbA3GfFif?dXx#XV!EiV3 zr(;QG5&nOLA-kFxZ5gx1&m8YmG=Ubso6eO28V)O6wRtUeUEZZ$;ynCf9Sm>Q<+$9= zKX$i7d;0Slo>lq6j%C&9StW;Xzoe!icNTRd1^!-bj_?9#jBmUq+>b2~A0O*8$i1Dq zayj|SQM;$vI3MAO4n~nP7c?CK$|{1)l9$2GzYVWY`<|6FPdoTjLFF^6p-+ND{PDm^ zf!T#@tfR>idw(T(D1(8SaKiSHWWO<*^6P|YXI(dC(TuE@o)e)l+XW!Z{NnUt zYMA)LBhf?xn{+R6{IG()YH5q6%5QRM_X~z99J_q}qZF>g8rJG9y*&~CqzVa0{I#Z< z7H~@ZWwIK<$+Vg-A|8o0skxv-R93<+cP)cYEFq%K!-EQ=UfJz`$|zc=kIY(mxc3B7 z@lT6~IR+>^+M8T`A2PZtKIn5}19ZQya>oK`?rc|L*V6~3*6C{(KKVmfBm~v_uea1M zTliPw%c^=7pdJzJm%ZP4rKIXL`Y-E7%&^mPAdO!ZVV)GcUKSi!?v!dhheuT6T6Dgc z+V4_PTxipQ%mWHZNsKSZ_ZyBLP7R;~fKDazD*lgf+;$k(Vj$sZix5dWsLFJRh2{ro9AKBlcE}>&c2AlrAdqvF zu;fsz-$KKng;wpaG*0PQeFAp3d_JAoZv@2>B9HDa(dE4hU{>w@f29 ztu-0q$GP|+&Kq|c#H*^Oac`444>KvL0G}O=8+Xi@of2Q2m`w8>iP)$_UNgvnk$&h$ zu)@6E!QnraL9d8cR|o>0zLEVrv79XLi3Xl2_c76yDGtRe@+|tbRXy9R!Ekk@E76J< zHFScb9XtOM&9LOd<`3*X!fYjda<#6J0%5f?6X zENPS63fcxUt!KwNxS{>UB{-3KuyswRZL{NmlFHw9!RWRgI^YT?A)Lr5i3i*(;q>2JZ7KHvvK zc(9)VvS7mN7NdsU+|mqGTI@tEsj)zo6DlJJRX<^XC=-h|A_6ubU*q>YAmh8 z1Dn5GFs^623>(Eh98vpqSNkrvu8=Rbnt9oi%8+iAD_U;Y#;xS-@(}YCIBosrd_z=Lf&^HGiP z;Lz7~Jgt8xtn1l+4f?x#!lt$#V$dFz4Bu;#T$}aBX}U390EID+2b=Wcv%NlQ6Q{{4m7f2e&q z!s~7=pKMO?3u?E|zU^qq^e<7rAc2cJw*I9J=8q^tZTmCiR*t&4ODp(7ah$!t<_Ow& z+ZEd}DkHdaSFz(058v zsixeqe9|eA*1YWMmc^PWp45z|y7wV$7ilwjoy&IR!Ge8)5xWNdEC0hv^mNI^-@;@6 zVE7+YtS6~Ij-2RtL*cZT>No_{gQU2L4>IFg-@D8aP|p2u##`C_`^Ik9gbA+dh@~*{ z`lBtY$=}lt>;ssTet>Uqoy1TdM)WCD>3pYt$cZmpug(L(4YVQU@jv_*B~WU4(&ok9 zp-?KEx^;3?W5z7Eh`sy_$SR_8hh;`G+=EbdQ?>S2?z*EhtkmOcV8knlvo84&EVCCZ z;Zn64GFdaeF400E3trmdum3!ba;v&qhC80C0?iTbMlo$`Q%_a>gL+_=cQ$Iq+)@+$ zL`z-|14w_)j7mm^dz0_Nmt}*dWj?dVvsC+-nZW%MBX;j(p~r5_Bf^VSC~&iP?yyjDRfaEocdsRUj&9Zm z%2eBW;Q7?SEOn&Yx({v6^#XN!p;pgLAe{3-9k^d-iJZ{~Hu4aE=#r_-$C{`sTV3)~ znywNEQs||<$^^@ard{cizuc~|j-0iV7tAv#YKXJD1UdWi;2fP`Ri4uFrl}{2Kz(cc z4gQvXBnGlsqBR*A6DNMqiMVRrt*Y48`L7k>9*4^LdwS{nS8;=my^p}`%=gO!MO@UQ zVx2>pH;1xMSN^01U1Vm}XeI%@$qQHJ=w7#~i~8TXp8VFuu805La#sH0z8wvKe6u~I z_V`@se?iqO$1yO;-H!*A{GscFDFR23%(QL#CS(eBqfXP*-rsup!mTP(7Ud9GvH?A5 zBL=d1xM5w?`8<)hQMD&J+w!{RIfFz}D*X0>;+*^aEa?3ii`p@BrOTZ2{=%~4U;ix! z9<%Ezy`agq0v|mwYYo^^tG*P}#;L(=UYf6|LS3~%$v>}Et)@K_ ze>g8E3kB!x(x@SA~C*9^&coW`MkMB@5@j;`KigAX~v=Mk?~41{1(v|5FH@w zgr9l3Zo6{bo!jd|G7zCa$O@>Vu$TT)LxvC5{_1nQIN|=3Ajitb=ORbi!#RYq_523{ z0+N?isVK#&+t^KYlAqBoD?Y9zGEE=e7fD=+|LIk2mZ+CJAAm$n{x>U)`03J4eJ;z3 zW|lxjz@nYVJYg;*kJn^2h|mBn(!?3WJEp{lHTe@>3QDr38%d4P%V?!sjbGbkBKN?* z`@2Kb^7znY#9Ad6VXs1$-n?AEt_Ey8m&wSvk}>ackwu-Mtw4F&kQWQv0<$(SxaD0! zSUEO0ALEj$D3!>H>-2}gDU#`j=yw^BwpE)*uox&W?#dtf^A~gY#@e0ui!udwTG-EG zm9G~a)44m`C?i7GkzN6KYDf8|eGEr6(L47}U^^gnhECa@D>SVO#FkXWLb1oc(V4#J znq3;R!|E{~t_x#|)td@U1Qo-N=Et{BeVZRjjMMH$Y!x9IGy6zSU6VGW>KgZpB1(qv zd+N$C9$OEVlr$8+qT@0Dg`a`%5^ag-WMk)lQ0$hO!an|a-e~qTzC#FKkVeZYmFK^8 z$%gp&%B&LYLt$=RKn%6ix_QmgkZxYH*&k~5FZ;4ln34*M) z27aQ8JLB^c2JN<^$Q};R-ufIz#GgYvT6!OU zvqHo-orWCJE6BQ$bA+`A^E$c5DV`gk0uuA1rHC`#EX9!63rS^kdvF)lgu~Y+=>{tS79V z^(IKm1?TK>fPune%wJjr;06qxxuW$Vk5*X(S_s}ooWQaar@oSmRil1W@qNehAnEbB zq$a`x`ps4>gQOX9+pqAseQj=dwx zJIR$N9l&V9_iN;8zqUH_HBjstgF}Dtj%UcMGdVDDgGHil1he5?1AJEeQE(gFPp}-N zT=33X3j5~?AJ*y!hyYG`u23&M@;jiD`%^VPYd0^Pa&C1biis8c`f;T?SHJ)4?@L!2an7ZCm&~j%k?k4vt`}m{t3^d`kS^sO{ zuVLjBq8pU6rWiwuv%x`-Z@L8j8ECi$UrtWeAQQkh%%iC+P!OLQTS&0J|BRiFYIo zhYt0*XQ?F8sKyH^nd2o|sakNVgqa!7OZQ-`ghzt68+7l_YT*DM#T5C7Pp*O~k0d@& zXZC2Sx@)*bXn8UDc zA7O~}iiU{x%r#r`OAFw?<-d4&X2CDR-$nW<^nb6=*H5=jQC6qYZI+@77vVD#{u%hKkSfg2b z6Wnmtphwqt79LdV^gGePxQNTRSJJ$_$NP%95VGYS*CbKOR!Jel=kK6KhWJjvy|f$# zmbN9?+XESM#~ulg+(AUv&28&!(9!@$?%+1PPP2xYJ&_hLa1?*^q2#uED9lMesM z3Ae%Ge(vnhR#Ad7gXc;gjE2h1>>WF`Z_~Fb=$9XXD8?ciup-bLL`0d%q5L5{=t*VF zmfw`iHsS1BM+TFW7G>=_N-v#)9=Arb%+Ca!l$X|sXmLR)B)8Q|Lt|*z8tKt)Lex&4 zPr4g>5K%+(1A+=mnm?^wvM*Be*^xPPnji7Zk@1bQDu4Q!%b-)mq3SGen<3#$hlMkk zyUz_5c^-AgJGw*#em}$C#mg;{i5qd1U<(sbadT%7BcM1S8AAb4n8Km!_fR%DC0mvQ zN0B!PYkom~rxY*4k;s##0`T@M*<&gSz|4d=82ff`Z!LO6W9&(7u||(?)LpHzSd!jX z<+A;dQweu43pcZUE(VnMLC*!0P=I#7IQG3EY;B9Aw8dP;GhTtt;O+F5a8J=-?}TsIoQ=`;85^DSGyZ z8`ic2TG!5b^qx9k?O2u+X~IlNQjKCFwTKs?+9uyrAWJL~0rA5#LGe;;h=L4VO##cHcA%BHW4#YMi{f{G|zS!wHlI2itmXH zXJn)CFtOsjl!-&8i#B2^Ry+Ie9Q*n*7lQ;4dcCT_FL)_fk+e#&K?dcD@Du+L%?i4N zT~HqPvOcInXetwPE$~^nY}9iYN~+pWg*{n#9Xq6$a&093XSNgru2S->T}b6>OPfJz z0@TlD;%-ebXdUp)%7jVKdCcXTRx(?6nRL?|{p5(theu+)uFtcNUN5dW!gAZhhaVS) zNz>L(*Jz_hL${HH(rrXi_}*c=&A%}LhgQo#-zQ3ZMsFh(H8bblj)G&t=bV164MpT1 z?+6esp6D>nP+z@SPZYkI*-6Q}3#^V9i4imWBLhJl6?!1)y{#6I)}{vFu~J-X-FD2r@=j(r;`nEIt#Y4sHG} z;tRf4fk4F=@zuKFwS`J#E|8n}bQboE;WdSgNZcwju$%zG1ZO-{tZ6(; zKvI${Us7GY#DAf=`A$A~;9^AjCgVY=RsJbK!TI>QS&Q!PYa)WQ2q`xbR8l^~rQdXR zO&_ggS0lyccOnCjd^b%6e=3;=wfKLOUiw3Qn-34tHwp4Z8;)YIfEK9QJ# z{fYAH!5{fi!6^#C=SvKtV|LBr3yjd0sj|Y8sC$MI4@mX-+ZmBQ^GEvD_9M?S1eU={ zpJmAg(x9ZI`W27+q$6>S#MOeK1A{8Wm*kh~p_C0erz`tWI7dIyN3!DO%6ybahWRi$GBg4O;^kM0mBJ|f# z>3g^PQ!n&1N8>j3`+Fq}jgf~l)&xXk?DUV{snUV9O`)Dnu{cFF%T9Kjumfj4vp;WD zLYBD^?r_{?U$-Bdxxy6+$rZyPBK9aK%sk!%YuQ_jwc(u(sLsOd;0C@s^H$7R&W&nP zLos(?VU^YsK23|PzdqW^-J!jk96)CHsncX)%;NH$(JhM^$vaWT$)}cOvh3ga!7J!U z?f!-^F}tAM7j@f}x1#l<3m`TA(L9V9T)5w7MQOo_xct|l2%0@BVKwjaz7V;eEODgw z3%Az+Hsg9*u6v&Jh<@n8qTFi|yI=f#FfO-c&x__A1yM!Eq15*OSX1ze9XvyMW zIKyH8l4S?j7W_%=YA}1-0PdrC*Q7aVXjOORe&*M@MT*UvEiPswC9@ex>4GMqsxyK3 z?D0`*~KtQM~_$OW;4D9rMF0L~S8P zEk5%X?OBq>2=;ZC_vdD~UyhqZg5l*6p5v!kT6Mgqs^ejvi#*+e)4mIxv<~W2Jgu@mn?d4Q{@On7Tn{^Tlm2*Q_Zj z@>QVHdXkIMfb9vz=A3HOjV8w+82k3zgl3&%QV*5oWH-2yj42hjD4ht~2QC&AV}>&G+!1Y%PXS&2@L(h+CSm-mvaCIFAOW z>B;~t0E2A`PS8h$shJj^@9j0fKk-!-2Axduzx09i(L(a&q)f4Yn(=b=VE>CL!&{T{ z5YGBSgV+kyMztB+Y}rpu_y}zX_icG#%(DnSkL7fgVp!(!Kz-;$kns9T@eI*{5o^mk zqd>!DLx`>^)!F>h%WC{v>Nlv1hwDe{bhpFeUtMzBTQH?n_cqY(-r_NKTN2}{?VeMA zjBA-h&nJQx!$+YlI5j(5-YgBL92Uarm5>cyG>A$2C^bEyn)@VzwRzWB;IVHe@xBU; z#E}+sR^~YOsUaEz>5;LAbItJIM?JglPErp1vxRehAQBO?o3dfQ%b>7Qb+e_Xvo02{ z|1y5dNe>9$1AcNFaY`IDKQaF2!2Zq@aaVdC2r!^rJ^eygOi9!!r6qU!vl`@& z!R{#EGA4poGE3D-k;+3@C}nxmv9-LXJL27Z!)#=vYi*zD?EN(>kn8@H&7uQp8d#kJ zA#7??N}Vsfwi|yls`kRH8@#jlm(?z1K#>Ho3*b|{voW(YBbIh`|E8%JpKvMzS0Rbg zW}0sWWVvV@gucQf)^XUU#JrhM={#EtpJKXL$1VIV+u&1(!?cy6rfdeH#x(E^ZVsfd zgSWpsq5WCu0-NRQte97$@VZMrC7N^em`B0;(G1+N6s}S0fbLmT^NDE8!jmEVsqN>5 z?lC6%36{l@AagY2@dTEQE0uVZ@Af%dw^r|vO(8?LFLQp&kJQmUs^V?A;RLXK61Kp% zo%SEe;0_%{D$bSW&fYfl3m7=BBgH3Wb_qve?B$E1N?_~ywGrpZ<)YG=^ z?022prvE&wk=(^qeUJmbqi&C-Z>lP>lRKyJ5DzKEXDu6Y%@Sg!?>8iN?Q1Bmf#OutUyvhXq2K`HgRt3e#Mn@brD7sCF=4LYWtaw`t78s zIh7|0?mF zf@}YZADF=v%|~H^3W6-T)A+aXuxE){N0;83Ul`pt`1Cc*3;AaeGwbS!jOX<9v&_t2 zD%$ksgsuZ~qYi!fb(-%tXo*zAPtZdRjPdqHQY?ELEUe_xXrxA$NjC4LZzxb);Ic)p zvLzMhfAGbuwgutJR-R{i>OX)%TW|658D^hwd(G6m{whpB>Y~YKD?6~|kt*nLnI_cx^NcKV0z3N=KEh5x zzHwt7Gf8AqXZO5-&FQOv*22-doVxqCgKv`8E}lG&_J1m37cOgpV24{liI@zN-B`z< z=Mcx03Ckgee{B~Pd%wZ2o*3EX3;gkrL>#$RFUR}jFR4flgQ+9@)>H=gP!Sils0j*- z`N^R+i1#v3DVL~*Cn;8FAk0*7JVh1^Uh3NLhwe5}Zw)Y~Ex={vJouMqQ|AsYunz7_ zRUQyCTv_Fqo0cK+&z3W8n=$G{o%(-~$wq=-7YvLUv7Y1~+=ic=i z{XbB-Sx+=|umnc(NPKX&Q@)chsnYxxW|E&DYR*9g3+gG4HH zvNXTiP6Vwn5}BP4 zcdA191{#OTizXR82|*rT+Z^FvbwWaa@h*AQ;_W!{;&p)Cd9CQn52hNb&zT15iT)I# zxof4ga{Q%LcAW9|C;0}?Ec@R{+-;aeoB7g~LbS4aRo8>rV|(H_(4s$hpN_Nx8t)m; zCED4LTV27Bvs>yBxjp$f#zbSb&%rY2F_Yc6`4frfVav`oaJ1>)R8khO<*Oq{Bcav+ z#2C(&TzJ}A2mKrpa}$i@DR!H6MDaJS zRVgu;JG&MKs28%ij>WTv-a}4sP`$t{ghLB0R5q?4p=u`PT`ssQeifqNiv8wgs72@g z_c`V>^#KO18h2W?6wbP@cd{NOhdcB$PKhSGb6L@$SbnnZbZ_GxSj+du9{jZlXk-4I z>?SAMk#r+1maX?Vj5_jS`uIYqJWV;ZP5~J;jDH>%3mfOAYCI>kdk#r^T(pvw>}T_R zrnn4`fBwWLBs&h^A@G_?V7;QrPfGxa;L#d7TN(B~gLqwR<>=x85_UXb)^nR3#v-1v z)G&k(?DLLoO`!DG;fX=wCw&hS&tB?*9%_T`zwZ(^Ni1c2QtB^LPQ$L z`svd&+J>1Ecq-N9KkX8wF48TFU60jNpn9uN;TO3tN78${E@?9Ev@$R+P6n;)6gHm^ zlfr9W2OHi#7+NJk`_1y63vQfsi$sf!c|puaN; z*<22bLUJ1h*ttzvx=U9%#efi6Jes}6AE)gwPv-a>Z%K+EcdNiKl{C#*mE3W1eEr0p zyM{%iUK1=h6~XKav78pYA>gWqG9;H6jViAvAb`0O5lb4#8nfmIFI2Z>7a%$F&{yLR&&^6y zgTS*Wg@X8s>zY-v#T8TQsI$JAGpQ46`!%{{MyH=1(c?3mceh1gJ;YW9J`i3#z!dh+ z6%TLj=Dxow)ru0+{I@MQ-Unc|P%!X�xu_KTjY>W6e}@uB*}uaE`7)Wg}KideN+X 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 literal 1751358 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 z%0D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 FZb9Q_mW0nQa`|zjX^=^02Ug?S~ME+oD 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

    OaGw0X0$FmR5F_ACy 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≫={Tk{t(BZD7H!-XU6b4sPhqL$hdLS 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

    oV{Ezg6C?D0&7e{@no9S+X1r++~X_7tF)g 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 T#+&My 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%0Dpv=6^sL;#aedv%=`Sz5;aoy8)F1fLkt61I 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

  • K7I{Rt0Ez+YPt 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%0A=z%r)FY!#;I7012uh!8fi}Z@m};c#wAb0 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
    uNBT8?bFx0Z7{W<3l}8j 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

    9C(vwyz$$*DzRy#7SAv(4y4p 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 z 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

    ?@cb#d=SP$whOLZR~aTb;Ya%6ZOUE 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�AB~4Eg1-OKvnr9Y;Q&bum%*bh5 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 z zrzY3`iwFQ-{D*UT73=(_dD<`Qu-Z24GyD~ea?{sL6zYD+Bf^E$4eYCu5ZN6Dy(HAj z1>K(2#)?v6rKepS=Id5G_Rv6!(xXgs8T`WwqNAOy zgHyq3?AVR)Jq;-F&Lg7>DP>zHdD&{JE$ZBp48PR=hZh1$&fL1C71Tmn>gg50JdX{* zHvd#VfuS8-%ddO6IPWR++AJse;W>1u;=-)*jk*dvWc+m&Wa$zF^%A=%eWRBcPF@C* zu-UeUY!}D(%=*igQDUU1L`Xv1#LGvMRIM!yf#FmAg2-(^##!aH(vTquz{89j>$`95 z=W{9H?r+=?UuRB|QGx}i?19eqip@m!u_4DPbG$33*ec{6UsU(q?g@vqiSxASdiSsf z<+z#UX>rHc+dL3hf41wN^nn_03cMF#9;{Y8EwFkC@7(QmQV5F`yKF z7h*+R(jnmPEB?y-nlBD^S>3P;7y?eDlf3^ur`i2nxD3fewA10sN7z=pX~9xO7Pp`5 z65vt^ni^63S56TI{_r~=+Y`F}l3bxK)o6Jk5tA?nlTJU6A94Bv5XJZQg4+G+2<8&f zxk)eiWZ1g`3G754NBcZ<>~==iLEKrt7CJE07|50749=8-bZ@joJ5T#Dw<0Mqx(mtA zzn>Wx@4dzvQ65!l)XkZjLxp)TY;5aOX@mkGOnx#ukJs|m=}-L0_hgC*cqQ-vmL9Wn zZ7p#c({4YcqeY^D3NWxOCuWaf$0C*1@PR`rXOA;%G@S?i{y zWbys6{)d@7wQ6?&H#<%|Vb8(TQqUQFdEenhT?w$Ay}&70dC4Py-0G-VYvJJEG^D9Rs{@QR-irVQ&km*7XJ>pm5 zxK(@a#`IYy*(G1^k2ck(fI4?Bb24`my5F<>0)xm?#oK3L-cz)doa;jiFtbsmjg{;l z^oQPFxv3Y459->MJu0k##LBJfWND>K4{mr<;uRIT5w279WnJ`^z7E9Ey?0Q?8?n1= zjYBB)(#&(Agc7*@FNZTSHdH%G^ys{7<6_=D!bSB}xIbu#WFJsw1hlyFw0ba)3~|-@ zmB@Xg*Hi>#%8I8ESe2auM7%rkx@nKY+E(!gi{VU;v{xy42*IZyqv$otVW zo+bt5a`7qS*^hcCy${s*>xu2Qea{jZr(TmDzFkneq_!ex3o*~L(3FJ_?}N4scd;5T zmIkL4iT@?Xp=jj?nulQFpJLRz4gd7ne46mGZTJ{^4=PGFa9 z{nX~LwyWU8E$$c`8}vCYuP(L>c=#=AtgPvNi;iUcjYYi%q+^B6*DCdnKPOs8Ctxq) z1@AY~T@paZ^U>_!X+(>0Dfzt3 zuhc6~zWF+Y`>V|}ZqHoiBidtLjuX>c&8*!gCX!h6ykn-r7vatpASijtaK3y!pa0ah zo!u|S6L)c*+DfBzKc*ExYq!TgjYpxcJ>e$~zU7Se=kqV_1_bV9I@GqeNS?T&t{lUX zt{2p+6m4Y9O>>rT)1r?R!_DdFpneUh0ch--lcApSsP*G z)m?^RJ7h4c5ML~NqdUhvp`f^cN{kI&XPxrgQGk-a#J&&CXru>)s3avBqAHR&?q9RqNYg+jSK=JmT8 zA03+XvnP~Pz5<8|(hVA`&R(lcdKHN}0h&0Nf9nYd^0<9rmyc`M?GDmIw}LTpmuRE} ziQPJl(E0Q()>b+DIb_d(i(S;GLDRxylJmdq5%~z(fk(ksQ=FKs;|${fu9F!P*7(cs zruU@mu0cP(Hqe68f4U

    *P@Y`qcO~;T+SwX8 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 TUH4=%HTny2dXYb>e(C%lSAWI3 diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 554e6a5a1..6adf76079 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -1014,7 +1014,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { // LIBRARY mode: no @main — the profile-declared external // symbols specified by the library IR instead. for (const line of emitter.emitLibDefs(globals, globalReleaseLines, stamps)) out.push(line); - out.push(...emitter.inlineRcTail(flushedDecls)); + for (const line of emitter.inlineRcTail(flushedDecls)) out.push(line); out.push(...functionAttributes(emitter, hasNoInlineRecordClone)); out.push(``); out.push(...emitter.fieldAliasMetadata()); @@ -1160,7 +1160,9 @@ export function emitModuleParts(emitter: LlEmitter): string[] { : [` %native_exit = call i32 @scr_exit_code_hint_get()`, ` ret i32 %native_exit`]), `}`, ``, - ...emitter.inlineRcTail(flushedDecls), + ); + for (const line of emitter.inlineRcTail(flushedDecls)) out.push(line); + out.push( // sanitize_address is inert under the plain pipeline; the sanitized // lane's -fsanitize=address link activates instrumentation over the // emitted functions too (the runtime TUs get theirs from clang). diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 31643bdfa..59ef43d7d 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -1219,7 +1219,9 @@ function collectionOps(host: LlvmEmitterContext, key: IrType, value: IrType): st function newCollection(host: LlvmEmitterContext, key: IrType, value: IrType): string { const B = host.B; const arms = key.kind === "union" ? host.unionsById.get(key.unionId)?.arms : undefined; - const kind = mapKeyKindNum(key, arms); + // SCR_MAP_KEY_REF (2): shape-union keys are record pointers. + const kind = + key.kind === "union" && host.nullableUnions.shapes(key.unionId) ? 2 : mapKeyKindNum(key, arms); const m = B.tmp(); host.declare(`declare ptr @scr_map_new(i32, i32, ptr)`); // SCR_MAP_VAL_CREF (3): string keys and compact values (compactMapValues). diff --git a/packages/compiler/src/backend/llvm/expr-control.ts b/packages/compiler/src/backend/llvm/expr-control.ts index 242f9997c..2f7c31813 100644 --- a/packages/compiler/src/backend/llvm/expr-control.ts +++ b/packages/compiler/src/backend/llvm/expr-control.ts @@ -7,6 +7,8 @@ import { DYN_KIND } from "./dyn.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js"; import { f64Lit } from "./common.js"; +import { NULLABLE_NULL, SHAPE_WORD_INDEX, shapeWordHeader } from "./nullable-unions.js"; +import { emitUnionTag } from "./union-repr.js"; export function emitControlExpr( host: LlvmEmitterContext, @@ -52,6 +54,8 @@ export function emitControlExpr( case "toBool": return { name: host.truthy(host.emitUnionProjection(e.operand)), type: e.type }; case "logical": { + const tagSet = e.op === "||" ? unionTagSet(e) : null; + if (tagSet) return { name: emitUnionTagSet(host, tagSet), type: e.type }; // JS value semantics: the result is the deciding operand itself. // Left evaluates once, ownership moves into the result slot; when // the branch takes the right operand the stale left releases first @@ -614,3 +618,85 @@ function literalChoice(e: IrExpr): boolean { if (e.kind === "strLit") return true; return e.kind === "ternary" && literalChoice(e.then) && literalChoice(e.else_); } + +type TagSet = { unionId: string; value: IrExpr; tags: Set }; + +/** An `||` chain of two or more tag tests of one local (`isTag(u, 1) || + * isTag(u, 4) || ...`, the shape of a union conversion's identity test). */ +function unionTagSet(e: IrExpr & { kind: "logical" }): TagSet | null { + const leaves: IrExpr[] = []; + const collect = (x: IrExpr): void => { + if (x.kind === "logical" && x.op === "||") { + collect(x.left); + collect(x.right); + } else leaves.push(x); + }; + collect(e); + const first = leaves[0]!; + if (first.kind !== "unionIsTag" || first.value.kind !== "varRef") return null; + const localId = first.value.localId; + const tags = new Set(); + for (const leaf of leaves) { + if ( + leaf.kind !== "unionIsTag" || + leaf.negated || + leaf.unionId !== first.unionId || + leaf.value.kind !== "varRef" || + leaf.value.localId !== localId + ) + return null; + tags.add(leaf.tag); + } + return { unionId: first.unionId, value: first.value, tags }; +} + +/** i1: the union value holds one of the tags. One read of the tag and one + * switch over the set; a shape union switches on the record's shape word + * after its unit encodings. */ +function emitUnionTagSet(host: LlvmEmitterContext, set: TagSet): string { + const B = host.B; + const u = host.emitUnionProjection(set.value).name; + const nullable = host.nullableUnions.get(set.unionId); + const yes = B.newLabel("tags.y"); + const no = B.newLabel("tags.n"); + const join = B.newLabel("tags.j"); + const target = (tag: number): string => (set.tags.has(tag) ? yes : no); + if (nullable?.repr === "shapes") { + if (nullable.unitTag >= 0) { + const isNull = B.tmp(); + const next = B.newLabel("tags.r"); + B.line(`${isNull} = icmp eq ptr ${u}, null`); + B.condBr(isNull, target(nullable.unitTag), next); + B.startBlock(next); + } + if (nullable.nullTag >= 0) { + const isNullArm = B.tmp(); + const next = B.newLabel("tags.r"); + B.line(`${isNullArm} = icmp eq ptr ${u}, ${NULLABLE_NULL}`); + B.condBr(isNullArm, target(nullable.nullTag), next); + B.startBlock(next); + } + const wp = B.tmp(); + const w = B.tmp(); + B.line( + `${wp} = getelementptr inbounds ${shapeWordHeader(host.sizeType)}, ptr ${u}, i32 0, i32 ${SHAPE_WORD_INDEX}`, + ); + B.line(`${w} = load i32, ptr ${wp}`); + const cases = nullable.arms + .filter((arm) => set.tags.has(arm.tag)) + .map((arm) => `i32 ${host.nullableUnions.shapeNumber(arm.shapeId)}, label %${yes}`); + B.terminate(`switch i32 ${w}, label %${no} [ ${cases.join(" ")} ]`); + } else { + const tag = nullable ? emitUnionTag(B, nullable, u) : host.unionTag(u, set.unionId); + const cases = [...set.tags].map((t) => `i32 ${t}, label %${yes}`); + B.terminate(`switch i32 ${tag}, label %${no} [ ${cases.join(" ")} ]`); + } + B.startBlock(yes); + B.br(join); + B.startBlock(no); + B.br(join); + B.startBlock(join); + const t = B.tmp(); + B.line(`${t} = phi i1 [ true, %${yes} ], [ false, %${no} ]`); + return t; +} diff --git a/packages/compiler/src/backend/llvm/nullable-unions.test.ts b/packages/compiler/src/backend/llvm/nullable-unions.test.ts index 47278453e..bf0941652 100644 --- a/packages/compiler/src/backend/llvm/nullable-unions.test.ts +++ b/packages/compiler/src/backend/llvm/nullable-unions.test.ts @@ -56,10 +56,11 @@ test("unions of two or more records become shape unions", () => { }); test("records of runtime-inspected unions keep every union over them boxed", () => { - const set: IrType = { kind: "set", elem: { kind: "union", unionId: "ab" } }; + const set: IrType = { kind: "set", elem: { kind: "union", unionId: "abf" } }; const unions = new NullableUnions( module( [ + { id: "abf", arms: [a, b, F64] }, { id: "ab", arms: [a, b] }, { id: "abu", arms: [a, b, UNDEFINED_T] }, { id: "bc", arms: [b, c] }, @@ -68,6 +69,7 @@ test("records of runtime-inspected unions keep every union over them boxed", () { globals: [{ id: "%g.s", name: "s", type: set, mutable: false }] }, ), ); + expect(unions.get("abf")).toBeNull(); expect(unions.get("ab")).toBeNull(); expect(unions.get("abu")).toBeNull(); expect(unions.get("bc")).toBeNull(); @@ -75,6 +77,34 @@ test("records of runtime-inspected unions keep every union over them boxed", () expect(unions.shapeNumber("c")).toBe(0); }); +test("unions of records alone stay shape unions as Map keys and Set elements", () => { + const set: IrType = { kind: "set", elem: { kind: "union", unionId: "ab" } }; + const map: IrType = { kind: "map", key: { kind: "union", unionId: "abc" }, value: F64 }; + const unions = new NullableUnions( + module( + [ + { id: "ab", arms: [a, b] }, + { id: "abc", arms: [a, b, c] }, + { id: "ba", arms: [b, a] }, + { id: "abu", arms: [a, b, UNDEFINED_T] }, + ], + { + globals: [ + { id: "%g.s", name: "s", type: set, mutable: false }, + { id: "%g.m", name: "m", type: map, mutable: false }, + ], + }, + ), + ); + expect(unions.shapes("ab")).not.toBeNull(); + expect(unions.shapes("abc")).not.toBeNull(); + expect(unions.shapes("abu")).not.toBeNull(); + // Unions over the same records share one release and trace helper. + expect(unions.helperOwner("ba")).toBe("ab"); + expect(unions.helperOwner("abu")).toBe("ab"); + expect(unions.helperOwner("abc")).toBe("abc"); +}); + test("library, lifetime-region and debug builds keep record unions boxed", () => { const unions = [{ id: "ab", arms: [a, b] }]; expect(new NullableUnions(module(unions), false).get("ab")).toBeNull(); diff --git a/packages/compiler/src/backend/llvm/nullable-unions.ts b/packages/compiler/src/backend/llvm/nullable-unions.ts index d9b9cdfcf..c51f3b5f6 100644 --- a/packages/compiler/src/backend/llvm/nullable-unions.ts +++ b/packages/compiler/src/backend/llvm/nullable-unions.ts @@ -118,6 +118,9 @@ export class NullableUnions { readonly retainWrappers = new Map(); /** Some nullable union encodes `null` with NULLABLE_NULL. */ readonly usesNullSentinel: boolean = false; + /** The shape union whose release and trace helpers each shape union + * uses: the first one over the same record arms. */ + private readonly helperOwners = new Map(); /** `shapes`: allow shape unions (debug builds keep boxes, whose * debug-info layouts describe every arm). */ @@ -151,6 +154,24 @@ export class NullableUnions { if (!this.shapeNumbers.has(arm.shapeId)) this.shapeNumbers.set(arm.shapeId, this.shapeNumbers.size + 2); } + // Release and trace dispatch on the shape word, which names a record + // shape module-wide, so unions over the same records share them. + const owners = new Map(); + for (const nullable of this.byId.values()) { + if (nullable.repr !== "shapes") continue; + const key = nullable.arms + .map((arm) => arm.shapeId) + .sort() + .join(" "); + const owner = owners.get(key) ?? nullable.unionId; + owners.set(key, owner); + this.helperOwners.set(nullable.unionId, owner); + } + } + + /** The shape union whose release and trace helpers this one uses. */ + helperOwner(unionId: string): string { + return this.helperOwners.get(unionId) ?? unionId; } get(unionId: string): PointerUnion | null { @@ -277,13 +298,19 @@ function shapeUnion( /** Unions the runtime itself inspects as tagged boxes keep that * representation: Map keys and Set elements hash and compare union keys by - * tag and payload inside the C map. */ + * tag and payload inside the C map. A union of records alone is the + * exception: its keys compare by record identity, which a shape union's + * record pointer carries as well (the map keys it by reference). */ function boxedUnionIds(mod: IrModule, nodes?: IrNodeIndex): Set { const boxed = new Set(); // Only the module's union definitions consult the result. if (!mod.unions?.length) return boxed; + const recordsOnly = new Set(); + for (const def of mod.unions) + if (def.arms.length >= 2 && def.arms.every((arm) => arm.kind === "record")) + recordsOnly.add(def.id); const mark = (t: IrType): void => { - if (t.kind === "union") boxed.add(t.unionId); + if (t.kind === "union" && !recordsOnly.has(t.unionId)) boxed.add(t.unionId); }; if (nodes !== undefined) { for (const t of nodes.types) { diff --git a/packages/compiler/src/backend/llvm/record-union-keys.test.ts b/packages/compiler/src/backend/llvm/record-union-keys.test.ts new file mode 100644 index 000000000..8c3a89b96 --- /dev/null +++ b/packages/compiler/src/backend/llvm/record-union-keys.test.ts @@ -0,0 +1,148 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../../index.js"; +import { BOOL, F64, VOID, type IrExpr, type IrModule, type IrType } from "../../ir/ir.js"; +import { validateModule } from "../../ir/validate.js"; +import { emitLlvmModule } from "./emitter.js"; + +const loc = { file: "record-union-keys.ts", start: 0, end: 0 }; + +async function llvmFor(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-record-union-keys-")); + try { + const entry = join(dir, "main.ts"), + output = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { + outDir: dir, + outPath: output, + outputKind: "ir", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n")); + const module = deserializeModule(await readFile(output, "utf8")); + expect(validateModule(module)).toEqual([]); + return emitLlvmModule(module); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function body(ir: string, name: string): string { + const found = new RegExp(`^define internal [^\\n]*@${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(ir); + expect(found, name).not.toBeNull(); + return found![0]; +} + +test("Sets and Maps of record unions key the record pointer", async () => { + const ir = await llvmFor(` +type Num = { kind: "num"; value: number }; +type Neg = { kind: "neg"; operand: Node }; +type Add = { kind: "add"; left: Node; right: Node }; +type Node = Num | Neg | Add; +const one: Node = { kind: "num", value: 1 }; +const nodes: Node[] = [one, { kind: "neg", operand: one }, { kind: "add", left: one, right: one }]; +const seen = new Set(); +const depth = new Map(); +for (const node of nodes) { + seen.add(node); + depth.set(node, node.kind === "num" ? 0 : 1); +} +console.log(seen.size, depth.get(nodes[1]!), seen.has({ kind: "num", value: 1 })); +`); + // SCR_MAP_KEY_REF (2), never SCR_MAP_KEY_UNION_REF (3), and no union box. + expect(ir).toMatch(/call ptr @scr_map_new\(i32 2,/); + expect(ir).not.toMatch(/call ptr @scr_map_new\(i32 3,/); + expect(ir).not.toMatch(/call ptr @scr_union_new_ref/); +}); + +const recordType = (shapeId: string): IrType => ({ kind: "record", shapeId }); +const wide: IrType = { kind: "union", unionId: "wide" }; + +/** `pick(u)` answers whether `u` holds arm 0, 2 or 3 of a five-record union + * through an `||` chain of tag tests; `same` is a second union over the same + * records in another order. */ +function tagChainModule(): IrModule { + const u: IrExpr = { kind: "varRef", localId: "u", type: wide, loc }; + const isTag = (tag: number): IrExpr => ({ + kind: "unionIsTag", + unionId: "wide", + tag, + negated: false, + value: u, + type: BOOL, + loc, + }); + const or = (left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "logical", + op: "||", + left, + right, + type: BOOL, + loc, + }); + const shapes = ["a", "b", "c", "d", "e"]; + return { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + records: shapes.map((id) => ({ id, fields: [{ name: id, type: F64 }] })), + unions: [ + { id: "wide", arms: shapes.map(recordType) }, + { id: "same", arms: [...shapes].reverse().map(recordType) }, + ], + functions: [ + { + name: "main", + params: [], + returnType: VOID, + locals: [], + body: [], + loc, + }, + { + name: "pick", + params: [{ localId: "u", name: "u", type: wide }], + returnType: BOOL, + locals: [{ id: "u", name: "u", type: wide, mutable: false }], + body: [{ kind: "return", value: or(or(isTag(0), isTag(2)), isTag(3)), loc }], + loc, + }, + { + name: "keep", + params: [{ localId: "s", name: "s", type: { kind: "union", unionId: "same" } }], + returnType: VOID, + locals: [ + { + id: "s", + name: "s", + type: { kind: "union", unionId: "same" }, + mutable: false, + }, + ], + body: [], + loc, + }, + ], + }; +} + +test("an || chain of tag tests is one switch on the shape word", () => { + const mod = tagChainModule(); + expect(validateModule(mod)).toEqual([]); + const ir = emitLlvmModule(mod, { pointerBits: 64 }); + const pick = body(ir, "sc_bf_pick"); + const switches = pick.match(/switch i32 [^\n]*/g) ?? []; + expect(switches).toHaveLength(1); + expect(switches[0]!.match(/label %tags\.y/g)).toHaveLength(3); + expect(pick).not.toMatch(/@sc_stag\./); +}); + +test("unions over the same records share one release", () => { + const ir = emitLlvmModule(tagChainModule(), { pointerBits: 64 }); + expect(ir.match(/^define internal void @sc_srelease\.[^\n]*noinline/gm)).toHaveLength(1); + expect(ir).toMatch(/^define internal void @sc_srelease\.wide\(/m); + expect(ir).not.toMatch(/^define internal void @sc_srelease\.same\(/m); +}); diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index a966ff305..d5a77c76d 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -233,7 +233,7 @@ export { computeTraced } from "../cycle-analysis.js"; * `_v`-shaped. */ export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release: string } { const nullable = host.nullableUnions.of(t); - if (nullable?.repr === "shapes") return shapeUnionRc(nullable); + if (nullable?.repr === "shapes") return shapeUnionRc(host, nullable); if (nullable) { const arm = vAdapters(host, nullable.arm); return { retain: nullableRetainSym(host, nullable.arm), release: arm.release }; @@ -290,7 +290,7 @@ export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release export function retainSym(host: ShapeHost, t: IrType): string { if (t.kind === "jsval") return jsvalRcSym(host, "retain"); const nullable = host.nullableUnions.of(t); - if (nullable?.repr === "shapes") return shapeUnionRc(nullable).retain; + if (nullable?.repr === "shapes") return shapeUnionRc(host, nullable).retain; if (nullable) return nullableRetainSym(host, nullable.arm); const stem = runtimeRcStem(t); if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "retain") !== null) { @@ -305,10 +305,10 @@ export function retainSym(host: ShapeHost, t: IrType): string { /** A shape union's RC entry points: every record retain is the same * NULL- and immortal-skipping increment (the null sentinel is immortal), * so the first arm's serves; release dispatches on the arm. */ -function shapeUnionRc(union: ShapeUnion): { retain: string; release: string } { +function shapeUnionRc(host: ShapeHost, union: ShapeUnion): { retain: string; release: string } { return { retain: `@${mangleRecordRetain(union.arms[0]!.shapeId)}`, - release: shapeReleaseFn(union.unionId), + release: shapeReleaseFn(host.nullableUnions.helperOwner(union.unionId)), }; } @@ -352,38 +352,72 @@ export function emitShapeUnionHelpers(host: ShapeHost): string[] { `}`, ``, ); - // release: unit encodings are NULL or immortal; each arm releases - // through its own (NULL- and immortal-tolerant) helper. - out.push( - `define internal void ${shapeReleaseFn(union.unionId)}(ptr %u) ${FN_ATTRS} { ; shape union release`, - `entry:`, - ` %t = call i32 ${tagFn}(ptr %u)`, - ` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`, - ...union.arms.flatMap((arm) => [ - `a${arm.tag}:`, - ` call void @${mangleRecordRelease(arm.shapeId)}(ptr %u)`, - ` br label %done`, - ]), + // Release and trace: one definition for the unions over the same + // record arms (helperOwner). Unit encodings are NULL or the immortal + // null sentinel; the shape word picks the arm. + if (host.nullableUnions.helperOwner(union.unionId) !== union.unionId) continue; + const units = [ + ` %isnull = icmp eq ptr %o, null`, + ` br i1 %isnull, label %done, label %${host.nullableUnions.usesNullSentinel ? "notnull" : "rec"}`, + ...(host.nullableUnions.usesNullSentinel + ? [ + `notnull:`, + ` %isnullarm = icmp eq ptr %o, ${NULLABLE_NULL}`, + ` br i1 %isnullarm, label %done, label %rec`, + ] + : []), + `rec:`, + ]; + const armSwitch = (call: (shapeId: string) => string): string[] => [ + ` %wp = getelementptr inbounds ${header}, ptr %o, i32 0, i32 ${SHAPE_WORD_INDEX}`, + ` %w = load i32, ptr %wp`, + ` switch i32 %w, label %done [ ${union.arms.map((arm) => `i32 ${host.nullableUnions.shapeNumber(arm.shapeId)}, label %a${arm.tag}`).join(" ")} ]`, + ...union.arms.flatMap((arm) => [`a${arm.tag}:`, call(arm.shapeId), ` br label %done`]), `done:`, ` ret void`, `}`, ``, - ); + ]; + // Every record arm shares the release fast path (the count at the same + // offset, one collector header choice for the whole union), so only a + // release that may free dispatches on the arm. The release stays a call: + // inlined at every release site, it grows code more than it saves. + const traced = union.arms.map((arm) => host.tracedShapes.has(`record:${arm.shapeId}`)); + const release = shapeReleaseFn(union.unionId); + if (traced.every((t) => t === traced[0])) + out.push( + `define internal void ${release}(ptr %o) noinline ${FN_ATTRS} { ; shape union release`, + `entry:`, + ...units, + ` %rc = load ${S}, ptr %o`, + ` %rcm2 = sub ${S} %rc, 2`, + ` %shared = icmp ult ${S} %rcm2, -3`, + ` br i1 %shared, label %dec, label %slow`, + `dec:`, + ` %n = sub ${S} %rc, 1`, + ` store ${S} %n, ptr %o`, + ...(traced[0] ? cycleRootLines(host, "done") : [` br label %done`]), + `slow:`, + ` %imm = icmp eq ${S} %rc, -1`, + ` br i1 %imm, label %done, label %free`, + `free:`, + ...armSwitch((shapeId) => ` call void @${mangleRecordRelease(shapeId)}_slow(ptr %o)`), + ); + else + out.push( + `define internal void ${release}(ptr %o) ${FN_ATTRS} { ; shape union release`, + `entry:`, + ...units, + ...armSwitch((shapeId) => ` call void @${mangleRecordRelease(shapeId)}(ptr %o)`), + ); if (shapeUnionTraced(host, union)) out.push( - `define internal void ${shapeTraceFn(union.unionId)}(ptr %u, ptr %visit, ptr %ctx) ${FN_ATTRS} { ; shape union trace`, + `define internal void ${shapeTraceFn(union.unionId)}(ptr %o, ptr %visit, ptr %ctx) ${FN_ATTRS} { ; shape union trace`, `entry:`, - ` %t = call i32 ${tagFn}(ptr %u)`, - ` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`, - ...union.arms.flatMap((arm) => [ - `a${arm.tag}:`, - ` call void @${mangleRecordTrace(arm.shapeId)}(ptr %u, ptr %visit, ptr %ctx)`, - ` br label %done`, - ]), - `done:`, - ` ret void`, - `}`, - ``, + ...units, + ...armSwitch( + (shapeId) => ` call void @${mangleRecordTrace(shapeId)}(ptr %o, ptr %visit, ptr %ctx)`, + ), ); } return out; @@ -435,7 +469,7 @@ export function emitNullableRetainWrappers(host: ShapeHost): string[] { export function releaseSym(host: ShapeHost, t: IrType): string { if (t.kind === "jsval") return jsvalRcSym(host, "release"); const nullable = host.nullableUnions.of(t); - if (nullable?.repr === "shapes") return shapeUnionRc(nullable).release; + if (nullable?.repr === "shapes") return shapeUnionRc(host, nullable).release; if (nullable) return releaseSym(host, nullable.arm); const stem = runtimeRcStem(t); if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "release") !== null) { @@ -985,7 +1019,9 @@ export function traceAdapter(host: ShapeHost, t: IrType): string | null { case "union": { const nullable = host.nullableUnions.get(t.unionId); if (nullable?.repr === "shapes") - return shapeUnionTraced(host, nullable) ? shapeTraceFn(nullable.unionId) : null; + return shapeUnionTraced(host, nullable) + ? shapeTraceFn(host.nullableUnions.helperOwner(nullable.unionId)) + : null; if (nullable) return traceAdapter(host, nullable.arm); if (!host.tracedUnions.has(t.unionId)) return null; host.declare(`declare void @scr_union_trace_v(ptr, ptr, ptr)`); diff --git a/packages/compiler/src/backend/llvm/test-boxed-unions.ts b/packages/compiler/src/backend/llvm/test-boxed-unions.ts index 15cf5aee9..8847d53a3 100644 --- a/packages/compiler/src/backend/llvm/test-boxed-unions.ts +++ b/packages/compiler/src/backend/llvm/test-boxed-unions.ts @@ -1,14 +1,29 @@ -import type { IrModule, IrType } from "../../ir/ir.js"; +import { F64, type IrModule, type IrType, type IrUnionDef } from "../../ir/ir.js"; /** Test helper: a Set of each union keeps it a tagged `%ScrUnion` box (Set * elements are runtime-inspected boxes), so stack-box tests exercise the - * paths that nullable-pointer unions (nullable-unions.ts) bypass. */ + * paths that nullable-pointer unions (nullable-unions.ts) bypass. A union of + * records alone is a Set element by record identity, so for those the Set + * holds a companion union with a number arm, which keeps the records out of + * every shape union. */ export function keepUnionsBoxed(mod: IrModule): IrModule { - const keep = (mod.unions ?? []).map((union) => ({ - id: `%g.keep.${union.id}`, - name: `keep_${union.id}`, - type: { kind: "set", elem: { kind: "union", unionId: union.id } } as IrType, - mutable: false, - })); - return { ...mod, globals: [...(mod.globals ?? []), ...keep] }; + const companions: IrUnionDef[] = []; + const keep = (mod.unions ?? []).map((union) => { + let elem = union.id; + if (union.arms.every((arm) => arm.kind === "record")) { + elem = `%keep.${union.id}`; + companions.push({ id: elem, arms: [...union.arms, F64] }); + } + return { + id: `%g.keep.${union.id}`, + name: `keep_${union.id}`, + type: { kind: "set", elem: { kind: "union", unionId: elem } } as IrType, + mutable: false, + }; + }); + return { + ...mod, + unions: [...(mod.unions ?? []), ...companions], + globals: [...(mod.globals ?? []), ...keep], + }; } From 53bddc6ed7b62eb3f9e9bbb81296ae7b64792f39 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 18:46:36 -0500 Subject: [PATCH 134/273] Give the module namespace handle its own tag --- packages/runtime/src/scr_dyn.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 04bb9bb22..a3264b5d0 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -186,7 +186,7 @@ typedef enum { X(MATCH_SOURCE, 55) /* a match result's pending metadata (scr_regex.c) */ \ X(MESSAGE_EVENT, 50) /* ScrEvent with data — BroadcastChannel's 'message' */ \ X(MESSAGE_PORT, 36) \ - X(MODULE_NAMESPACE, 78) /* module namespace object read through its exports (scr_module_namespace.c) */ \ + X(MODULE_NAMESPACE, 79) /* module namespace object read through its exports (scr_module_namespace.c) */ \ X(NET_BLOCK_LIST, 48) /* net.BlockList (scr_net.c) */ \ X(NET_SERVER, 3) /* ScrNetServer — net.Server (http.Server rides the same handle) */ \ X(NET_SOCKET, 2) /* ScrNetSocket — net.Socket */ \ From b7491676bbd23afbe423b406133321886c68637c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 19:01:18 -0500 Subject: [PATCH 135/273] Read the runtime ABI in the runtime pack test fixture from the compiler --- packages/compiler/src/backend/runtime-pack-native.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/backend/runtime-pack-native.test.ts b/packages/compiler/src/backend/runtime-pack-native.test.ts index 293d814b8..7a8a2e6c4 100644 --- a/packages/compiler/src/backend/runtime-pack-native.test.ts +++ b/packages/compiler/src/backend/runtime-pack-native.test.ts @@ -16,6 +16,7 @@ import { afterEach, expect, test, vi } from "vitest"; import { nativeCodegenDiag } from "../diagnostics/diagnostic.js"; import { compilerReleaseVersion } from "../library/sidecar.js"; import type { NativeLinkFeatures } from "./native-link-info.js"; +import { RUNTIME_ABI_MARKER, RUNTIME_ABI_VERSION } from "./runtime-abi.js"; import { RuntimePackError, type RuntimePackManifest } from "./runtime-pack-core.js"; import { runtimePackManifest, @@ -114,7 +115,7 @@ async function packFixture() { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" }, + runtime_abi: { version: RUNTIME_ABI_VERSION, marker: RUNTIME_ABI_MARKER }, compiler: { command: "clang", identity: "fixture clang", target: target.llvmTriple }, macros: { executable: [], From c41af6c36dc2a741ee762dbc57c485f39cb6225f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 19:04:38 -0500 Subject: [PATCH 136/273] Move the MIME handle tags to 81 and 82 Main now uses tag 78 (STRING_DECODER), 79 (MODULE_NAMESPACE, after #2161) and 80; 81 and 82 are the next free values. --- packages/runtime/src/scr_dyn.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 4fcba7bfe..f7014f259 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -186,8 +186,8 @@ typedef enum { X(MATCH_SOURCE, 55) /* a match result's pending metadata (scr_regex.c) */ \ X(MESSAGE_EVENT, 50) /* ScrEvent with data — BroadcastChannel's 'message' */ \ X(MESSAGE_PORT, 36) \ - X(MIME_PARAMS, 78) /* util.MIMEParams (scr_mime.c) */ \ - X(MIME_TYPE, 79) /* util.MIMEType (scr_mime.c) */ \ + X(MIME_PARAMS, 81) /* util.MIMEParams (scr_mime.c) */ \ + X(MIME_TYPE, 82) /* util.MIMEType (scr_mime.c) */ \ X(NET_BLOCK_LIST, 48) /* net.BlockList (scr_net.c) */ \ X(NET_SERVER, 3) /* ScrNetServer — net.Server (http.Server rides the same handle) */ \ X(NET_SOCKET, 2) /* ScrNetSocket — net.Socket */ \ From 7464f12e6b4649b2aa67489b812c2c5a0e58d652 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 20:01:49 -0500 Subject: [PATCH 137/273] Emit unwinding code for textual LLVM and assembly output on Linux --- packages/compiler/src/index.ts | 3 +++ packages/compiler/src/native/prepare.ts | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 51230703c..345e7565e 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -809,6 +809,9 @@ async function prepareExecutableInput( pointerBits: buildPlatform === "wasi" ? 32 : 64, wasi: buildPlatform === "wasi", runtimeAbiMarker: outputKind === "obj", + // Textual output shows the code an executable gets; objects keep + // the pending flag, since their link may not add an unwinder. + unwindAbi: buildPlatform === "linux" && outputKind !== "obj", objectAudit: opts.sanitize === true, lowRegion: lowRegionTarget(buildPlatform, opts.sanitize === true), ...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}), diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index c15bfbf9b..13d32fd3b 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -408,7 +408,11 @@ export function prepareNativeExecutable( pointerBits: toolchain.target.pointerBits, wasi: toolchain.target.platform === "wasi", runtimeAbiMarker: outputKind === "obj" || outputKind === "exe", - unwindAbi: outputKind === "exe" && toolchain.target.platform === "linux", + // As for the Node-hosted build: executables and their textual + // LLVM and assembly, not objects. + unwindAbi: + (outputKind === "exe" || outputKind === "llvm" || outputKind === "asm") && + toolchain.target.platform === "linux", objectAudit: options.sanitize === true, lowRegion: lowRegionTarget(toolchain.target.platform, options.sanitize === true), functionWorkers: emitWorkers(), From e2db1ef344d3e09c8db683b4f32fddff8751de6f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 20:07:36 -0500 Subject: [PATCH 138/273] Hold Reflect.ownKeys's function value in the Reflect namespace object Reading `ownKeys` from Reflect read as a value (`const r = Reflect; r.ownKeys`) gave the member function that throws SC1090 when called, although the spelled `Reflect.ownKeys` read already has a function value. The namespace object now holds that function value, so calls through the alias run and `r.ownKeys === Reflect.ownKeys` is true, as in Node. The spelled read and the namespace member share one lowering (reflectMemberValue). --- .../frontend/lowering/builtin-namespaces.ts | 3 ++- .../frontend/lowering/builtins/reflection.ts | 19 ++++++++++++++++++- .../lowering/expressions/property-access.ts | 16 +++------------- tests/corpus/7186-namespace-object-values.js | 5 +++++ 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/builtin-namespaces.ts b/packages/compiler/src/frontend/lowering/builtin-namespaces.ts index bfe3c5e39..c1e40d2e6 100644 --- a/packages/compiler/src/frontend/lowering/builtin-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/builtin-namespaces.ts @@ -24,6 +24,7 @@ import type { Lowerer } from "./lowerer.js"; import { InternalCompilerError } from "../../errors.js"; import { mathMemberValue } from "./lower-stdlib.js"; import { jsonMemberValue } from "./builtins/json.js"; +import { reflectMemberValue } from "./builtins/reflection.js"; /** A function member with its `length`, or a constant (`length` -1). */ interface NamespaceMember { @@ -244,7 +245,7 @@ function functionValue(lowerer: Lowerer, canonical: string, key: string, loc: Sr ? mathMemberValue(lowerer, key, loc) : canonical === "JSON" ? jsonMemberValue(lowerer, key, loc) - : null; + : reflectMemberValue(lowerer, key, loc); if (value !== null) return value.type.kind === "dyn" ? value diff --git a/packages/compiler/src/frontend/lowering/builtins/reflection.ts b/packages/compiler/src/frontend/lowering/builtins/reflection.ts index 8e1e6fd94..5171ebfdb 100644 --- a/packages/compiler/src/frontend/lowering/builtins/reflection.ts +++ b/packages/compiler/src/frontend/lowering/builtins/reflection.ts @@ -3,7 +3,24 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; import { builtinModuleFnOf } from "../surfaces.js"; -import { BOOL, DYN, type IrExpr, STRING, arrayOf } from "../../../ir/ir.js"; +import { BOOL, DYN, type IrExpr, STRING, type SrcLoc, arrayOf } from "../../../ir/ir.js"; + +/** The function value of the Reflect member `member` (ownKeys, on native + * computed-key reflection), or null for members without one. */ +export function reflectMemberValue(lowerer: Lowerer, member: string, loc: SrcLoc): IrExpr | null { + if (member !== "ownKeys") return null; + const value = lowerer.lowerNativeCallableValue( + { + fn: "dyn.ownKeys", + params: [DYN], + result: DYN, + valueParams: [{ mode: "required", type: DYN }], + }, + "Reflect.ownKeys", + loc, + )!; + return { kind: "dynFrom", value, fnName: "ownKeys", type: DYN, loc }; +} /** Reflect.ownKeys uses native computed-key reflection. Reflect.apply(target, * thisArg, argsList) where the TARGET is a builtin diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 536ef93c9..7c931aae6 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -161,6 +161,7 @@ import { lowerNetAddressStaticRead } from "../builtins/net-address.js"; import { STATS_TYPE_TESTS } from "../builtins/filesystem.js"; import { arrayValueRead, arrayValueType } from "../array-values.js"; import { lowerClassMethodValue } from "../class-method-values.js"; +import { reflectMemberValue } from "../builtins/reflection.js"; import { objLitGenericFnInfoOf, objLitGenericFnNodeOf, @@ -1691,19 +1692,8 @@ export function lowerIntrinsicProperty( ); } } - if (lowerer.stdlibGlobalMember(expr, "Reflect") === "ownKeys") { - const value = lowerer.lowerNativeCallableValue( - { - fn: "dyn.ownKeys", - params: [DYN], - result: DYN, - valueParams: [{ mode: "required", type: DYN }], - }, - "Reflect.ownKeys", - locOf(expr), - )!; - return { kind: "dynFrom", value, fnName: "ownKeys", type: DYN, loc: locOf(expr) }; - } + if (lowerer.stdlibGlobalMember(expr, "Reflect") === "ownKeys") + return reflectMemberValue(lowerer, "ownKeys", locOf(expr)); if (lowerer.chainBlocked(expr)) return null; // Object.prototype.constructor: the Object constructor value. if ( diff --git a/tests/corpus/7186-namespace-object-values.js b/tests/corpus/7186-namespace-object-values.js index c61d96e11..33c30e12f 100644 --- a/tests/corpus/7186-namespace-object-values.js +++ b/tests/corpus/7186-namespace-object-values.js @@ -20,3 +20,8 @@ console.log(parse("[1,2]").length, JSON.stringify({ a: [1] })); console.log(typeof Math === "object" && typeof Reflect === "object"); console.log(Math.max.length, Math.min.length, Math.hypot.length, Math.abs.length); + +const r = Reflect; +const ownKeys = r.ownKeys; +console.log(ownKeys === Reflect.ownKeys, r.ownKeys === Reflect.ownKeys, typeof ownKeys); +console.log(r.ownKeys({ a: 1, b: 2 }).length, ownKeys({ c: 3 })[0], ownKeys.name, ownKeys.length); From 8032e6c6c67211e4e8d5187a9a74d6444af49b3c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 20:08:30 -0500 Subject: [PATCH 139/273] Free frozen records when a shape union drops their last reference A frozen record's rc word carries SCR_RC_FROZEN beside its count. The shared shape-union release compared the whole word, so the last reference to a frozen record took the shared-reference branch: it decremented the count and never freed the record or its fields. When an arm is freezable, the release now compares the count without the frozen bit, as the record's own release does. --- .../backend/llvm/record-union-keys.test.ts | 21 ++++++ packages/compiler/src/backend/llvm/shapes.ts | 13 +++- .../8291-frozen-record-union-keys.ts.json | 6 ++ tests/corpus/8291-frozen-record-union-keys.ts | 65 +++++++++++++++++++ .../8291-frozen-record-union-keys.ts.json | 1 + 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8291-frozen-record-union-keys.ts.json create mode 100644 tests/corpus/8291-frozen-record-union-keys.ts create mode 100644 tests/harness/corpus-costs/8291-frozen-record-union-keys.ts.json diff --git a/packages/compiler/src/backend/llvm/record-union-keys.test.ts b/packages/compiler/src/backend/llvm/record-union-keys.test.ts index 8c3a89b96..3c961cabb 100644 --- a/packages/compiler/src/backend/llvm/record-union-keys.test.ts +++ b/packages/compiler/src/backend/llvm/record-union-keys.test.ts @@ -146,3 +146,24 @@ test("unions over the same records share one release", () => { expect(ir).toMatch(/^define internal void @sc_srelease\.wide\(/m); expect(ir).not.toMatch(/^define internal void @sc_srelease\.same\(/m); }); + +test("a union with a freezable arm compares its count without the frozen bit", async () => { + const ir = await llvmFor(` +type Name = { kind: "name"; text: string }; +type Count = { kind: "count"; value: number }; +type Key = Name | Count; +function name(text: string): Key { + const built: Name = { kind: "name", text }; + return Object.freeze(built); +} +const seen = new Set(); +seen.add(name("a")); +seen.add({ kind: "count", value: 1 }); +console.log(seen.size); +`); + const release = /^define internal void @sc_srelease\.[^\n]*\n[^]*?^}/m.exec(ir); + expect(release).not.toBeNull(); + // SCR_RC_FROZEN is bit 62: a frozen record's last reference has count 1. + expect(release![0]).toContain("and i64 %rc, -4611686018427387905"); + expect(release![0]).toContain("icmp ult i64 %rcm2, -4611686018427387907"); +}); diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index d5a77c76d..ab623f934 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -383,6 +383,10 @@ export function emitShapeUnionHelpers(host: ShapeHost): string[] { // release that may free dispatches on the arm. The release stays a call: // inlined at every release site, it grows code more than it saves. const traced = union.arms.map((arm) => host.tracedShapes.has(`record:${arm.shapeId}`)); + // A freezable arm's count is the word without its frozen bit, as in + // releaseFastPath: a frozen record's last reference takes the slow path. + const frozen = union.arms.some((arm) => host.recordsById.get(arm.shapeId)?.freezable === true); + const notFrozen = ~recordFrozenBit(S); const release = shapeReleaseFn(union.unionId); if (traced.every((t) => t === traced[0])) out.push( @@ -390,8 +394,13 @@ export function emitShapeUnionHelpers(host: ShapeHost): string[] { `entry:`, ...units, ` %rc = load ${S}, ptr %o`, - ` %rcm2 = sub ${S} %rc, 2`, - ` %shared = icmp ult ${S} %rcm2, -3`, + ...(frozen + ? [ + ` %refs = and ${S} %rc, ${sizeLiteral(S, notFrozen)}`, + ` %rcm2 = sub ${S} %refs, 2`, + ` %shared = icmp ult ${S} %rcm2, ${sizeLiteral(S, notFrozen - 2n)}`, + ] + : [` %rcm2 = sub ${S} %rc, 2`, ` %shared = icmp ult ${S} %rcm2, -3`]), ` br i1 %shared, label %dec, label %slow`, `dec:`, ` %n = sub ${S} %rc, 1`, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8291-frozen-record-union-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8291-frozen-record-union-keys.ts.json new file mode 100644 index 000000000..3dcadeb0a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8291-frozen-record-union-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8291-frozen-record-union-keys.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8291-frozen-record-union-keys.ts b/tests/corpus/8291-frozen-record-union-keys.ts new file mode 100644 index 000000000..b6173ef4e --- /dev/null +++ b/tests/corpus/8291-frozen-record-union-keys.ts @@ -0,0 +1,65 @@ +// Frozen records in record-only unions used as Set elements and Map keys: +// the union's release frees a frozen record when it drops the last +// reference, for unions the cycle collector traces and ones it does not. +type Leaf = { kind: "leaf"; label: string }; +type Pair = { kind: "pair"; left: Node; right: Node }; +type Node = Leaf | Pair; + +type Name = { kind: "name"; text: string }; +type Count = { kind: "count"; value: number; unit: string }; +type Key = Name | Count; + +function leaf(label: string): Node { + const built: Leaf = { kind: "leaf", label }; + return Object.freeze(built); +} + +function pair(left: Node, right: Node): Node { + const built: Pair = { kind: "pair", left, right }; + return Object.freeze(built); +} + +function key(i: number): Key { + if (i % 2 === 0) { + const name: Name = { kind: "name", text: "n" + i }; + return Object.freeze(name); + } + const count: Count = { kind: "count", value: i, unit: "u" + i }; + return Object.freeze(count); +} + +function nodes(n: number): number { + const seen = new Set(); + const weight = new Map(); + let last = leaf("start"); + for (let i = 0; i < n; i++) { + const next = i % 3 === 0 ? leaf("l" + i) : pair(last, leaf("r" + i)); + seen.add(next); + weight.set(next, i); + last = next; + } + let total = 0; + for (const node of seen) total += (weight.get(node) ?? 0) + (node.kind === "pair" ? 1 : 0); + seen.clear(); + weight.clear(); + return total + (last.kind === "pair" ? 1 : 0); +} + +function keys(n: number): number { + const seen = new Set(); + const size = new Map(); + for (let i = 0; i < n; i++) { + const k = key(i); + seen.add(k); + size.set(k, k.kind === "name" ? k.text.length : k.value); + } + let total = 0; + for (const k of seen) total += size.get(k) ?? 0; + seen.clear(); + size.clear(); + return total; +} + +let sum = 0; +for (let r = 0; r < 50; r++) sum += nodes(40) + keys(40); +console.log(sum); diff --git a/tests/harness/corpus-costs/8291-frozen-record-union-keys.ts.json b/tests/harness/corpus-costs/8291-frozen-record-union-keys.ts.json new file mode 100644 index 000000000..77d20b756 --- /dev/null +++ b/tests/harness/corpus-costs/8291-frozen-record-union-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 5000 } From c091adc0e4ff0a333d163318bb7d288254439e91 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 20:09:28 -0500 Subject: [PATCH 140/273] Build symbol-keyed fields into frozen class views whatever their attributes A JavaScript class whose instances track which fields they have copies the instance's property bag into its view first, attributes included, and then wrote each symbol-keyed field with an ordinary assignment. Once the instance was frozen, its symbol entry was read-only and the copy non-extensible, so any later reflection that rebuilt the view (Object.isFrozen, a property read) threw "Cannot assign to a read only symbol property". String-keyed fields already use scr_dyn_obj_set_field, which replaces a field's value whatever the attributes copied into the view. The new scr_dyn_symbol_field_set does the same for symbol-keyed fields, and the view uses it when it copies the bag or carries integrity attributes. --- .../src/backend/llvm/expr-stream-bridges.ts | 10 ++++-- ...7408-frozen-js-class-symbol-fields.js.json | 6 ++++ packages/runtime/src/scr_dyn.h | 4 +++ packages/runtime/src/scr_json.c | 31 ++++++++++++++++++ .../7408-frozen-js-class-symbol-fields.js | 32 +++++++++++++++++++ 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7408-frozen-js-class-symbol-fields.js.json create mode 100644 tests/corpus/7408-frozen-js-class-symbol-fields.js diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 1f66324ad..0a502eea7 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -1004,9 +1004,15 @@ export function streamTypedRefMaterializeAdapter( host.declare(`declare void @scr_union_release(ptr)`); B.line(`call void @scr_union_release(ptr ${fieldValue})`); } - host.declare(`declare void @scr_dyn_symbol_key_set(ptr, ptr, ptr)`); + // As for string-keyed fields, attributes copied from the bag (a frozen + // instance's) do not stop the view taking the value. + const symbolSetter = + meta.def.tracksOwnFields || withAttributes + ? "scr_dyn_symbol_field_set" + : "scr_dyn_symbol_key_set"; + host.declare(`declare void @${symbolSetter}(ptr, ptr, ptr)`); host.declare(`declare void @scr_dyn_release_v(ptr)`); - B.line(`call void @scr_dyn_symbol_key_set(ptr ${out}, ptr ${key}, ptr ${value})`); + B.line(`call void @${symbolSetter}(ptr ${out}, ptr ${key}, ptr ${value})`); B.line(`call void @scr_dyn_release_v(ptr ${value})`); if (afterSymbol) { B.terminate(`br label %${afterSymbol}`); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7408-frozen-js-class-symbol-fields.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7408-frozen-js-class-symbol-fields.js.json new file mode 100644 index 000000000..a57d1c73b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7408-frozen-js-class-symbol-fields.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7408-frozen-js-class-symbol-fields.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 3e5cdd4a5..691453783 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -870,6 +870,10 @@ bool scr_disposable_stack_use(const ScrDyn *stack, const ScrDyn *value); bool scr_disposable_stack_use_callback(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *dispose); bool scr_disposable_stack_adopt(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *on_dispose); void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored); +/* scr_dyn_obj_set_field for a symbol-keyed field of a class instance's + * view: replaces the value whatever the attributes copied into the view. + * Does not consume stored. */ +void scr_dyn_symbol_field_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored); void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key); bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key); bool scr_dyn_has_own_computed(const ScrDyn *value, const ScrDyn *key); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index e3f27cf61..a32958e51 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -7989,6 +7989,37 @@ void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored) { scr_dyn_release(receiver); } +/* scr_dyn_obj_set_field for a symbol-keyed field of a class instance's + * view: the view takes the native value whatever the attributes copied into + * it from the instance's property bag (a frozen instance's view is built the + * same way as any other). An accessor entry takes the ordinary assignment. + * Does not consume value. */ +void scr_dyn_symbol_field_set(ScrDyn *obj, ScrDyn *key, ScrDyn *value) { + ScrDynEntry *entry = obj->kind == SCR_DYN_OBJ && obj->symbol_properties + ? scr_dyn_symbol_entry(obj, key, true) + : NULL; + if (entry && !entry->accessor) { + scr_dyn_region_store(obj->symbol_properties, value); + ScrDyn *old = entry->value; + entry->value = scr_dyn_retain(value); + scr_dyn_release(old); + return; + } + if (entry || obj->kind != SCR_DYN_OBJ) { + scr_dyn_symbol_key_set(obj, key, value); + return; + } + bool non_extensible = obj->non_extensible; + ScrDyn *table = obj->symbol_properties; + bool table_non_extensible = table && table->non_extensible; + obj->non_extensible = false; + if (table) table->non_extensible = false; + scr_dyn_symbol_key_set(obj, key, value); + obj->non_extensible = non_extensible; + if (obj->symbol_properties) + obj->symbol_properties->non_extensible = table ? table_non_extensible : non_extensible; +} + static ScrDyn *scr_dyn_define_symbol(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) { ScrDyn *receiver = scr_dyn_symbol_receiver(target); if (!receiver) { diff --git a/tests/corpus/7408-frozen-js-class-symbol-fields.js b/tests/corpus/7408-frozen-js-class-symbol-fields.js new file mode 100644 index 000000000..9ce5a1c53 --- /dev/null +++ b/tests/corpus/7408-frozen-js-class-symbol-fields.js @@ -0,0 +1,32 @@ +// A JavaScript class whose instances track which fields they have (one +// field is created by a method) and carry a symbol-keyed field. Freezing +// or sealing such an instance makes its symbol entry read-only; later +// reflection rebuilds the instance's view and takes the field's value. +const tag = Symbol("tag"); +class Box { + constructor(v) { + this.v = v; + this[tag] = v; + } + later() { + this.w = 3; + } +} + +const frozen = new Box(1); +frozen.extra = 2; +Object.freeze(frozen); +console.log(Object.isFrozen(frozen)); +console.log(Object.isFrozen(frozen), Object.isSealed(frozen), Object.isExtensible(frozen)); +console.log(frozen[tag], frozen.v, frozen.extra, frozen.w); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(frozen, tag))); + +const sealed = new Box(2); +sealed.later(); +Object.seal(sealed); +console.log(Object.isSealed(sealed), Object.isFrozen(sealed), sealed[tag], sealed.w); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(sealed, tag))); + +const closed = new Box(4); +Object.preventExtensions(closed); +console.log(Object.isExtensible(closed), Object.isSealed(closed), closed[tag]); From 1547fd47cd31249b0417a5237f5c79d8fbb0d518 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 20:10:29 -0500 Subject: [PATCH 141/273] Keep record factory parameters that a closure in the record captures findRecordFieldProducers counted only direct reads of a parameter, so a factory such as `(text) => ({ text, upper: () => text.toUpperCase() })` counted as storing `text` only in its field. A caller's slice passed to it was written into a stack buffer, and the closure kept by the caller then read whatever the next iteration wrote there. Captures by closures and classes now count as reads, so such a factory is not a producer. --- .../src/backend/llvm/string-slices.test.ts | 17 ++++++++++++++- .../src/backend/llvm/string-slices.ts | 3 +++ tests/corpus/8114-record-field-slices.ts | 21 +++++++++++++++++-- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/backend/llvm/string-slices.test.ts b/packages/compiler/src/backend/llvm/string-slices.test.ts index 43bec955a..e53bf38c6 100644 --- a/packages/compiler/src/backend/llvm/string-slices.test.ts +++ b/packages/compiler/src/backend/llvm/string-slices.test.ts @@ -365,17 +365,32 @@ test.each([ "escapes-callee", "captured", "producer-reuse", + "producer-capture", ])("a slice stored in a record that may be kept (%s) stays owned", (reason) => { + // The factory also stores a closure over its text parameter. + const capturing = producerHelper("capturing"); + const literal = (capturing.body[0] as IrStmt & { kind: "return" }).value as IrExpr & { + kind: "recordLit"; + }; + literal.fields.push({ + name: "get", + value: { kind: "closure", fnName: "get", captures: ["text"], type: F64, loc }, + }); const helpers = [ reads, keepsText, escapes, producerHelper("make"), producerHelper("twice", [keepIn(ref("text"))]), + capturing, ]; const init = slice("source", 1, 5); const declare = - reason === "producer-reuse" ? callOf("twice", ref("word")) : tokenLit(ref("word")); + reason === "producer-reuse" + ? callOf("twice", ref("word")) + : reason === "producer-capture" + ? callOf("capturing", ref("word")) + : tokenLit(ref("word")); if (declare.kind === "call") declare.type = TOKEN; const uses: IrStmt[] = [{ kind: "exprStmt", expr: equals(textOf(record("tok"))), loc }]; if (reason === "kept-field") uses.push(keepIn(textOf(record("tok")))); diff --git a/packages/compiler/src/backend/llvm/string-slices.ts b/packages/compiler/src/backend/llvm/string-slices.ts index 81a4a5a3e..d1bb197ee 100644 --- a/packages/compiler/src/backend/llvm/string-slices.ts +++ b/packages/compiler/src/backend/llvm/string-slices.ts @@ -419,6 +419,9 @@ export function findRecordFieldProducers( everyStmtList(fn.body, { expr: (value) => { if (value.kind === "varRef") reads.set(value.localId, (reads.get(value.localId) ?? 0) + 1); + // A closure or class that captures a parameter keeps it too. + if (value.kind === "closure" || value.kind === "classRef") + for (const id of value.captures ?? []) reads.set(id, (reads.get(id) ?? 0) + 1); return true; }, stmt: () => true, diff --git a/tests/corpus/8114-record-field-slices.ts b/tests/corpus/8114-record-field-slices.ts index 69d41a1c8..110cd3acb 100644 --- a/tests/corpus/8114-record-field-slices.ts +++ b/tests/corpus/8114-record-field-slices.ts @@ -2,8 +2,9 @@ // whose fields are only read for their length, comparisons and keyword // searches, next to records that are kept after the iteration that built // them (pushed into an array, held in an outer binding, captured by a -// closure, read into a kept array, returned from a helper). Slices of -// non-ASCII text and slices longer than a short token are included. +// closure, read into a kept array, returned from a helper, captured by a +// closure the factory stores beside it). Slices of non-ASCII text and slices +// longer than a short token are included. interface Token { kind: string; text: string; @@ -16,6 +17,15 @@ function token(kind: string, text: string, pos: number): Token { return { kind, text, pos }; } +interface LazyToken { + text: string; + upper: () => string; +} + +function lazyToken(text: string): LazyToken { + return { text, upper: () => text.toUpperCase() }; +} + function classify(text: string): string { return KEYWORDS.includes(text) ? "keyword" : "ident"; } @@ -74,6 +84,8 @@ function keptTokens(source: string): string { const texts: string[] = []; const later: (() => string)[] = []; const returned: string[] = []; + const uppers: (() => string)[] = []; + let lazyLength = 0; let last: Token | null = null; for (let i = 0, start = -1; i <= source.length; i++) { const c = i < source.length ? source.charCodeAt(i) : 32; @@ -93,6 +105,10 @@ function keptTokens(source: string): string { later.push(() => captured.text); const viaHelper = token("helper", source.slice(start, i), start); returned.push(textOf(viaHelper)); + const lazyText = source.slice(start, i); + const lazy = lazyToken(lazyText); + lazyLength += lazy.text.length; + uppers.push(lazy.upper); start = -1; } return [ @@ -101,6 +117,7 @@ function keptTokens(source: string): string { `texts ${texts.join("|")}`, `later ${later.map((f) => f()).join("|")}`, `returned ${returned.join("|")}`, + `upper ${lazyLength} ${uppers.map((f) => f()).join("|")}`, ].join("\n"); } From d64f18d2a8e17539afef33f4d3019c0c0cb7c584 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 20:11:25 -0500 Subject: [PATCH 142/273] Evaluate constructor arguments before the class-value check `new kinds[i](args)` over an array of class values read the element and narrowed it to the class value before the arguments ran, so an index past the end threw "is not a constructor" without evaluating them. JavaScript evaluates the arguments first: the construction is now a call of a lifted helper (checkedElementConstruction) that takes the element read and the arguments, in that order, then runs the check and constructs. Keeping the arguments in one call also keeps a spread's argument pack alive until every argument has read it. A checked class-value boundary accepted every subclass's class value into a base class's slot, so construction through the slot could call a subclass constructor that takes different arguments with the base constructor's. It now accepts only the subclasses whose class-value constructor takes the same arguments (classValueAbiEquals, the rule the classval upcast validator already applied); any other subclass fails the check with a TypeError. --- packages/compiler/src/backend/llvm/dyn.ts | 10 +- packages/compiler/src/backend/llvm/emitter.ts | 26 ++-- .../src/frontend/lowering/lower-classes.ts | 113 +++++++++++++++--- packages/compiler/src/ir/ir.ts | 22 ++++ packages/compiler/src/ir/validate.ts | 21 ++-- tests/corpus/6937-class-value-arrays.mjs | 23 ++++ tests/corpus/6938-class-value-arrays.ts | 20 ++++ 7 files changed, 198 insertions(+), 37 deletions(-) diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 1d2a14964..24db045c8 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -174,6 +174,10 @@ export interface DynHost extends WalkerHost { dynPromiseAdapter(t: IrType): string; isErrorClass(className: string): boolean; classSubtypes(className: string): readonly string[]; + /** The strict subclasses whose class-value constructor takes the same + * arguments as `className`'s (classValueAbiEquals): the class values a + * checked `classval` slot accepts besides its own class. */ + classValueSubtypes(className: string): readonly string[]; /** emitter.ts's pendingTestLines: the inline pending-exception test. */ pendingTestLines(dest: string): string[]; } @@ -1808,8 +1812,10 @@ export class LlDyn { const B = new BlockBuilder(); if (t.kind === "classval") { host.declare(`declare ptr @scr_dyn_class_check(ptr, ptr, ptr)`); - // A subclass's class value checks into the base class's slot. - const subclasses = host.classSubtypes(t.className).filter((name) => name !== t.className); + // A subclass's class value checks into the base class's slot when + // construction through the slot can call its thunk with the base + // constructor's arguments; any other subclass fails the check. + const subclasses = host.classValueSubtypes(t.className); if (subclasses.length > 0) { host.declare(`declare zeroext i1 @scr_dyn_class_is(ptr, ptr)`); for (const className of subclasses) { diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 6760f3b74..26641c667 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -55,6 +55,7 @@ import { typeKey, VOID, classJsName, + classValueAbiEquals, } from "../../ir/ir.js"; import { moduleRuntimeFeatures, type RuntimeFeatures } from "../../ir/runtime-features.js"; import { captureFreeFunctions, observesStacks } from "../stack-observations.js"; @@ -1895,14 +1896,14 @@ export class LlEmitter { const meta = this.classMeta.get(t.className); return meta !== undefined && programLifetimeClass(this.shapeHost, meta); }, - classSubtypes: (name) => { - const target = this.classMetaOf(name); - const intervals = classMembershipIntervals(this.classMeta, target.def.name); - return [...this.classMeta.values()] - .filter((meta) => - intervals.some((range) => range.pre <= meta.pre && meta.pre <= range.post), - ) - .map((meta) => meta.def.name); + classSubtypes: (name) => this.classSubtypes(name), + classValueSubtypes: (name) => { + const target = this.classMetaOf(name).def; + return this.classSubtypes(name).filter( + (sub) => + sub !== target.name && + classValueAbiEquals(this.classMetaOf(sub).def, target, this.fnByName), + ); }, pendingTestLines: (dest) => this.pendingTestLines(dest), virtualEntry: (implFn) => this.virtualEntry(implFn), @@ -3965,6 +3966,15 @@ export class LlEmitter { // ── class plumbing ────────────────────────────────────────────────────── + /** `name` and every class whose membership interval falls in its own. */ + private classSubtypes(name: string): string[] { + const target = this.classMetaOf(name); + const intervals = classMembershipIntervals(this.classMeta, target.def.name); + return [...this.classMeta.values()] + .filter((meta) => intervals.some((range) => range.pre <= meta.pre && meta.pre <= range.post)) + .map((meta) => meta.def.name); + } + classMetaOf(className: string): LlClassMeta { const meta = this.classMeta.get(className); if (!meta) throw new InternalCompilerError(`llvm emitter bug: unknown class ${className}`); diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index a91b3c30a..e1b4e890e 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -9782,6 +9782,75 @@ function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType return lowerer.currentClass?.fields.get(parent.left.name.text) ?? null; } +/** An array element read that construction narrows to a class value with a + * check (`%new.callee`, which throws "is not a constructor"). */ +interface NarrowedConstructorRead { + read: IrExpr; + check: string; + unionT: IrType & { kind: "union" }; + calleeT: IrType & { kind: "classval" }; + message: IrExpr; +} + +/** `new arr[i](args)` through a narrowed element read: an interned lifted + * function takes the read and the completed arguments, which evaluate in + * that order as call arguments, then runs the check and constructs. JS + * evaluates the arguments before it checks that the callee constructs, and + * the arguments stay in the one call, where a spread's argument pack lives + * until every argument has read it. */ +function checkedElementConstruction( + lowerer: Lowerer, + info: ClassInfo, + narrowed: NarrowedConstructorRead, + args: IrExpr[], + loc: SrcLoc, +): IrExpr { + const type: IrType = { kind: "object", className: info.def.name }; + const key = `newChecked:${narrowed.check}:${info.def.name}:${args.map((a) => typeKey(a.type)).join(",")}`; + let name = lowerer.valueHelpers.get(key); + if (!name) { + name = keyedHelperName(lowerer, "%new.checked", key); + lowerer.valueHelpers.set(key, name); + const params: IrParam[] = [ + { localId: "value", name: "value", type: narrowed.unionT }, + { localId: "message", name: "message", type: STRING }, + ...args.map((arg, i) => ({ localId: `arg.${i}`, name: `arg${i}`, type: arg.type })), + ]; + lowerer.liftedFns.push({ + name, + params, + returnType: type, + locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), + body: [ + { + kind: "return", + value: { + kind: "newValue", + callee: { + kind: "call", + callee: narrowed.check, + args: [varRef("value", narrowed.unionT, loc), varRef("message", STRING, loc)], + type: narrowed.calleeT, + loc, + }, + args: params.slice(2).map((p) => varRef(p.localId, p.type, loc)), + type, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { + kind: "call", + callee: name, + args: [narrowed.read, narrowed.message, ...args], + type, + loc, + }; +} /** Map, Set and the error classes construct in lowerNew's identifier * branch, which a module member alias of one (`new mod.M()`) also takes. */ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean { @@ -11045,6 +11114,9 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { } let callee = lowerer.lowerExpr(expr.expression); if (callee.type.kind === "dyn") return lowerCheckedConstructor(lowerer, expr, callee); + /** The union-typed element read a narrowing check guards, and the + * check (`%new.callee`) that yields the class value or throws. */ + let narrowedCallee: NarrowedConstructorRead | null = null; // An array element read (`new T[0]()` over `[Dog, Animal]`, whose // checker type is the common base) keeps the undefined an index past // the end reads: the class value is narrowed out with a check, which @@ -11118,18 +11190,25 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { loc, }); } + // JavaScript evaluates the arguments before it checks that the + // callee constructs, so the check runs at the construction + // (checkedElementConstruction). + narrowedCallee = { + read: callee, + check: name, + unionT, + calleeT, + message: { + kind: "strLit", + value: `${expr.expression.getText()} is not a constructor`, + type: STRING, + loc, + }, + }; callee = { kind: "call", callee: name, - args: [ - callee, - { - kind: "strLit", - value: `${expr.expression.getText()} is not a constructor`, - type: STRING, - loc, - }, - ], + args: [callee, narrowedCallee.message], type: calleeT, loc, }; @@ -11175,13 +11254,15 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const built = constructedFromValue( lowerer, info, - { - kind: "newValue", - callee, - args, - type: { kind: "object", className: info.def.name }, - loc, - }, + narrowedCallee !== null + ? checkedElementConstruction(lowerer, info, narrowedCallee, args, loc) + : { + kind: "newValue", + callee, + args, + type: { kind: "object", className: info.def.name }, + loc, + }, expr, ); // A runtime class slot widened from a union (`[Readable, Writable]`'s diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 3818433f8..b936e265e 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1630,6 +1630,28 @@ export function classValueConstructor(def: { name: string; runtime?: true }): { : { fn: `%${def.name}.constructor`, firstArg: 1 }; } +/** Whether construction through a class value typed as `sup` may call + * `sub`'s construct thunk: both class-value constructors exist and take the + * same argument types after the receiver. The classval upcast rule and the + * checked class-value boundary both rest on it. */ +export function classValueAbiEquals( + sub: { name: string; runtime?: true }, + sup: { name: string; runtime?: true }, + functions: ReadonlyMap, +): boolean { + const subValueCtor = classValueConstructor(sub); + const supValueCtor = classValueConstructor(sup); + const subCtor = functions.get(subValueCtor.fn); + const supCtor = functions.get(supValueCtor.fn); + if (!subCtor || !supCtor) return false; + const subArgs = subCtor.params.slice(subValueCtor.firstArg); + const supArgs = supCtor.params.slice(supValueCtor.firstArg); + return ( + subArgs.length === supArgs.length && + subArgs.every((p, i) => typeEquals(p.type, supArgs[i]!.type)) + ); +} + /** The JavaScript `.name` of a class: its jsName, or the runtime-provided * class's own name (`%TypeError` is TypeError). */ export function classJsName(def: { name: string; runtime?: true; jsName?: string }): string { diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 5bae241b1..928b1647b 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -58,7 +58,12 @@ import { unionContainerArmsOk, VOID, } from "./ir.js"; -import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from "./ir.js"; +import { + DYN_CLASS_PROPERTIES, + callSiteReturnType, + classValueAbiEquals, + classValueConstructor, +} from "./ir.js"; import { unionWideningTags } from "./analysis.js"; import { alwaysReturns } from "./control-flow.js"; import { dynamicOperationOf, functionTier } from "./tiers.js"; @@ -2997,19 +3002,13 @@ function validateFunction( } const subClass = classes.get(sub); const supClass = classes.get(sup); - const subValueCtor = subClass ? classValueConstructor(subClass) : null; - const supValueCtor = supClass ? classValueConstructor(supClass) : null; - const subCtor = subValueCtor ? functions.get(subValueCtor.fn) : undefined; - const supCtor = supValueCtor ? functions.get(supValueCtor.fn) : undefined; - if (!subCtor || !supCtor || !subValueCtor || !supValueCtor) { + const subCtor = subClass ? functions.get(classValueConstructor(subClass).fn) : undefined; + const supCtor = supClass ? functions.get(classValueConstructor(supClass).fn) : undefined; + if (!subClass || !supClass || !subCtor || !supCtor) { err(`classval upcast: "${sub}"/"${sup}" lack constructor functions`, e.loc); break; } - const subArgs = subCtor.params.slice(subValueCtor.firstArg); - const supArgs = supCtor.params.slice(supValueCtor.firstArg); - const abiEqual = - subArgs.length === supArgs.length && - subArgs.every((p, i) => typeEquals(p.type, supArgs[i]!.type)); + const abiEqual = classValueAbiEquals(subClass, supClass, functions); if (!abiEqual) { err(`classval upcast: "${sub}" and "${sup}" constructor ABIs differ`, e.loc); } diff --git a/tests/corpus/6937-class-value-arrays.mjs b/tests/corpus/6937-class-value-arrays.mjs index a563d7514..8fcfcb815 100644 --- a/tests/corpus/6937-class-value-arrays.mjs +++ b/tests/corpus/6937-class-value-arrays.mjs @@ -28,3 +28,26 @@ try { } const byName = { dog: Dog, cat: Cat }; console.log(new byName.dog().speak(), new byName["cat"]().speak()); +// The arguments are evaluated before the missing element throws. +class Point { + constructor(x, y) { + this.sum = x + y; + } +} +class Point3 extends Point {} +const points = [Point3, Point]; +const evaluated = []; +const arg = (v) => (evaluated.push(v), v); +console.log(new points[0](arg(1), arg(2)).sum, evaluated.join()); +try { + new points[2](arg(3), arg(4)); +} catch (e) { + console.log(e.name, e.message, evaluated.join()); +} +// Spread arguments too, through a valid index and past the end. +console.log(new points[1](...[5, 6]).sum); +try { + new points[3](...[arg(7), arg(8)]); +} catch (e) { + console.log(e.name, e.message, evaluated.join()); +} diff --git a/tests/corpus/6938-class-value-arrays.ts b/tests/corpus/6938-class-value-arrays.ts index 672700cbf..db695bc0a 100644 --- a/tests/corpus/6938-class-value-arrays.ts +++ b/tests/corpus/6938-class-value-arrays.ts @@ -26,3 +26,23 @@ try { } catch (e) { console.log((e as Error).name, (e as Error).message); } +// A class value checked into a `typeof Shape` slot: a subclass whose +// constructor takes the same arguments constructs, and one whose constructor +// takes an argument the slot does not pass throws a TypeError (Node's from +// reading the missing argument, the compiled program's from the check). +class Sized extends Shape { + label: string; + constructor(side: number) { + super(); + this.label = side.toFixed(1); + } +} +const values: any[] = [Square, Sized, Shape]; +for (const value of values) { + try { + const S: typeof Shape = value; + console.log(new S().area()); + } catch (e) { + console.log((e as Error).name); + } +} From 320a97bf3b8dd57c35376dfedcb5cb67aef44051 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 20:19:50 -0500 Subject: [PATCH 143/273] Throw Node's TypeError for util.diff of two distinct empty sequences util.diff([], []) with two arrays read one element past its trace row while backtracking. Node's myersDiff answers undefined for two empty sequences, and util.diff's ArrayPrototypeReverse throws "Cannot convert undefined or null to object"; scr_util_diff now throws the same TypeError before it allocates the trace. --- packages/runtime/src/scr_util_callable.c | 11 +++++++++++ tests/corpus/7705-util-diff.cjs | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/scr_util_callable.c b/packages/runtime/src/scr_util_callable.c index df6a68272..574b9d423 100644 --- a/packages/runtime/src/scr_util_callable.c +++ b/packages/runtime/src/scr_util_callable.c @@ -881,6 +881,17 @@ ScrDyn *scr_util_diff(ScrDyn *actual, ScrDyn *expected) { scr_throw_error_msg_code(SCR_ERR_RANGE, message, (size_t)n, "ERR_OUT_OF_RANGE"); return NULL; } + /* Two distinct empty sequences: Node's myersDiff reads past its + * one-element state, finds no end and answers undefined, which the + * ArrayPrototypeReverse of util.diff rejects. The trace below would be read + * out of bounds. */ + if (max == 0) { + uc_diff_seq_free(&a); + uc_diff_seq_free(&e); + static const char message[] = "Cannot convert undefined or null to object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } size_t width = 2 * max + 1; int32_t *v = calloc(width, sizeof *v); int32_t **trace = malloc((max + 1) * sizeof *trace); diff --git a/tests/corpus/7705-util-diff.cjs b/tests/corpus/7705-util-diff.cjs index 9e52fd165..489586a3b 100644 --- a/tests/corpus/7705-util-diff.cjs +++ b/tests/corpus/7705-util-diff.cjs @@ -7,7 +7,7 @@ const show = (a, e) => { try { console.log(JSON.stringify(util.diff(a, e))); } catch (err) { console.log(err.name, err.code, err.message); } }; show("abc", "abd"); show("kitten", "sitting"); show("", "x"); show("same", "same"); show("", ""); -show(["a", "b", "c"], ["a", "c", "d"]); show(["x"], []); show([], ["y", "z"]); +show(["a", "b", "c"], ["a", "c", "d"]); show(["x"], []); show([], ["y", "z"]); show([], []); const arr = ["p"]; show(arr, arr); show(1, 1); show(1, 2); show("a", 2); show(["a", 1], ["a"]); show(["a"], [, "b"]); show(null, "x"); show("x", undefined); show("héllo😀", "hello😀"); show({}, "x"); show("the quick brown fox", "the quack brown box"); From e6a8d735ca0355d24c3e57fb3a5320119aeab75f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 20:20:39 -0500 Subject: [PATCH 144/273] Bound strict throttle state and evaluate MIME arguments before the call A strict throttler kept every invocation timestamp until its whole window emptied, so sustained traffic grew the array without bound; ticks that left the window now give their slots back, and at most limit ticks are live. new MIMEType, new MIMEParams and MIMEType.parse in JavaScript evaluated surplus arguments after the call. All arguments now evaluate first, in order, as in JavaScript, so a surplus argument's effects happen before the constructor coerces or throws. --- .../src/frontend/lowering/builtins/mime.ts | 80 +++++++++---------- packages/runtime/src/scr_util_timing.c | 7 ++ tests/corpus/7707-util-mime-type.cjs | 8 ++ 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/builtins/mime.ts b/packages/compiler/src/frontend/lowering/builtins/mime.ts index da3d16712..27f95226c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/mime.ts +++ b/packages/compiler/src/frontend/lowering/builtins/mime.ts @@ -2,7 +2,7 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; import { dynUndefinedExpr, numLit } from "../../../ir/build.js"; -import { BOOL, DYN, type IrExpr } from "../../../ir/ir.js"; +import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js"; import { builtinImportOf } from "./module-bindings.js"; /* node:util's MIMEType and MIMEParams are native checked-dynamic handles @@ -34,38 +34,51 @@ function mimeValueClass(lowerer: Lowerer, node: ts.Expression): MimeClass | null return lowerer.isStdlibSymbol(sym) ? name : null; } -/** Every argument in order, as checked-dynamic values; the first `count` - * fill the call, missing ones as undefined, and the others still - * evaluate. */ -function mimeArguments( +/** The call of `fn` over the first `count` arguments, as checked-dynamic + * values (missing ones as undefined). Every argument evaluates in order + * before the call, as in JavaScript: with surplus arguments, the used ones + * are held in hidden locals while the others evaluate. */ +function mimeCall( lowerer: Lowerer, + fn: IrLibFn, nodes: readonly ts.Expression[], count: number, loc: ReturnType, -): { used: IrExpr[]; extra: IrExpr[] } { - const values = nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); - const used = values.slice(0, count); - while (used.length < count) used.push(dynUndefinedExpr(loc)); - return { used, extra: values.slice(count) }; -} - -/** The call, after which any surplus arguments evaluate. */ -function withSurplus( - lowerer: Lowerer, - call: IrExpr, - extra: IrExpr[], - loc: ReturnType, ): IrExpr { - if (extra.length === 0) return call; - const local = lowerer.declareHiddenLocal("%mimeResult", call.type); + const values = nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + if (values.length <= count) { + const args = [...values]; + while (args.length < count) args.push(dynUndefinedExpr(loc)); + return { kind: "libCall", fn, args, type: DYN, loc }; + } + const locals = values.slice(0, count).map((value) => ({ + value, + local: lowerer.declareHiddenLocal("%mimeArg", DYN), + })); return { kind: "seqExpr", stmts: [ - { kind: "varDecl", localId: local.id, init: call, loc }, - ...extra.map((arg) => ({ kind: "exprStmt" as const, expr: arg, loc: arg.loc })), + ...locals.map(({ value, local }) => ({ + kind: "varDecl" as const, + localId: local.id, + init: value, + loc, + })), + ...values.slice(count).map((arg) => ({ kind: "exprStmt" as const, expr: arg, loc: arg.loc })), ], - result: { kind: "varRef", localId: local.id, type: call.type, loc }, - type: call.type, + result: { + kind: "libCall", + fn, + args: locals.map(({ local }): IrExpr => ({ + kind: "varRef", + localId: local.id, + type: DYN, + loc, + })), + type: DYN, + loc, + }, + type: DYN, loc, }; } @@ -77,15 +90,8 @@ export function lowerMimeNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | const nodes = expr.arguments?.nodes ?? []; if (nodes.some(ts.isSpreadElement)) return null; const loc = locOf(expr); - const { used, extra } = mimeArguments(lowerer, nodes, cls === "MIMEType" ? 2 : 0, loc); - const call: IrExpr = { - kind: "libCall", - fn: cls === "MIMEType" ? "mime.typeNew" : "mime.paramsNew", - args: used, - type: DYN, - loc, - }; - return withSurplus(lowerer, call, extra, loc); + const fn: IrLibFn = cls === "MIMEType" ? "mime.typeNew" : "mime.paramsNew"; + return mimeCall(lowerer, fn, nodes, cls === "MIMEType" ? 2 : 0, loc); } const INSTANCE_METHODS: Record> = { @@ -113,13 +119,7 @@ export function lowerMimeCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr const name = access.name.text; const loc = locOf(call); if (name === "parse" && mimeClassOf(lowerer, access.expression) === "MIMEType") { - const { used, extra } = mimeArguments(lowerer, call.arguments.nodes, 1, loc); - return withSurplus( - lowerer, - { kind: "libCall", fn: "mime.typeParse", args: used, type: DYN, loc }, - extra, - loc, - ); + return mimeCall(lowerer, "mime.typeParse", call.arguments.nodes, 1, loc); } const cls = mimeValueClass(lowerer, access.expression); if (cls === null || !INSTANCE_METHODS[cls].has(name)) return null; diff --git a/packages/runtime/src/scr_util_timing.c b/packages/runtime/src/scr_util_timing.c index c5393da27..a8a700d47 100644 --- a/packages/runtime/src/scr_util_timing.c +++ b/packages/runtime/src/scr_util_timing.c @@ -392,6 +392,13 @@ static void th_reserve(UtState *s, double now) { s->window_count++; return; } + /* Ticks that left the window free their slots: at most limit ticks are + * live, so the storage stays within twice that. */ + if (s->tick_len == s->tick_cap && s->tick_head > 0) { + s->tick_len -= s->tick_head; + memmove(s->ticks, s->ticks + s->tick_head, s->tick_len * sizeof *s->ticks); + s->tick_head = 0; + } if (s->tick_len == s->tick_cap) { size_t cap = s->tick_cap ? s->tick_cap * 2 : 8; double *grown = realloc(s->ticks, cap * sizeof *grown); diff --git a/tests/corpus/7707-util-mime-type.cjs b/tests/corpus/7707-util-mime-type.cjs index 06d159306..e860d1d30 100644 --- a/tests/corpus/7707-util-mime-type.cjs +++ b/tests/corpus/7707-util-mime-type.cjs @@ -76,4 +76,12 @@ console.log(seen, `${m}`); const bare = new MIMEType("a/b"); console.log(Object.keys(bare), bare.foo, bare.params.size); console.log(format("%s|%s", bare, bare.params), "%s", new MIMEType("c/d;e=f")); + // Every argument evaluates before the constructor or parse runs. + const order = []; + const note = (label) => { order.push(label); return label; }; + const coerced = { toString() { order.push("coerce"); return "a/b"; } }; + show("surplus, then coercion", () => { new MIMEType(coerced, undefined, note("third")); return order.splice(0).join(","); }); + show("surplus, then parse error", () => { try { new MIMEType("bad", undefined, note("x")); } catch (e) { order.push(e.code); } return order.splice(0).join(","); }); + show("parse surplus", () => { MIMEType.parse(coerced, note("second")); return order.splice(0).join(","); }); + show("params surplus", () => { new MIMEParams(note("p")); return order.splice(0).join(","); }); } From 120c65bfbbd3c3298dc0232aacc5a3b2e85c8e08 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 20:24:11 -0500 Subject: [PATCH 145/273] Read an iterator's next once, and await return()'s value in for await JavaScript reads an iterator's `next` when the iterator opens and keeps calling that function, so a `next` the iterator assigns while it runs is not called. The for-of loop and the spread drain over an object-literal iterator read the field on every step; they now read it once after the open (classIteratorNextRead) and call the function they read. The checked-dynamic spread drain (%iterator.pack.checked) does the same, using the stepping the for-of and Array.from drains already use. `for await` over a synchronous iterator closes it on an early exit the way the async-from-sync iterator does: return()'s result is read for done and value, and the value is awaited, so a pending promise settles before the loop continues and a rejected one rejects the loop (after a throw from the body, the body's error still wins). --- .../lowering/expressions/array-literals.ts | 123 ++++++++++++++---- .../src/frontend/lowering/lower-classes.ts | 45 ++++++- .../src/frontend/lowering/lower-stmts.ts | 41 +++++- .../src/frontend/lowering/lower-stream.ts | 2 +- .../compiler/src/frontend/lowering/lowerer.ts | 14 +- tests/corpus/6928-class-iterator-protocol.ts | 19 +++ tests/corpus/6929-class-iterator-protocol.mjs | 19 +++ .../6935-class-async-iterator-protocol.ts | 40 ++++++ .../6936-class-async-iterator-protocol.mjs | 39 ++++++ 9 files changed, 306 insertions(+), 36 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 6153b87f2..09f3e1a2c 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -29,9 +29,10 @@ import { lowerCollectionSpread } from "../containers/collection-methods.js"; import { strCharsCall } from "../containers/array-construction.js"; import { generatorDrain } from "../iterator-adapters.js"; import { isSearchParamsIterable } from "../builtins/url.js"; -import { varRef } from "../../../ir/build.js"; +import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import { arrayValueStore } from "../array-values.js"; import { classGeneratorIterable } from "../class-iterables.js"; +import { iteratorCanStep } from "../iterator-consumption.js"; /** JavaScript: whether `expr` is or holds an existing checked-dynamic * object — a binding stored as a dyn value whose checker type is an @@ -717,13 +718,17 @@ function lowerTupleSpreadLiteral( return { kind: "seqExpr", stmts, result: { kind: "recordLit", fields, type, loc }, type, loc }; } -/** Drain using checked dispatch so native class and generator iterators keep their methods. - * `spell` is the TypeError message for a source that is not iterable - * (notIterableSpelling). */ +/** Drain a checked-dynamic iterable: `next` is read once when the iterator + * opens, as JS keeps it in the iterator record, so a `next` the iterator + * assigns while it runs is not called. Runtime iterators step natively, and + * other iterators call the `next` read (class methods keep their emitted + * dispatch). `spell` is the TypeError message for a source that is not + * iterable (notIterableSpelling). */ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, spell: string, loc: SrcLoc): IrExpr { const name = "%iterator.pack.checked"; if (!lowerer.liftedFns.some((fn) => fn.name === name)) { const ref = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: DYN, loc }); + const flag = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: BOOL, loc }); const property = (key: string): IrExpr => ({ kind: "dynKeyGet", value: ref("step"), @@ -731,6 +736,7 @@ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, spell: string, lo type: DYN, loc, }); + const finished: IrStmt = { kind: "return", value: ref("result"), loc }; lowerer.liftedFns.push({ name, params: [ @@ -739,12 +745,13 @@ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, spell: string, lo ], returnType: DYN, locals: [ - ...["source", "iterator", "step", "result"].map((id) => ({ + ...["source", "iterator", "next", "step", "value", "result"].map((id) => ({ id, name: id, type: DYN, mutable: id !== "source", })), + { id: "fast", name: "fast", type: BOOL, mutable: false }, { id: "spell", name: "spell", type: STRING, mutable: false }, ], body: [ @@ -760,6 +767,24 @@ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, spell: string, lo }, loc, }, + { + kind: "varDecl", + localId: "next", + init: { + kind: "dynKeyGet", + value: ref("iterator"), + key: { kind: "strLit", value: "next", type: STRING, loc }, + type: DYN, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: "fast", + init: iteratorCanStep(ref("iterator"), ref("next"), loc), + loc, + }, { kind: "varDecl", localId: "result", @@ -770,25 +795,77 @@ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, spell: string, lo kind: "while", cond: { kind: "boolLit", value: true, type: BOOL, loc }, body: [ - { - kind: "varDecl", - localId: "step", - init: { - kind: "dynInvoke", - recv: ref("iterator"), - method: "next", - calleeName: "iterator.next", - args: [], - type: DYN, - loc, - }, - loc, - }, + // Declared before the branches, which each assign it: a local + // declared in a branch is released when the branch ends. + { kind: "varDecl", localId: "value", init: dynUndefinedExpr(loc), loc }, { kind: "if", - cond: { kind: "dynTest", test: "truthy", value: property("done"), type: BOOL, loc }, - then: [{ kind: "return", value: ref("result"), loc }], - else_: null, + cond: flag("fast"), + then: [ + { + kind: "assign", + localId: "value", + value: { + kind: "libCall", + fn: "dyn.iteratorStep", + args: [ref("iterator")], + type: DYN, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.iteratorStepDone", + args: [ref("iterator")], + type: BOOL, + loc, + }, + then: [finished], + else_: null, + loc, + }, + ], + else_: [ + { + kind: "varDecl", + localId: "step", + init: { + kind: "libCall", + fn: "dyn.iteratorResult", + args: [ + { + kind: "dynCall", + callee: ref("next"), + receiver: ref("iterator"), + calleeName: "iterator.next", + args: [], + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "dynTest", + test: "truthy", + value: property("done"), + type: BOOL, + loc, + }, + then: [finished], + else_: null, + loc, + }, + { kind: "assign", localId: "value", value: property("value"), loc }, + ], loc, }, { @@ -796,7 +873,7 @@ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, spell: string, lo expr: { kind: "libCall", fn: "dyn.packPush", - args: [ref("result"), property("value")], + args: [ref("result"), ref("value")], type: VOID, loc, }, diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 9d994b76b..2e077bfcf 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -8016,15 +8016,42 @@ export function classIteratorOf( }; } -/** The `it.next()` step of a class iterator as an ordinary (possibly - * virtual) method call. */ -export function classIteratorNextCall( +/** The `next` an object-literal iterator's protocol calls, read once when + * the iterator opens: JS keeps the `next` it reads then in the iterator + * record, so a `next` the iterator assigns while it runs is not called. + * Null for an iterator class, whose `next` is a method. */ +export function classIteratorNextRead( lowerer: Lowerer, cit: ClassIteratorInfo, itRef: IrExpr, loc: SrcLoc, +): (IrExpr & { type: IrType & { kind: "func" } }) | null { + const iterT = cit.iterT; + if (iterT.kind === "object") return null; + const field = lowerer.shapes.get(iterT.shapeId)!.fields.find((f) => f.name === "next")!; + return { + kind: "recordGet", + obj: itRef, + shapeId: iterT.shapeId, + field: "next", + type: field.type as IrType & { kind: "func" }, + loc, + }; +} + +/** The `it.next()` step of a class iterator: an ordinary (possibly + * virtual) method call, or the call of `next`, the object-literal + * iterator's `next` that classIteratorNextRead read when it opened. */ +export function classIteratorNextCall( + lowerer: Lowerer, + cit: ClassIteratorInfo, + itRef: IrExpr, + next: IrExpr | null, + loc: SrcLoc, ): IrExpr { - return classIteratorMethodCall(lowerer, cit, "next", itRef, loc); + if (next === null || next.type.kind !== "func") + return classIteratorMethodCall(lowerer, cit, "next", itRef, loc); + return { kind: "callValue", callee: next, args: [], type: next.type.ret, loc }; } /** `it.next()` or `it.return()` of a class iterator: a (possibly virtual) @@ -8108,6 +8135,8 @@ export function classIteratorDrainCall( const recvT: IrType = { kind: "object", className: cit.className }; const recvRef: IrExpr = { kind: "varRef", localId: "r.0", type: recvT, loc }; const itRef: IrExpr = { kind: "varRef", localId: "it.0", type: cit.iterT, loc }; + const nextRead = classIteratorNextRead(lowerer, cit, itRef, loc); + const nextRef: IrExpr | null = nextRead ? varRef("next.0", nextRead.type, loc) : null; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: outT, loc }; const resRef: IrExpr = { kind: "varRef", localId: "res.0", type: cit.resultT, loc }; const valueRead: IrExpr = { @@ -8122,7 +8151,7 @@ export function classIteratorDrainCall( { kind: "varDecl", localId: "res.0", - init: classIteratorNextCall(lowerer, cit, itRef, loc), + init: classIteratorNextCall(lowerer, cit, itRef, nextRef, loc), loc, }, ...(cit.hasDone @@ -8163,6 +8192,7 @@ export function classIteratorDrainCall( locals: [ { id: "r.0", name: "r", type: recvT, mutable: false }, { id: "it.0", name: "it", type: cit.iterT, mutable: false }, + ...(nextRead ? [{ id: "next.0", name: "next", type: nextRead.type, mutable: false }] : []), { id: "out.0", name: "out", type: outT, mutable: false }, { id: "res.0", name: "res", type: cit.resultT, mutable: true }, ], @@ -8179,6 +8209,9 @@ export function classIteratorDrainCall( init: classIteratorOpenCall(lowerer, cit, recvRef, loc), loc, }, + ...(nextRead + ? [{ kind: "varDecl", localId: "next.0", init: nextRead, loc } satisfies IrStmt] + : []), { kind: "while", cond: { kind: "boolLit", value: true, type: BOOL, loc }, @@ -8226,7 +8259,7 @@ export function classIteratorRestDrainCall( { kind: "varDecl", localId: "res.0", - init: classIteratorNextCall(lowerer, cit, itRef, loc), + init: classIteratorNextCall(lowerer, cit, itRef, null, loc), loc, }, ...(cit.hasDone diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index bee887e0f..b942a538b 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -2578,7 +2578,7 @@ export function lowerBindingPattern( const itRef = (): IrExpr => ({ kind: "varRef", localId: it.id, type: citPat.iterT, loc }); for (const el of pattern.elements.nodes) { const stepLoc = locOf(el); - const step = lowerer.classIteratorNextCall(citPat, itRef(), stepLoc); + const step = lowerer.classIteratorNextCall(citPat, itRef(), null, stepLoc); if (ts.isOmittedExpression(el) || el.name === undefined) { out.push({ kind: "exprStmt", expr: step, loc: stepLoc }); continue; @@ -10726,6 +10726,8 @@ function lowerForOfClassIterator( const r = lowerer.declareHiddenLocal("%cir", cit.resultT); const recvRef: IrExpr = { kind: "varRef", localId: recv.id, type: iterable.type, loc }; const itRef = (): IrExpr => ({ kind: "varRef", localId: it.id, type: cit.iterT, loc }); + const nextRead = lowerer.classIteratorNextRead(cit, itRef(), loc); + const next = nextRead ? lowerer.declareHiddenLocal("%cin", nextRead.type) : null; const rRef = (): IrExpr => ({ kind: "varRef", localId: r.id, type: cit.resultT, loc }); const done = cit.hasReturn ? lowerer.declareHiddenLocal("%cid", BOOL) : null; if (done) done.mutable = true; @@ -10756,7 +10758,12 @@ function lowerForOfClassIterator( { kind: "varDecl", localId: r.id, - init: lowerer.classIteratorNextCall(cit, itRef(), loc), + init: lowerer.classIteratorNextCall( + cit, + itRef(), + next ? varRef(next.id, next.type, loc) : null, + loc, + ), loc, }, ...(cit.hasDone @@ -10820,6 +10827,9 @@ function lowerForOfClassIterator( init: lowerer.classIteratorOpenCall(cit, recvRef, loc), loc, }, + ...(next && nextRead + ? [{ kind: "varDecl", localId: next.id, init: nextRead, loc } satisfies IrStmt] + : []), ...(done ? [ { @@ -10887,6 +10897,7 @@ function lowerForOfDyn( done.mutable = true; const value = lowerer.declareHiddenLocal("%dofValue", DYN); const returned = lowerer.declareHiddenLocal("%dofReturn", DYN); + const closed = lowerer.declareHiddenLocal("%dofClosed", DYN); const needsClose = lowerer.declareHiddenLocal("%dofNeedsClose", BOOL); needsClose.mutable = true; const error = lowerer.declareHiddenLocal("%dofError", CAUGHT); @@ -10952,9 +10963,10 @@ function lowerForOfDyn( }, then: [ { - kind: "exprStmt", + kind: "varDecl", + localId: closed.id, // return()'s (awaited) result must be an object, as for next(). - expr: asyncLoop + init: asyncLoop ? check( awaitDyn({ kind: "dynCall", @@ -10969,6 +10981,27 @@ function lowerForOfDyn( : call(varRef(returned.id, DYN, loc)), loc, }, + // A synchronous iterator's return() result is read as the + // async-from-sync iterator reads it: done, then the value, which is + // awaited, so a rejected promise there rejects the loop. + ...(syncIterator + ? [ + { + kind: "if", + cond: varRef(syncIterator.id, BOOL, loc), + then: [ + { kind: "exprStmt", expr: get(varRef(closed.id, DYN, loc), "done"), loc }, + { + kind: "exprStmt", + expr: awaitDyn(get(varRef(closed.id, DYN, loc), "value")), + loc, + }, + ], + else_: null, + loc, + } satisfies IrStmt, + ] + : []), ], else_: null, loc, diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 90bdf917e..e17dee274 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -1982,7 +1982,7 @@ function classIteratorObject( { kind: "varDecl", localId: r.id, - init: classIteratorNextCall(lowerer, cit, varRef(it.id, cit.iterT, loc), loc), + init: classIteratorNextCall(lowerer, cit, varRef(it.id, cit.iterT, loc), null, loc), loc, }, { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ea503e1f1..76c4e5c0d 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -362,6 +362,7 @@ import { classIteratorDrainCall, classIteratorMethodCall, classIteratorNextCall, + classIteratorNextRead, classIteratorOf, classIteratorOpenCall, classIteratorRestDrainCall, @@ -13063,8 +13064,17 @@ export class Lowerer { return classIteratorOpenCall(this, cit, recv, loc); } - classIteratorNextCall(cit: ClassIteratorInfo, itRef: IrExpr, loc: SrcLoc): IrExpr { - return classIteratorNextCall(this, cit, itRef, loc); + classIteratorNextRead(cit: ClassIteratorInfo, itRef: IrExpr, loc: SrcLoc): IrExpr | null { + return classIteratorNextRead(this, cit, itRef, loc); + } + + classIteratorNextCall( + cit: ClassIteratorInfo, + itRef: IrExpr, + next: IrExpr | null, + loc: SrcLoc, + ): IrExpr { + return classIteratorNextCall(this, cit, itRef, next, loc); } classIteratorDrainCall(src: IrExpr, loc: SrcLoc, elemT?: IrType): IrExpr | null { diff --git a/tests/corpus/6928-class-iterator-protocol.ts b/tests/corpus/6928-class-iterator-protocol.ts index f097368de..2c2eecccc 100644 --- a/tests/corpus/6928-class-iterator-protocol.ts +++ b/tests/corpus/6928-class-iterator-protocol.ts @@ -91,3 +91,22 @@ class Range { let total = 0; for (const v of new Range(4)) total += v; console.log(total, [...new Range(3)].join()); + +// An object-literal iterator that replaces its next() while it runs: the +// loop and the spread keep calling the next() they read when it opened. +class Swapping { + [Symbol.iterator]() { + let i = 0; + const it = { + next(): { value: number; done: boolean } { + i++; + if (i === 2) it.next = () => ({ value: -1, done: true }); + return { value: i, done: i > 4 }; + }, + }; + return it; + } +} +const swapped: number[] = []; +for (const v of new Swapping()) swapped.push(v); +console.log(swapped.join(), [...new Swapping()].join()); diff --git a/tests/corpus/6929-class-iterator-protocol.mjs b/tests/corpus/6929-class-iterator-protocol.mjs index b5255ba35..6a7827f15 100644 --- a/tests/corpus/6929-class-iterator-protocol.mjs +++ b/tests/corpus/6929-class-iterator-protocol.mjs @@ -76,3 +76,22 @@ try { } catch (e) { console.log("throw:", e.message); } + +// An object-literal iterator that replaces its next() while it runs: the +// loop and the spread keep calling the next() they read when it opened. +class Swapping { + [Symbol.iterator]() { + let i = 0; + const it = { + next() { + i++; + if (i === 2) it.next = () => ({ value: -1, done: true }); + return { value: i, done: i > 4 }; + }, + }; + return it; + } +} +const swapped = []; +for (const v of new Swapping()) swapped.push(v); +console.log(swapped.join(), [...new Swapping()].join()); diff --git a/tests/corpus/6935-class-async-iterator-protocol.ts b/tests/corpus/6935-class-async-iterator-protocol.ts index 9ec5a61a5..25626a773 100644 --- a/tests/corpus/6935-class-async-iterator-protocol.ts +++ b/tests/corpus/6935-class-async-iterator-protocol.ts @@ -56,5 +56,45 @@ async function main(): Promise { console.log("found", await find()); for await (const p of new Pages()) console.log(p); for await (const l of new Letters()) console.log("letter", l); + // return()'s value is awaited when the loop leaves early: a pending + // promise settles first, and a rejection rejects the loop after a break + // (after a throw from the body, the body's error wins). + for await (const v of new Closing((resolve) => setTimeout(() => resolve(1), 5))) + if (v === 1) break; + console.log("after break"); + try { + for await (const v of new Closing((_, reject) => reject(new Error("closing failed")))) + if (v === 1) break; + } catch (e) { + console.log("break:", (e as Error).message); + } + try { + for await (const v of new Closing((_, reject) => reject(new Error("closing failed")))) + if (v === 1) throw new Error("body failed"); + } catch (e) { + console.log("throw:", (e as Error).message); + } +} +class Closing { + readonly settle: (resolve: (v: number) => void, reject: (e: Error) => void) => void; + constructor(settle: (resolve: (v: number) => void, reject: (e: Error) => void) => void) { + this.settle = settle; + } + [Symbol.iterator]() { + let i = 0; + const settle = this.settle; + return { + next(): IteratorResult { + return { value: i++, done: false }; + }, + return(): IteratorResult { + console.log("return called"); + const value = new Promise((resolve, reject) => + settle((v) => (console.log("value settled"), resolve(v)), reject), + ); + return { value: value as unknown as number, done: true }; + }, + }; + } } main(); diff --git a/tests/corpus/6936-class-async-iterator-protocol.mjs b/tests/corpus/6936-class-async-iterator-protocol.mjs index cb7b1e68a..4cc67b0d1 100644 --- a/tests/corpus/6936-class-async-iterator-protocol.mjs +++ b/tests/corpus/6936-class-async-iterator-protocol.mjs @@ -56,5 +56,44 @@ async function main() { console.log("found", await find()); for await (const p of new Pages()) console.log(p); for await (const l of new Letters()) console.log("letter", l); + // return()'s value is awaited when the loop leaves early: a pending + // promise settles first, and a rejection rejects the loop after a break + // (after a throw from the body, the body's error wins). + for await (const v of new Closing((resolve) => setTimeout(() => resolve(1), 5))) + if (v === 1) break; + console.log("after break"); + try { + for await (const v of new Closing((_, reject) => reject(new Error("closing failed")))) + if (v === 1) break; + } catch (e) { + console.log("break:", e.message); + } + try { + for await (const v of new Closing((_, reject) => reject(new Error("closing failed")))) + if (v === 1) throw new Error("body failed"); + } catch (e) { + console.log("throw:", e.message); + } +} +class Closing { + constructor(settle) { + this.settle = settle; + } + [Symbol.iterator]() { + let i = 0; + const settle = this.settle; + return { + next() { + return { value: i++, done: false }; + }, + return() { + console.log("return called"); + const value = new Promise((resolve, reject) => + settle((v) => (console.log("value settled"), resolve(v)), reject), + ); + return { value, done: true }; + }, + }; + } } main(); From e73dbe0bb3842e18d793600a449a9b3ae62c8d40 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 20:41:45 -0500 Subject: [PATCH 146/273] Run the three.js nodes fixture unsanitized in the sanitized lane The sanitized nodes build compiles one 1.14 GB LLVM module, and its clang compile peaks at about 16 GB, more than the CI runners have, so the fixture failed in every san shard that ran it. It now builds without the sanitizer there, as in the plain lane; every other three.js fixture keeps it. The fixture goes back under the sanitizer once sanitized dev builds compile large programs in shards. --- tests/harness/three.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/harness/three.test.ts b/tests/harness/three.test.ts index 27fb86d1f..967c8654a 100644 --- a/tests/harness/three.test.ts +++ b/tests/harness/three.test.ts @@ -72,7 +72,11 @@ test.for(cases)( const previousTarget = process.env["SCRIPTC_TARGET"]; const previousCc = process.env["SCRIPTC_CC"]; const wasm = target === "wasm32-wasi"; - const sanitize = !wasm && process.env["SCRIPTC_SAN"] === "1"; + // The sanitized nodes program is one 1.14 GB LLVM module, whose clang + // compile needs about 16 GB, more than the CI runners have. It runs + // unsanitized until sanitized dev builds compile large programs in + // shards; every other fixture keeps the sanitizer. + const sanitize = !wasm && fixture !== "nodes" && process.env["SCRIPTC_SAN"] === "1"; try { if (wasm) { process.env["SCRIPTC_TARGET"] = target; From 0eb7cc12e5e74822145c4a50430bf999c3b302a6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 20:51:37 -0500 Subject: [PATCH 147/273] Reuse symbol storage keys and checked-dynamic symbol boxes Every read or write of a symbol-keyed property on a checked-dynamic value formatted the symbol's address into a new string to find its storage key, and every conversion of a symbol to a checked value allocated a new box. Programs that key hot properties by symbols, such as Effect's type ids checked on each combinator, spent a large share of their time there. The storage key is a function of the address, so a 256-entry per-thread cache by address now returns the existing string, released at exit. A symbol also keeps a weak pointer to its live box: conversions share it, and the box clears the pointer when it is freed. --- .../corpus/symbol-property-key-reuse.mjs.json | 6 ++ packages/runtime/src/scr_json.c | 39 ++++++++++- packages/runtime/src/scr_symbol.c | 16 ++++- tests/corpus/symbol-property-key-reuse.mjs | 69 +++++++++++++++++++ .../symbol-property-key-reuse.mjs.json | 1 + 5 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/symbol-property-key-reuse.mjs.json create mode 100644 tests/corpus/symbol-property-key-reuse.mjs create mode 100644 tests/harness/corpus-costs/symbol-property-key-reuse.mjs.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/symbol-property-key-reuse.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/symbol-property-key-reuse.mjs.json new file mode 100644 index 000000000..e8fb5f353 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/symbol-property-key-reuse.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/symbol-property-key-reuse.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 9b7c13483..8da68a5a5 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -6615,11 +6615,44 @@ ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr /* Symbol descriptors use the ordinary descriptor implementation in an * inaccessible table. Keep a retained key list so identities cannot be - * recycled while properties exist, and reflection returns real symbols. */ + * recycled while properties exist, and reflection returns real symbols. + * + * The storage key is the symbol's address in text, which depends on the + * address alone, so a small cache by address answers repeated defines and + * lookups without formatting a new string each time. A recycled address + * names the same key text, so entries never go stale. */ +#define SCR_SYMBOL_KEY_CACHE 256 +static SCR_TL struct { + const void *symbol; + ScrStr *key; +} scr_symbol_key_cache[SCR_SYMBOL_KEY_CACHE]; +static SCR_TL bool scr_symbol_key_cache_registered; + +static void scr_symbol_key_cache_cleanup(void) { + for (size_t i = 0; i < SCR_SYMBOL_KEY_CACHE; i++) { + scr_str_release(scr_symbol_key_cache[i].key); + scr_symbol_key_cache[i].key = NULL; + scr_symbol_key_cache[i].symbol = NULL; + } + scr_symbol_key_cache_registered = false; +} + static ScrStr *scr_dyn_symbol_storage_key(const ScrDyn *key) { + const void *symbol = key->v.symbol.value; + size_t slot = ((uintptr_t)symbol >> 4) & (SCR_SYMBOL_KEY_CACHE - 1); + if (scr_symbol_key_cache[slot].key && scr_symbol_key_cache[slot].symbol == symbol) + return scr_str_retain(scr_symbol_key_cache[slot].key); char buffer[2 * sizeof(void *) + 4]; - int length = snprintf(buffer, sizeof buffer, "%p", (void *)key->v.symbol.value); - return scr_str_new(buffer, (size_t)length); + int length = snprintf(buffer, sizeof buffer, "%p", (void *)symbol); + ScrStr *name = scr_str_new(buffer, (size_t)length); + if (!scr_symbol_key_cache_registered) { + scr_symbol_key_cache_registered = true; + scr_atexit(scr_symbol_key_cache_cleanup); + } + scr_str_release(scr_symbol_key_cache[slot].key); + scr_symbol_key_cache[slot].symbol = symbol; + scr_symbol_key_cache[slot].key = scr_str_retain(name); + return name; } static ScrDyn *scr_dyn_symbol_receiver(const ScrDyn *value) { diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 727e008b5..3169d6b13 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -22,6 +22,10 @@ struct ScrSym { ScrStr *desc; /* owned; NULL = no description (Symbol()) */ ScrStr *reg_key; /* owned; non-NULL exactly for Symbol.for symbols */ struct ScrSym *reg_next; /* the registry's intrusive chain */ + /* The symbol's checked-dynamic box while one is alive (not retained: the + * box retains the symbol and clears this when it is freed), shared by + * every conversion of the symbol to a checked value. */ + ScrDyn *box; }; ScrSym *scr_sym_retain(ScrSym *s) { @@ -50,6 +54,7 @@ ScrSym *scr_sym_new(ScrStr *desc) { s->desc = desc ? scr_str_retain(desc) : NULL; s->reg_key = NULL; s->reg_next = NULL; + s->box = NULL; return s; } @@ -142,8 +147,17 @@ ScrStr *scr_sym_desc(ScrSym *s) { return s->desc ? scr_str_retain(s->desc) : NUL ScrStr *scr_sym_key_for(ScrSym *s) { return s->reg_key ? scr_str_retain(s->reg_key) : NULL; } +/* The release of a symbol's shared box: the box is going away. */ +static void scr_sym_release_box(ScrSym *s) { + s->box = NULL; + scr_sym_release(s); +} + ScrDyn *scr_dyn_new_symbol(ScrSym *value) { - return scr_dyn_symbol_ref(scr_sym_retain(value), scr_sym_release, scr_sym_to_string, scr_sym_desc); + if (value->box) return scr_dyn_retain(value->box); + ScrDyn *box = scr_dyn_symbol_ref(scr_sym_retain(value), scr_sym_release_box, scr_sym_to_string, scr_sym_desc); + value->box = box; + return box; } ScrStr *scr_sym_to_string(ScrSym *s) { diff --git a/tests/corpus/symbol-property-key-reuse.mjs b/tests/corpus/symbol-property-key-reuse.mjs new file mode 100644 index 000000000..f4fc2a0c4 --- /dev/null +++ b/tests/corpus/symbol-property-key-reuse.mjs @@ -0,0 +1,69 @@ +// Symbol-keyed properties read and written many times: on plain objects, on +// prototypes and class instances, with Symbol.for symbols, and with symbols +// created in a loop (each a distinct key, even after earlier ones are +// unreachable), plus the same symbol converted to a checked value repeatedly. +const tag = Symbol("tag"); +const shared = Symbol.for("shared"); +const proto = { [tag]: "from proto" }; +const child = Object.create(proto); +child[shared] = 0; +for (let i = 0; i < 1000; i++) child[shared] += i; +console.log(child[tag], child[shared], Object.getOwnPropertySymbols(child).length); + +const count = Symbol("count"); +class Counter { + constructor() { + this[count] = 0; + } + bump() { + this[count]++; + return this; + } + get count() { + return this[count]; + } +} +const c = new Counter(); +for (let i = 0; i < 500; i++) c.bump(); +console.log(c.count, c[count], count.toString()); + +const keys = []; +const holder = {}; +for (let i = 0; i < 300; i++) { + const key = Symbol(`k${i}`); + holder[key] = i; + if (i % 100 === 0) keys.push(key); +} +let sum = 0; +for (const key of Object.getOwnPropertySymbols(holder)) sum += holder[key]; +console.log(sum, keys.map((k) => holder[k]).join(), keys[1].description); + +// Symbols that become unreachable between rounds: a new symbol never reads +// a property stored under an earlier one. +const scratch = {}; +let stale = 0; +for (let round = 0; round < 200; round++) { + const key = Symbol("round"); + if (scratch[key] !== undefined) stale++; + scratch[key] = round; +} +console.log(stale, Object.getOwnPropertySymbols(scratch).length); + +// A symbol stored, deleted and dropped each round, so its storage can be +// reused by the next round's symbol: the new symbol still reads nothing. +const persist = {}; +let reused = 0; +for (let round = 0; round < 200; round++) { + const key = Symbol("dropped"); + if (key in persist) reused++; + persist[key] = round; + delete persist[key]; +} +console.log(reused, Object.getOwnPropertySymbols(persist).length); + +const viaFor = {}; +for (let i = 0; i < 10; i++) viaFor[Symbol.for("same")] = i; +console.log(viaFor[Symbol.for("same")], Object.getOwnPropertySymbols(viaFor).length); +console.log(Symbol.for("same") === Symbol.for("same"), Symbol("x") === Symbol("x"), tag === tag); +const boxes = [tag, tag, shared]; +console.log(boxes[0] === boxes[1], boxes.indexOf(shared), new Set(boxes).size); diff --git a/tests/harness/corpus-costs/symbol-property-key-reuse.mjs.json b/tests/harness/corpus-costs/symbol-property-key-reuse.mjs.json new file mode 100644 index 000000000..ddd7f7805 --- /dev/null +++ b/tests/harness/corpus-costs/symbol-property-key-reuse.mjs.json @@ -0,0 +1 @@ +{ "plain": 5174, "san": 9089 } From ab93f728edbeea66d4c2777515603e70d36e4786 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 20:53:18 -0500 Subject: [PATCH 148/273] Initialize a deferred module binding that a reached body assigns A module binding whose initializer only allocates data (a function, an arrow function, a literal or a registry object) is initialized only when a reached body uses the binding. Only reads and updates counted as uses, so a binding that reached code only assigned was never initialized, and the assignment, which checks that the binding is initialized and releases its value, threw "Cannot access '' before initialization" where Node runs. Assignments now count as uses. Corpus 8320 assigns two such bindings, one through an assignment expression, without reading them. --- .../deferred-module-initializers.test.ts | 30 +++++++++++++++---- .../lowering/deferred-module-initializers.ts | 24 ++++++++++----- .../main.ts.json | 7 +++++ .../8320-assign-only-deferred-binding/main.ts | 8 +++++ .../scanner.ts | 21 +++++++++++++ .../main.ts.json | 1 + 6 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8320-assign-only-deferred-binding/main.ts.json create mode 100644 tests/corpus/8320-assign-only-deferred-binding/main.ts create mode 100644 tests/corpus/8320-assign-only-deferred-binding/scanner.ts create mode 100644 tests/harness/corpus-costs/8320-assign-only-deferred-binding/main.ts.json 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 4696659bc..4786585d9 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts @@ -168,17 +168,19 @@ test("reports the functions whose placeholders it fills", () => { expect(filled).toEqual([functions[0]]); }); -test("an update of the binding demands its initializer", () => { +test("an update or an assignment of the binding demands its initializer", () => { const { initializers, pending, lowerer } = fixture(); const updated = block(loc), - written = block({ ...loc, start: 2, end: 3 }); + written = block({ ...loc, start: 2, end: 3 }), + assigned = block({ ...loc, start: 4, end: 5 }), + unused = block({ ...loc, start: 6, end: 7 }); const update = (id: string, prefix: boolean): IrStmt => ({ kind: "exprStmt", expr: { kind: "incDec", op: "+", prefix, localId: id, type: F64, loc }, loc, }); const functions = [ - fn("init", [updated, written]), + fn("init", [updated, written, assigned, unused]), fn("use", [ update("updated", false), { @@ -187,13 +189,29 @@ test("an update of the binding demands its initializer", () => { value: { kind: "numLit", value: 1, type: F64, loc }, loc, }, + { + kind: "exprStmt", + expr: { + kind: "assignExpr", + localId: "assigned", + value: { kind: "numLit", value: 2, type: F64, loc }, + type: F64, + loc, + }, + loc, + }, ]), ]; pending.set("updated", [{ loc, lower: () => [value(2)] }]); pending.set("written", [{ loc: written.loc, lower: () => [value(3)] }]); + pending.set("assigned", [{ loc: assigned.loc, lower: () => [value(4)] }]); + pending.set("unused", [{ loc: unused.loc, lower: () => [value(5)] }]); expect(initializers.process(lowerer, functions)).toBe(true); expect(updated.body).toEqual([value(2)]); - // A plain write never observes the initial value. - expect(written.body).toEqual([]); - expect(pending.has("written")).toBe(true); + // An assignment checks that the binding is initialized and releases its + // value, so the declaration must have run. + expect(written.body).toEqual([value(3)]); + expect(assigned.body).toEqual([value(4)]); + expect(unused.body).toEqual([]); + expect(pending.has("unused")).toBe(true); }); diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts index a44df8f75..3af054dd6 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts @@ -387,18 +387,22 @@ function scanStatements( const placeholders: PlaceholderBlock[] = []; const typeEvents = lowerer ? typeScanner(lowerer, body) : null; const reads = new Set(); + const observe = (id: string): void => { + if (deferred!.has(id) && !reads.has(id)) { + reads.add(id); + body.push({ kind: "read", id }); + } + }; everyStmtList(statements, { stmt: (statement) => { if (statement.kind === "block" && statement.body.length === 0) placeholders.push(statement); + if (typeEvents !== null && statement.kind === "assign") observe(statement.localId); 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 }); - } + if (observesBinding(expr)) observe(expr.localId); // Every call, as the helpers are visited from a stack in call order. if ( expr.kind === "call" && @@ -411,10 +415,14 @@ function scanStatements( return { body, placeholders }; } -/** A read of a binding's current value: a plain read, or an update - * (`n++`, `--n`) that reads before it writes. */ -function readsBinding(expr: IrExpr): expr is IrExpr & { kind: "varRef" | "incDec" } { - return expr.kind === "varRef" || expr.kind === "incDec"; +/** A use that needs the binding initialized: a plain read, an update + * (`n++`, `--n`) that reads before it writes, or an assignment, which + * checks that the binding is initialized and releases its current value. + * Assignment statements count the same way (scanStatements). */ +function observesBinding( + expr: IrExpr, +): expr is IrExpr & { kind: "varRef" | "incDec" | "assignExpr" } { + return expr.kind === "varRef" || expr.kind === "incDec" || expr.kind === "assignExpr"; } function registryInitializer( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8320-assign-only-deferred-binding/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8320-assign-only-deferred-binding/main.ts.json new file mode 100644 index 000000000..399baa239 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8320-assign-only-deferred-binding/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/8320-assign-only-deferred-binding/scanner.ts", + "/tests/corpus/8320-assign-only-deferred-binding/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8320-assign-only-deferred-binding/main.ts b/tests/corpus/8320-assign-only-deferred-binding/main.ts new file mode 100644 index 000000000..d35a863ae --- /dev/null +++ b/tests/corpus/8320-assign-only-deferred-binding/main.ts @@ -0,0 +1,8 @@ +import { installCount, useFormat, useScanner } from "./scanner.ts"; + +useScanner((bytes, start) => start + bytes.length * 2); +console.log("scanner installed"); +const formatter = useFormat((value) => `[${value}]`); +console.log(formatter(7)); +useScanner((bytes, start) => start); +console.log("installs", installCount()); diff --git a/tests/corpus/8320-assign-only-deferred-binding/scanner.ts b/tests/corpus/8320-assign-only-deferred-binding/scanner.ts new file mode 100644 index 000000000..601df60ba --- /dev/null +++ b/tests/corpus/8320-assign-only-deferred-binding/scanner.ts @@ -0,0 +1,21 @@ +// Module bindings that only assignments use after their declarations. +export function defaultEnd(bytes: Uint8Array, start: number): number { + return start + bytes.length; +} + +let valueEnd: (bytes: Uint8Array, start: number) => number = defaultEnd; +let format: (value: number) => string = (value) => `#${value}`; +let installs = 0; + +export function useScanner(scan: (bytes: Uint8Array, start: number) => number): void { + valueEnd = (bytes, start) => scan(bytes, start) + 1; + installs++; +} + +export function useFormat(next: (value: number) => string): (value: number) => string { + return (format = next); +} + +export function installCount(): number { + return installs; +} diff --git a/tests/harness/corpus-costs/8320-assign-only-deferred-binding/main.ts.json b/tests/harness/corpus-costs/8320-assign-only-deferred-binding/main.ts.json new file mode 100644 index 000000000..0606e0393 --- /dev/null +++ b/tests/harness/corpus-costs/8320-assign-only-deferred-binding/main.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 48000 } From 4e9a4311b00c2c2983ae1a7c140663f0e64d492e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 21:15:01 -0500 Subject: [PATCH 149/273] Prove a typed-array loop's views disjoint and give each its own alias scope A loop that reads one typed array and writes another kept every element in memory: separate views can share a buffer, so LLVM had to assume a store to one could change the other's elements. A loop of eight lanes held in Float64Arrays (zr[k], zi[k], tr[k], ti[k]) ran as scalar loads and stores of each lane on every step. When a typed-array loop version covers at least two views and writes at least one of them, its guard now also requires every pair of the views' byte ranges to be disjoint: one ends at or before the other starts. In the fast copy, each view's element reads and writes carry an alias scope of their own and no-alias with the others' scopes, so LLVM keeps the elements in registers and vectorizes across lanes. Views that share bytes, or one array passed twice, take the checked copy as before. The scopes are string-named, written inline, and named after the function and the version, so every copy of a loop has its own. Worker programs scope only views the guard proves thread-owned. Corpus 8182 runs such loops over disjoint, adjacent, overlapping and identical views, views of different element types over one buffer, and empty views. --- .../compiler/src/backend/llvm/bytes-alias.ts | 4 +- .../src/backend/llvm/bytes-loop-versions.ts | 81 ++++++++++++++++- .../src/backend/llvm/emit-function.ts | 1 + .../src/backend/llvm/emit-statements.ts | 38 +++++++- packages/compiler/src/backend/llvm/emitter.ts | 25 +++++- .../compiler/src/backend/llvm/expr-bytes.ts | 42 +++++++++ .../8182-typed-array-alias-scopes.ts.json | 6 ++ .../typed-array-alias-scope-emission.test.ts | 57 ++++++++++++ tests/corpus/8182-typed-array-alias-scopes.ts | 89 +++++++++++++++++++ .../8182-typed-array-alias-scopes.ts.json | 1 + 10 files changed, 336 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8182-typed-array-alias-scopes.ts.json create mode 100644 packages/compiler/test/typed-array-alias-scope-emission.test.ts create mode 100644 tests/corpus/8182-typed-array-alias-scopes.ts create mode 100644 tests/harness/corpus-costs/8182-typed-array-alias-scopes.ts.json diff --git a/packages/compiler/src/backend/llvm/bytes-alias.ts b/packages/compiler/src/backend/llvm/bytes-alias.ts index d43620fd9..b7a672754 100644 --- a/packages/compiler/src/backend/llvm/bytes-alias.ts +++ b/packages/compiler/src/backend/llvm/bytes-alias.ts @@ -18,8 +18,10 @@ export function bytesHeaderAlias(host: LlvmEmitterContext): string { return tag === "" ? "" : `${tag}, !invariant.load !{}`; } +/** Element storage's tag, and the alias scope of the access being emitted + * in a loop version's fast copy (bytesAccessScope). */ export function bytesElementAlias(host: LlvmEmitterContext): string { - return host.memoryAliasAttachment("bytes:elements"); + return host.memoryAliasAttachment("bytes:elements") + host.bytesAccessScope; } /** A module global's slot is its own category for whole-slot loads of a diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index 301f65de2..ac280df00 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -1,4 +1,11 @@ -import { F64, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js"; +import { + BYTES_ELEMENT_SIZE, + F64, + type IrExpr, + type IrLocal, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; import type { IntegerRange, IntegerRanges } from "../../ir/integer-ranges.js"; import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; @@ -10,7 +17,7 @@ import { emitCountedLoopSafety, emitExactIntegerTest, } from "./counted-loops.js"; -import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js"; +import { emitBytesData, emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js"; import { f64Lit } from "./common.js"; import { emitStringAsciiFlag, @@ -106,6 +113,12 @@ export interface BytesLoopVersion { /** Integer ranges of affine index operands that hold in the unchecked * loop (where the guard bounds the counter). */ ranges: ReadonlyMap; + /** Typed-array receivers whose element ranges the guard proves disjoint, + * and the receiver of each of their element reads and writes. The fast + * copy gives each receiver its own alias scope, so a store to one view + * leaves the elements of the others in registers. Null when the loop + * writes no element, or reads and writes a single view. */ + scopes: { receivers: number[]; nodes: ReadonlyMap } | null; } export interface BytesLoopFacts { @@ -136,6 +149,8 @@ export interface BytesLoopFacts { const MAX_COST = 400; const MAX_FIXED_END = 2 ** 31; const MAX_CHECKS = 8; +/** Alias-scoped views per loop: the guard tests every pair's ranges. */ +const MAX_SCOPED_RECEIVERS = 6; const MAX_OFFSET = 2 ** 31; const SAFE = Number.MAX_SAFE_INTEGER; /** The range the guard requires of an invariant index term without a @@ -547,11 +562,24 @@ export function matchBytesLoopVersion( group.nodes.push(node); } else fixed.set(receiver, { check: { receiver, end }, nodes: [node] }); }; + // Element reads and writes of each typed-array receiver, for alias scopes. + const scoped = new Map(); + const writtenViews = new Set(); + const scope = (node: Node, array: IrExpr, write: boolean): void => { + const ref = root(array); + if (!ref || ref.type.kind !== "bytes") return; + const receiver = receiverOf(ref); + scoped.set(node, receiver); + if (write) writtenViews.add(receiver); + }; const visitExpr = (e: IrExpr): boolean => { if (e.kind === "bytesIntrinsic") { 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.method === "get" && e.args[0]) { + access(e, e.receiver, e.args[0], 1); + scope(e, e.receiver, false); + } } else if ( e.kind === "strIntrinsic" && (e.method === "charCodeAt" || e.method === "charAt") && @@ -561,7 +589,10 @@ export function matchBytesLoopVersion( return everyExprChild(e, visitExpr, visitStmt); }; const visitStmt = (s: IrStmt): boolean => { - if (s.kind === "bytesSet") access(s, s.arr, s.index, 1); + if (s.kind === "bytesSet") { + access(s, s.arr, s.index, 1); + scope(s, s.arr, true); + } return everyStmtChild(s, visitExpr, visitStmt); }; loop.body.every(visitStmt); @@ -684,6 +715,17 @@ export function matchBytesLoopVersion( : [], }); } + // Alias scopes: at least two views, one of them written, each read only + // through plain loads (in a worker program, a view the guard proves + // thread-owned). + const scopedReceivers = [...new Set(scoped.values())].sort((a, b) => a - b); + const ownedSet = new Set(ownedKept.map(([receiver]) => receiver)); + const scoping = + scopedReceivers.length >= 2 && + scopedReceivers.length <= MAX_SCOPED_RECEIVERS && + scopedReceivers.some((receiver) => writtenViews.has(receiver)) && + (!facts.workers || scopedReceivers.every((receiver) => ownedSet.has(receiver))); + if (scoping) for (const receiver of scopedReceivers) used.add(receiver); // 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])); @@ -737,6 +779,12 @@ export function matchBytesLoopVersion( : null, counterBound: needsBound ? bound : null, ranges, + scopes: scoping + ? { + receivers: scopedReceivers.map((receiver) => renumber.get(receiver)!), + nodes: new Map([...scoped].map(([node, receiver]) => [node, renumber.get(receiver)!])), + } + : null, }; } @@ -1126,6 +1174,31 @@ export function emitBytesLoopVersionGuard( B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`); conditions.push(ok); } + if (version.scopes) { + // Every pair of scoped views covers disjoint bytes: one ends at or + // before the other starts. An empty view covers none. + const spans = version.scopes.receivers.map((index) => { + const type = version.receivers[index]!.ref.type as IrType & { kind: "bytes" }; + const start = B.tmp(), + size = B.tmp(), + end = B.tmp(); + B.line(`${start} = ptrtoint ptr ${emitBytesData(host, receivers[index]!)} to i64`); + B.line(`${size} = mul nuw i64 ${lengths[index]}, ${BYTES_ELEMENT_SIZE[type.elem]}`); + B.line(`${end} = add nuw i64 ${start}, ${size}`); + return { start, end }; + }); + spans.forEach((a, i) => { + for (const b of spans.slice(i + 1)) { + const before = B.tmp(), + after = B.tmp(), + apart = B.tmp(); + B.line(`${before} = icmp ule i64 ${a.end}, ${b.start}`); + B.line(`${after} = icmp ule i64 ${b.end}, ${a.start}`); + B.line(`${apart} = or i1 ${before}, ${after}`); + conditions.push(apart); + } + }); + } for (const counter of version.counters) { const value = host.emitExpr(counter.ref); // A counter held as an integer (an int32 slot) is exact already and diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index ca45a6624..b79e2406d 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -64,6 +64,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.currentFunctionName = fn.name; emitter.currentUnwindAbi = emitter.unwindAbi.has(fn.name); emitter.usesLandingPads = false; + emitter.aliasScopedVersions = 0; emitter.findInt32Allocations(fn); emitter.currentTier = fn.tier; // Fiber switches restore the active exception cell before control diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 3d3aa6781..36c28bf97 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -570,6 +570,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { s.value, emitter.bytesBounds.has(s), emitter.bytesOwned.has(s), + emitter.bytesAliasScopes.get(s), ); break; } @@ -2080,6 +2081,32 @@ function emitSlowCopy(emitter: LlEmitter, s: IrStmt): void { * stops trying it. */ const SPECULATION_BAILS = 8; +/** The attachment of each scoped access in a version's fast copy: the + * scope of its own view, and no alias with the scopes of the other views, + * whose bytes the guard proved disjoint. The scopes are named after the + * function and the version, so every copy of a loop has its own, and + * written inline (string-named scopes need no numbered metadata). */ +function bytesAliasScopes( + emitter: LlEmitter, + scopes: NonNullable, +): Map { + const version = emitter.aliasScopedVersions++; + const name = emitter.currentFunctionName.replace(/[^A-Za-z0-9_.$%]/g, "_"); + const domain = '!{!"scriptc typed-array views"}'; + const scope = (receiver: number): string => `!{!"${name}.${version}.${receiver}", ${domain}}`; + const attachments = new Map(); + for (const receiver of scopes.receivers) { + const others = scopes.receivers.filter((other) => other !== receiver).map(scope); + attachments.set( + receiver, + `, !alias.scope !{${scope(receiver)}}, !noalias !{${others.join(", ")}}`, + ); + } + const result = new Map(); + for (const [node, receiver] of scopes.nodes) result.set(node, attachments.get(receiver)!); + return result; +} + /** 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 @@ -2104,7 +2131,8 @@ function emitVersionedLoop( originalOwned = emitter.bytesOwned, originalRanges = emitter.integerRanges, originalUnitReads = emitter.stringUnitReads, - originalLengthReads = emitter.stringLengthReads; + originalLengthReads = emitter.stringLengthReads, + originalScopes = emitter.bytesAliasScopes; LOOPS.of(emitter).bytesLoopVersioning.add(s); try { B.startBlock(slow); @@ -2113,6 +2141,13 @@ function emitVersionedLoop( if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]); if (version.ranges.size > 0) emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); + // Both fast copies run under the guard's span test, so they share the + // version's scopes. + if (version.scopes && emitter.debug === null) + emitter.bytesAliasScopes = new Map([ + ...originalScopes, + ...bytesAliasScopes(emitter, version.scopes), + ]); if (version.counted?.guarded || version.paired?.loop.guarded) LOOPS.of(emitter).provenCountedGuards.add(s); B.startBlock(fast); @@ -2146,6 +2181,7 @@ function emitVersionedLoop( emitter.integerRanges = originalRanges; emitter.stringUnitReads = originalUnitReads; emitter.stringLengthReads = originalLengthReads; + emitter.bytesAliasScopes = originalScopes; LOOPS.of(emitter).provenCountedGuards.delete(s); LOOPS.of(emitter).bytesLoopVersioning.delete(s); } diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index bcb8a5ba2..d5964f1ba 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1624,6 +1624,14 @@ export class LlEmitter { /** Worker programs: typed-array accesses whose views an enclosing version * guard proved thread-owned. */ bytesOwned: ReadonlySet = new Set(); + /** The alias-scope attachment of each typed-array element read and write + * in the current loop version's fast copy (emitVersionedLoop), and the + * one the element access being emitted carries (bytesElementAlias). */ + bytesAliasScopes: ReadonlyMap = new Map(); + bytesAccessScope = ""; + /** Loop versions with alias scopes in the current function; it names + * their scopes. */ + aliasScopedVersions = 0; integerViews = new Map(); /** Module globals the current function owns outright (initializer * bindings): their function-local slot, which stands in for the global. */ @@ -7275,8 +7283,9 @@ export class LlEmitter { inBounds = false, invalidNaN = false, owned = false, + scope = "", ): LlValue { - return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN, owned); + return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN, owned, scope); } emitToUint32(value: string, expr?: IrExpr, uint32?: string): string { @@ -7340,8 +7349,20 @@ export class LlEmitter { expr?: IrExpr, inBounds = false, owned = false, + scope = "", ): void { - return emitBytesSet(this, elem, receiver, index, value, indexExpr, expr, inBounds, owned); + return emitBytesSet( + this, + elem, + receiver, + index, + value, + indexExpr, + expr, + inBounds, + owned, + scope, + ); } 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 23025d00c..a50464cd6 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -235,6 +235,26 @@ export function emitBytesGet( inBounds = false, invalidNaN = false, owned = false, + scope = "", +): LlValue { + const outer = host.bytesAccessScope; + host.bytesAccessScope = scope; + try { + return emitScopedBytesGet(host, elem, receiver, index, expr, inBounds, invalidNaN, owned); + } finally { + host.bytesAccessScope = outer; + } +} + +function emitScopedBytesGet( + host: LlvmEmitterContext, + elem: IrBytesElem, + receiver: string, + index: LlValue, + expr: IrExpr | undefined, + inBounds: boolean, + invalidNaN: boolean, + owned: boolean, ): LlValue { const B = host.B; // Any invalid index traps (the array runtime's discipline). @@ -547,6 +567,27 @@ export function emitBytesSet( expr?: IrExpr, inBounds = false, owned = false, + scope = "", +): void { + const outer = host.bytesAccessScope; + host.bytesAccessScope = scope; + try { + emitScopedBytesSet(host, elem, receiver, index, value, indexExpr, expr, inBounds, owned); + } finally { + host.bytesAccessScope = outer; + } +} + +function emitScopedBytesSet( + host: LlvmEmitterContext, + elem: IrBytesElem, + receiver: string, + index: LlValue, + value: LlValue, + indexExpr: IrExpr | undefined, + expr: IrExpr | undefined, + inBounds: boolean, + owned: boolean, ): void { const B = host.B; // Invalid integer-indexed writes are ignored after evaluating the RHS. @@ -768,6 +809,7 @@ export function emitBytesIntrinsic( host.bytesBounds.has(e), e.invalidNaN === true, host.bytesOwned.has(e), + host.bytesAliasScopes.get(e), ); case "slice": return call( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8182-typed-array-alias-scopes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8182-typed-array-alias-scopes.ts.json new file mode 100644 index 000000000..c941e8b5d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8182-typed-array-alias-scopes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8182-typed-array-alias-scopes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/typed-array-alias-scope-emission.test.ts b/packages/compiler/test/typed-array-alias-scope-emission.test.ts new file mode 100644 index 000000000..a32df03e7 --- /dev/null +++ b/packages/compiler/test/typed-array-alias-scope-emission.test.ts @@ -0,0 +1,57 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +test("a typed-array loop version proves its views disjoint and scopes their accesses", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-alias-scopes-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile( + entry, + ` +function copy(dst: Float64Array, src: Float64Array, n: number): void { + for (let i = 0; i < n; i++) dst[i] = src[i]! + 1; +} +function scale(xs: Float64Array, n: number): void { + for (let i = 0; i < n; i++) xs[i] = xs[i]! * 2; +} +function dot(a: Float64Array, b: Float64Array, n: number): number { + let sum = 0; + for (let i = 0; i < n; i++) sum += a[i]! * b[i]!; + return sum; +} +const a = new Float64Array(8), b = new Float64Array(8); +copy(a, b, 8); +scale(a, 8); +console.log(dot(a, b, 8)); +`, + ); + 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")); + const ll = emitLlvmModule(mod); + const fn = (name: string): string => + new RegExp(`^define internal [^\\n]*@sc_bf_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(ll)![0]; + // Two views, one written: the guard compares their byte ranges, and + // each view's accesses in the fast copy carry its own scope. + const copy = fn("copy"); + expect(copy).toMatch(/ptrtoint ptr %\w+ to i64/); + expect(copy).toMatch(/icmp ule i64 %\w+, %\w+/); + expect(copy).toMatch( + /load double, ptr %\w+, align 1[^\n]*!alias\.scope !\{!\{!"copy\.0\.\d", !\{!"scriptc typed-array views"\}\}\}, !noalias/, + ); + expect(copy).toMatch(/store double %\w+, ptr %\w+, align 1[^\n]*!alias\.scope/); + // One view, or views that are only read, need no scopes. + for (const name of ["scale", "dot"]) { + expect(fn(name)).not.toContain("!alias.scope"); + expect(fn(name)).not.toContain("ptrtoint"); + } + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/tests/corpus/8182-typed-array-alias-scopes.ts b/tests/corpus/8182-typed-array-alias-scopes.ts new file mode 100644 index 000000000..4dbf52e47 --- /dev/null +++ b/tests/corpus/8182-typed-array-alias-scopes.ts @@ -0,0 +1,89 @@ +// Loops that read one typed array and write another, where the two views +// may share bytes: overlapping and disjoint views of one buffer, the same +// array passed twice, views of different element types over one buffer, +// and empty views. A loop over views that overlap must see its own writes. +function shiftAdd(dst: Float64Array, src: Float64Array, n: number): void { + for (let i = 0; i < n; i++) dst[i] = src[i]! + 1; +} + +function mixed(bytes: Uint8Array, words: Float64Array, n: number): number { + let sum = 0; + for (let i = 0; i < n; i++) { + words[i] = i * 0.5 + 1; + sum += bytes[i]!; + } + return sum; +} + +function lanes(zr: Float64Array, zi: Float64Array, tr: Float64Array, ti: Float64Array): number { + for (let step = 0; step < 6; step++) + for (let k = 0; k < 4; k++) { + const r = zr[k]!; + const i = zi[k]!; + zi[k] = 2 * r * i + 0.25; + zr[k] = tr[k]! - ti[k]! - 0.5; + tr[k] = zr[k]! * zr[k]!; + ti[k] = zi[k]! * zi[k]!; + } + let sum = 0; + for (let k = 0; k < 4; k++) sum += zr[k]! + zi[k]! + tr[k]! + ti[k]!; + return sum; +} + +function show(a: Float64Array): string { + const parts: string[] = []; + for (let i = 0; i < a.length; i++) parts.push(a[i]!.toFixed(3)); + return parts.join(","); +} + +// Disjoint arrays. +const a = new Float64Array([1, 2, 3, 4]); +const b = new Float64Array(4); +shiftAdd(b, a, 4); +console.log("disjoint", show(b)); + +// One array as both views: each element reads its own old value. +const same = new Float64Array([1, 2, 3, 4]); +shiftAdd(same, same, 4); +console.log("same", show(same)); + +// Overlapping views of one buffer: the destination starts one element +// after the source, so every read sees the previous iteration's write. +const buffer = new ArrayBuffer(8 * 8); +const whole = new Float64Array(buffer); +whole.fill(1); +shiftAdd(new Float64Array(buffer, 8, 6), new Float64Array(buffer, 0, 6), 6); +console.log("overlap forward", show(whole)); +whole.fill(1); +shiftAdd(new Float64Array(buffer, 0, 6), new Float64Array(buffer, 8, 6), 6); +console.log("overlap backward", show(whole)); + +// Adjacent views of one buffer share no bytes. +whole.set([1, 2, 3, 4, 5, 6, 7, 8]); +shiftAdd(new Float64Array(buffer, 32, 4), new Float64Array(buffer, 0, 4), 4); +console.log("adjacent", show(whole)); + +// A byte view over the doubles it writes. +const shared = new ArrayBuffer(64); +console.log("mixed", mixed(new Uint8Array(shared), new Float64Array(shared), 8), show(new Float64Array(shared))); +console.log("mixed disjoint", mixed(new Uint8Array(new ArrayBuffer(64)), new Float64Array(8), 8)); + +// Empty views. +shiftAdd(new Float64Array(0), new Float64Array(0), 0); +console.log("empty", new Float64Array(0).length); + +// Four lanes in separate arrays, and the same arrays aliased. +const lane = (): Float64Array => new Float64Array([0.1, -0.2, 0.3, -0.4]); +console.log("lanes", lanes(lane(), lane(), lane(), lane()).toFixed(6)); +const one = lane(); +console.log("lanes aliased", lanes(one, one, lane(), lane()).toFixed(6)); +const pool = new Float64Array(16); +pool.set([0.1, -0.2, 0.3, -0.4, 0.1, -0.2, 0.3, -0.4, 0.1, -0.2, 0.3, -0.4, 0.1, -0.2, 0.3, -0.4]); +console.log( + "lanes subarrays", + lanes(pool.subarray(0, 4), pool.subarray(4, 8), pool.subarray(8, 12), pool.subarray(12, 16)).toFixed(6), +); +console.log( + "lanes overlapping", + lanes(pool.subarray(0, 4), pool.subarray(2, 6), pool.subarray(8, 12), pool.subarray(9, 13)).toFixed(6), +); diff --git a/tests/harness/corpus-costs/8182-typed-array-alias-scopes.ts.json b/tests/harness/corpus-costs/8182-typed-array-alias-scopes.ts.json new file mode 100644 index 000000000..974b44e1e --- /dev/null +++ b/tests/harness/corpus-costs/8182-typed-array-alias-scopes.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 3800 } From 5f602ff3b85989be80e44c6f82d01bbce84c9e61 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 21:16:59 -0500 Subject: [PATCH 150/273] Decode asserted optional typed arrays other than Uint8Array `decoder.decode(view!)` on a `Uint16Array | undefined` (or another typed array that may be undefined) was refused with SC2020: the assertion keeps the undefined arm for decode, and the decode input only read the undefined arm of `Uint8Array | undefined`. The undefined arm of any typed array that may be undefined now reads as an empty array of the same type, so a defined input decodes its bytes and an undefined one decodes no bytes, as in Node. --- .../frontend/lowering/builtins/text-codecs.ts | 30 ++++++++++++------- tests/corpus/text-decoder-undefined-input.ts | 7 +++++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index 34cc7aa14..39156940e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -16,7 +16,6 @@ import { type IrType, STRING, type SrcLoc, - typeEquals, } from "../../../ir/ir.js"; import { lowerBuiltinValuePreservingUndefined, @@ -27,21 +26,30 @@ import { type BuiltinModuleFn } from "../surfaces.js"; /** The input of `decoder.decode(input)`. An undefined input decodes no bytes, * as `decode()` does in Node: a statically undefined argument (after its - * effects) and the undefined arm of `Uint8Array | undefined` read as an - * empty Uint8Array. */ + * effects) reads as an empty Uint8Array, and the undefined arm of a typed + * array or view that may be undefined (`Uint8Array | undefined`, + * `Uint16Array | undefined`) as an empty view of the same type. */ function lowerDecodeInput(lowerer: Lowerer, node: ts.Expression, loc: SrcLoc): IrExpr { - const empty: IrExpr = { kind: "bytesNew", source: null, type: BYTES_U8, loc }; const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, node); - if (undefinedArg) return defaultAfterUndefined(undefinedArg, empty); + if (undefinedArg) + return defaultAfterUndefined(undefinedArg, { + kind: "bytesNew", + source: null, + type: BYTES_U8, + loc, + }); const value = lowerer.lowerExpr(node); if (value.type.kind === "union") { const def = lowerer.unions.get(value.type.unionId); - if ( - def?.arms.length === 2 && - def.arms.some((arm) => arm.kind === "undefinedT") && - def.arms.some((arm) => typeEquals(arm, BYTES_U8)) - ) - return { kind: "nullish", left: value, right: empty, type: BYTES_U8, loc }; + const view = def?.arms.find((arm) => arm.kind === "bytes"); + if (def?.arms.length === 2 && view && def.arms.some((arm) => arm.kind === "undefinedT")) + return { + kind: "nullish", + left: value, + right: { kind: "bytesNew", source: null, type: view, loc }, + type: view, + loc, + }; } return value; } diff --git a/tests/corpus/text-decoder-undefined-input.ts b/tests/corpus/text-decoder-undefined-input.ts index 6a3be354c..32bca7f3a 100644 --- a/tests/corpus/text-decoder-undefined-input.ts +++ b/tests/corpus/text-decoder-undefined-input.ts @@ -29,3 +29,10 @@ const streaming = new TextDecoder(); let text = ""; for (const chunk of chunks) text += streaming.decode(chunk.value, { stream: !chunk.done }); console.log(JSON.stringify(text), text.length); + +// Other typed arrays decode their bytes, with or without the assertion. +const wide = new Map([["present", new Uint16Array([0x6968])]]); +const asserted = wide.get("present")!; +console.log(JSON.stringify(decoder.decode(asserted)), JSON.stringify(decoder.decode(wide.get("present")!))); +console.log(JSON.stringify(decoder.decode(wide.get("present"))), JSON.stringify(decoder.decode(wide.get("absent")))); +console.log(JSON.stringify(new TextDecoder("utf-8").decode(wide.get("absent")!))); From 031c6172f905f45e9838961d350f88deaf73ea62 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 21:20:10 -0500 Subject: [PATCH 151/273] Emit serialized IR with the unwinding ABI in the serialized IR emission test compile() emits textual LLVM on Linux with the unwinding ABI since #2163, so the test's emission from serialized IR passes the same option. --- .../compiler/src/backend/llvm/serialized-ir-emission.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/compiler/src/backend/llvm/serialized-ir-emission.test.ts b/packages/compiler/src/backend/llvm/serialized-ir-emission.test.ts index cdbba146c..6d2457c57 100644 --- a/packages/compiler/src/backend/llvm/serialized-ir-emission.test.ts +++ b/packages/compiler/src/backend/llvm/serialized-ir-emission.test.ts @@ -41,6 +41,8 @@ test("sort-heavy programs emit equal LLVM from memory and from serialized IR", a pointerBits: 64, wasi: false, runtimeAbiMarker: false, + // compile() emits Linux textual LLVM with the unwinding ABI. + unwindAbi: process.platform === "linux", objectAudit: sanitize, ...inlineRcOption(options.optimization, sanitize), }); From 492d1558be1a4884dd3c22c4b796361614791b91 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 21:22:54 -0500 Subject: [PATCH 152/273] Complete consecutive synchronous writes in one afterWrite step Node's onwrite queues one afterWrite nextTick for a synchronous write completion and lets later synchronous completions with the same callback join it (afterWriteTickInfo.count); writes without a callback share one. The step then checks 'drain' once, calls the callback once per write and runs finishMaybe once. The runtime queued a stream tick and a nextTick entry per write, which ran the callbacks of later writes after nextTicks queued between them. The queued afterWrite tick now counts the writes that join it, as Node's does. --- .../7514-stream-after-write-order.ts.json | 6 ++ packages/runtime/src/scr_stream.c | 42 ++++++++-- tests/corpus/7514-stream-after-write-order.ts | 76 +++++++++++++++++++ .../7514-stream-after-write-order.ts.json | 1 + 4 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7514-stream-after-write-order.ts.json create mode 100644 tests/corpus/7514-stream-after-write-order.ts create mode 100644 tests/harness/corpus-costs/7514-stream-after-write-order.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7514-stream-after-write-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7514-stream-after-write-order.ts.json new file mode 100644 index 000000000..a6c705cd7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7514-stream-after-write-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7514-stream-after-write-order.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 1214ce5fd..cc0b277f3 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -176,6 +176,9 @@ struct ScrStreamState { size_t n, cap, head; int corked; bool writing, wsync, need_drain, after_scheduled; + /* the queued SCR_ST_AFTER_WRITE tick later synchronous writes with + * the same callback join (Node's afterWriteTickInfo), or NULL */ + struct ScrStreamTick *after_tick; /* 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 @@ -543,6 +546,9 @@ typedef struct ScrStreamTick { ScrError *err; /* owned or NULL (SCR_ST_ERROR) */ ScrClosure *cb; /* owned or NULL (SCR_ST_AFTER_WRITE / SCR_ST_WCB_ERR / SCR_ST_WRITE_ERR) */ bool hold; /* SCR_ST_AFTER_WRITE: counted in w.after_holds */ + /* SCR_ST_AFTER_WRITE: the synchronous writes it completes, which share + * cb (Node's afterWriteTickInfo.count) */ + size_t count; /* The AsyncLocalStorage context active when the tick was scheduled (+1, * NULL outside a store): Node schedules these steps with * process.nextTick, so their events and callbacks run in it. */ @@ -2117,7 +2123,7 @@ static void scr_stream_error_buffer(ScrStream *s) { * empty), THEN the user's write(chunk, cb) callback, then finishMaybe. * The buffered queue was already restarted by write_done (Node's onwrite * calls clearBuffer before afterWrite). cb moves. */ -static void scr_stream_after_write(ScrStream *s, ScrClosure *cb) { +static void scr_stream_after_write(ScrStream *s, ScrClosure *cb, size_t count) { ScrStreamState *st = s->st; if (st->w.length == 0 && st->w.need_drain && !st->w.ending && !st->destroyed) { st->w.need_drain = false; @@ -2137,7 +2143,11 @@ static void scr_stream_after_write(ScrStream *s, ScrClosure *cb) { } } if (cb) { - ((void (*)(ScrClosure *))cb->fn)(cb); + /* afterWrite: the callback once per write the tick completes */ + for (size_t i = 0; i < count; i++) { + ((void (*)(ScrClosure *))cb->fn)(cb); + if (scr_exc_pending()) break; + } scr_closure_release(cb); if (scr_exc_pending()) return; } @@ -7170,13 +7180,24 @@ void scr_stream_write_done(ScrStream *s, ScrError *err /*moves*/) { } 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; + ScrStreamTick *pending = st->w.after_tick; + if (pending && pending->cb == ucb) { + /* Node's onwrite: a synchronous write with the callback of the + * afterWrite tick already queued joins it (count++) */ + pending->count++; + if (ucb) scr_closure_release(ucb); + } else { + scr_st_tick(s, SCR_ST_AFTER_WRITE, NULL, ucb); + pending = scr_st_tail; + pending->count = 1; + st->w.after_tick = pending; + } + if (hold && !pending->hold) { + pending->hold = true; st->w.after_holds++; } } else { - scr_stream_after_write(s, ucb); + scr_stream_after_write(s, ucb, 1); } } @@ -7722,13 +7743,17 @@ static void scr_stream_run_tick(ScrStreamTick *t) { case SCR_ST_AFTER_WRITE: { st->w.after_scheduled = false; if (t->hold) st->w.after_holds--; + if (st->w.after_tick == t) st->w.after_tick = NULL; ScrClosure *cb = t->cb; t->cb = NULL; if (!st->destroyed) { - scr_stream_after_write(s, cb); + scr_stream_after_write(s, cb, t->count); } else { if (cb) { - ((void (*)(ScrClosure *))cb->fn)(cb); + for (size_t i = 0; i < t->count; i++) { + ((void (*)(ScrClosure *))cb->fn)(cb); + if (scr_exc_pending()) break; + } scr_closure_release(cb); if (scr_exc_pending()) break; } @@ -7898,6 +7923,7 @@ static void scr_stream_run_tick_in_context(ScrStreamTick *t) { } static void scr_stream_tick_free(ScrStreamTick *t) { + if (t->s->st && t->s->st->w.after_tick == t) t->s->st->w.after_tick = NULL; scr_stream_release(t->s); if (t->err) scr_error_release(t->err); if (t->cb) scr_closure_release(t->cb); diff --git a/tests/corpus/7514-stream-after-write-order.ts b/tests/corpus/7514-stream-after-write-order.ts new file mode 100644 index 000000000..5647f5bc8 --- /dev/null +++ b/tests/corpus/7514-stream-after-write-order.ts @@ -0,0 +1,76 @@ +// Write callbacks of synchronous writes run in Node's order: consecutive +// writes with the same callback complete in one afterWrite step, ahead of +// a process.nextTick queued between them, and a write with another +// callback (or none) starts the next step; end's callback and 'finish' +// follow. A PassThrough read by 'data' runs each write's own callback. +import { PassThrough, Writable } from "node:stream"; + +const log: string[] = []; + +function run(): Promise { + return new Promise((resolve) => { + const w = new Writable({ + highWaterMark: 4, + write(chunk: Buffer, _enc, cb) { + log.push(`write ${String(chunk)}`); + cb(); + }, + }); + const shared = (): void => { + log.push("shared callback"); + }; + w.on("finish", () => { + log.push("finish"); + resolve(); + }); + w.write("a", shared); + w.write("b", shared); + process.nextTick(() => log.push("tick between")); + w.write("c", shared); + w.write("d", () => log.push("own callback d")); + w.write("e"); + w.write("f"); + queueMicrotask(() => log.push("microtask")); + w.write("g", shared); + w.write("h", shared); + process.nextTick(() => log.push("tick after")); + w.end(() => log.push("end callback")); + }); +} + +function passThrough(): Promise { + return new Promise((resolve) => { + const pt = new PassThrough({ highWaterMark: 8 }); + let bytes = 0; + pt.on("data", (chunk: Buffer) => { + bytes += chunk.length; + }); + pt.on("end", () => { + log.push(`passthrough end ${bytes}`); + resolve(); + }); + let i = 0; + const more = (): void => { + while (i < 40) { + i++; + const n = i; + if (!pt.write(`chunk${n}`, () => { + if (n % 10 === 0) log.push(`callback ${n}`); + })) { + pt.once("drain", more); + return; + } + } + pt.end(); + }; + more(); + }); +} + +async function main(): Promise { + await run(); + await passThrough(); + console.log(log.join("\n")); +} + +main(); diff --git a/tests/harness/corpus-costs/7514-stream-after-write-order.ts.json b/tests/harness/corpus-costs/7514-stream-after-write-order.ts.json new file mode 100644 index 000000000..56aa0985f --- /dev/null +++ b/tests/harness/corpus-costs/7514-stream-after-write-order.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 3600 } From dd2e53f00cad23959425d645324dc6ef2f5de365 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 21:40:50 -0500 Subject: [PATCH 153/273] Look up a number Map key on its integer view A Map or Set operation on a number key that the emitter knows is an integer (exactInteger: an integer view, a range proof or a literal) now looks the key up through sc_map_entry_f64_int, which tests the map's direct index with an unsigned compare of that integer. Before, the lookup converted the double back to an integer and compared it with the original to prove it integral. The hashed probe still takes the double, so hashing and SameValueZero are unchanged. --- .../src/backend/llvm/emit-function.ts | 1 + .../compiler/src/backend/llvm/emit-module.ts | 36 +++++++++++ packages/compiler/src/backend/llvm/emitter.ts | 5 +- .../src/backend/llvm/expr-containers.ts | 8 ++- .../backend/llvm/local-union-storage.test.ts | 3 +- .../backend/llvm/map-number-lookup.test.ts | 28 ++++++++- .../src/backend/llvm/map-number-lookup.ts | 24 ++++++- .../src/backend/llvm/map-read-lifetimes.ts | 2 + .../6308-number-map-integer-keys.ts.json | 6 ++ tests/corpus/6308-number-map-integer-keys.ts | 62 +++++++++++++++++++ .../6308-number-map-integer-keys.ts.json | 1 + 11 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6308-number-map-integer-keys.ts.json create mode 100644 tests/corpus/6308-number-map-integer-keys.ts create mode 100644 tests/harness/corpus-costs/6308-number-map-integer-keys.ts.json diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index ca45a6624..d7c5696cf 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -200,6 +200,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.callLifetimes, ); emitter.readNumberMapWrites = findReadNumberMapWrites(fn); + emitter.numberMapKeyIntegers = new Map(); emitter.mapEntryFusion = emitter.debug === null ? findMapEntryFusions(fn) : null; emitter.localStackUnions = findLocalStackUnions( fn, diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 6adf76079..c6d3dbff4 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -1301,6 +1301,42 @@ export function helperDefs(emitter: LlEmitter): string[] { ` ret ptr %hashed`, `}`, ``, + // The same lookup for a key the emitter knows is an integer (`%ki`, + // the double `%k`'s exact value): the direct index tests the integer + // itself, so the double is converted only by the hashed probe. A + // negative key is not below the index's size as an unsigned value. + `define internal ptr @sc_map_entry_f64_int(ptr %m, i64 %ki, double %k) ${FN_ATTRS} {`, + `entry:`, + ` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`, + ` %nd32 = load i32, ptr %ndp`, + ` %nd = zext i32 %nd32 to i64`, + ` %inside = icmp ult i64 %ki, %nd`, + ` br i1 %inside, label %slot, label %outside`, + `slot:`, + sz === "i64" ? ` %i = add i64 %ki, 0` : ` %i = trunc i64 %ki to ${sz}`, + ` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`, + ` %dense = load ptr, ptr %dp`, + ` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`, + ` %s = load i32, ptr %sp`, + ` %empty = icmp eq i32 %s, 0`, + ` br i1 %empty, label %miss, label %hit`, + `hit:`, + ` %e32 = sub i32 %s, 1`, + sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`, + ` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`, + ` %entries = load ptr, ptr %ep`, + ` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`, + ` ret ptr %found`, + `miss:`, + ` ret ptr null`, + `outside:`, + ` %dense.any = icmp ne i64 %nd, 0`, + ` br i1 %dense.any, label %miss, label %slow`, + `slow:`, + ` %hashed = call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)`, + ` ret ptr %hashed`, + `}`, + ``, // Hashing (scr_map_find_f64): the SameValueZero key word, its // scr_key_mix hash (zero remapped to one) and the absent-key filter. `define internal ptr @sc_map_entry_f64_hashed(ptr %m, double %k) noinline ${FN_ATTRS} {`, diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index bcb8a5ba2..0dae48868 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -123,7 +123,7 @@ import { } from "./int32-abi.js"; import { type Int32Local } from "./int32-locals.js"; import { findIntegerClones, NO_INTEGER_CLONES, type IntegerClones } from "./integer-clones.js"; -import { exactInteger, narrowInteger, widenInteger } from "./integer-values.js"; +import { exactInteger, narrowInteger, widenInteger, type LlInteger } from "./integer-values.js"; import { findInitializerBindings } from "../../ir/initializer-bindings.js"; import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js"; import { @@ -1241,6 +1241,9 @@ export class LlEmitter { callArrayReads = new Map(); mapReadLifetimes: MapReadLifetimes = { locals: new Map(), arguments: new Map() }; readNumberMapWrites: ReadonlySet = new Set(); + /** The integer views of the current function's number Map/Set keys, by + * the key's double operand (noteNumberMapKey, map-number-lookup.ts). */ + numberMapKeyIntegers = new Map(); /** The current function's fused string-key Map get-then-set pairs (map-entry-fusion.ts). */ mapEntryFusion: MapEntryFusion | null = null; readonly callLifetimes: CallLifetimes; diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 59ef43d7d..98389b2e5 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -42,6 +42,7 @@ import { emitStringMapGetRef, emitStringMapGetScalar, emitStringMapHas, + noteNumberMapKey, } from "./map-number-lookup.js"; import { emitBorrowedInput } from "./borrowed-inputs.js"; import { emitFusedMapSet, rememberMapEntry } from "./map-entry-fusion.js"; @@ -1548,6 +1549,7 @@ export function emitMapLikeIntrinsic( // Key borrowed (the runtime retains stored string keys); the // value MOVES in (replacement releases the old value inside). const k = emitBorrowedInput(host, e.args[0]!); + if (kAcc === "f64") noteNumberMapKey(host, k, e.args[0]!); const v = host.emitExpr(e.args[1]!); const vAcc = elemAccess(receiverType.value); if (vAcc === "ref") host.toHeap(v); @@ -1574,6 +1576,7 @@ export function emitMapLikeIntrinsic( // Element borrowed (the runtime retains stored strings); the unit // value is 0. Re-adding overwrites in place, preserving insertion. const k = emitBorrowedInput(host, e.args[0]!); + if (kAcc === "f64") noteNumberMapKey(host, k, e.args[0]!); if (kAcc === "f64" && host.readNumberMapWrites.has(e)) emitNumberMapSetScalar(host, r.name, k.name, null, () => host.mapSet(r.name, kAcc, "f64", k.name, f64Lit(0)), @@ -1583,7 +1586,10 @@ export function emitMapLikeIntrinsic( } case "has": { const k = borrowInputs ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!); - if (kAcc === "f64") return { name: emitNumberMapHas(host, r.name, k.name), type: e.type }; + if (kAcc === "f64") { + noteNumberMapKey(host, k, e.args[0]!); + return { name: emitNumberMapHas(host, r.name, k.name), type: e.type }; + } if (kAcc === "str") return { name: emitStringMapHas(host, r.name, k.name, compact), type: e.type }; host.declare(`declare zeroext i1 @scr_map_has_${kAcc}(ptr, ${kTy})`); diff --git a/packages/compiler/src/backend/llvm/local-union-storage.test.ts b/packages/compiler/src/backend/llvm/local-union-storage.test.ts index b8f44408d..81019cadc 100644 --- a/packages/compiler/src/backend/llvm/local-union-storage.test.ts +++ b/packages/compiler/src/backend/llvm/local-union-storage.test.ts @@ -113,7 +113,8 @@ test("conditional and reassigned locals own payload snapshots without heap wrapp expect(facts(mod).get("item")?.ownerType).toEqual(STRING); for (const bits of [32, 64] as const) { const ir = body(mod, bits); - expect(ir).toContain("call ptr @sc_map_entry_f64("); // inline number-key read + // The inline number-key read, on the integer view of the literal key. + expect(ir).toMatch(/call ptr @sc_map_entry_f64_int\(ptr %\S+, i64 \d+, double /); expect(ir).toContain("@scr_str_release"); expect(ir).not.toMatch(/@scr_union_(?:new|retain|release)/); expect(ir).toContain("store i64 0"); diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts index 989b8b3b0..28b6a0552 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts @@ -118,7 +118,7 @@ test("string keys and reference values are retained inline, boolean slots compar }); /** work(map, k): map.has(k) on a number-keyed map. */ -function lookupFixture(): IrModule { +function lookupFixture(key: IrExpr = ref("k", F64)): IrModule { const map: IrType = { kind: "map", key: F64, value: F64 }; const params = [ { localId: "map", name: "map", type: map }, @@ -128,7 +128,7 @@ function lookupFixture(): IrModule { kind: "mapIntrinsic", method: "has", receiver: ref("map", map), - args: [ref("k", F64)], + args: [key], type: BOOL, loc, }; @@ -182,6 +182,30 @@ test("hashed number-key lookups probe inline and call the runtime only for wide } }); +test("a key with an integer view tests the direct index on the integer", () => { + for (const bits of [32, 64] as const) { + const sz = bits === 64 ? "i64" : "i32"; + const integral = emitted(lookupFixture({ kind: "numLit", value: 12, type: F64, loc }), bits); + expect(integral.work).toMatch(/call ptr @sc_map_entry_f64_int\(ptr %\S+, i64 12, double /); + const helper = + /^define internal ptr @sc_map_entry_f64_int\(ptr %m, i64 %ki, double %k\)[^]*?^}/m.exec( + integral.module, + )![0]; + // An unsigned compare also rejects negative keys; the hashed probe takes + // the double. + expect(helper).toContain("icmp ult i64 %ki, %nd"); + expect(helper).toContain(`getelementptr inbounds i32, ptr %dense, ${sz} %i`); + expect(helper).toContain("call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)"); + expect(helper).not.toContain("fptosi"); + // A fractional key and a key without a proof keep the double lookup. + for (const key of [{ kind: "numLit", value: 0.5, type: F64, loc } as IrExpr, ref("k", F64)]) { + const { work } = emitted(lookupFixture(key), bits); + expect(work).toContain("call ptr @sc_map_entry_f64("); + expect(work).not.toContain("@sc_map_entry_f64_int("); + } + } +}); + /** work(map): map.size, and keep(map): map returned as an owned value. */ function sizeFixture(): IrModule { const map: IrType = { kind: "map", key: F64, value: F64 }; diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.ts b/packages/compiler/src/backend/llvm/map-number-lookup.ts index 6d219b50a..6cb7f1ea3 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.ts @@ -28,16 +28,34 @@ * guard excludes. for-of starts at scr_map_iter_start, past a deleted * prefix. */ -import type { IrType } from "../../ir/ir.js"; -import type { LlvmEmitterContext } from "./expr-context.js"; +import type { IrExpr, IrType } from "../../ir/ir.js"; +import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; +import { exactInteger, widenInteger } from "./integer-values.js"; import { mapKeyAccess } from "./shapes.js"; export const NUMBER_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_f64(ptr, double)"; +/** Record the integer view of a number Map/Set key operand, when the + * emitter has one (exactInteger), for the entry lookups of that key. */ +export function noteNumberMapKey(host: LlvmEmitterContext, key: LlValue, expr: IrExpr): void { + if (key.type.kind !== "f64" || host.numberMapKeyIntegers.has(key.name)) return; + const integer = exactInteger(host, key, expr); + if (integer) host.numberMapKeyIntegers.set(key.name, integer); +} -/** The entry for `key`, or null when absent. Valid until the next mutation. */ +/** The entry for `key`, or null when absent. Valid until the next mutation. + * A key with an integer view (noteNumberMapKey) tests the direct index on + * that integer instead of converting the double back. */ export function emitNumberMapEntry(host: LlvmEmitterContext, map: string, key: string): string { host.declare(NUMBER_MAP_ENTRY_DECL); const entry = host.B.tmp(); + const integer = host.numberMapKeyIntegers.get(key); + if (integer) { + const wide = widenInteger(host, integer); + host.B.line( + `${entry} = call ptr @sc_map_entry_f64_int(ptr ${map}, i64 ${wide}, double ${key})`, + ); + return entry; + } host.B.line(`${entry} = call ptr @sc_map_entry_f64(ptr ${map}, double ${key})`); return entry; } diff --git a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts index 6053d70eb..c44e28cb2 100644 --- a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts @@ -16,6 +16,7 @@ import { emitStringMapGetScalar, emitStringMapGetRef, compactMapValues, + noteNumberMapKey, } from "./map-number-lookup.js"; import { mapKeyAccess, mapKeyParamType } from "./shapes.js"; import { rememberMapEntry } from "./map-entry-fusion.js"; @@ -265,6 +266,7 @@ export function emitMapLookupKey( const value = borrow ? host.emitReadReceiver(key) : host.emitExpr(key); const access = mapKeyAccess(key.type); const type = mapKeyParamType(access); + if (access === "f64") noteNumberMapKey(host, value, key); return { access, types: type, args: `${type} ${value.name}`, value: value.name }; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6308-number-map-integer-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6308-number-map-integer-keys.ts.json new file mode 100644 index 000000000..b58240a3e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6308-number-map-integer-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6308-number-map-integer-keys.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6308-number-map-integer-keys.ts b/tests/corpus/6308-number-map-integer-keys.ts new file mode 100644 index 000000000..16110f80a --- /dev/null +++ b/tests/corpus/6308-number-map-integer-keys.ts @@ -0,0 +1,62 @@ +// Number-keyed Map and Set operations whose keys the compiler knows are +// integers (loop counters, `| 0` results, typed-array bytes, integer +// arithmetic) test the map's direct index on the integer. Results must +// match Node while the index covers the keys, after a key outside it moves +// the map to hashing, for negative keys and for keys past the index, and +// when a double key and an integer key name the same entry. + +const counts = new Map(); +for (let i = 0; i < 200; i++) { + const key = (i * 7) % 13; + counts.set(key, (counts.get(key) ?? 0) + 1); +} +const dense: string[] = []; +for (let k = -2; k < 16; k++) dense.push(`${k}:${counts.has(k) ? counts.get(k) : "-"}`); +console.log(dense.join(" ")); + +// A key past the direct index moves the map to hashing; integer reads still +// find every key. +counts.set(1_000_000, 5); +let found = 0; +for (let k = -2; k < 16; k++) if (counts.has(k)) found += counts.get(k)!; +console.log(found, counts.get(1_000_000), counts.has(999_999), counts.size); + +// Negative keys, and keys built with `| 0` and shifts. +const signed = new Map(); +for (let i = -5; i <= 5; i++) signed.set((i * 3) | 0, `v${i}`); +const reads: string[] = []; +for (let i = -16; i <= 16; i += 4) reads.push(`${i}:${signed.get(i) ?? "-"}`); +console.log(reads.join(" "), signed.get(-0), signed.get(0)); + +// Bytes of a typed array as keys, in a Set. +const bytes = new Uint8Array([3, 250, 3, 0, 17, 250, 255, 0]); +const seen = new Set(); +let repeats = 0; +for (let i = 0; i < bytes.length; i++) { + if (seen.has(bytes[i]!)) repeats++; + seen.add(bytes[i]!); +} +console.log([...seen].join(","), repeats); + +// A double key and an integer key name the same entry. +const mixed = new Map(); +mixed.set(2.0, 1); +mixed.set(0.5, 2); +for (let i = 0; i < 4; i++) mixed.set(i, (mixed.get(i) ?? 0) + 10); +console.log([...mixed.entries()].map(([k, v]) => `${k}=${v}`).join(" "), mixed.get(0.5)); + +// Deletion through integer keys, then reinsertion order. +const order = new Map(); +for (let i = 0; i < 10; i++) order.set(i, i * i); +for (let i = 0; i < 10; i += 3) order.delete(i); +for (let i = 9; i >= 0; i -= 2) order.set(i, -i); +console.log([...order.entries()].map(([k, v]) => `${k}=${v}`).join(" ")); + +// Large integer keys near 2^53, from integer arithmetic. +const wide = new Map(); +let key = 2 ** 52; +for (let i = 0; i < 5; i++) { + wide.set(key, i); + key = key * 2 - 1; +} +console.log([...wide.keys()].join(" "), wide.get(2 ** 52), wide.get(2 ** 53 - 1)); diff --git a/tests/harness/corpus-costs/6308-number-map-integer-keys.ts.json b/tests/harness/corpus-costs/6308-number-map-integer-keys.ts.json new file mode 100644 index 000000000..a6e4996ff --- /dev/null +++ b/tests/harness/corpus-costs/6308-number-map-integer-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 2800, "san": 3500 } From f025b5243345daa0b2855173f93806be557e6b3c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 21:50:36 -0500 Subject: [PATCH 154/273] Declare a typed-array loop version's alias scopes where its fast copy starts The guard proves the views disjoint for one entry of the loop. An enclosing loop may pass other views on its next pass, such as two buffers swapped after every pass, so one pass's write and the next pass's read can be the same bytes under different scopes. Without a declaration, a scope holds across the whole function, and LLVM could apply one entry's no-alias facts to another's accesses once it unrolled the enclosing loop. Each fast copy now begins with llvm.experimental.noalias.scope.decl for each of its scopes. A declared scope holds from its declaration, and LLVM gives each copy of the declaration its own scopes when it unrolls or duplicates the code around it. Corpus 8182 adds ping-pong passes over swapped Float64Array and Int32Array views; the emission test checks the declarations. --- .../src/backend/llvm/emit-statements.ts | 32 +++++++++---- .../typed-array-alias-scope-emission.test.ts | 7 +++ tests/corpus/8182-typed-array-alias-scopes.ts | 47 ++++++++++++++++++- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 36c28bf97..9b7ed9d8f 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -2085,11 +2085,12 @@ const SPECULATION_BAILS = 8; * scope of its own view, and no alias with the scopes of the other views, * whose bytes the guard proved disjoint. The scopes are named after the * function and the version, so every copy of a loop has its own, and - * written inline (string-named scopes need no numbered metadata). */ + * written inline (string-named scopes need no numbered metadata). Also + * the scopes, which each fast copy declares on entry (declareAliasScopes). */ function bytesAliasScopes( emitter: LlEmitter, scopes: NonNullable, -): Map { +): { attachments: Map; scopes: string[] } { const version = emitter.aliasScopedVersions++; const name = emitter.currentFunctionName.replace(/[^A-Za-z0-9_.$%]/g, "_"); const domain = '!{!"scriptc typed-array views"}'; @@ -2104,7 +2105,20 @@ function bytesAliasScopes( } const result = new Map(); for (const [node, receiver] of scopes.nodes) result.set(node, attachments.get(receiver)!); - return result; + return { attachments: result, scopes: scopes.receivers.map(scope) }; +} + +/** Declares a version's scopes where a fast copy starts. The guard proves + * the views disjoint for this entry only: an enclosing loop may pass other + * views on its next pass (`[cur, next] = [next, cur]`). A declared scope + * holds from its declaration, and LLVM gives each copy of the declaration + * its own scopes when it unrolls or duplicates the enclosing code, so the + * no-alias facts of one entry never apply to the accesses of another. */ +function declareAliasScopes(emitter: LlEmitter, scopes: readonly string[]): void { + if (scopes.length === 0) return; + emitter.declare("declare void @llvm.experimental.noalias.scope.decl(metadata)"); + for (const scope of scopes) + emitter.B.line(`call void @llvm.experimental.noalias.scope.decl(metadata !{${scope}})`); } /** Emit a loop's condition, body and update through `emitCopy`, twice @@ -2142,15 +2156,14 @@ function emitVersionedLoop( if (version.ranges.size > 0) emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); // Both fast copies run under the guard's span test, so they share the - // version's scopes. - if (version.scopes && emitter.debug === null) - emitter.bytesAliasScopes = new Map([ - ...originalScopes, - ...bytesAliasScopes(emitter, version.scopes), - ]); + // version's scopes, and each declares them on entry. + const scoped = + version.scopes && emitter.debug === null ? bytesAliasScopes(emitter, version.scopes) : null; + if (scoped) emitter.bytesAliasScopes = new Map([...originalScopes, ...scoped.attachments]); if (version.counted?.guarded || version.paired?.loop.guarded) LOOPS.of(emitter).provenCountedGuards.add(s); B.startBlock(fast); + declareAliasScopes(emitter, scoped?.scopes ?? []); emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); // Both fast copies read the string's length the guard took. const length = guard?.length; @@ -2164,6 +2177,7 @@ function emitVersionedLoop( if (unitsFast !== undefined && units !== null) { // The string reads take the receiver's UTF-16 units. B.startBlock(unitsFast); + declareAliasScopes(emitter, scoped?.scopes ?? []); emitter.bytesBounds = new Set([ ...originalBounds, ...[...version.bounds].filter((node) => !version.stringReads.has(node)), diff --git a/packages/compiler/test/typed-array-alias-scope-emission.test.ts b/packages/compiler/test/typed-array-alias-scope-emission.test.ts index a32df03e7..c94b58d59 100644 --- a/packages/compiler/test/typed-array-alias-scope-emission.test.ts +++ b/packages/compiler/test/typed-array-alias-scope-emission.test.ts @@ -46,6 +46,13 @@ console.log(dot(a, b, 8)); /load double, ptr %\w+, align 1[^\n]*!alias\.scope !\{!\{!"copy\.0\.\d", !\{!"scriptc typed-array views"\}\}\}, !noalias/, ); expect(copy).toMatch(/store double %\w+, ptr %\w+, align 1[^\n]*!alias\.scope/); + // The fast copy declares both scopes where it starts, so they hold for + // this entry only, and declares nothing else. + for (const receiver of [0, 1]) + expect(copy).toContain( + `call void @llvm.experimental.noalias.scope.decl(metadata !{!{!"copy.0.${receiver}", !{!"scriptc typed-array views"}}})`, + ); + expect(copy.match(/noalias\.scope\.decl/g)).toHaveLength(2); // One view, or views that are only read, need no scopes. for (const name of ["scale", "dot"]) { expect(fn(name)).not.toContain("!alias.scope"); diff --git a/tests/corpus/8182-typed-array-alias-scopes.ts b/tests/corpus/8182-typed-array-alias-scopes.ts index 4dbf52e47..f56face07 100644 --- a/tests/corpus/8182-typed-array-alias-scopes.ts +++ b/tests/corpus/8182-typed-array-alias-scopes.ts @@ -1,7 +1,9 @@ // Loops that read one typed array and write another, where the two views // may share bytes: overlapping and disjoint views of one buffer, the same // array passed twice, views of different element types over one buffer, -// and empty views. A loop over views that overlap must see its own writes. +// and empty views, and passes whose views an enclosing loop swaps. A loop +// over views that overlap must see its own writes, and each pass the +// previous pass's writes. function shiftAdd(dst: Float64Array, src: Float64Array, n: number): void { for (let i = 0; i < n; i++) dst[i] = src[i]! + 1; } @@ -87,3 +89,46 @@ console.log( "lanes overlapping", lanes(pool.subarray(0, 4), pool.subarray(2, 6), pool.subarray(8, 12), pool.subarray(9, 13)).toFixed(6), ); + +// Ping-pong passes: an enclosing loop swaps the two views after every pass, +// so each pass reads what the previous one wrote. +function pingPongPairs(a: Float64Array, b: Float64Array, steps: number): string { + let cur = a; + let next = b; + for (let s = 0; s < steps; s++) { + for (let i = 0; i < 2; i++) next[i] = cur[i]! + cur[1 - i]! * 2 + s; + const t = cur; + cur = next; + next = t; + } + return Array.from(cur).join(","); +} +function pingPongWords(a: Int32Array, b: Int32Array): string { + let cur = a; + let next = b; + for (let s = 0; s < 4; s++) { + for (let i = 0; i < 4; i++) next[i] = (cur[i]! * 3 + cur[(i + 1) & 3]! + 1) | 0; + const t = cur; + cur = next; + next = t; + } + return Array.from(cur).join(","); +} +function pingPongRows(a: Float64Array, b: Float64Array, steps: number): number { + let cur = a; + let next = b; + for (let s = 0; s < steps; s++) { + for (let i = 0; i < cur.length; i++) next[i] = cur[i]! * 0.5 + 1; + const t = cur; + cur = next; + next = t; + } + let sum = 0; + for (let i = 0; i < cur.length; i++) sum += cur[i]!; + return sum; +} +for (let k = 0; k < 3; k++) { + console.log("ping-pong pairs", pingPongPairs(new Float64Array([1, 2]), new Float64Array(2), 3 + k)); + console.log("ping-pong words", pingPongWords(new Int32Array([1, 2, 3, 4]), new Int32Array(4))); +} +console.log("ping-pong rows", pingPongRows(new Float64Array(37).fill(8), new Float64Array(37), 9)); From d6c8b0f260e3d18829f67ce0c0808a68bf66a549 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 21:52:18 -0500 Subject: [PATCH 155/273] Close the afterWrite batch whenever an afterWrite tick runs Node's afterWriteTick sets afterWriteTickInfo to null before it runs the callbacks, whichever tick it is. The runtime cleared the queued batch only when the running tick was that batch, so a write made from an earlier tick's callback joined a later queued tick with the same callback and completed ahead of nextTicks queued between them. Any afterWrite tick now closes the batch, and such a write queues its own. --- packages/runtime/src/scr_stream.c | 4 ++- tests/corpus/7514-stream-after-write-order.ts | 28 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index cc0b277f3..406d91599 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -7743,7 +7743,9 @@ static void scr_stream_run_tick(ScrStreamTick *t) { case SCR_ST_AFTER_WRITE: { st->w.after_scheduled = false; if (t->hold) st->w.after_holds--; - if (st->w.after_tick == t) st->w.after_tick = NULL; + /* afterWriteTick: the batch closes when any afterWrite tick runs, so + * a write its callback makes queues a new tick */ + st->w.after_tick = NULL; ScrClosure *cb = t->cb; t->cb = NULL; if (!st->destroyed) { diff --git a/tests/corpus/7514-stream-after-write-order.ts b/tests/corpus/7514-stream-after-write-order.ts index 5647f5bc8..1c14de6eb 100644 --- a/tests/corpus/7514-stream-after-write-order.ts +++ b/tests/corpus/7514-stream-after-write-order.ts @@ -2,7 +2,9 @@ // writes with the same callback complete in one afterWrite step, ahead of // a process.nextTick queued between them, and a write with another // callback (or none) starts the next step; end's callback and 'finish' -// follow. A PassThrough read by 'data' runs each write's own callback. +// follow. A write made from a write callback starts a new afterWrite step +// even when its callback matches a later queued one. A PassThrough read by +// 'data' runs each write's own callback. import { PassThrough, Writable } from "node:stream"; const log: string[] = []; @@ -38,6 +40,29 @@ function run(): Promise { }); } +function nested(): Promise { + return new Promise((resolve) => { + const w = new Writable({ + write(chunk: Buffer, _enc, cb) { + log.push(`nested write ${String(chunk)}`); + cb(); + }, + }); + let laterCalls = 0; + const later = (): void => { + log.push("later callback"); + if (++laterCalls === 2) resolve(); + }; + w.write("a", () => { + log.push("first callback"); + w.write("from first", later); + }); + process.nextTick(() => log.push("nested tick 1")); + w.write("b", later); + process.nextTick(() => log.push("nested tick 2")); + }); +} + function passThrough(): Promise { return new Promise((resolve) => { const pt = new PassThrough({ highWaterMark: 8 }); @@ -69,6 +94,7 @@ function passThrough(): Promise { async function main(): Promise { await run(); + await nested(); await passThrough(); console.log(log.join("\n")); } From 4241ca533f86e2c9bb7df0854f610fbd2f4dc1d6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 21:59:28 -0500 Subject: [PATCH 156/273] Read elements of native arrays passed to untyped code by index A typed array such as a number[] reaches untyped code as a capsule of the native array. A keyed read `xs[i]` there missed the inline cache, which answers only dyn arrays, and went to the member helper, which converted the index to a string; the capsule's key lookup then parsed the string back into a number for every element. Keyed sites now read an element of a native number[] capsule in place when the index is an integer below the array's hole-free prefix, and `length` sites read the native array's length. Other capsule elements (holes, out-of-range and fractional indices, arrays of other element types) are answered by the keyed probe through scr_dyn_index_get without forming the key, or by the helper as before. --- packages/compiler/src/backend/llvm/js-ic.ts | 128 ++++++++++++++++-- packages/compiler/test/js-ic-emission.test.ts | 41 ++++++ .../main.ts.json | 7 + packages/runtime/src/scr_dyn.h | 4 +- packages/runtime/src/scr_js_object.c | 3 + .../8300-native-array-capsule-reads/lib.cjs | 36 +++++ .../8300-native-array-capsule-reads/main.ts | 59 ++++++++ .../package.json | 1 + .../main.ts.json | 1 + 9 files changed, 268 insertions(+), 12 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8300-native-array-capsule-reads/main.ts.json create mode 100644 tests/corpus/8300-native-array-capsule-reads/lib.cjs create mode 100644 tests/corpus/8300-native-array-capsule-reads/main.ts create mode 100644 tests/corpus/8300-native-array-capsule-reads/package.json create mode 100644 tests/harness/corpus-costs/8300-native-array-capsule-reads/main.ts.json diff --git a/packages/compiler/src/backend/llvm/js-ic.ts b/packages/compiler/src/backend/llvm/js-ic.ts index 7516673cd..b8be1ecb1 100644 --- a/packages/compiler/src/backend/llvm/js-ic.ts +++ b/packages/compiler/src/backend/llvm/js-ic.ts @@ -384,6 +384,15 @@ function emitGet(host: Host, e: ExprOf<"call">, callee: IrFunction, key: string) incoming.push(`[ ${numberResult(host, count, e)}, %${B.currentLabel()} ]`); B.br(join); B.startBlock(other); + // So is the length of a native array passed in from typed code. + const rest = B.newLabel("icg.rest"); + const arr = nativeArray(host, receiver.name, rest); + const native = load(host, "i32", arr, 8); + const nativeCount = B.tmp(); + B.line(`${nativeCount} = uitofp i32 ${native} to double`); + incoming.push(`[ ${numberResult(host, nativeCount, e)}, %${B.currentLabel()} ]`); + B.br(join); + B.startBlock(rest); } // Inline hits (borrowed values) and probe hits (owned ones) share one // conversion to the result type. @@ -623,6 +632,98 @@ function numberKey(host: Host, key: IrExpr): NumberKey | null { }; } +/** Branches to `yes` when `d` is a capsule of a native array passed in + * from typed code (a TYPED_REF bound to live array ops, not a record or + * class instance), with the ScrArr in `yes`; else to `no`. */ +function nativeArray(host: Host, d: string, no: string): string { + const B = host.B; + const capsule = B.newLabel("ica.cap"); + const bound = B.newLabel("ica.ops"); + const yes = B.newLabel("ica.arr"); + kindIs(host, d, DYN_KIND.TYPED_REF, capsule, no); + B.startBlock(capsule); + // The flag word after `casts`: record, klass and bag_class are its top + // three bits; any of them means `array` holds other ops. + const flags = load(host, "i32", d, 88); + const kinds = B.tmp(); + const plain = B.tmp(); + B.line(`${kinds} = lshr i32 ${flags}, 29`); + B.line(`${plain} = icmp eq i32 ${kinds}, 0`); + B.condBr(plain, bound, no); + B.startBlock(bound); + const ops = load(host, "ptr", d, 64); + const live = B.tmp(); + B.line(`${live} = icmp ne ptr ${ops}, null`); + B.condBr(live, yes, no); + B.startBlock(yes); + return load(host, "ptr", d, 16); +} + +/** `xs[i]` on a native number[] passed in from typed code: an integral + * index below the array's hole-free prefix reads the element in place. + * The other cases branch to `miss`, whose probe and helper answer them as + * before. The result joins `incoming` at `join`. */ +function emitNativeNumberElement( + host: Host, + receiver: string, + e: IrExpr, + indexIn: (next: string) => string, + miss: string, + join: string, + incoming: string[], +): void { + const B = host.B; + const arr = nativeArray(host, receiver, miss); + const numbers = B.newLabel("ica.f64"); + const elem = load(host, "i8", arr, 16); + const f64 = B.tmp(); + B.line(`${f64} = icmp eq i8 ${elem}, 0 ; SCR_ELEM_F64`); + B.condBr(f64, numbers, miss); + B.startBlock(numbers); + const index = indexIn(B.newLabel("ica.idx")); + const filled = load(host, "i32", arr, 20); + const filledD = B.tmp(); + const nonNegative = B.tmp(); + const below = B.tmp(); + const ok = B.tmp(); + B.line(`${filledD} = uitofp i32 ${filled} to double`); + B.line(`${nonNegative} = fcmp oge double ${index}, 0.0`); + B.line(`${below} = fcmp olt double ${index}, ${filledD}`); + B.line(`${ok} = and i1 ${nonNegative}, ${below}`); + const ranged = B.newLabel("ica.rng"); + B.condBr(ok, ranged, miss); + B.startBlock(ranged); + const i = B.tmp(); + const back = B.tmp(); + const integral = B.tmp(); + B.line(`${i} = fptoui double ${index} to i64`); + B.line(`${back} = uitofp i64 ${i} to double`); + B.line(`${integral} = fcmp oeq double ${back}, ${index}`); + const hit = B.newLabel("ica.hit"); + B.condBr(integral, hit, miss); + B.startBlock(hit); + const data = load(host, "ptr", arr, 24); + const slot = B.tmp(); + const value = B.tmp(); + B.line(`${slot} = getelementptr inbounds double, ptr ${data}, i64 ${i}`); + B.line(`${value} = load double, ptr ${slot}`); + let out: string; + if (e.type.kind === "jsval") { + const raw = B.tmp(); + const nan = B.tmp(); + const bits = B.tmp(); + out = B.tmp(); + B.line(`${raw} = bitcast double ${value} to i64`); + B.line(`${nan} = fcmp uno double ${value}, ${value}`); + B.line(`${bits} = select i1 ${nan}, i64 ${JS_BITS.canonicalNan}, i64 ${raw}`); + B.line(`${out} = inttoptr i64 ${bits} to ptr`); + } else { + out = numberResult(host, value, e); + } + incoming.push(`[ ${out}, %${B.currentLabel()} ]`); + B.br(join); +} + function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue { const B = host.B; const receiver = emitBorrowedInput(host, e.args[0]!); @@ -645,18 +746,23 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue { B.line(`store ptr null, ptr ${keySlot}`); host.ownSlot(keySlot, DYN); } - kindIs(host, receiver.name, DYN_KIND.ARR, array, probe); + /** The key as a double in block `next`; other keys go to the probe. */ + const indexIn = (next: string): string => { + if (numeric === null) { + kindIs(host, key!.name, DYN_KIND.NUM, next, probe); + B.startBlock(next); + return load(host, "double", key!.name, 16); + } + B.condBr(numeric.present, next, probe); + B.startBlock(next); + return numeric.value; + }; + const other = B.newLabel("ick.other"); + kindIs(host, receiver.name, DYN_KIND.ARR, array, other); + B.startBlock(other); + emitNativeNumberElement(host, receiver.name, e, indexIn, probe, join, incoming); B.startBlock(array); - let index: string; - if (numeric === null) { - kindIs(host, key!.name, DYN_KIND.NUM, number, probe); - B.startBlock(number); - index = load(host, "double", key!.name, 16); - } else { - B.condBr(numeric.present, number, probe); - B.startBlock(number); - index = numeric.value; - } + const index = indexIn(number); const len = load(host, "i64", receiver.name, 16); const i = B.tmp(); const back = B.tmp(); diff --git a/packages/compiler/test/js-ic-emission.test.ts b/packages/compiler/test/js-ic-emission.test.ts index 54ea13d26..7a1d44f34 100644 --- a/packages/compiler/test/js-ic-emission.test.ts +++ b/packages/compiler/test/js-ic-emission.test.ts @@ -134,3 +134,44 @@ console.log(len2({ x: 3, y: 4 }), typeof parsed); await rm(dir, { recursive: true, force: true }); } }); + +test("keyed and length sites read native number arrays in place", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-native-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(join(dir, "package.json"), `{ "type": "module" }`); + await writeFile( + join(dir, "lib.cjs"), + `exports.total = function (xs) { + let s = 0; + for (let i = 0; i < xs.length; i++) s += xs[i]; + return s; +}; +`, + ); + await writeFile( + entry, + `import { createRequire } from "node:module"; +const require = createRequire(import.meta.url); +const lib: { total(xs: number[]): number } = require("./lib.cjs"); +console.log(lib.total([1, 2, 3])); +`, + ); + 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 llvm = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const fn = llvm.split("\ndefine ").find((f) => f.includes("ica.f64")) ?? ""; + // The keyed read tests the capsule's element kind and loads the double + // in place; other receivers keep the probe. + expect(fn).toMatch(/icmp eq i8 %t\d+, 0 ; SCR_ELEM_F64/); + expect(fn).toMatch(/load double, ptr %t\d+/); + expect(fn).toContain("@scr_js_keyed_probe("); + // The length read answers from the native array's length. + expect(fn).toContain("icg.rest"); + expect(fn).toContain("@scr_js_load_probe("); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8300-native-array-capsule-reads/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8300-native-array-capsule-reads/main.ts.json new file mode 100644 index 000000000..fb6e3a0bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8300-native-array-capsule-reads/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/8300-native-array-capsule-reads/lib.cjs", + "/tests/corpus/8300-native-array-capsule-reads/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 6270c8cdc..2f9d7eb82 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -523,7 +523,9 @@ struct ScrDyn { * identity while the source reference remains observable. */ ScrDyn *materialized; ScrDynTypedCast *casts; - /* One word keeps this, the largest payload, at its previous size. */ + /* One word keeps this, the largest payload, at its previous size. + * Dynamic-tier inline caches read record, klass and bag_class as + * the word's top three bits (backend/llvm/js-ic.ts nativeArray). */ uint32_t type_key_len : 27; uint32_t traced : 1; /* native referent has a cycle header */ uint32_t observed : 1; /* native release reports this identity's disposal */ diff --git a/packages/runtime/src/scr_js_object.c b/packages/runtime/src/scr_js_object.c index 4b6e3141e..e09226fd1 100644 --- a/packages/runtime/src/scr_js_object.c +++ b/packages/runtime/src/scr_js_object.c @@ -355,6 +355,9 @@ bool scr_js_store_probe(ScrDyn *receiver, const ScrStr *key, ScrDyn *value, ScrJ /* ── keyed loads ──────────────────────────────────────────────────────── */ ScrDyn *scr_js_index_probe(ScrDyn *receiver, double index) { + /* A native array passed in from typed code answers its elements in + * place, without forming the index's key string. */ + if (receiver->kind == SCR_DYN_TYPED_REF) return scr_dyn_index_get(receiver, index); if (receiver->kind != SCR_DYN_ARR || !(index >= 0) || index >= (double)receiver->v.arr.len) return NULL; size_t i = (size_t)index; diff --git a/tests/corpus/8300-native-array-capsule-reads/lib.cjs b/tests/corpus/8300-native-array-capsule-reads/lib.cjs new file mode 100644 index 000000000..6f386216b --- /dev/null +++ b/tests/corpus/8300-native-array-capsule-reads/lib.cjs @@ -0,0 +1,36 @@ +"use strict"; +// Reads and writes the arrays the typed program passes in. +exports.total = function (xs) { + let s = 0; + for (let i = 0; i < xs.length; i++) s += xs[i]; + return s; +}; +exports.at = function (xs, i) { + return xs[i]; +}; +exports.describe = function (xs, keys) { + const out = []; + for (const k of keys) { + const v = xs[k]; + out.push(typeof v === "number" && Object.is(v, -0) ? "-0" : String(v)); + } + return out.join(" "); +}; +exports.has = function (xs, i) { + return i in xs; +}; +exports.write = function (xs, i, v) { + xs[i] = v; + return xs[i]; +}; +exports.grow = function (xs, v) { + xs.push(v); + return xs.length; +}; +exports.cut = function (xs, n) { + xs.length = n; + return xs.length; +}; +exports.lengthOf = function (xs) { + return xs.length; +}; diff --git a/tests/corpus/8300-native-array-capsule-reads/main.ts b/tests/corpus/8300-native-array-capsule-reads/main.ts new file mode 100644 index 000000000..5b1513ee4 --- /dev/null +++ b/tests/corpus/8300-native-array-capsule-reads/main.ts @@ -0,0 +1,59 @@ +// A typed program passes its arrays to untyped CommonJS functions, which +// read their elements and lengths, write them and resize them. Element reads +// cover integral, negative, fractional, non-finite and out-of-range indexes, +// string keys, holes, frozen arrays and arrays of other element types. +import { createRequire } from "node:module"; + +interface Lib { + total(xs: readonly unknown[]): number; + at(xs: readonly unknown[], i: unknown): unknown; + describe(xs: readonly unknown[], keys: unknown[]): string; + has(xs: readonly unknown[], i: number): boolean; + write(xs: unknown[], i: number, v: unknown): unknown; + grow(xs: unknown[], v: unknown): number; + cut(xs: unknown[], n: number): number; + lengthOf(xs: readonly unknown[]): number; +} +const require = createRequire(import.meta.url); +const lib: Lib = require("./lib.cjs"); + +// Element reads of a number[] at integral, negative, fractional, +// non-finite and out-of-range indexes, and through string keys. +const nums: number[] = [1, 2.5, -0, NaN, Infinity, -7]; +console.log(lib.total([1, 2, 3, 4]), lib.total(nums), lib.total([])); +console.log(lib.describe(nums, [0, 1, 2, 3, 4, 5, 6, -1, 1.5, -0, NaN, Infinity, 2 ** 32, "1", "01", "length"])); +console.log(lib.at(nums, 5), lib.at(nums, 6), typeof lib.at(nums, 2), Object.is(lib.at(nums, 2), -0)); + +// Holes: below and past the hole-free prefix, and an array cut longer. +const holey: number[] = [1, 2]; +holey[4] = 5; +console.log(lib.describe(holey, [0, 1, 2, 3, 4, 5]), lib.has(holey, 3), lib.has(holey, 4), lib.lengthOf(holey)); +const longer: number[] = [3, 4, 5]; +longer.length = 6; +console.log(lib.describe(longer, [0, 2, 3, 5]), lib.lengthOf(longer)); + +// Writes from the package and from the program are seen by both. +const live: number[] = [10, 20, 30]; +console.log(lib.write(live, 0, 11), lib.write(live, 3, 40), lib.grow(live, 50), live.join(",")); +live.push(60); +live[1] = 21; +console.log(lib.total(live), lib.lengthOf(live), lib.at(live, 5)); +console.log(lib.cut(live, 2), lib.total(live), live.length, lib.at(live, 2)); + +// Frozen and empty arrays. +const frozen: readonly number[] = Object.freeze([4, 5, 6]); +console.log(lib.total(frozen), lib.at(frozen, 1), lib.at([], 0), lib.lengthOf([])); + +// Arrays of other element types read through the same sites. +const words: string[] = ["a", "b", "c"]; +const flags: boolean[] = [true, false]; +const points: { x: number }[] = [{ x: 1 }, { x: 2 }]; +console.log(lib.describe(words, [0, 2, 3]), lib.describe(flags, [0, 1, 2]), (lib.at(points, 1) as { x: number }).x); +console.log(lib.at(words, 1.5), lib.has(words, 2), lib.lengthOf(points)); + +// A loop over a larger array. +const big: number[] = []; +for (let i = 0; i < 1000; i++) big.push(i % 7); +let sum = 0; +for (let round = 0; round < 50; round++) sum += lib.total(big); +console.log(sum); diff --git a/tests/corpus/8300-native-array-capsule-reads/package.json b/tests/corpus/8300-native-array-capsule-reads/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/8300-native-array-capsule-reads/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/harness/corpus-costs/8300-native-array-capsule-reads/main.ts.json b/tests/harness/corpus-costs/8300-native-array-capsule-reads/main.ts.json new file mode 100644 index 000000000..a37fc12d7 --- /dev/null +++ b/tests/harness/corpus-costs/8300-native-array-capsule-reads/main.ts.json @@ -0,0 +1 @@ +{ "plain": 4860, "san": 7734 } From d1432eeb187e2cf269cd06ddddb9374813724aaa Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 24 Oct 2026 13:14:39 -0700 Subject: [PATCH 157/273] Compile namespace-style npm packages: guarded declarations and var exports Two refusals kept pre-module JavaScript packages from compiling: - `var ns = ns || {}` (and `?? {}`): the read in the variable's own initializer made the checker type the namespace `any`, and requirers then failed to reach the binding (SC1030). In a module, the read sees the hoisted `undefined` whenever nothing else writes the variable, so a new package-source normalization respells the declaration as its default (`var ns = ({})`) when no other declaration, assignment, update, function, class, parameter or local of that name exists in the file and no earlier top-level statement mentions it. - `module.exports = { ns }` with a `var` binding was refused as exporting a mutable binding by reference. A `var` (or `let`) whose initialized declaration precedes the export statement at top level and that nothing reassigns exports exactly the value Node copies, so it now aliases like a const. The namespace benchmark (results/design-shape-inference/nsbench) now compiles and matches Node; the new npm case legacy-namespaces pins a namespace library with guarded declarations, constructor functions with prototype methods, enums, a revealing module and a handler registry. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-stmts.ts | 20 ++- .../npm-static-namespace-defaults.test.ts | 63 ++++++++ .../frontend/npm-static-namespace-defaults.ts | 138 ++++++++++++++++++ packages/compiler/src/frontend/npm-static.ts | 5 + packages/compiler/src/frontend/services.ts | 11 ++ .../npm/cases/legacy-namespaces/main.ts.json | 6 + .../npm/cases/legacy-namespaces/main.ts | 26 ++++ .../node_modules/legacy-ns/index.d.ts | 2 + .../node_modules/legacy-ns/index.js | 97 ++++++++++++ .../node_modules/legacy-ns/package.json | 1 + .../npm/cases/legacy-namespaces/package.json | 1 + 11 files changed, 368 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/src/frontend/npm-static-namespace-defaults.test.ts create mode 100644 packages/compiler/src/frontend/npm-static-namespace-defaults.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/legacy-namespaces/main.ts.json create mode 100644 tests/fixtures/npm/cases/legacy-namespaces/main.ts create mode 100644 tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/index.d.ts create mode 100644 tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/index.js create mode 100644 tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/package.json create mode 100644 tests/fixtures/npm/cases/legacy-namespaces/package.json diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index c50676863..851cae4c9 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -7445,7 +7445,7 @@ function lowerCjsExportStatement( const fenceMutable = (nameNode: ts.Identifier): void => { const sym = lowerer.resolveValueSymbol(nameNode); const g = sym ? lowerer.globalsBySymbol.get(sym) : undefined; - if (g?.mutable) { + if (g?.mutable && !(sym && settledBeforeExport(lowerer, sym, cjs.expr))) { lowerer.unsupported( "SC1090", nameNode, @@ -7605,7 +7605,7 @@ function lowerCjsSingleValueExport( } const g = lowerer.globalsBySymbol.get(sym); if (g) { - if (g.mutable) { + if (g.mutable && !settledBeforeExport(lowerer, sym, rhs)) { lowerer.unsupported( "SC1090", rhs, @@ -12707,3 +12707,19 @@ function isAssertionInitializer(node: ts.Expression): boolean { while (ts.isParenthesizedExpression(e)) e = e.expression; return ts.isAsExpression(e) || ts.isTypeAssertion(e); } + +/** A `var` (or `let`) whose initialized declaration precedes the export + * statement in the module's top-level statements and that nothing ever + * reassigns: exporting it by reference reads the value Node copies. The + * legacy `var ns = ns || {}; …; module.exports = { ns }` shape. */ +function settledBeforeExport(lowerer: Lowerer, sym: ts.Symbol, exportNode: ts.Node): boolean { + const decl = sym.valueDeclaration; + if (!decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return false; + const stmt = decl.parent?.parent; + if (!stmt || !ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent)) return false; + let exportStmt: ts.Node = exportNode; + while (exportStmt.parent !== undefined && !ts.isSourceFile(exportStmt.parent)) + exportStmt = exportStmt.parent; + if (exportStmt.parent !== stmt.parent || stmt.end > exportStmt.pos) return false; + return bindingNeverReassigned(lowerer, sym, decl); +} diff --git a/packages/compiler/src/frontend/npm-static-namespace-defaults.test.ts b/packages/compiler/src/frontend/npm-static-namespace-defaults.test.ts new file mode 100644 index 000000000..93a776f7e --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-namespace-defaults.test.ts @@ -0,0 +1,63 @@ +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 rewrite(source: string): string | null { + return nodeFrontendServices().namespaceDefaults(source, "/pkg/index.js"); +} + +/** Node prints the same for the original and the rewritten module. */ +function expectSameOutput(source: string): void { + const result = rewrite(source); + expect(typeof result).toBe("string"); + const dir = mkdtempSync(join(tmpdir(), "scriptc-namespace-defaults-")); + dirs.push(dir); + const outputs = [source, result!].map((text, i) => { + const file = join(dir, `m${i}.cjs`); + writeFileSync(file, text); + const r = spawnSync(process.execPath, [file], { encoding: "utf8" }); + expect(r.status).toBe(0); + return r.stdout; + }); + expect(outputs[1]).toBe(outputs[0]); +} + +test("a guarded namespace declaration becomes its default", () => { + const source = `var app = app || {}; +app.util = app.util || {}; +app.util.twice = function (x) { return 2 * x; }; +var cfg = cfg ?? { depth: 1 }; +console.log(app.util.twice(3), cfg.depth, Object.keys(app).join(",")); +`; + expect(rewrite(source)).toBe(`var app = {}; +app.util = app.util || {}; +app.util.twice = function (x) { return 2 * x; }; +var cfg = { depth: 1 }; +console.log(app.util.twice(3), cfg.depth, Object.keys(app).join(",")); +`); + expectSameOutput(source); +}); + +test("any other write, declaration or earlier mention keeps the text", () => { + // Redeclared: the second declaration reads the first one's object. + expect(rewrite("var ns = ns || {};\nvar ns = ns || {};\n")).toBeNull(); + // Assigned elsewhere, updated, or declared as a function. + expect(rewrite("var ns = ns || {};\nfunction reset() { ns = {}; }\n")).toBeNull(); + expect(rewrite("var n = n || 0;\nn++;\n")).toBeNull(); + // A local of the same name somewhere in the file. + expect(rewrite("var ns = ns || {};\nfunction f(ns) { return ns; }\n")).toBeNull(); + // Mentioned by an earlier statement. + expect(rewrite("console.log(typeof ns);\nvar ns = ns || {};\n")).toBeNull(); + // let and const read their own binding in its temporal dead zone. + expect(rewrite("let ns = ns || {};\n")).toBeNull(); + // Not the declared name on the left. + expect(rewrite("var ns = other || {};\n")).toBeNull(); +}); diff --git a/packages/compiler/src/frontend/npm-static-namespace-defaults.ts b/packages/compiler/src/frontend/npm-static-namespace-defaults.ts new file mode 100644 index 000000000..2e4bf046c --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-namespace-defaults.ts @@ -0,0 +1,138 @@ +/* The guarded namespace declaration of pre-module JavaScript: + * + * var ns = ns || {}; + * + * In a module (CommonJS or ES), `var` is module-local and hoisted, so the + * read in its own initializer sees `undefined` unless something writes the + * variable first. When nothing else in the file ever writes it (no other + * declaration, assignment or update of that name anywhere, no function or + * class of that name) and no earlier top-level statement mentions it, the + * initializer is its right operand: `var ns = {};`. Inference then types + * the namespace from that literal and the members later statements add, + * instead of `any` (the self-reference makes the checker give up). `??` + * is the same. Anything else keeps the original text. */ +import * as ts from "./ts7/syntax.js"; +import { tokenStartOfNode } from "./ts7/node-functions.js"; +import { applyEdits, unparen, type Edit } from "./npm-static-bundled-chunks.js"; + +/** A cheap probe: some `var X = X ||` or `?? ` may be present. */ +export function mayHoldNamespaceDefault(source: string): boolean { + return /\bvar\s+([A-Za-z_$][\w$]*)\s*=\s*\(?\s*\1\s*(\|\||\?\?)/.test(source); +} + +/** The module text with each guarded namespace declaration respelled as a + * plain one, or null when there is none. */ +export function rewriteNamespaceDefaults(sf: ts.SourceFile): string | null { + const stmts = sf.statements.nodes; + const edits: Edit[] = []; + for (let i = 0; i < stmts.length; i++) { + const stmt = stmts[i]!; + if (!ts.isVariableStatement(stmt)) continue; + const list = stmt.declarationList; + if ((list.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0) continue; + for (const decl of list.declarations.nodes) { + if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) continue; + const name = decl.name.text; + const init = unparen(decl.initializer); + if ( + !ts.isBinaryExpression(init) || + (init.operatorToken.kind !== ts.SyntaxKind.BarBarToken && + init.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionToken) + ) + continue; + const left = unparen(init.left); + if (!ts.isIdentifier(left) || left.text !== name) continue; + if (mentionedBefore(stmts, i, name) || writtenElsewhere(sf, name, decl)) continue; + const right = init.right; + edits.push({ + start: tokenStartOfNode(decl.initializer), + end: decl.initializer.end, + // A literal stays bare: inference types `var ns = {}` from the + // members later statements add (a parenthesized one it does not). + text: + ts.isObjectLiteralExpression(unparen(right)) || + ts.isArrayLiteralExpression(unparen(right)) + ? sf.text.slice(tokenStartOfNode(right), right.end) + : `(${sf.text.slice(tokenStartOfNode(right), right.end)})`, + }); + } + } + if (edits.length === 0) return null; + return applyEdits(sf.text, 0, sf.text.length, edits); +} + +/** Whether a top-level statement before index `at` mentions `name`. */ +function mentionedBefore(stmts: readonly ts.Statement[], at: number, name: string): boolean { + for (let i = 0; i < at; i++) { + let found = false; + ts.walkPreorder(stmts[i]!, (n) => { + if (found) return "stop"; + if (ts.isIdentifier(n) && n.text === name) found = true; + return undefined; + }); + if (found) return true; + } + return false; +} + +/** Whether anything other than `decl` declares or writes `name`. */ +function writtenElsewhere(sf: ts.SourceFile, name: string, decl: ts.VariableDeclaration): boolean { + let written = false; + ts.walkPreorder(sf, (n) => { + if (written) return "stop"; + if (n === decl.name) return undefined; + if (ts.isBinaryExpression(n) && isAssignmentOperator(n.operatorToken.kind)) { + if (isName(n.left, name)) written = true; + } else if ( + (ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) && + (n.operator === ts.SyntaxKind.PlusPlusToken || + n.operator === ts.SyntaxKind.MinusMinusToken) && + isName(n.operand, name) + ) { + written = true; + } else if ( + (ts.isFunctionDeclaration(n) || ts.isClassDeclaration(n)) && + n.name !== undefined && + n.name.text === name + ) { + written = true; + } else if ( + (ts.isVariableDeclaration(n) || ts.isParameter(n) || ts.isBindingElement(n)) && + n !== decl && + n.name !== undefined && + bindsName(n.name, name) + ) { + // Another declaration of the name (a redeclaration, or a local that + // shadows it) — keep the original text rather than reason about it. + written = true; + } else if ( + (ts.isForInStatement(n) || ts.isForOfStatement(n)) && + !ts.isVariableDeclarationList(n.initializer) && + isName(n.initializer, name) + ) { + written = true; + } + return undefined; + }); + return written; +} + +function bindsName(binding: ts.Node, name: string): boolean { + if (ts.isIdentifier(binding)) return binding.text === name; + let found = false; + ts.walkPreorder(binding, (n) => { + if (found) return "stop"; + if (ts.isIdentifier(n) && n.text === name) found = true; + return undefined; + }); + return found; +} + +function isName(node: ts.Node, name: string): boolean { + const n = unparen(node as ts.Expression); + return ts.isIdentifier(n) && n.text === name; +} + +function isAssignmentOperator(kind: ts.SyntaxKind): boolean { + return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment; +} diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index b00e14aa0..f51c6bbe2 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -505,6 +505,10 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow // literals they build. const enums = original === null ? null : services.enumIifes(original, path); if (enums !== null) original = enums; + // Guarded namespace declarations (`var ns = ns || {}`) read the + // hoisted `undefined`: the declaration is its default. + const namespaces = original === null ? null : services.namespaceDefaults(original, path); + if (namespaces !== null) original = namespaces; // A module that names its export object through a variable reaches // its members through `exports`. const exportsVar = original === null ? null : services.cjsExportsVar(original, path); @@ -517,6 +521,7 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow aliased !== null || fnExports !== null || enums !== null || + namespaces !== null || exportsVar !== null; if (original !== null) { const namepaths = services.jsDocNamepaths(path, original); diff --git a/packages/compiler/src/frontend/services.ts b/packages/compiler/src/frontend/services.ts index 14b366bfa..11fa0de72 100644 --- a/packages/compiler/src/frontend/services.ts +++ b/packages/compiler/src/frontend/services.ts @@ -14,6 +14,10 @@ import { } from "./npm-static-builtin-aliases.js"; import { mayHoldUmdWrapper, unwrapUmdFactory } from "./npm-static-umd.js"; import { mayHoldEnumIife, rewriteEnumIifes } from "./npm-static-enum-iife.js"; +import { + mayHoldNamespaceDefault, + rewriteNamespaceDefaults, +} from "./npm-static-namespace-defaults.js"; import { mayHoldTscBuiltinImport, unwrapTscBuiltinImports } from "./npm-static-tsc-helpers.js"; import { builtinPackageAliasFor } from "./package-shims.js"; import * as syntax from "./npm-static-declaration-syntax.js"; @@ -132,6 +136,13 @@ export class FrontendServices { if (!mayHoldEnumIife(source)) return null; return rewriteEnumIifes(this.parse(path, source, "js")); } + /** A package module with its guarded namespace declarations + * (`var ns = ns || {}`) respelled as plain ones (null: none). */ + namespaceDefaults(source: string, path: string): string | null { + this.ensureOpen(); + if (!mayHoldNamespaceDefault(source)) return null; + return rewriteNamespaceDefaults(this.parse(path, source, "js")); + } /** A package module with its requires of builtin-aliased polyfill * packages respelled as the builtins (null: none). */ builtinAliasRequires(source: string, path: string): string | null { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/legacy-namespaces/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/legacy-namespaces/main.ts.json new file mode 100644 index 000000000..7f50545d6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/legacy-namespaces/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/legacy-namespaces/main.ts" + ], + "diags": [] +} diff --git a/tests/fixtures/npm/cases/legacy-namespaces/main.ts b/tests/fixtures/npm/cases/legacy-namespaces/main.ts new file mode 100644 index 000000000..2f9483ba9 --- /dev/null +++ b/tests/fixtures/npm/cases/legacy-namespaces/main.ts @@ -0,0 +1,26 @@ +// A namespace-style library in the pre-module idiom (guarded namespace +// declarations, constructor functions with prototype methods, enums as +// objects, a revealing module, a handler registry, exported through +// `module.exports = { app }`), used from TypeScript. +import { app } from "legacy-ns"; + +const v = new app.geom.Vec(3, 4); +console.log(v.length(), v.add(new app.geom.Vec(1, 1)).scale(2).x, v instanceof app.geom.Vec); +console.log(Object.keys(app).join(","), Object.keys(app.math).join(","), Object.keys(app.geom.Kind).join(",")); +console.log(app.math.clamp(12, 0, 10), app.math.lerp(0, 10, 0.25), app.text.pad(7, 3), app.text.label(1, 42)); + +const shapes = []; +for (let i = 0; i < 12; i++) { + const center = new app.geom.Vec(app.math.lerp(-5, 5, (i % 4) / 3), app.math.lerp(-5, 5, Math.floor(i / 4) / 2)); + shapes.push(new app.geom.Shape(i % 3, center, 1 + (i % 4))); +} +let area = 0; +let hits = 0; +for (let step = 0; step < 200; step++) { + const probe = new app.geom.Vec(app.math.clamp(Math.sin(step) * 7, -5, 5), app.math.clamp(Math.cos(step * 0.7) * 7, -5, 5)); + for (const s of shapes) { + area += app.registry.run("area", s, probe); + hits += app.registry.run("hits", s, probe); + } +} +console.log(Math.round(area), hits, shapes[4].contains(shapes[4].center), shapes[0].area()); diff --git a/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/index.d.ts b/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/index.d.ts new file mode 100644 index 000000000..16c9fa62a --- /dev/null +++ b/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/index.d.ts @@ -0,0 +1,2 @@ +declare const app: any; +export { app }; diff --git a/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/index.js b/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/index.js new file mode 100644 index 000000000..2efa926f5 --- /dev/null +++ b/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/index.js @@ -0,0 +1,97 @@ +// A namespace-style geometry and text library in the pre-module idiom: +// guarded namespace declarations, constructor functions with prototype +// methods, enums as objects, a revealing module, and a static registry. +var app = app || {}; +app.math = app.math || {}; +app.geom = app.geom || {}; + +app.math.EPSILON = 1e-9; + +app.math.clamp = function (value, lo, hi) { + return value < lo ? lo : value > hi ? hi : value; +}; + +app.math.lerp = function (a, b, t) { + return a + (b - a) * t; +}; + +app.math.hypot = function (x, y) { + return Math.sqrt(x * x + y * y); +}; + +app.geom.Kind = { POINT: 0, CIRCLE: 1, RECT: 2 }; + +/** @constructor */ +app.geom.Vec = function (x, y) { + this.x = x; + this.y = y; +}; + +app.geom.Vec.prototype.add = function (other) { + return new app.geom.Vec(this.x + other.x, this.y + other.y); +}; + +app.geom.Vec.prototype.scale = function (k) { + return new app.geom.Vec(this.x * k, this.y * k); +}; + +app.geom.Vec.prototype.length = function () { + return app.math.hypot(this.x, this.y); +}; + +/** @constructor */ +app.geom.Shape = function (kind, center, size) { + this.kind = kind; + this.center = center; + this.size = size; +}; + +app.geom.Shape.prototype.area = function () { + switch (this.kind) { + case app.geom.Kind.CIRCLE: + return Math.PI * this.size * this.size; + case app.geom.Kind.RECT: + return this.size * this.size; + default: + return 0; + } +}; + +app.geom.Shape.prototype.contains = function (p) { + var d = p.add(this.center.scale(-1)).length(); + return d <= this.size + app.math.EPSILON; +}; + +app.text = (function () { + var digits = "0123456789"; + function pad(n, width) { + var s = ""; + var v = n; + do { + s = digits.charAt(v % 10) + s; + v = Math.floor(v / 10); + } while (v > 0); + while (s.length < width) s = "0" + s; + return s; + } + function label(kind, n) { + return (kind === app.geom.Kind.CIRCLE ? "c" : kind === app.geom.Kind.RECT ? "r" : "p") + pad(n, 4); + } + return { pad: pad, label: label }; +})(); + +app.registry = { + handlers: { + area: function (shape) { + return shape.area(); + }, + hits: function (shape, probe) { + return shape.contains(probe) ? 1 : 0; + }, + }, + run: function (name, shape, probe) { + return app.registry.handlers[name](shape, probe); + }, +}; + +module.exports = { app }; diff --git a/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/package.json b/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/package.json new file mode 100644 index 000000000..cde18c613 --- /dev/null +++ b/tests/fixtures/npm/cases/legacy-namespaces/node_modules/legacy-ns/package.json @@ -0,0 +1 @@ +{ "name": "legacy-ns", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/legacy-namespaces/package.json b/tests/fixtures/npm/cases/legacy-namespaces/package.json new file mode 100644 index 000000000..140c08a1c --- /dev/null +++ b/tests/fixtures/npm/cases/legacy-namespaces/package.json @@ -0,0 +1 @@ +{ "name": "legacy-namespaces-case", "private": true, "type": "module" } From fe8ba8900f45c0d12615142f210feec282ce4a5f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 22:16:05 -0500 Subject: [PATCH 158/273] Start integer loops from the integers their starts carry --- .../backend/llvm/bytes-loop-versions.test.ts | 3 +- .../src/backend/llvm/bytes-loop-versions.ts | 5 +- .../src/backend/llvm/counted-loops.ts | 55 ++++++++++++++++++- .../src/backend/llvm/dense-array-loops.ts | 4 +- .../src/backend/llvm/emit-statements.ts | 13 ++--- .../8196-counted-loops-integer-starts.ts.json | 6 ++ .../8196-counted-loops-integer-starts.ts | 50 +++++++++++++++++ 7 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8196-counted-loops-integer-starts.ts.json create mode 100644 tests/corpus/8196-counted-loops-integer-starts.ts 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 0c0f1aaa0..096712c97 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -701,7 +701,8 @@ test("a two-counter loop versions the typed arrays it indexes at either counter" }; 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).toMatch(/icmp sge i64 %\S+, 0/); + // `i` starts at the literal 0, so its test compares the integer itself. + expect(guard).toMatch(/icmp sge i64 0, 0/); expect(guard).toMatch(/icmp slt i64 %\S+, %\S+/); const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0]; // The fast copy keeps only the `src[i + 1]` check, and no second guard. diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index 301f65de2..faec650a4 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -9,6 +9,7 @@ import { emitCountedLoopLimit, emitCountedLoopSafety, emitExactIntegerTest, + emitStartIntegerTest, } from "./counted-loops.js"; import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js"; import { f64Lit } from "./common.js"; @@ -1251,8 +1252,8 @@ export function emitBytesLoopVersionGuard( // access at either one is in bounds when `lo >= 0` and `hi < length`. const { loop } = version.paired; const [low, high] = [loop.low.start, loop.high.start].map((expr) => { - const tested = emitExactIntegerTest(host, host.emitExpr(expr).name, -SAFE, SAFE); - conditions.push(tested.ok); + const tested = emitStartIntegerTest(host, expr, -SAFE, SAFE); + if (tested.ok !== "true") conditions.push(tested.ok); return tested.integer; }); const nonNegative = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts index bb402d5b8..39f97717b 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -7,6 +7,7 @@ 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"; +import { exactInteger, widenInteger } from "./integer-values.js"; /** The caller has proved or guarded the direction's finite endpoint. Clamp * the opposite infinity before integer conversion. Fractional limits become @@ -121,6 +122,7 @@ export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPair } function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string { + if (ok === "true") return safe; if (safe === "true") return ok; const both = host.B.tmp(); host.B.line(`${both} = and i1 ${safe}, ${ok}`); @@ -129,10 +131,25 @@ function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string { /** 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. */ + * the ordered range tests. A start with an integer form (an int32 local, + * `p0 - 1` of one) is an exact integer already: its range answers the test, + * or two integer compares do. */ function emitExactStart(host: LlvmEmitterContext, expr: IrExpr): string { const B = host.B; const start = host.emitExpr(expr); + const integer = exactInteger(host, start, expr); + if (integer) { + const limit = Number.MAX_SAFE_INTEGER; + if (integer.range.min >= -limit && integer.range.max <= limit) return "true"; + const wide = widenInteger(host, integer), + low = B.tmp(), + high = B.tmp(), + ok = B.tmp(); + B.line(`${low} = icmp sge i64 ${wide}, ${-limit}`); + B.line(`${high} = icmp sle i64 ${wide}, ${limit}`); + B.line(`${ok} = and i1 ${low}, ${high}`); + return ok; + } const low = B.tmp(), high = B.tmp(), raw = B.tmp(), @@ -188,3 +205,39 @@ export function emitExactIntegerTest( B.line(`${ok} = and i1 ${inRange}, ${exact}`); return { ok, integer }; } + +/** The i64 start of an integer counter: the integer form the start value + * carries (an int32 local, `p0 - 1` of one), or the double's conversion, + * which the loop's guard or proof makes exact. */ +export function emitCountedStart(host: LlvmEmitterContext, expr: IrExpr): string { + const value = host.emitExpr(expr); + const integer = exactInteger(host, value, expr); + if (integer) return widenInteger(host, integer); + const start = host.B.tmp(); + host.B.line(`${start} = fptosi double ${value.name} to i64`); + return start; +} + +/** emitExactIntegerTest of a start expression: when the value carries an + * integer form (an exact integer, never -0), its range answers the test or + * two integer compares do, and the integer is the form's. */ +export function emitStartIntegerTest( + host: LlvmEmitterContext, + expr: IrExpr, + min: number, + max: number, +): { ok: string; integer: string } { + const value = host.emitExpr(expr); + const integer = exactInteger(host, value, expr); + if (!integer) return emitExactIntegerTest(host, value.name, min, max); + const wide = widenInteger(host, integer); + if (integer.range.min >= min && integer.range.max <= max) return { ok: "true", integer: wide }; + const B = host.B; + const low = B.tmp(), + high = B.tmp(), + ok = B.tmp(); + B.line(`${low} = icmp sge i64 ${wide}, ${min}`); + B.line(`${high} = icmp sle i64 ${wide}, ${max}`); + B.line(`${ok} = and i1 ${low}, ${high}`); + return { ok, integer: wide }; +} diff --git a/packages/compiler/src/backend/llvm/dense-array-loops.ts b/packages/compiler/src/backend/llvm/dense-array-loops.ts index b806939b5..020ea582a 100644 --- a/packages/compiler/src/backend/llvm/dense-array-loops.ts +++ b/packages/compiler/src/backend/llvm/dense-array-loops.ts @@ -36,7 +36,7 @@ import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { loadArrCount, SCR_ARR_FIELD } from "./common.js"; -import { emitCountedLoopLimit, emitExactIntegerTest } from "./counted-loops.js"; +import { emitCountedLoopLimit, emitStartIntegerTest } from "./counted-loops.js"; export const INT32_MAX = 2147483647; @@ -453,7 +453,7 @@ function emitPairedDenseGuard( ): string { const B = host.B; const start = (expr: IrExpr, min: number, max: number) => - emitExactIntegerTest(host, host.emitExpr(expr).name, min, max); + emitStartIntegerTest(host, expr, min, max); const low = start(pair.lowStart, 0, INT32_MAX - 1); const high = start(pair.highStart, -1, INT32_MAX - 1); let valid = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 3d3aa6781..89e36a203 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -61,6 +61,7 @@ import { import { emitCountedLoopLimit, emitCountedLoopSafety, + emitCountedStart, emitPairedLoopSafety, } from "./counted-loops.js"; import { emitBytesLoopVersionGuard, type BytesLoopVersion } from "./bytes-loop-versions.js"; @@ -1112,9 +1113,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.entryAllocas.push( `${integerSlot} = alloca i64 ; integer induction ${emitter.currentLocals.get(countedLoop.localId)!.name}`, ); - const startNumber = emitter.emitExpr(countedLoop.start); - const start = B.tmp(); - B.line(`${start} = fptosi double ${startNumber.name} to i64`); + const start = emitCountedStart(emitter, countedLoop.start); B.line(`store i64 ${start}, ptr ${integerSlot}`); emitter.integerLoopBindings.set(countedLoop.localId, { slot: integerSlot, @@ -1130,9 +1129,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { 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`); + const followerStart = emitCountedStart(emitter, follower.start); B.line(`store i64 ${followerStart}, ptr ${slot}`); followerSlots.push(slot); emitter.integerLoopBindings.set(follower.localId, { @@ -1149,9 +1146,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { 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`); + const start = emitCountedStart(emitter, counter.start); B.line(`store i64 ${start}, ptr ${slot}`); pairedSlots.push(slot); const dense = LOOPS.of(emitter).denseLoop; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8196-counted-loops-integer-starts.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8196-counted-loops-integer-starts.ts.json new file mode 100644 index 000000000..6569a8326 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8196-counted-loops-integer-starts.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8196-counted-loops-integer-starts.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8196-counted-loops-integer-starts.ts b/tests/corpus/8196-counted-loops-integer-starts.ts new file mode 100644 index 000000000..d608bc4b5 --- /dev/null +++ b/tests/corpus/8196-counted-loops-integer-starts.ts @@ -0,0 +1,50 @@ +// Counted, paired and dense array loops whose start is an int32 value +// (an int32 array element, `p0 | 0` or one minus either): starts at -1, 0, +// inside the array, at its end, and at the int32 extremes, where the loop +// must not run or must stop at the array's end as Node does. Number arrays +// and an Int32Array both run their paired loops. +function reverse(pp: number[], p0: number): number { + let swaps = 0; + for (let i = 1, j = (p0 | 0) - 1; i < j; i++, j--) { + const t = pp[i]!; + pp[i] = pp[j]!; + pp[j] = t; + swaps++; + } + return swaps; +} + +function reverseBytes(values: Int32Array, p0: number): number { + let swaps = 0; + for (let i = 0, j = (p0 | 0) - 1; i < j; i++, j--) { + const t = values[i]!; + values[i] = values[j]!; + values[j] = t; + swaps++; + } + return swaps; +} + +function sumFrom(values: number[], start: number): number { + let total = 0; + for (let k = start - 1; k < values.length; k++) total += values[k] ?? 100; + return total; +} + +function countdown(values: Int32Array, from: number): number { + let total = 0; + for (let k = from; k >= 0; k--) total += values[k] ?? 1000; + return total; +} + +const starts: number[] = [0, 1, 2, 5, 9, 10, 11, 2147483647, -2147483648]; +const ints = new Int32Array(starts); +for (let s = 0; s < ints.length; s++) { + const p0 = ints[s]!; + const pp = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + const swaps = p0 <= 11 ? reverse(pp, p0) : 0; + const bytes = new Int32Array(pp); + const byteSwaps = p0 <= 11 ? reverseBytes(bytes, p0) : 0; + console.log(p0, swaps, pp.join(""), p0 <= 11 && p0 >= -1 ? sumFrom(pp, p0) : "-", p0 <= 11 ? countdown(ints, p0) : "-"); + console.log(" bytes", byteSwaps, bytes.join("")); +} From 33f63655a0ddd712253790c260657f6d51aba080 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 22:34:45 -0500 Subject: [PATCH 159/273] Infer untyped JavaScript collection types once, without re-entering The inference reads types while it runs: resolving a candidate's constructor through a destructured require (const { TextDecoder } = util) asks for the namespace's type, which asked for the inferred collection types again and restarted the inference until the stack overflowed. The lowerer now stores an empty table before inferring, so reads during the inference see no inferred types, and stores the result after. --- .../frontend/lowering/js-collection-types.ts | 10 +++++-- .../compiler/test/js-collection-types.test.ts | 29 ++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/js-collection-types.ts b/packages/compiler/src/frontend/lowering/js-collection-types.ts index a4c182523..2be4e0dbd 100644 --- a/packages/compiler/src/frontend/lowering/js-collection-types.ts +++ b/packages/compiler/src/frontend/lowering/js-collection-types.ts @@ -75,9 +75,15 @@ export function initializedCollectionType( /** The inferred collection type of an identifier that names a proven * binding, or null. Computed for every JavaScript file of the program on - * first use. */ + * first use. The inference itself reads types (a candidate's constructor + * through a destructured require, for example); while it runs, those reads + * see no inferred types rather than starting it again. */ export function inferredCollectionType(lowerer: Lowerer, node: ts.Identifier): ts.Type | null { - const types = (lowerer.inferredCollectionTypes ??= inferProgram(lowerer)); + if (lowerer.inferredCollectionTypes === undefined) { + lowerer.inferredCollectionTypes = new Map(); + lowerer.inferredCollectionTypes = inferProgram(lowerer); + } + const types = lowerer.inferredCollectionTypes; return types.size === 0 ? null : (types.get(node) ?? null); } diff --git a/packages/compiler/test/js-collection-types.test.ts b/packages/compiler/test/js-collection-types.test.ts index 6a6f65798..7a5ef967a 100644 --- a/packages/compiler/test/js-collection-types.test.ts +++ b/packages/compiler/test/js-collection-types.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; @@ -62,3 +62,30 @@ test("untyped JavaScript collections take the types their uses prove, and only t rmSync(dir, { recursive: true, force: true }); } }); + +// Resolving a candidate's constructor through a destructured require reads +// types while the inference runs; those reads must not start it again. +test("the inference runs once when a candidate's values come from a destructured require", async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-js-collection-reentry-")); + try { + const entry = join(dir, "main.cjs"); + writeFileSync( + entry, + [ + 'const util = require("node:util");', + "const { TextDecoder } = util;", + "const decoder = new TextDecoder();", + 'const decoders = new Map([["utf-8", decoder]]);', + "const seen = new Set([decoder]);", + "console.log(decoders.size, seen.size);", + "", + ].join("\n"), + ); + const output = join(dir, "main.ir.json"); + const lowered = await compile(entry, { outputKind: "ir", outDir: dir, outPath: output }); + if (!lowered.ok) throw new Error(JSON.stringify(lowered.diagnostics)); + expect(validateModule(deserializeModule(readFileSync(output, "utf8")))).toEqual([]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); From f4eda0fb6c9fcc475e5b7f3eb4392d7af02063c2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 22:43:27 -0500 Subject: [PATCH 160/273] Number hoisted speculative leaves per function --- .../src/backend/llvm/emit-function.ts | 1 + packages/compiler/src/backend/llvm/emitter.ts | 7 ++- tests/harness/atomics-emission.test.ts | 2 +- tests/harness/interface-dispatch.test.ts | 4 +- .../speculative-leaves-emission.test.ts | 63 +++++++++++++++++++ 5 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 tests/harness/speculative-leaves-emission.test.ts diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index b79e2406d..802a0c384 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -66,6 +66,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.usesLandingPads = false; emitter.aliasScopedVersions = 0; emitter.findInt32Allocations(fn); + emitter.speculativeLeaves = 0; emitter.currentTier = fn.tier; // Fiber switches restore the active exception cell before control // returns to a synchronous frame (emitPendingCheck). diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index d5964f1ba..7a49e2884 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1546,8 +1546,11 @@ export class LlEmitter { speculationResume: Map | null = null; /** The rewritten values of the writes `speculationResume` resumes at. */ speculationResumeValues: ReadonlyMap | null = null; - /** Hoisted speculative leaves numbered so far in the module. */ - private speculativeLeaves = 0; + /** Hoisted speculative leaves numbered so far in the function being + * emitted (emitFunction resets it): a function's local names must not + * depend on which functions the same emitter emitted before it, since + * function workers each emit a share of the module. */ + speculativeLeaves = 0; private readonly inlinableFunctions = new Map(); /** What speculative loop planning may assume here (SpeculationContext). */ diff --git a/tests/harness/atomics-emission.test.ts b/tests/harness/atomics-emission.test.ts index 2d708e382..9068ad59e 100644 --- a/tests/harness/atomics-emission.test.ts +++ b/tests/harness/atomics-emission.test.ts @@ -59,7 +59,7 @@ console.log(work(new Int32Array(buffer), new Uint8Array(buffer), new Int16Array( // Every runtime call is the cold path for an index out of range. const calls = work.match(/call double @scr_atomics_op\([^\n]*/g) ?? []; expect(calls).toHaveLength(9); - for (const call of calls) expect(call).toMatch(/\) cold$/); + for (const call of calls) expect(call).toMatch(/\)(?: nounwind)? cold$/); }); test("an unchanged local array is borrowed across Atomics operations", async () => { diff --git a/tests/harness/interface-dispatch.test.ts b/tests/harness/interface-dispatch.test.ts index 21e455e70..bf70ecafb 100644 --- a/tests/harness/interface-dispatch.test.ts +++ b/tests/harness/interface-dispatch.test.ts @@ -71,7 +71,7 @@ describe("interface method dispatch", () => { test("calls through an interface reach the implementing methods with typed values", async () => { const llvm = await emittedLlvm("direct", shapes("")); const run = bodies(llvm, "sc_bf_run"); - expect(run).toMatch(/call double @sc_bf__x25_dyn_class_protocol_\d+/); + expect(run).toMatch(/(?:call|invoke) double @sc_bf__x25_dyn_class_protocol_\d+/); expect(run).not.toContain("@scr_dyn_call"); expect(run).not.toContain("@sc_bf__x25_dyn_class_property_"); const dispatch = llvm.slice(llvm.indexOf("; %dyn.class.protocol.0")); @@ -90,7 +90,7 @@ describe("interface method dispatch", () => { console.log(sum([circle, new Rect(1)]));`), ); const sum = bodies(llvm, "sc_bf_sum"); - expect(sum).toMatch(/call double @sc_bf__x25_dyn_class_protocol_\d+/); + expect(sum).toMatch(/(?:call|invoke) double @sc_bf__x25_dyn_class_protocol_\d+/); expect(sum).not.toContain("@scr_dyn_iterator"); expect(sum).not.toContain("@sc_bf__x25_dyn_class_computed_"); }); diff --git a/tests/harness/speculative-leaves-emission.test.ts b/tests/harness/speculative-leaves-emission.test.ts new file mode 100644 index 000000000..16fd298d4 --- /dev/null +++ b/tests/harness/speculative-leaves-emission.test.ts @@ -0,0 +1,63 @@ +/* Speculative integer loops (behavior is in the differential corpus, 7757): + * the locals that hold hoisted calls are numbered per function, so a + * function's text does not depend on the functions emitted before it (the + * native compiler's function workers each emit a share of the module). */ +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); + +async function emittedLlvm(name: string, source: string): Promise { + const key = createHash("sha256").update(source).digest("hex").slice(0, 16); + const outDir = join(cacheDir, `spec-leaves-${key}`); + mkdirSync(outDir, { recursive: true }); + const file = join(outDir, `${name}.ts`); + writeFileSync(file, source); + const result = await compile(file, { + outPath: join(outDir, name), + outDir, + backend: "llvm", + outputKind: "llvm", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return readFileSync(result.artifact.path, "utf8"); +} + +function body(llvm: string, name: string): string { + const start = llvm.search(new RegExp(`^define internal [^\\n]*@${name}[.(]`, "m")); + expect(start).toBeGreaterThanOrEqual(0); + return llvm.slice(start, llvm.indexOf("\n}\n", start)); +} + +test("hoisted speculative leaves are numbered per function", async () => { + const llvm = await emittedLlvm( + "leaves", + `let calls = 0; +function noisy(x: number): number { + calls++; + return x % 5 === 0 ? x / 2 : x * 2; +} +function first(n: number): number { + let acc = 1; + for (let i = 0; i < n; i++) acc = (acc * 7 + noisy(i)) % 1000003; + return acc; +} +function second(n: number): number { + let acc = 3; + for (let i = 0; i < n; i++) acc = (acc * 5 + noisy(i)) % 1000003; + return acc; +} +console.log(first(20), second(20), calls); +`, + ); + for (const name of ["sc_f_first", "sc_f_second"]) { + const leaves = body(llvm, name).match(/%\w*spec_leaf_\d+ = alloca double/g) ?? []; + expect(leaves).toHaveLength(1); + expect(leaves[0]).toMatch(/_spec_leaf_0 = /); + } +}); From add5f4869b12444003785a3f5da2045b8523722e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 22:53:15 -0500 Subject: [PATCH 161/273] Convert a loop's string receiver to UTF-16 at once, and publish it only when the scan holds another string --- .../src/backend/llvm/bytes-loop-versions.ts | 31 +++-- .../compiler/src/backend/llvm/string-reads.ts | 4 + .../test/string-scan-emission.test.ts | 4 +- .../test/string-unit-loop-emission.test.ts | 64 ++++++++++- .../corpus/8242-string-scan-republish.ts.json | 6 + packages/runtime/src/scr_string.c | 15 +++ packages/runtime/src/scr_string_methods.h | 5 + packages/runtime/test/test_string.c | 25 ++++ tests/corpus/8242-string-scan-republish.ts | 108 ++++++++++++++++++ 9 files changed, 249 insertions(+), 13 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8242-string-scan-republish.ts.json create mode 100644 tests/corpus/8242-string-scan-republish.ts diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index 0fffacb25..de704ad42 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -23,7 +23,7 @@ import { f64Lit } from "./common.js"; import { emitStringAsciiFlag, emitStringByteLength, - STRING_CHAR_CODE_AT_DECL, + STRING_SCAN_UNITS_DECL, stringScanType, } from "./string-reads.js"; import { unitSafeLoop } from "./string-unit-versions.js"; @@ -1360,10 +1360,11 @@ export function emitBytesLoopVersionGuard( } /** The length of a version's units receiver `s` in UTF-16 units: its byte - * length when it is ASCII; otherwise, after one runtime read publishes it - * as the scan receiver, the published unit count (0 when the runtime does - * not hold its units). Also the units pointer, whether the loop is to - * read it, and whether the published receiver is `s`. */ + * length when it is ASCII; otherwise, once it is the scan receiver, the + * published unit count (0 when the runtime does not hold its units). When + * `s` is not already the scan receiver, the runtime converts it to units + * and publishes it (scr_str_scan_units). Also the units pointer, whether + * the loop is to read it, and whether the published receiver is `s`. */ function emitUnitsLength( host: LlvmEmitterContext, s: string, @@ -1378,16 +1379,28 @@ function emitUnitsLength( join = B.newLabel("bytes.version.length"); B.condBr(ascii, join, publish); B.startBlock(publish); - host.declare(STRING_CHAR_CODE_AT_DECL); - const read = B.tmp(), - receiver = B.tmp(), + // When `s` is already the scan receiver (an earlier loop over it published + // it), its units stay published until the runtime clears the scan: skip + // the call. + const held = B.tmp(), + holds = B.tmp(); + B.line(`${held} = load ptr, ptr @scr_str_scan`); + B.line(`${holds} = icmp eq ptr ${held}, ${s}`); + const call = B.newLabel("bytes.version.publish.units"), + load = B.newLabel("bytes.version.publish.load"); + B.condBr(holds, load, call); + B.startBlock(call); + host.declare(STRING_SCAN_UNITS_DECL); + const receiver = B.tmp(), same = B.tmp(), pointerSlot = B.tmp(), pointer = B.tmp(), countSlot = B.tmp(), count = B.tmp(), published = B.tmp(); - B.line(`${read} = call double @scr_str_char_code_at(ptr ${s}, double 0.0)`); + B.line(`call void @scr_str_scan_units(ptr ${s})`); + B.br(load); + B.startBlock(load); B.line(`${receiver} = load ptr, ptr @scr_str_scan`); B.line(`${same} = icmp eq ptr ${receiver}, ${s}`); B.line(`${pointerSlot} = getelementptr inbounds ${type}, ptr @scr_str_scan, i32 0, i32 1`); diff --git a/packages/compiler/src/backend/llvm/string-reads.ts b/packages/compiler/src/backend/llvm/string-reads.ts index 08df79cf2..9fd2fa78c 100644 --- a/packages/compiler/src/backend/llvm/string-reads.ts +++ b/packages/compiler/src/backend/llvm/string-reads.ts @@ -24,6 +24,10 @@ import { literalCapWord, literalCapWord32 } from "./string-key-hash.js"; export const STRING_UTF16_LEN_DECL = "declare double @scr_str_utf16_len(ptr)"; export const STRING_CHAR_CODE_AT_DECL = "declare double @scr_str_char_code_at(ptr, double)"; +/** Converts a string to UTF-16 units and publishes it as the scan receiver + * (bytes-loop-versions.ts). Like the fallbacks below, it can evict an + * index entry, so it carries no memory restriction. */ +export const STRING_SCAN_UNITS_DECL = "declare void @scr_str_scan_units(ptr)"; /** The runtime fallbacks are cold: both common cases read inline. They * carry no * memory restriction: a fallback can evict an index entry, which resets diff --git a/packages/compiler/test/string-scan-emission.test.ts b/packages/compiler/test/string-scan-emission.test.ts index 427e2d543..76242e3d4 100644 --- a/packages/compiler/test/string-scan-emission.test.ts +++ b/packages/compiler/test/string-scan-emission.test.ts @@ -37,7 +37,9 @@ console.log(scan("é-é", "ü-ü", "ö-ö"), at("é", 0.5)); expect(llvm).toContain("@scr_str_scan = external global"); const fallbacks = llvm .split("\n") - .filter((line) => /call double @scr_str_(utf16_len|char_code_at)\(/.test(line)); + .filter((line) => + /call (double|void) @scr_str_(utf16_len|char_code_at|scan_units)\(/.test(line), + ); expect(fallbacks.length).toBeGreaterThanOrEqual(3); for (const line of fallbacks) expect(line).not.toMatch(/\bmemory\(/); } finally { diff --git a/packages/compiler/test/string-unit-loop-emission.test.ts b/packages/compiler/test/string-unit-loop-emission.test.ts index 32ee8411b..fa11af007 100644 --- a/packages/compiler/test/string-unit-loop-emission.test.ts +++ b/packages/compiler/test/string-unit-loop-emission.test.ts @@ -50,9 +50,9 @@ console.log(total, spaces("plain ascii text"), spaces("")); )![0]; const spaces = body("spaces"); - // A string that is not ASCII is published as the scan receiver once, - // before the loop, and its copy loads units. - expect(spaces).toMatch(/call double @scr_str_char_code_at\(ptr %\S+, double 0\.0\)/); + // A string that is not ASCII is converted to units and published as the + // scan receiver once, before the loop, and its copy loads units. + expect(spaces).toMatch(/call void @scr_str_scan_units\(ptr %\S+\)/); // The copies follow the checked one: bytes, then units. const bytesAt = spaces.search(/^bytes\.version\.fast\d+:/m); const unitsAt = spaces.search(/^bytes\.version\.fast\.units\d+:/m); @@ -92,3 +92,61 @@ console.log(total, spaces("plain ascii text"), spaces("")); await rm(dir, { recursive: true, force: true }); } }); + +test("loops that start at each token publish the scan receiver only when it changed", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-unit-republish-")); + try { + const entry = join(dir, "main.ts"); + await writeFile( + entry, + ` +function words(s: string): number { + let n = 0, + pos = 0; + while (pos < s.length) { + while (pos < s.length && s.charCodeAt(pos) !== 32) pos++; + while (pos < s.length && s.charCodeAt(pos) === 32) pos++; + n++; + } + return n; +} +let text = ""; +for (let k = 0; k < 400; k++) text += k % 3 === 0 ? "café " : "naïve façade "; +console.log(words(text), words(text.replace(/a/g, "ä")), words("plain ascii")); +`, + ); + const outPath = join(dir, "main.ir.json"); + const lowered = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!lowered.ok) + throw new Error(lowered.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const words = ll.match( + /define internal [^\n]+ @sc_bf_words(?:\.i32)?\([^\n]*\) #0 \{[\s\S]*?\n\}/, + )![0]; + // Each inner loop's entry compares the scan receiver with `s` and calls + // the runtime to publish `s` only when they differ. + const guards = [ + ...words.matchAll( + /^bytes\.version\.publish\d+:[^\n]*\n\s+(%\S+) = load ptr, ptr @scr_str_scan\n\s+(%\S+) = icmp eq ptr \1, (%\S+)\n\s+br i1 \2, label %(bytes\.version\.publish\.load\d+), label %(bytes\.version\.publish\.units\d+)\n/gm, + ), + ]; + expect(guards.length).toBeGreaterThanOrEqual(2); + for (const [, , , s, load, convert] of guards) { + const block = words.slice(words.search(new RegExp(`^${convert}:`, "m"))); + expect(block).toMatch( + new RegExp( + `^${convert}:[^\\n]*\\n\\s+call void @scr_str_scan_units\\(ptr ${s}\\)\\n\\s+br label %${load}\\n`, + ), + ); + } + const calls = words.match(/call void @scr_str_scan_units\(ptr %\S+\)/g)!; + expect(calls.length).toBe(guards.length); + + 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("666 666 2\n"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8242-string-scan-republish.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8242-string-scan-republish.ts.json new file mode 100644 index 000000000..78a4f52e6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8242-string-scan-republish.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8242-string-scan-republish.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 2d7f46d85..d2d397e25 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -1889,6 +1889,21 @@ static bool scr_sidx_units(const ScrStr *s, ScrSidx *e) { return true; } +/* A loop about to read every unit of s (bytes-loop-versions.ts) converts it + * at once instead of after the threshold's reads, and publishes it as the + * scan's receiver. ASCII receivers and receivers past SCR_SIDX_UNITS_MAX + * publish nothing; the caller compares scr_str_scan.s with s. */ +void scr_str_scan_units(ScrStr *s) { + if (s->ascii || s->len > SCR_SIDX_UNITS_MAX) return; + ScrSidx *e = scr_sidx(s); + if (!e->units) e->units = scr_str_units(s, &e->nunits); + if (scr_str_scan.s != s) { + scr_str_scan.s = s; + scr_str_scan.units = e->units; + scr_str_scan.n = e->nunits; + } +} + typedef enum { SCR_UNIT_FIRST, SCR_UNIT_LAST, SCR_UNIT_AT } ScrUnitSearch; /* The UTF-16 index of needle in s: the first match at or after `from`, diff --git a/packages/runtime/src/scr_string_methods.h b/packages/runtime/src/scr_string_methods.h index 1f25b8766..820b1c9e6 100644 --- a/packages/runtime/src/scr_string_methods.h +++ b/packages/runtime/src/scr_string_methods.h @@ -42,6 +42,11 @@ extern SCR_TL ScrStrScan scr_str_scan; * [0, length) — note charCodeAt(1.5) is index 1, matching Node. */ double scr_str_char_code_at(ScrStr *s, double i); +/* Converts a non-ASCII s up to the units cap to UTF-16 at once and + * publishes it in scr_str_scan, for a loop about to read all of it. Leaves + * scr_str_scan unchanged for any other s. */ +void scr_str_scan_units(ScrStr *s); + /* indexOf(needle, fromIndex): UTF-16 index of the first occurrence at or * after fromIndex (clamped to [0, length]), or -1. Empty needle returns the * clamped fromIndex per spec. */ diff --git a/packages/runtime/test/test_string.c b/packages/runtime/test/test_string.c index e49d8eb8b..3e14a5958 100644 --- a/packages/runtime/test/test_string.c +++ b/packages/runtime/test/test_string.c @@ -522,6 +522,30 @@ static void short_string_asserts(void) { scr_str_release(source); } +/* A loop's publish converts a non-ASCII receiver before any read, while a + * lone read still walks bytes. ASCII receivers publish nothing. */ +static void scan_units_asserts(void) { + ScrStr *s = scr_str_new("a\xC3\xA9\xF0\x9F\x98\x80" "b", 8); /* a é 😀 b */ + static const double codes[] = {97, 233, 0xD83D, 0xDE00, 98}; + if (scr_str_char_code_at(s, 0) != 97 || scr_str_scan.s == s) failed++; + scr_str_scan_units(s); + if (scr_str_scan.s != s || scr_str_scan.n != 5) failed++; + for (size_t i = 0; i < 5; i++) + if (scr_str_scan.n == 5 && (scr_str_scan.units[i] != codes[i] || + scr_str_char_code_at(s, (double)i) != codes[i])) failed++; + if (!isnan(scr_str_char_code_at(s, 5))) failed++; + scr_str_scan_units(s); /* republishing keeps the same units */ + const uint16_t *units = scr_str_scan.units; + scr_str_scan_units(s); + if (scr_str_scan.s != s || scr_str_scan.units != units) failed++; + ScrStr *ascii = scr_str_new("plain", 5); + scr_str_scan_units(ascii); + if (scr_str_scan.s != s) failed++; + scr_str_release(ascii); + scr_str_release(s); + if (scr_str_scan.s != NULL) failed++; + total++; +} #ifdef SCR_SIDX_TEST static void sidx_fail(const char *what) { failed++; @@ -1153,6 +1177,7 @@ int main(int argc, char **argv) { split_scratch_asserts(); accumulation_asserts(); short_string_asserts(); + scan_units_asserts(); #ifdef SCR_SIDX_TEST sparse_index_asserts(); sparse_ascii_prefix_asserts(); diff --git a/tests/corpus/8242-string-scan-republish.ts b/tests/corpus/8242-string-scan-republish.ts new file mode 100644 index 000000000..9800bb099 --- /dev/null +++ b/tests/corpus/8242-string-scan-republish.ts @@ -0,0 +1,108 @@ +// Token scans that alternate between two non-ASCII strings: each inner loop +// starts at a token and runs while the class holds, so every token enters a +// loop over one of the strings again. Also a string extended in place +// between scans, strings rebuilt with the same length and new content, and +// slices of a scanned string. + +function isWord(c: number): boolean { + return (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || c >= 0xc0; +} + +function isDigit(c: number): boolean { + return c >= 48 && c <= 57; +} + +// The kinds and lengths of s's tokens, hashed. +function lex(s: string): number { + let h = 0, + tokens = 0, + pos = 0; + const end = s.length; + while (pos < end) { + const c = s.charCodeAt(pos); + let start = pos; + if (isWord(c)) { + while (pos < end && isWord(s.charCodeAt(pos))) pos++; + h = (h * 31 + 1) | 0; + } else if (isDigit(c)) { + while (pos < end && isDigit(s.charCodeAt(pos))) pos++; + h = (h * 31 + 2) | 0; + } else if (c === 32) { + while (pos < end && s.charCodeAt(pos) === 32) pos++; + start = pos; + } else { + pos++; + h = (h * 31 + c) | 0; + } + h = (h * 31 + (pos - start)) | 0; + tokens++; + } + return h ^ tokens; +} + +// Two strings lexed token by token in turn: each token's loop runs over a +// different receiver than the one before it. +function interleave(a: string, b: string): number { + let i = 0, + j = 0, + h = 0; + while (i < a.length || j < b.length) { + const fromA = i < a.length; + while (i < a.length && a.charCodeAt(i) !== 32) { + h = (h * 31 + a.charCodeAt(i)) | 0; + i++; + } + i++; + while (j < b.length && b.charCodeAt(j) !== 32) { + h = (h * 37 + b.charCodeAt(j)) | 0; + j++; + } + j++; + if (fromA) h = (h + 1) | 0; + } + return h; +} + +// The sum of every unit of s. +function units(s: string): number { + let sum = 0; + for (let i = 0; i < s.length; i++) sum += s.charCodeAt(i); + return sum; +} + +function text(seed: number, words: number): string { + const parts = ["café", "naïve", "façade", "x", "😀", "Ωmega", "über", "ab"]; + let s = ""; + for (let k = 0; k < words; k++) { + s += parts[(seed + k * 7) % parts.length]!; + s += k % 5 === 0 ? " " + ((seed * k) % 1000) + " " : k % 3 === 0 ? "; " : " "; + } + return s; +} + +const first = text(1, 300), + second = text(4, 280); +for (let pass = 0; pass < 3; pass++) { + console.log(lex(first), lex(second), lex(first), units(second), units(first)); + console.log(interleave(first, second), interleave(second, first)); +} + +// Extended in place between scans: the second scan reads the new units. +let grown = text(2, 120); +console.log(lex(grown), units(grown), grown.length); +for (let round = 0; round < 4; round++) { + grown += round % 2 === 0 ? " é1 ü22 " : " 😀 tail "; + console.log(lex(grown), units(grown), grown.length, interleave(grown, first)); +} + +// Same length, different content, built after the previous one is dropped. +for (let round = 0; round < 6; round++) { + const s = round % 2 === 0 ? "é".repeat(200) + " a" : "ü".repeat(200) + " b"; + console.log(lex(s), units(s), units(s.slice(0, 100)), lex(second)); +} + +// Slices of a scanned string, scanned between scans of the whole. +for (const at of [0, 7, 101, first.length - 9]) { + const part = first.slice(at, at + 64); + console.log(lex(part), units(part), lex(first), units(part.slice(3))); +} From faa54f6443d35873d37b29ea9e4385707553744f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 22:58:41 -0500 Subject: [PATCH 162/273] Cache only number-key integer views that need no conversion A Map/Set lookup's integer key view was cached for the function by the key operand's name. When the view was a conversion emitted at a lookup in a branch, a later lookup of the same operand (a wide integer constant) reused it from a block the conversion does not dominate, and LLVM rejected the module. Only the value's own integer or uint32 view and integral literals are cached now. --- .../backend/llvm/map-number-lookup.test.ts | 49 +++++++++++++++++++ .../src/backend/llvm/map-number-lookup.ts | 16 ++++-- tests/corpus/6308-number-map-integer-keys.ts | 15 ++++++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts index 28b6a0552..7457db42d 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts @@ -206,6 +206,55 @@ test("a key with an integer view tests the direct index on the integer", () => { } }); +test("a key view converted in a branch is not reused by a later lookup of the same constant", () => { + // work(map, k, taken): if (taken) return map.has((k = 2 ** 40)); return map.has(2 ** 40). + const wide = 2 ** 40; + const mod = lookupFixture(); + const work = mod.functions[1]!; + const map = work.params[0]!.type; + const has = (key: IrExpr): IrExpr => ({ + kind: "mapIntrinsic", + method: "has", + receiver: ref("map", map), + args: [key], + type: BOOL, + loc, + }); + const literal: IrExpr = { kind: "numLit", value: wide, type: F64, loc }; + const taken = { localId: "taken", name: "taken", type: BOOL }; + work.params.push(taken); + work.locals.push({ id: "taken", name: "taken", type: BOOL, mutable: true }); + work.body = [ + { + kind: "if", + cond: ref("taken", BOOL), + then: [ + { + kind: "return", + value: has({ + kind: "assignExpr", + localId: "k", + value: literal, + type: F64, + loc, + }), + loc, + }, + ], + else_: null, + loc, + }, + { kind: "return", value: has(literal), loc }, + ]; + for (const bits of [32, 64] as const) { + const { work: text } = emitted(mod, bits); + // The lookup after the branch takes the literal, not a conversion made + // inside the branch. + expect(text).toMatch(/call ptr @sc_map_entry_f64_int\(ptr %\S+, i64 1099511627776, double /); + expect(text).not.toMatch(/@sc_map_entry_f64_int\(ptr %\S+, i64 %/); + } +}); + /** work(map): map.size, and keep(map): map returned as an owned value. */ function sizeFixture(): IrModule { const map: IrType = { kind: "map", key: F64, value: F64 }; diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.ts b/packages/compiler/src/backend/llvm/map-number-lookup.ts index 6cb7f1ea3..0038cbb96 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.ts @@ -34,12 +34,22 @@ import { exactInteger, widenInteger } from "./integer-values.js"; import { mapKeyAccess } from "./shapes.js"; export const NUMBER_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_f64(ptr, double)"; -/** Record the integer view of a number Map/Set key operand, when the - * emitter has one (exactInteger), for the entry lookups of that key. */ +/** Record the integer view of a number Map/Set key operand for the entry + * lookups of that key, when the view needs no instruction of its own: the + * value's integer or uint32 view, defined with the value, or an integral + * literal. A view exactInteger converts at this lookup is not recorded: the + * key cache spans the function, and a later lookup of the same operand (a + * constant, say) can be in a block the conversion does not dominate. */ export function noteNumberMapKey(host: LlvmEmitterContext, key: LlValue, expr: IrExpr): void { if (key.type.kind !== "f64" || host.numberMapKeyIntegers.has(key.name)) return; + if (!key.integer && key.uint32 === undefined && expr.kind !== "numLit") return; const integer = exactInteger(host, key, expr); - if (integer) host.numberMapKeyIntegers.set(key.name, integer); + if (!integer) return; + const defined = + integer.name === key.integer?.name || + integer.name === key.uint32 || + /^-?\d+$/.test(integer.name); + if (defined) host.numberMapKeyIntegers.set(key.name, integer); } /** The entry for `key`, or null when absent. Valid until the next mutation. diff --git a/tests/corpus/6308-number-map-integer-keys.ts b/tests/corpus/6308-number-map-integer-keys.ts index 16110f80a..75efb3a72 100644 --- a/tests/corpus/6308-number-map-integer-keys.ts +++ b/tests/corpus/6308-number-map-integer-keys.ts @@ -60,3 +60,18 @@ for (let i = 0; i < 5; i++) { key = key * 2 - 1; } console.log([...wide.keys()].join(" "), wide.get(2 ** 52), wide.get(2 ** 53 - 1)); + +// A lookup whose key is an assignment of a wide integer constant, in a +// branch, then a lookup of the same constant after the branch. +const far = new Map(); +far.set(1099511627776, 7); +function probeFar(taken: boolean): number { + let k = 0; + let hits = 0; + if (taken) { + if (far.has((k = 1099511627776))) hits += 1; + } + hits += far.get(1099511627776) ?? -1; + return hits + k; +} +console.log(probeFar(true), probeFar(false)); From 6a299479988ccc7ee188e458786cdbcbf1259b47 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 22:59:02 -0500 Subject: [PATCH 163/273] Run the native compiler's nursery passes every 1024 candidates The native compiler now starts with the cycle collector's nursery trigger at 1024 new candidates instead of 256. scr_cyc_set_nursery_threshold sets the trigger that SCR_CYCLE_THRESHOLD sets, on the pattern of scr_cyc_set_growth_cap; the environment variable still takes precedence. The compiler sets it from a constructor in its host object, before its modules initialize. Set from main, the trigger leaves the build's collection work where it was: the cost depends on the collector's first passes. Programs keep the default trigger. A cold tsc-ts build takes 4% less wall time (12 paired runs) and its peak RSS goes from 1.41 GB to 1.31 GB. The program's IR is unchanged. --- packages/compiler/native/host.c | 11 +++++++++++ packages/runtime/src/scr_cycle.c | 19 +++++++++++++++---- packages/runtime/src/scr_rc.h | 6 ++++++ packages/runtime/test/test_cycle.c | 19 +++++++++++++++++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/compiler/native/host.c b/packages/compiler/native/host.c index f54bdaa3f..4e1a0b754 100644 --- a/packages/compiler/native/host.c +++ b/packages/compiler/native/host.c @@ -135,3 +135,14 @@ void scr_cyc_set_growth_cap(long cap); void scriptc_native_bound_cycle_growth(void) { scr_cyc_set_growth_cap(2); } + +/* The runtime's nursery trigger (scr_rc.h). */ +void scr_cyc_set_nursery_threshold(long threshold); + +/* Runs nursery passes every 1024 new candidates instead of every 256. It + * has to be in place before the compiler's modules initialize: set from + * main it leaves the build's collection work as it is. SCR_CYCLE_THRESHOLD + * still overrides it. */ +__attribute__((constructor)) static void scriptc_native_widen_cycle_nursery(void) { + scr_cyc_set_nursery_threshold(1024); +} diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index dbd636306..d08f54a21 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -637,14 +637,25 @@ static SCR_TL bool scr_cyc_idle_points = false; static SCR_TL size_t scr_cyc_death_due_live = 0; #endif +/* The nursery trigger a program sets for itself + * (scr_cyc_set_nursery_threshold), 0 for the default; SCR_CYCLE_THRESHOLD + * takes precedence over it. */ +static long scr_cyc_nursery_threshold_set = 0; +static SCR_TL size_t scr_cyc_nursery_threshold_cached = 0; + static size_t scr_cyc_nursery_threshold(void) { - static SCR_TL size_t cached = 0; - if (cached == 0) { + if (scr_cyc_nursery_threshold_cached == 0) { const char *env = scr_getenv("SCR_CYCLE_THRESHOLD"); long v = env ? strtol(env, NULL, 10) : 0; - cached = v > 0 ? (size_t)v : SCR_CYC_NURSERY_CANDIDATES; + if (v <= 0) v = scr_cyc_nursery_threshold_set; + scr_cyc_nursery_threshold_cached = v > 0 ? (size_t)v : SCR_CYC_NURSERY_CANDIDATES; } - return cached; + return scr_cyc_nursery_threshold_cached; +} + +void scr_cyc_set_nursery_threshold(long threshold) { + scr_cyc_nursery_threshold_set = threshold; + scr_cyc_nursery_threshold_cached = 0; } /* The number of buffered candidates (across every generation) at which the diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 4d8e059a9..de9f21f44 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -700,6 +700,12 @@ void scr_collect_cycles_phase(void); * that knows its own memory profile; the environment variable takes * precedence. Call it before the program allocates much. */ void scr_cyc_set_growth_cap(long cap); +/* Sets the nursery trigger SCR_CYCLE_THRESHOLD sets (scr_cycle.c): the + * number of new candidates at which the release path runs a pass, for a + * program that knows its own collection profile. The environment variable + * takes precedence; 0 restores the default. The trigger re-arms with it at + * the end of the next pass. Call it before the program allocates much. */ +void scr_cyc_set_nursery_threshold(long threshold); void scr_cyc_context_cleanup(void); /* One pass on the normal generational schedule, for callers that reach a diff --git a/packages/runtime/test/test_cycle.c b/packages/runtime/test/test_cycle.c index 160083d6b..0ab827e47 100644 --- a/packages/runtime/test/test_cycle.c +++ b/packages/runtime/test/test_cycle.c @@ -557,6 +557,24 @@ static void check_program_growth_cap(void) { scr_cyc_set_growth_cap(0); } +/* scr_cyc_set_nursery_threshold sets the trigger SCR_CYCLE_THRESHOLD sets, + * from the end of the next pass: with a trigger of 1024, dead rings released + * after a full pass wait until the 1024th, which collects them all. 0 + * restores the default. */ +static void check_program_nursery_threshold(void) { + enum { THRESHOLD = 1024 }; + if (getenv("SCR_CYCLE_THRESHOLD") != NULL) return; /* it takes precedence */ + scr_cyc_set_nursery_threshold(THRESHOLD); + scr_collect_cycles(); /* re-arm with the new trigger */ + size_t before = freed; + for (size_t i = 0; i < THRESHOLD - 1; i++) release_live(make_ring(1)); + check(freed == before, "a program's nursery trigger did not hold the pass back"); + release_live(make_ring(1)); + check(freed - before == THRESHOLD, "a program's nursery trigger did not run the pass"); + scr_cyc_set_nursery_threshold(0); + scr_collect_cycles(); +} + /* The phase sweep (scr_collect_cycles_phase) is for a caller that knows a * phase's data has just died: it sweeps, and the growth schedule starts * over. After passes that find nothing have widened the old level's @@ -1522,6 +1540,7 @@ int main(void) { check_unjudged_backlog_follows_growth(); check_acyclic_objects(); check_program_growth_cap(); + check_program_nursery_threshold(); printf("cycle collection checks passed: threshold=%zu growth_cap=%zu\n", configured_nursery_threshold(), configured_growth_cap()); return 0; From f716dec7567fc2deffc7a05dcdef5bfe69802ad3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 23:26:01 -0500 Subject: [PATCH 164/273] Convert Buffers to and from latin1 and hex text a word at a time - toString("latin1"): counts the high bytes eight at a time, copies words of ASCII bytes, and writes other bytes from a table of their UTF-8 pairs without branches. ASCII-only results are marked ASCII, and other results record their UTF-16 length (one unit per byte), so `.length` does not walk the string. - Buffer.from(text, "latin1" | "ascii" | "binary"): copies text known to be ASCII, copies words of ASCII bytes, and decodes eight bytes of one- and two-byte sequences in one word, storing each output byte without branches. Longer sequences take the previous path. - Buffer.from(text, "hex"): reads digit values from a table. - toString("hex") and toString("base64"/"base64url") mark their results ASCII, so a later decode, length or index read skips the scan for non-ASCII bytes. --- .../corpus/8005-buffer-encoding-paths.ts.json | 6 + packages/runtime/src/scr_bytes.c | 214 +++++++++++++++--- packages/runtime/src/scr_string.c | 13 ++ packages/runtime/src/scr_string.h | 3 + tests/corpus/8005-buffer-encoding-paths.ts | 105 +++++++++ .../8005-buffer-encoding-paths.ts.json | 1 + 6 files changed, 315 insertions(+), 27 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8005-buffer-encoding-paths.ts.json create mode 100644 tests/corpus/8005-buffer-encoding-paths.ts create mode 100644 tests/harness/corpus-costs/8005-buffer-encoding-paths.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8005-buffer-encoding-paths.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8005-buffer-encoding-paths.ts.json new file mode 100644 index 000000000..7d6dac570 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8005-buffer-encoding-paths.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8005-buffer-encoding-paths.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index e6df837c8..c1638596c 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -1577,6 +1577,7 @@ static ScrStr *scr_bytes_encode_b64(const uint8_t *in, size_t n, bool url) { if (!url) out[o++] = '='; } out[outlen] = '\0'; + scr_str_note_ascii(s); return s; } @@ -2497,6 +2498,106 @@ static ScrStr *scr_bytes_decode_utf16le(const uint8_t *in, size_t n) { return s; } +/* The UTF-8 bytes of U+0000..U+00FF: the byte itself below 0x80 (the + * second entry is overwritten by the next write), else a two-byte + * sequence. */ +static const uint8_t scr_latin1_utf8[256][2] = { + {0x00, 0x00}, {0x01, 0x00}, {0x02, 0x00}, {0x03, 0x00}, {0x04, 0x00}, {0x05, 0x00}, {0x06, 0x00}, {0x07, 0x00}, + {0x08, 0x00}, {0x09, 0x00}, {0x0a, 0x00}, {0x0b, 0x00}, {0x0c, 0x00}, {0x0d, 0x00}, {0x0e, 0x00}, {0x0f, 0x00}, + {0x10, 0x00}, {0x11, 0x00}, {0x12, 0x00}, {0x13, 0x00}, {0x14, 0x00}, {0x15, 0x00}, {0x16, 0x00}, {0x17, 0x00}, + {0x18, 0x00}, {0x19, 0x00}, {0x1a, 0x00}, {0x1b, 0x00}, {0x1c, 0x00}, {0x1d, 0x00}, {0x1e, 0x00}, {0x1f, 0x00}, + {0x20, 0x00}, {0x21, 0x00}, {0x22, 0x00}, {0x23, 0x00}, {0x24, 0x00}, {0x25, 0x00}, {0x26, 0x00}, {0x27, 0x00}, + {0x28, 0x00}, {0x29, 0x00}, {0x2a, 0x00}, {0x2b, 0x00}, {0x2c, 0x00}, {0x2d, 0x00}, {0x2e, 0x00}, {0x2f, 0x00}, + {0x30, 0x00}, {0x31, 0x00}, {0x32, 0x00}, {0x33, 0x00}, {0x34, 0x00}, {0x35, 0x00}, {0x36, 0x00}, {0x37, 0x00}, + {0x38, 0x00}, {0x39, 0x00}, {0x3a, 0x00}, {0x3b, 0x00}, {0x3c, 0x00}, {0x3d, 0x00}, {0x3e, 0x00}, {0x3f, 0x00}, + {0x40, 0x00}, {0x41, 0x00}, {0x42, 0x00}, {0x43, 0x00}, {0x44, 0x00}, {0x45, 0x00}, {0x46, 0x00}, {0x47, 0x00}, + {0x48, 0x00}, {0x49, 0x00}, {0x4a, 0x00}, {0x4b, 0x00}, {0x4c, 0x00}, {0x4d, 0x00}, {0x4e, 0x00}, {0x4f, 0x00}, + {0x50, 0x00}, {0x51, 0x00}, {0x52, 0x00}, {0x53, 0x00}, {0x54, 0x00}, {0x55, 0x00}, {0x56, 0x00}, {0x57, 0x00}, + {0x58, 0x00}, {0x59, 0x00}, {0x5a, 0x00}, {0x5b, 0x00}, {0x5c, 0x00}, {0x5d, 0x00}, {0x5e, 0x00}, {0x5f, 0x00}, + {0x60, 0x00}, {0x61, 0x00}, {0x62, 0x00}, {0x63, 0x00}, {0x64, 0x00}, {0x65, 0x00}, {0x66, 0x00}, {0x67, 0x00}, + {0x68, 0x00}, {0x69, 0x00}, {0x6a, 0x00}, {0x6b, 0x00}, {0x6c, 0x00}, {0x6d, 0x00}, {0x6e, 0x00}, {0x6f, 0x00}, + {0x70, 0x00}, {0x71, 0x00}, {0x72, 0x00}, {0x73, 0x00}, {0x74, 0x00}, {0x75, 0x00}, {0x76, 0x00}, {0x77, 0x00}, + {0x78, 0x00}, {0x79, 0x00}, {0x7a, 0x00}, {0x7b, 0x00}, {0x7c, 0x00}, {0x7d, 0x00}, {0x7e, 0x00}, {0x7f, 0x00}, + {0xc2, 0x80}, {0xc2, 0x81}, {0xc2, 0x82}, {0xc2, 0x83}, {0xc2, 0x84}, {0xc2, 0x85}, {0xc2, 0x86}, {0xc2, 0x87}, + {0xc2, 0x88}, {0xc2, 0x89}, {0xc2, 0x8a}, {0xc2, 0x8b}, {0xc2, 0x8c}, {0xc2, 0x8d}, {0xc2, 0x8e}, {0xc2, 0x8f}, + {0xc2, 0x90}, {0xc2, 0x91}, {0xc2, 0x92}, {0xc2, 0x93}, {0xc2, 0x94}, {0xc2, 0x95}, {0xc2, 0x96}, {0xc2, 0x97}, + {0xc2, 0x98}, {0xc2, 0x99}, {0xc2, 0x9a}, {0xc2, 0x9b}, {0xc2, 0x9c}, {0xc2, 0x9d}, {0xc2, 0x9e}, {0xc2, 0x9f}, + {0xc2, 0xa0}, {0xc2, 0xa1}, {0xc2, 0xa2}, {0xc2, 0xa3}, {0xc2, 0xa4}, {0xc2, 0xa5}, {0xc2, 0xa6}, {0xc2, 0xa7}, + {0xc2, 0xa8}, {0xc2, 0xa9}, {0xc2, 0xaa}, {0xc2, 0xab}, {0xc2, 0xac}, {0xc2, 0xad}, {0xc2, 0xae}, {0xc2, 0xaf}, + {0xc2, 0xb0}, {0xc2, 0xb1}, {0xc2, 0xb2}, {0xc2, 0xb3}, {0xc2, 0xb4}, {0xc2, 0xb5}, {0xc2, 0xb6}, {0xc2, 0xb7}, + {0xc2, 0xb8}, {0xc2, 0xb9}, {0xc2, 0xba}, {0xc2, 0xbb}, {0xc2, 0xbc}, {0xc2, 0xbd}, {0xc2, 0xbe}, {0xc2, 0xbf}, + {0xc3, 0x80}, {0xc3, 0x81}, {0xc3, 0x82}, {0xc3, 0x83}, {0xc3, 0x84}, {0xc3, 0x85}, {0xc3, 0x86}, {0xc3, 0x87}, + {0xc3, 0x88}, {0xc3, 0x89}, {0xc3, 0x8a}, {0xc3, 0x8b}, {0xc3, 0x8c}, {0xc3, 0x8d}, {0xc3, 0x8e}, {0xc3, 0x8f}, + {0xc3, 0x90}, {0xc3, 0x91}, {0xc3, 0x92}, {0xc3, 0x93}, {0xc3, 0x94}, {0xc3, 0x95}, {0xc3, 0x96}, {0xc3, 0x97}, + {0xc3, 0x98}, {0xc3, 0x99}, {0xc3, 0x9a}, {0xc3, 0x9b}, {0xc3, 0x9c}, {0xc3, 0x9d}, {0xc3, 0x9e}, {0xc3, 0x9f}, + {0xc3, 0xa0}, {0xc3, 0xa1}, {0xc3, 0xa2}, {0xc3, 0xa3}, {0xc3, 0xa4}, {0xc3, 0xa5}, {0xc3, 0xa6}, {0xc3, 0xa7}, + {0xc3, 0xa8}, {0xc3, 0xa9}, {0xc3, 0xaa}, {0xc3, 0xab}, {0xc3, 0xac}, {0xc3, 0xad}, {0xc3, 0xae}, {0xc3, 0xaf}, + {0xc3, 0xb0}, {0xc3, 0xb1}, {0xc3, 0xb2}, {0xc3, 0xb3}, {0xc3, 0xb4}, {0xc3, 0xb5}, {0xc3, 0xb6}, {0xc3, 0xb7}, + {0xc3, 0xb8}, {0xc3, 0xb9}, {0xc3, 0xba}, {0xc3, 0xbb}, {0xc3, 0xbc}, {0xc3, 0xbd}, {0xc3, 0xbe}, {0xc3, 0xbf}, +}; + +/* Each byte is U+00XX ("binary" normalizes here upstream): a byte below + * 0x80 is one UTF-8 byte and any other byte is two. Eight bytes without a + * high bit copy as one word; other bytes take their two table bytes and + * advance by their length, without branches (the terminator slot holds + * the last spare byte). */ +/* The number of bytes from 0x80: eight per step, each step adding its + * high bits into byte lanes that are summed every 255 steps. */ +static size_t scr_bytes_count_high(const uint8_t *in, size_t n) { + const uint64_t ones = UINT64_C(0x0101010101010101); + const uint64_t pairs = UINT64_C(0x00ff00ff00ff00ff); + size_t high = 0, i = 0; + while (n - i >= 8) { + size_t steps = (n - i) / 8 < 255 ? (n - i) / 8 : 255; + uint64_t lanes = 0; + for (size_t end = i + steps * 8; i < end; i += 8) { + uint64_t w; + memcpy(&w, in + i, 8); + lanes += (w >> 7) & ones; + } + lanes = (lanes & pairs) + ((lanes >> 8) & pairs); + high += (size_t)((lanes * UINT64_C(0x0001000100010001)) >> 48); + } + for (; i < n; i++) high += in[i] >> 7; + return high; +} + +static ScrStr *scr_bytes_latin1_text(const uint8_t *in, size_t n) { + size_t high = scr_bytes_count_high(in, n); + if (n > SIZE_MAX - high) scr_bytes_oom(); + ScrStr *s = scr_str_alloc_raw(n + high, n + high); + uint8_t *out = (uint8_t *)s->data; + if (high == 0) { + memcpy(out, in, n); + out[n] = '\0'; + scr_str_note_ascii(s); + return s; + } + const uint64_t top = UINT64_C(0x8080808080808080); + size_t i = 0, o = 0; + while (n - i >= 8) { + uint64_t w; + memcpy(&w, in + i, 8); + if (!(w & top)) { + memcpy(out + o, &w, 8); + i += 8; + o += 8; + continue; + } + for (size_t end = i + 8; i < end; i++) { + memcpy(out + o, scr_latin1_utf8[in[i]], 2); + o += 1 + (in[i] >> 7); + } + } + for (; i < n; i++) { + memcpy(out + o, scr_latin1_utf8[in[i]], 2); + o += 1 + (in[i] >> 7); + } + out[o] = '\0'; + scr_str_note_utf16_len(s, n); + return s; +} + ScrStr *scr_bytes_to_str(const ScrBytes *b, const ScrStr *enc) { SCR_BYTES_SNAPSHOT(b); const uint8_t *in = b->data; @@ -2512,21 +2613,12 @@ ScrStr *scr_bytes_to_str(const ScrBytes *b, const ScrStr *enc) { out[i * 2 + 1] = scr_hex_digits[in[i] & 0xf]; } out[n * 2] = '\0'; + scr_str_note_ascii(s); return s; } if (scr_enc_is(enc, "base64")) return scr_bytes_encode_b64(in, n, false); if (scr_enc_is(enc, "base64url")) return scr_bytes_encode_b64(in, n, true); - if (scr_enc_is(enc, "latin1")) { - /* Each byte is U+00XX ("binary" normalizes here upstream). */ - size_t high = 0; - for (size_t i = 0; i < n; i++) high += in[i] >> 7; - if (n > SIZE_MAX - high) scr_bytes_oom(); - ScrStr *s = scr_str_alloc_raw(n + high, n + high); - size_t o = 0; - for (size_t i = 0; i < n; i++) o = scr_bytes_put_cp(s->data, o, in[i]); - s->data[o] = '\0'; - return s; - } + if (scr_enc_is(enc, "latin1")) return scr_bytes_latin1_text(in, n); if (scr_enc_is(enc, "ascii")) { /* Node's ascii decode masks the high bit: byte & 0x7f. */ ScrStr *s = scr_str_alloc_raw(n, n); @@ -2621,12 +2713,25 @@ ScrStr *scr_bytes_to_str_checked_range(const ScrBytes *b, const ScrStr *enc, return out; } -static int scr_hex_val(uint8_t c) { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - return -1; -} +/* Hex digit values, -1 for any other byte. */ +static const int8_t scr_hex_values[256] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +}; /* Both alphabets are accepted by both Buffer base64 encoding names. */ static const int8_t scr_b64_values[256] = { @@ -2720,17 +2825,71 @@ static ScrBytes *scr_bytes_encode_str(ScrBytesSink *sink, const ScrStr *s, const size_t n = s->len; if (scr_enc_is(enc, "latin1") || scr_enc_is(enc, "ascii")) { /* Node writes charCodeAt(i) & 0xFF for BOTH spellings — an astral - * code point contributes its two surrogates' low bytes. */ + * code point contributes its two surrogates' low bytes. ASCII text + * copies. Elsewhere eight ASCII bytes copy as one word. In eight bytes + * holding two or more high bytes and no sequence longer than two + * bytes, every continuation byte's output byte (the code point's low + * byte, from the continuation and the lead before it) is computed in + * one word; each byte is then stored without branches, a continuation + * replacing the byte its lead wrote. */ ScrBytes *b = scr_bytes_conversion_storage_in(sink, n); + if (scr_str_known_ascii(s)) { + memcpy(b->data, in, n); + b->len = n; + return b; + } + /* A local copy: a byte store could otherwise change b->data. */ + uint8_t *dst = b->data; + const uint64_t top = UINT64_C(0x8080808080808080); size_t o = 0, i = 0; while (i < n) { + if (n - i >= 8) { + uint64_t w; + memcpy(&w, in + i, 8); + uint64_t high = w & top; + if (!high) { + memcpy(dst + o, &w, 8); + i += 8; + o += 8; + continue; + } +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + /* Byte k of the input is bits 8k..8k+7 of w. */ + if (i > 0 && (high & (high - 1)) && !(w & (w << 1) & (w << 2) & top)) { + uint64_t prev = (w << 8) | in[i - 1]; + uint64_t cont = w & ~(w << 1) & top; + uint64_t low = ((prev & UINT64_C(0x0303030303030303)) << 6) | + (w & UINT64_C(0x3f3f3f3f3f3f3f3f)); + uint64_t mask = (cont >> 7) * 0xff; + uint64_t out = (w & ~mask) | (low & mask); + for (unsigned k = 0; k < 8; k++) { + o -= (size_t)(cont >> (8 * k + 7)) & 1; + dst[o++] = (uint8_t)(out >> (8 * k)); + } + i += 8; + continue; + } +#endif + } + unsigned c = in[i]; + if (c < 0x80) { + dst[o++] = (uint8_t)c; + i++; + continue; + } + if (c >> 6 == 2) { + /* The rest of a two-byte sequence whose lead ended a word. */ + dst[o - 1] = (uint8_t)(((in[i - 1] & 3u) << 6) | (c & 0x3f)); + i++; + continue; + } uint32_t cp = scr_bytes_next_cp(in, &i); if (cp > 0xffff) { cp -= 0x10000; - b->data[o++] = (uint8_t)(0xd800 + (cp >> 10)); - b->data[o++] = (uint8_t)(0xdc00 + (cp & 0x3ff)); + dst[o++] = (uint8_t)(0xd800 + (cp >> 10)); + dst[o++] = (uint8_t)(0xdc00 + (cp & 0x3ff)); } else { - b->data[o++] = (uint8_t)cp; + dst[o++] = (uint8_t)cp; } } b->len = o; /* never grows past n: multi-byte cps shrink */ @@ -2762,7 +2921,7 @@ static ScrBytes *scr_bytes_encode_str(ScrBytesSink *sink, const ScrStr *s, const /* Node interprets encoded text as the low byte of each UTF-16 unit. * ASCII can borrow storage; non-ASCII must preserve that truncation. */ ScrBytes *narrowed = NULL; - if (scr_bytes_ascii_prefix(in, n) != n) { + if (!scr_str_known_ascii(s) && scr_bytes_ascii_prefix(in, n) != n) { ScrStr *latin1 = scr_str_new("latin1", 6); narrowed = scr_bytes_encode_str(NULL, s, latin1); scr_str_release(latin1); @@ -2773,11 +2932,12 @@ static ScrBytes *scr_bytes_encode_str(ScrBytesSink *sink, const ScrStr *s, const if (hex) { /* Stop at the first invalid pair, discarding an odd tail. */ b = scr_bytes_conversion_storage_in(sink, n / 2); - size_t o = 0; - for (size_t i = 0; i + 1 < n; i += 2) { - int hi = scr_hex_val(in[i]), lo = scr_hex_val(in[i + 1]); - if (hi < 0 || lo < 0) break; - b->data[o++] = (uint8_t)((hi << 4) | lo); + uint8_t *dst = b->data; + size_t pairs = n / 2, o = 0; + for (; o < pairs; o++) { + int hi = scr_hex_values[in[2 * o]], lo = scr_hex_values[in[2 * o + 1]]; + if ((hi | lo) < 0) break; + dst[o] = (uint8_t)((hi << 4) | lo); } b->len = o; } else { diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 2d7f46d85..a5cc15ba8 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -1991,6 +1991,19 @@ double scr_str_utf16_len(ScrStr *s) { return (double)(int64_t)units; } +/* Records the UTF-16 length of a string whose producer counted it (latin1 + * text has one unit per byte), so scr_str_utf16_len does not walk it. Only + * strings that scr_str_utf16_len would otherwise record are kept. */ +void scr_str_note_utf16_len(ScrStr *s, size_t units) { + if (s->ascii || s->len <= 64 || s->rc == SIZE_MAX) return; + if (!scr_u16len_tab) { + scr_u16len_tab = calloc(SCR_U16LEN_N, sizeof *scr_u16len_tab); + if (!scr_u16len_tab) return; + scr_sidx_register_cleanup(); + } + *scr_u16len_slot(s) = (ScrU16Len){s, units}; +} + double scr_str_char_code_at(ScrStr *s, double i) { /* A sequential scan of a non-ASCII receiver stays inside the bounded * ASCII span cached at its last resolved position. Its unit offset is an diff --git a/packages/runtime/src/scr_string.h b/packages/runtime/src/scr_string.h index fc2f15562..e269498b1 100644 --- a/packages/runtime/src/scr_string.h +++ b/packages/runtime/src/scr_string.h @@ -83,6 +83,9 @@ static inline void scr_str_hash_forget(ScrStr *s) { /* True only when every byte is proven below 0x80 (see above). */ static inline bool scr_str_known_ascii(const ScrStr *s) { return s->ascii; } +/* Record the UTF-16 length a string's producer counted (scr_string.c). */ +void scr_str_note_utf16_len(ScrStr *s, size_t units); + /* Record a proven-ASCII fact. Immortal strings may live in read-only * memory and are shared across threads, so they are never written. */ static inline void scr_str_note_ascii(ScrStr *s) { diff --git a/tests/corpus/8005-buffer-encoding-paths.ts b/tests/corpus/8005-buffer-encoding-paths.ts new file mode 100644 index 000000000..014f22abf --- /dev/null +++ b/tests/corpus/8005-buffer-encoding-paths.ts @@ -0,0 +1,105 @@ +// Buffer latin1, ascii, hex and base64 conversions in both directions: +// ASCII, binary and mixed contents at sizes around eight-byte words and the +// pool threshold, strings with code points past U+00FF, invalid hex and +// base64 text, and the lengths and characters of the strings produced. + +let seed = 12345; +function next(): number { + seed = (seed * 1103515245 + 12345) & 0x7fffffff; + return seed >>> 8; +} + +function fill(size: number, kind: string): Buffer { + const buf = Buffer.alloc(size); + for (let i = 0; i < size; i++) { + const r = next(); + if (kind === "ascii") buf[i] = 32 + (r % 95); + else if (kind === "binary") buf[i] = r & 255; + else if (kind === "high") buf[i] = 128 + (r % 128); + else buf[i] = r % 23 === 0 ? 160 + (r % 96) : 97 + (r % 26); + } + return buf; +} + +function digest(text: string): number { + let h = 0; + for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) >>> 0; + return h; +} + +const sizes = [0, 1, 7, 8, 9, 15, 16, 17, 63, 64, 65, 4095, 4096, 4097, 70001]; +for (const kind of ["ascii", "binary", "high", "sparse"]) { + const rows: string[] = []; + for (const size of sizes) { + const buf = fill(size, kind); + const latin1 = buf.toString("latin1"); + const back = Buffer.from(latin1, "latin1"); + const ascii = Buffer.from(latin1, "ascii"); + const binary = Buffer.from(buf.toString("binary"), "binary"); + const hex = buf.toString("hex"); + const b64 = buf.toString("base64"); + const url = buf.toString("base64url"); + rows.push( + [ + size, + latin1.length, + digest(latin1), + back.equals(buf), + ascii.equals(buf), + binary.equals(buf), + Buffer.from(hex, "hex").equals(buf), + Buffer.from(b64, "base64").equals(buf), + Buffer.from(url, "base64url").equals(buf), + hex.length, + b64.length, + Buffer.byteLength(latin1, "latin1"), + size > 2 ? latin1.charCodeAt(size - 2) : -1, + size > 4 ? latin1.slice(1, 4).length : -1, + ].join(","), + ); + } + console.log(kind, rows.join(" ")); +} + +// Strings with code points past U+00FF keep the low byte of each UTF-16 +// unit, and an astral code point gives its two surrogates' low bytes. +const wide = [ + "abcéÿ", + "ĀŁ߿", + "xࠀ€￿y", + "\u{1F600}a\u{10FFFF}", + "é".repeat(9) + "€" + "è".repeat(9), + "aaaaaaaébbbbbbbb中cccccccc\u{1F600}", + "ÀÁÂÃÄÅÆÇࠀÈ", +]; +for (const text of wide) { + console.log( + Buffer.from(text, "latin1").toString("hex"), + Buffer.from(text, "ascii").toString("hex"), + Buffer.byteLength(text, "latin1"), + ); +} +const mixed = "é".repeat(5000) + "中" + "z".repeat(5000) + "ÿ".repeat(3000); +const mixedBytes = Buffer.from(mixed, "latin1"); +console.log(mixedBytes.length, mixedBytes[0], mixedBytes[5000], mixedBytes[5001], mixedBytes[13000]); + +// Hex stops at the first invalid pair and drops an odd last digit. +for (const text of ["0aFf", "0aF", "0a1g22", "zz00", "", "ABCDEF0123456789", "0aİ0b", "00a"]) { + console.log(JSON.stringify(text), Buffer.from(text, "hex").toString("hex"), Buffer.byteLength(text, "hex")); +} +const longHex = "0123456789abcdef".repeat(600) + "0x" + "ff".repeat(10); +console.log(Buffer.from(longHex, "hex").length); + +// base64 skips characters outside both alphabets and stops at padding. +for (const text of ["aGVsbG8=", "aGVsbG8", "aGV sbG8=", "aGV\nsbG8", "-_-_", "+/+/", "aGVsbG8=d29ybGQ=", "a", "ab", "abc", "aébc", "====", "YQ"]) { + console.log(JSON.stringify(text), Buffer.from(text, "base64").toString("hex"), Buffer.from(text, "base64url").toString("hex")); +} + +// Strings produced by hex and base64 behave as ASCII text afterwards. +const produced = fill(5000, "binary"); +const producedHex = produced.toString("hex"); +const joined = producedHex + "é" + produced.toString("base64"); +console.log(joined.length, joined.indexOf("é"), digest(joined.slice(9990, 10020))); +const latinText = fill(9000, "high").toString("latin1"); +const appended = latinText + "€"; +console.log(latinText.length, appended.length, appended.charCodeAt(8999), appended.charCodeAt(9000), appended.slice(8998).length); diff --git a/tests/harness/corpus-costs/8005-buffer-encoding-paths.ts.json b/tests/harness/corpus-costs/8005-buffer-encoding-paths.ts.json new file mode 100644 index 000000000..3aafbe296 --- /dev/null +++ b/tests/harness/corpus-costs/8005-buffer-encoding-paths.ts.json @@ -0,0 +1 @@ +{ "plain": 2558, "san": 4160 } From 20041b4262e9be646fa30215f5efb3300925bdea Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 23:27:15 -0500 Subject: [PATCH 165/273] Keep speculative copies in typed-array loops over shared storage In a program that can start a worker, a typed-array loop version's guard requires thread-owned storage, so every view of a SharedArrayBuffer runs the version's checked copy. That copy gave its loops no speculative or int32 kind copies, on the assumption that it rarely runs, so loops over shared views, such as k-nucleotide's frequency count in every worker, ran only the double copy. A version whose guard requires thread-owned storage now keeps those copies in its checked copy. --- .../src/backend/llvm/emit-statements.ts | 9 ++++-- .../test/speculative-loop-emission.test.ts | 30 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 378c82bee..63bae226e 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -2061,7 +2061,8 @@ function emitCallbackVersionedLoop( /** The copy of a versioned loop that runs when its guard fails. Its loops * get no speculative or int32 kind copies, which would emit each loop again * for a path that rarely runs. Proven integer loops, which emit one copy, - * are kept. */ + * are kept. A typed-array version whose guard requires thread-owned + * storage does not use it (emitVersionedLoop). */ function emitSlowCopy(emitter: LlEmitter, s: IrStmt): void { const copies = emitter.loopCopiesEnabled; emitter.loopCopiesEnabled = false; @@ -2145,7 +2146,11 @@ function emitVersionedLoop( LOOPS.of(emitter).bytesLoopVersioning.add(s); try { B.startBlock(slow); - emitSlowCopy(emitter, s); + // A guard that requires thread-owned storage fails for every view of + // shared memory, which worker programs pass routinely: that checked + // copy keeps its loops' speculative and int32 kind copies. + if (version.ownedReceivers.length > 0) emitter.emitStmt(s); + else emitSlowCopy(emitter, s); B.br(done); if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]); if (version.ranges.size > 0) diff --git a/packages/compiler/test/speculative-loop-emission.test.ts b/packages/compiler/test/speculative-loop-emission.test.ts index 3f6aef158..748fd50c5 100644 --- a/packages/compiler/test/speculative-loop-emission.test.ts +++ b/packages/compiler/test/speculative-loop-emission.test.ts @@ -129,3 +129,33 @@ if (isMainThread) { expect(order.slice(slow, fast).some((l) => l.startsWith("integer.leave"))).toBe(true); expect(order.slice(fast).some((l) => l.startsWith("integer.leave"))).toBe(true); }); + +test("the shared-storage copy of a typed-array version keeps its speculative copy", async () => { + // In a program that can start a worker, the guard requires thread-owned + // storage, so every view of a SharedArrayBuffer runs the checked copy. + const llvm = await emitLlvm(` +import { Worker, isMainThread } from "node:worker_threads"; +function trim(value: number, bits: number): number { + return value - Math.floor(value / Math.pow(2, bits)) * Math.pow(2, bits); +} +function key(bytes: Uint8Array, bits: number): number { + let k = 0; + for (let i = 0; i < bytes.length; i++) k = trim(k, bits) * 4 + bytes[i]!; + return k; +} +if (isMainThread) { + if (process.argv[2] === "spawn") new Worker(new URL(import.meta.url)); + const shared = new Uint8Array(new SharedArrayBuffer(4)); + shared.set([1, 2, 3, 0]); + console.log(key(shared, 6), key(new Uint8Array([3, 2, 1, 0]), 6)); +} +`); + const fn = /^define internal double @sc_bf_\w*key[\w.]*\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + const order = labels(fn); + const slow = order.findIndex((l) => l.startsWith("bytes.version.slow")); + const fast = order.findIndex((l) => l.startsWith("bytes.version.fast")); + expect(slow).toBeGreaterThanOrEqual(0); + expect(fast).toBeGreaterThan(slow); + expect(order.slice(slow, fast).some((l) => l.startsWith("spec.fast"))).toBe(true); + expect(order.slice(fast).some((l) => l.startsWith("spec.fast"))).toBe(true); +}); From f06f47aca1fbd4d2af6a354ce206750f324569e7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 23:27:46 -0500 Subject: [PATCH 166/273] Resolve promises with an argument in the rejectUnauthorized corpus program 8179-tls-reject-unauthorized-values.mjs called the resolve function of an untyped new Promise() with no argument. tsgo types that function as taking one argument and reported SC0001 for each call, while tsc-ts accepted them, so the checker-identity comparison failed. The calls now pass undefined. --- tests/corpus/8179-tls-reject-unauthorized-values.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/corpus/8179-tls-reject-unauthorized-values.mjs b/tests/corpus/8179-tls-reject-unauthorized-values.mjs index d0275ab4a..4e87099d4 100644 --- a/tests/corpus/8179-tls-reject-unauthorized-values.mjs +++ b/tests/corpus/8179-tls-reject-unauthorized-values.mjs @@ -31,10 +31,10 @@ for (const [label, value] of values) { c.on("secureConnect", () => events.push("client secureConnect")); c.on("data", (d) => events.push(`client data ${d}`)); c.on("error", (e) => events.push(`client error ${e.code}`)); - c.on("close", () => setTimeout(done, 50)); + c.on("close", () => setTimeout(() => done(undefined), 50)); }); console.log("rejectUnauthorized", label, "|", events.join(" ; ")); - await new Promise((res) => server.close(() => res())); + await new Promise((res) => server.close(() => res(undefined))); } { @@ -47,7 +47,7 @@ for (const [label, value] of values) { c.on("secureConnect", () => events.push(`secureConnect authorized=${c.authorized} error=${c.authorizationError}`)); c.on("data", (d) => events.push(`data ${d}`)); c.on("error", (e) => events.push(`error ${e.code}`)); - c.on("close", () => done()); + c.on("close", () => done(undefined)); }); console.log("tls.connect rejectUnauthorized", label, "|", events.join(" ; ")); } From 182a108270f0884f856335e6d58c0e256e11131e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 23:41:28 -0500 Subject: [PATCH 167/273] Correct the native compiler's nursery trigger comment --- packages/compiler/native/host.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/compiler/native/host.c b/packages/compiler/native/host.c index 4e1a0b754..24d330cc0 100644 --- a/packages/compiler/native/host.c +++ b/packages/compiler/native/host.c @@ -139,10 +139,12 @@ void scriptc_native_bound_cycle_growth(void) { /* The runtime's nursery trigger (scr_rc.h). */ void scr_cyc_set_nursery_threshold(long threshold); -/* Runs nursery passes every 1024 new candidates instead of every 256. It - * has to be in place before the compiler's modules initialize: set from - * main it leaves the build's collection work as it is. SCR_CYCLE_THRESHOLD - * still overrides it. */ +/* Runs nursery passes every 1024 new candidates instead of every 256. Much + * of a build's cyclic garbage outlives 256 releases: in the shorter window + * it survives a nursery pass, is promoted, and only full passes over the + * whole live heap find it. A constructor sets it before the compiler's + * modules initialize, so every pass of a build uses it. + * SCR_CYCLE_THRESHOLD still overrides it. */ __attribute__((constructor)) static void scriptc_native_widen_cycle_nursery(void) { scr_cyc_set_nursery_threshold(1024); } From 3e115ce08c8aff475cf6f125e34f2282c4c52e3a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 23:47:41 -0500 Subject: [PATCH 168/273] Cache absent properties in the dynamic tier's property caches A read site's cache only remembered properties it found on the receiver or its first prototype. Reads of properties an object lacks, which validation libraries make constantly (zod reads ctx.skipChecks, payload.memo, x.aborted and _zod.optin on every check), walked the whole shape chain and then repeated the lookup on the generic path every time: 7.9 million such reads in one zod benchmark run. - Read sites call a new probe, scr_js_get_probe, that answers undefined for a key the receiver and its chain of plain shaped objects (at most one prototype, ending in the default prototype) lack, and caches that answer. The entry holds while the receiver's shape, its prototype link, and that prototype's shape are unchanged. Its tag and placeholder holder make the inline entry-0 test in compiled code miss, so the answer always comes from the probe. Method reads keep scr_js_load_probe, so a missing method still reaches the slow path and its "is not a function" error. Names of Object.prototype members are never cached as absent. - Receivers of one shape with different prototypes (zod's internals objects) each keep an entry instead of replacing one another. - A site whose four-way record is full consults a per-thread table keyed by shape and key before walking shapes, and fills it. --- packages/compiler/src/backend/llvm/js-ic.ts | 8 +- packages/compiler/test/js-ic-emission.test.ts | 4 +- .../corpus/absent-property-reads.mjs.json | 6 + packages/runtime/src/scr_js.h | 4 + packages/runtime/src/scr_js_object.c | 145 +++++++++++++++++- packages/runtime/src/scr_js_object.test.c | 120 +++++++++++++++ packages/runtime/src/scr_js_object.test.ts | 12 +- tests/corpus/absent-property-reads.mjs | 87 +++++++++++ .../absent-property-reads.mjs.json | 1 + 9 files changed, 370 insertions(+), 17 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/absent-property-reads.mjs.json create mode 100644 tests/corpus/absent-property-reads.mjs create mode 100644 tests/harness/corpus-costs/absent-property-reads.mjs.json diff --git a/packages/compiler/src/backend/llvm/js-ic.ts b/packages/compiler/src/backend/llvm/js-ic.ts index b8be1ecb1..b69d3199d 100644 --- a/packages/compiler/src/backend/llvm/js-ic.ts +++ b/packages/compiler/src/backend/llvm/js-ic.ts @@ -6,7 +6,8 @@ * * load ordinary object whose shape is entry 0's own-property shape: * read entry `slot` inline unless it is an accessor; otherwise - * scr_js_load_probe (polymorphic, prototype hits, misses) + * scr_js_get_probe (polymorphic, prototype hits, absent keys, + * misses) * store ordinary object whose shape is entry 0's replace shape, outside * an intact parse region, writable data entry: replace inline; * otherwise scr_js_store_probe (adds, polymorphic, misses) @@ -402,9 +403,10 @@ function emitGet(host: Host, e: ExprOf<"call">, callee: IrFunction, key: string) B.startBlock(object); inlineLoad(host, receiver.name, ic, INLINE_WAYS, INLINE_PROTO, (cell) => cell, hits, hit, probe); B.startBlock(probe); - host.declare(`declare ptr @scr_js_load_probe(ptr, ptr, ptr)`); + // The read's probe also answers keys the receiver's plain chain lacks. + host.declare(`declare ptr @scr_js_get_probe(ptr, ptr, ptr)`); const probed = B.tmp(); - B.line(`${probed} = call ptr @scr_js_load_probe(ptr ${receiver.name}, ptr ${name}, ptr ${ic})`); + B.line(`${probed} = call ptr @scr_js_get_probe(ptr ${receiver.name}, ptr ${name}, ptr ${ic})`); const found = B.tmp(); B.line(`${found} = icmp ne ptr ${probed}, null`); const probeEnd = B.currentLabel(); diff --git a/packages/compiler/test/js-ic-emission.test.ts b/packages/compiler/test/js-ic-emission.test.ts index 7a1d44f34..437ac34e4 100644 --- a/packages/compiler/test/js-ic-emission.test.ts +++ b/packages/compiler/test/js-ic-emission.test.ts @@ -48,7 +48,7 @@ console.log(read(o), at(dyn([5, 6]), 1), call(o), o.n); expect(llvm).toMatch( /@sc_js_ic_0 = internal (thread_local )?global \[21 x i64\] zeroinitializer/, ); - expect(body(llvm, "read")).toContain("@scr_js_load_probe("); + expect(body(llvm, "read")).toContain("@scr_js_get_probe("); expect(body(llvm, "write")).toContain("@scr_js_store_probe("); expect(body(llvm, "at")).toContain("@scr_js_keyed_probe("); expect(body(llvm, "call")).toContain("@scr_js_load_probe("); @@ -100,7 +100,7 @@ console.log(read(o), read(p), read(t), at(dyn([5, 6]), 1), at(p, "x"), at(t, "ta const mod = deserializeModule(await readFile(outPath, "utf8")); expect(validateModule(mod)).toEqual([]); const llvm = emitLlvmModule(mod); - expect(body(llvm, "read")).toContain("@scr_js_load_probe("); + expect(body(llvm, "read")).toContain("@scr_js_get_probe("); expect(body(llvm, "write")).toContain("@scr_js_store_probe("); expect(body(llvm, "at")).toContain("@scr_js_keyed_probe("); } finally { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/absent-property-reads.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/absent-property-reads.mjs.json new file mode 100644 index 000000000..3c0646fa9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/absent-property-reads.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/absent-property-reads.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_js.h b/packages/runtime/src/scr_js.h index 1d874b31e..3ff9becd3 100644 --- a/packages/runtime/src/scr_js.h +++ b/packages/runtime/src/scr_js.h @@ -231,6 +231,10 @@ typedef struct ScrJsIc { /* `receiver.key` for a plain data property: +1 value, or NULL. */ ScrDyn *scr_js_load_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic); +/* The same for a property read site, which also answers (+1) undefined for + * a key that the receiver and its plain prototype chain lack, and caches + * that answer. A method read uses scr_js_load_probe. */ +ScrDyn *scr_js_get_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic); /* `receiver.key = value` (value borrowed) when it replaces a writable own * data property or adds one the prototype chain lacks: true when done. */ bool scr_js_store_probe(ScrDyn *receiver, const ScrStr *key, ScrDyn *value, ScrJsIc *ic); diff --git a/packages/runtime/src/scr_js_object.c b/packages/runtime/src/scr_js_object.c index e09226fd1..e97651557 100644 --- a/packages/runtime/src/scr_js_object.c +++ b/packages/runtime/src/scr_js_object.c @@ -76,9 +76,21 @@ static SCR_TL size_t scr_js_filled_capacity; * which answer with a miss until the key set changes. */ static ScrJsShape scr_js_unshaped; +/* The second-level cache of load sites whose own record is full: one entry + * per (receiver shape, key bytes) slot, filled by the probe and checked like + * a site entry. It names shapes, so the shape reset clears it. */ +#define SCR_JS_SHARED_LOADS 2048 +typedef struct ScrJsSharedLoad { + const char *key; + ScrJsIcEntry entry; +} ScrJsSharedLoad; +static SCR_TL ScrJsSharedLoad *scr_js_shared_loads; + static void scr_js_shapes_reset(void) { for (size_t i = 0; i < scr_js_filled_count; i++) memset(scr_js_filled_ics[i], 0, sizeof *scr_js_filled_ics[i]); + free(scr_js_shared_loads); + scr_js_shared_loads = NULL; free(scr_js_filled_ics); scr_js_filled_ics = NULL; scr_js_filled_count = 0; @@ -244,38 +256,155 @@ static ScrDyn *scr_js_load_hit(ScrDyn *receiver, const ScrJsIcEntry *entry) { return found->accessor ? NULL : scr_dyn_retain(found->value); } -ScrDyn *scr_js_load_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic) { +/* An absent entry: its target is this tag and its holder is a placeholder + * that is never a prototype, so the inline test of entry 0 in compiled code + * (a prototype hit needs the receiver's prototype to be the holder) always + * sends such a receiver to the probe. Its holder_shape and slot name the one + * prototype the read walked past (shape and object), or NULL and 0 when the + * receiver had no prototype. */ +static const ScrJsShape scr_js_absent; +static const ScrDyn scr_js_absent_holder; + +/* Keys whose read on a plain object is not just its [[Get]]: the members of + * Object.prototype, which reads of an absent own property answer through + * the inherited-member path, and the error constructor read. */ +static bool scr_js_absent_cacheable(const ScrStr *key) { +#define SCR_JS_INHERITED(name) {name, sizeof name - 1} + static const struct { + const char *name; + size_t len; + } inherited[] = { + SCR_JS_INHERITED("constructor"), SCR_JS_INHERITED("__proto__"), + SCR_JS_INHERITED("toString"), SCR_JS_INHERITED("toLocaleString"), + SCR_JS_INHERITED("valueOf"), SCR_JS_INHERITED("hasOwnProperty"), + SCR_JS_INHERITED("isPrototypeOf"), SCR_JS_INHERITED("propertyIsEnumerable"), + SCR_JS_INHERITED("__defineGetter__"), SCR_JS_INHERITED("__defineSetter__"), + SCR_JS_INHERITED("__lookupGetter__"), SCR_JS_INHERITED("__lookupSetter__"), + }; +#undef SCR_JS_INHERITED + for (size_t i = 0; i < sizeof inherited / sizeof inherited[0]; i++) + if (key->len == inherited[i].len && memcmp(key->data, inherited[i].name, key->len) == 0) + return false; + return true; +} + +static ScrJsSharedLoad *scr_js_shared_load(const ScrJsShape *shape, const ScrStr *key) { + if (!scr_js_shared_loads) { + scr_js_shared_loads = calloc(SCR_JS_SHARED_LOADS, sizeof *scr_js_shared_loads); + if (!scr_js_shared_loads) scr_trap("scriptc: out of memory\n"); + } + uint64_t mixed = scr_key_mix((uint64_t)(uintptr_t)shape ^ ((uint64_t)(uintptr_t)key->data << 1)); + return &scr_js_shared_loads[mixed & (SCR_JS_SHARED_LOADS - 1)]; +} + +/* Whether an absent entry still holds for `receiver`, whose shape matched: + * its own keys lack the key, so the walked prototype (if any) must be the + * same object with the same shape, and the chain must end there. */ +static bool scr_js_absent_hit(const ScrDyn *receiver, const ScrJsIcEntry *entry) { + const ScrDyn *prototype = receiver->prototype; + const ScrDyn *walked = (const ScrDyn *)entry->slot; + if (!walked) return prototype == NULL; + return prototype == walked && walked->kind == SCR_DYN_OBJ && + walked->v.obj.shape == entry->holder_shape && walked->prototype == NULL; +} + +/* A load site's probe. With `absent`, a read of a key that the receiver and + * its chain of plain shaped objects (ending in the default prototype) lack + * answers undefined and caches that answer. Method reads leave absent + * methods to the slow path, which names the missing function. */ +/* The answer of a matching entry for `receiver`, or NULL when the entry no + * longer holds. */ +static ScrDyn *scr_js_entry_answer(ScrDyn *receiver, const ScrJsIcEntry *entry) { + if (entry->target == &scr_js_absent) + return scr_js_absent_hit(receiver, entry) ? scr_dyn_retain(scr_dyn_undefined()) : NULL; + return scr_js_load_hit(receiver, entry); +} + +/* The object an entry's answer depends on besides the receiver: the + * prototype it read from or walked past, or NULL for an own property. */ +static const ScrDyn *scr_js_entry_prototype(const ScrJsIcEntry *entry) { + if (entry->target == &scr_js_absent) return (const ScrDyn *)entry->slot; + return entry->target ? entry->holder : NULL; +} + +/* Records `entry` in the site and, when the site's record is full, in the + * shared slot. A matching entry whose answer failed is replaced when it + * depends on the same prototype (that prototype changed); one that names + * another prototype stays, so receivers of one shape with different + * prototypes each keep an entry. */ +static void scr_js_load_fill(ScrJsIc *ic, uint32_t stale, ScrJsSharedLoad *shared, + const ScrStr *key, ScrJsIcEntry entry) { + if (stale < ic->count && + scr_js_entry_prototype(&ic->entry[stale]) != scr_js_entry_prototype(&entry)) + stale = UINT32_MAX; + if (shared) *shared = (ScrJsSharedLoad){key->data, entry}; + scr_js_ic_add(ic, stale, entry); +} + +/* A load site's probe. With `absent`, a read of a key that the receiver and + * its chain of plain shaped objects (ending in the default prototype) lack + * answers undefined and caches that answer. Method reads leave absent + * methods to the slow path, which names the missing function. */ +static ScrDyn *scr_js_probe_load(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic, bool absent) { const ScrJsShape *shape = scr_js_shape_of(receiver); if (!shape) return NULL; uint32_t stale = UINT32_MAX; for (uint32_t i = 0; i < ic->count; i++) { if (ic->entry[i].shape != shape) continue; - ScrDyn *value = scr_js_load_hit(receiver, &ic->entry[i]); + ScrDyn *value = scr_js_entry_answer(receiver, &ic->entry[i]); if (value) return value; - stale = i; - break; + if (stale == UINT32_MAX) stale = i; + } + ScrJsSharedLoad *shared = NULL; + if (ic->count == SCR_JS_IC_WAYS) { + shared = scr_js_shared_load(shape, key); + if (shared->key == key->data && shared->entry.shape == shape && + (absent || shared->entry.target != &scr_js_absent)) { + ScrDyn *value = scr_js_entry_answer(receiver, &shared->entry); + if (value) return value; + } } size_t slot = scr_js_shape_find(shape, key->data, key->len); if (slot != SIZE_MAX) { const ScrDynEntry *found = &receiver->v.obj.entries[slot]; if (found->accessor) return NULL; - scr_js_ic_add(ic, stale, (ScrJsIcEntry){shape, NULL, NULL, NULL, slot}); + scr_js_load_fill(ic, stale, shared, key, (ScrJsIcEntry){shape, NULL, NULL, NULL, slot}); return scr_dyn_retain(found->value); } + absent = absent && scr_js_absent_cacheable(key); /* One prototype link: an ordinary object other than the builtin * Object.prototype (whose reads the generic path fences). */ ScrDyn *holder = receiver->prototype; - if (!holder) return NULL; + if (!holder) { + if (!absent) return NULL; + scr_js_load_fill(ic, stale, shared, key, + (ScrJsIcEntry){shape, &scr_js_absent, &scr_js_absent_holder, NULL, 0}); + return scr_dyn_retain(scr_dyn_undefined()); + } const ScrJsShape *holder_shape = scr_js_shape_of(holder); if (!holder_shape) return NULL; slot = scr_js_shape_find(holder_shape, key->data, key->len); - if (slot == SIZE_MAX) return NULL; + if (slot == SIZE_MAX) { + if (!absent || holder->prototype != NULL) return NULL; + scr_js_load_fill(ic, stale, shared, key, + (ScrJsIcEntry){shape, &scr_js_absent, &scr_js_absent_holder, holder_shape, + (size_t)holder}); + return scr_dyn_retain(scr_dyn_undefined()); + } const ScrDynEntry *found = &holder->v.obj.entries[slot]; if (found->accessor) return NULL; - scr_js_ic_add(ic, stale, (ScrJsIcEntry){shape, holder_shape, holder, NULL, slot}); + scr_js_load_fill(ic, stale, shared, key, (ScrJsIcEntry){shape, holder_shape, holder, NULL, slot}); return scr_dyn_retain(found->value); } +ScrDyn *scr_js_load_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic) { + return scr_js_probe_load(receiver, key, ic, false); +} + +ScrDyn *scr_js_get_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic) { + return scr_js_probe_load(receiver, key, ic, true); +} + /* ── stores ───────────────────────────────────────────────────────────── */ /* The generic store keeps a native Error's fields in sync with these keys diff --git a/packages/runtime/src/scr_js_object.test.c b/packages/runtime/src/scr_js_object.test.c index 6b6934dea..06b804759 100644 --- a/packages/runtime/src/scr_js_object.test.c +++ b/packages/runtime/src/scr_js_object.test.c @@ -103,8 +103,128 @@ static void immortal_objects_stay_unshaped(void) { scr_str_release(key); } +/* A read site's probe answers a key that the receiver and its plain + * prototype chain lack with undefined, and caches that answer until the + * receiver's keys, its prototype, or the prototype's keys change. */ +static ScrJsIc get_ic; +static ScrJsIc other_ic; + +/* Each site reads one key, so each key below has its own record. */ +static bool get_reads_undefined_at(ScrJsIc *ic, ScrDyn *object, const ScrStr *key) { + ScrDyn *value = scr_js_get_probe(object, key, ic); + if (!value) return false; + assert(value->kind == SCR_DYN_UNDEF); + scr_dyn_release(value); + return true; +} + +static bool get_reads_undefined(ScrDyn *object, const ScrStr *key) { + ScrDyn *value = scr_js_get_probe(object, key, &get_ic); + if (!value) return false; + assert(value->kind == SCR_DYN_UNDEF); + scr_dyn_release(value); + return true; +} + +static bool get_reads(ScrDyn *object, const ScrStr *key, double n) { + ScrDyn *value = scr_js_get_probe(object, key, &get_ic); + if (!value) return false; + assert(value->kind == SCR_DYN_NUM && value->v.num == n); + scr_dyn_release(value); + return true; +} + +static void absent_reads(void) { + ScrStr *own = scr_str_new("own", 3); + ScrStr *missing = scr_str_new("missing", 7); + ScrStr *inherited = scr_str_new("toString", 8); + + /* No prototype: undefined, from the cache the second time. */ + ScrDyn *object = object_with(own, 1); + memset(&get_ic, 0, sizeof get_ic); + assert(get_reads_undefined(object, missing)); + assert(get_ic.count == 1); + assert(get_reads_undefined(object, missing)); + assert(get_ic.count == 1); + /* Method reads and Object.prototype member names keep the slow path. */ + memset(&load_ic, 0, sizeof load_ic); + assert(!scr_js_load_probe(object, missing, &load_ic)); + memset(&other_ic, 0, sizeof other_ic); + assert(!get_reads_undefined_at(&other_ic, object, inherited)); + /* The key added: the same record answers its value. */ + scr_dyn_obj_set(object, missing->data, missing->len, scr_dyn_new_num(2)); + assert(get_reads(object, missing, 2)); + + /* One plain prototype that lacks the key, then gains it. */ + ScrDyn *prototype = object_with(own, 5); + ScrDyn *child = object_with(own, 6); + child->prototype = scr_dyn_retain(prototype); + memset(&get_ic, 0, sizeof get_ic); + assert(get_reads_undefined(child, missing)); + assert(get_reads_undefined(child, missing)); + scr_dyn_obj_set(prototype, missing->data, missing->len, scr_dyn_new_num(3)); + assert(get_reads(child, missing, 3)); + + /* A receiver of the same shape with another prototype that lacks the + * key: both answers stay cached. */ + ScrDyn *other_prototype = object_with(own, 7); + ScrDyn *sibling = object_with(own, 8); + sibling->prototype = scr_dyn_retain(other_prototype); + assert(get_reads_undefined(sibling, missing)); + assert(get_reads(child, missing, 3)); + assert(get_reads_undefined(sibling, missing)); + + /* A longer chain is left to the generic read, even for a receiver whose + * absent answer was cached. */ + ScrDyn *grandparent = object_with(own, 9); + other_prototype->prototype = scr_dyn_retain(grandparent); + assert(!get_reads_undefined(sibling, missing)); + + /* A prototype swapped on a cached receiver: the entry no longer holds. */ + ScrDyn *plain = object_with(own, 10); + memset(&get_ic, 0, sizeof get_ic); + assert(get_reads_undefined(plain, missing)); + plain->prototype = scr_dyn_retain(prototype); + assert(get_reads(plain, missing, 3)); + + /* More shapes than the record holds: the shared cache answers the reads + * that the full record misses, absent keys included. */ + memset(&get_ic, 0, sizeof get_ic); + memset(&other_ic, 0, sizeof other_ic); + ScrDyn *shapes[SCR_JS_IC_WAYS + 3]; + char name[16]; + for (size_t i = 0; i < sizeof shapes / sizeof shapes[0]; i++) { + int n = snprintf(name, sizeof name, "k%zu", i); + shapes[i] = object_with(own, (double)i); + scr_dyn_obj_set(shapes[i], name, (size_t)n, scr_dyn_new_num((double)i)); + } + for (int round = 0; round < 3; round++) + for (size_t i = 0; i < sizeof shapes / sizeof shapes[0]; i++) { + assert(get_reads(shapes[i], own, (double)i)); + assert(get_reads_undefined_at(&other_ic, shapes[i], missing)); + } + for (size_t i = 0; i < sizeof shapes / sizeof shapes[0]; i++) scr_dyn_release(shapes[i]); + + scr_dyn_release(plain); + scr_dyn_release(sibling); + scr_dyn_release(other_prototype); + scr_dyn_release(grandparent); + scr_dyn_release(child); + scr_dyn_release(prototype); + scr_dyn_release(object); + scr_str_release(inherited); + scr_str_release(missing); + scr_str_release(own); +} + int main(int argc, char **argv) { scr_init(); + /* Shapes count against the budget, so these checks run on their own. */ + if (argc > 1 && strcmp(argv[1], "absent") == 0) { + absent_reads(); + puts("absent read checks passed"); + return 0; + } immortal_objects_stay_unshaped(); if (argc > 1 && strcmp(argv[1], "key-bytes") == 0) { /* 64 KiB keys spend the key-byte budget after 64 shapes. */ diff --git a/packages/runtime/src/scr_js_object.test.ts b/packages/runtime/src/scr_js_object.test.ts index 0a5aa1d5b..1fb90cdaa 100644 --- a/packages/runtime/src/scr_js_object.test.ts +++ b/packages/runtime/src/scr_js_object.test.ts @@ -7,7 +7,7 @@ import { expect, test } from "vitest"; const exec = promisify(execFile); -test("dynamic-tier shapes stay within the shape and key-byte budgets", async () => { +test("dynamic-tier shapes stay within the shape and key-byte budgets, and absent reads are cached", async () => { const scratch = await mkdtemp(join(tmpdir(), "scriptc-js-object-")); const src = import.meta.dirname; const binary = join(scratch, "js-object"); @@ -50,15 +50,19 @@ test("dynamic-tier shapes stay within the shape and key-byte budgets", async () ].map((file) => join(src, file)), ...(process.platform === "linux" ? ["-D_GNU_SOURCE", "-lm"] : []), ]); - for (const args of [[], ["key-bytes"]]) { - const result = await exec(binary, args, { + for (const [args, stdout] of [ + [[], "shape budget checks passed\n"], + [["key-bytes"], "shape budget checks passed\n"], + [["absent"], "absent read checks passed\n"], + ] as const) { + const result = await exec(binary, [...args], { env: { ...process.env, ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1", UBSAN_OPTIONS: "halt_on_error=1", }, }); - expect(result.stdout).toBe("shape budget checks passed\n"); + expect(result.stdout).toBe(stdout); expect(result.stderr).toBe(""); } } finally { diff --git a/tests/corpus/absent-property-reads.mjs b/tests/corpus/absent-property-reads.mjs new file mode 100644 index 000000000..cd568b11c --- /dev/null +++ b/tests/corpus/absent-property-reads.mjs @@ -0,0 +1,87 @@ +// Reads of properties an object lacks, from the same untyped sites many +// times: they answer undefined until the object, its prototype, or the +// prototype link changes, and Object.prototype members still answer. +function read(object) { + return object.flag; +} +function readName(object) { + return object.name; +} +function describe(object) { + return [object.toString === Object.prototype.toString, typeof object.constructor, typeof object.hasOwnProperty].join(); +} +function callIt(object) { + try { + return object.run(); + } catch (e) { + return `${e.name}: ${e.message}`; + } +} + +const seen = []; +const plain = { name: "plain" }; +for (let i = 0; i < 3; i++) seen.push(read(plain)); +plain.flag = "added"; +seen.push(read(plain)); +delete plain.flag; +seen.push(read(plain)); +console.log(seen.join()); + +// A prototype that gains, changes and loses the key. +const base = { kind: "base" }; +const child = Object.create(base); +child.name = "child"; +const fromProto = []; +for (let i = 0; i < 3; i++) fromProto.push(read(child)); +base.flag = "inherited"; +fromProto.push(read(child)); +base.flag = "changed"; +fromProto.push(read(child)); +delete base.flag; +fromProto.push(read(child)); +console.log(fromProto.join()); + +// Objects of one shape with different prototypes, read from one site. +const left = Object.create({ flag: "left" }); +const right = Object.create({ other: 1 }); +left.name = "l"; +right.name = "r"; +const mixed = []; +for (let i = 0; i < 3; i++) mixed.push(read(left), read(right)); +console.log(mixed.join()); + +// Changing the prototype link of an object whose absent read was cached. +const moved = { name: "moved" }; +for (let i = 0; i < 3; i++) read(moved); +Object.setPrototypeOf(moved, { flag: "after setPrototypeOf" }); +console.log(read(moved)); +Object.setPrototypeOf(moved, null); +console.log(read(moved), readName(moved)); + +// A longer prototype chain. +const grand = { flag: "grand" }; +const parent = Object.create(grand); +const deep = Object.create(parent); +console.log(read(deep), read(Object.create(Object.create({})))); + +// Object.prototype members and missing methods still answer as in Node. +const bare = { name: "bare" }; +for (let i = 0; i < 3; i++) describe(bare); +console.log(describe(bare), describe({ other: 2 })); +for (let i = 0; i < 3; i++) callIt(bare); +console.log(callIt(bare)); +bare.run = () => "ran"; +console.log(callIt(bare)); + +// Many shapes through one site. +const shapes = []; +for (let i = 0; i < 12; i++) { + const object = { name: `s${i}` }; + object[`k${i}`] = i; + if (i % 3 === 0) object.flag = i; + shapes.push(object); +} +let total = 0; +for (let round = 0; round < 4; round++) + for (const object of shapes) total += read(object) === undefined ? 1 : 100; +console.log(total, shapes.map(readName).join()); diff --git a/tests/harness/corpus-costs/absent-property-reads.mjs.json b/tests/harness/corpus-costs/absent-property-reads.mjs.json new file mode 100644 index 000000000..a9e46230d --- /dev/null +++ b/tests/harness/corpus-costs/absent-property-reads.mjs.json @@ -0,0 +1 @@ +{ "plain": 8432, "san": 13661 } From 944cae84a8b4cc4c2f7b0af2af0dcf8793e72591 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 00:06:13 -0500 Subject: [PATCH 169/273] Keep an unchanged executable without selecting the runtime pack An unchanged rebuild that kept its output still selected the runtime pack's artifacts first, reading the stored selection, and computed the identity of every selected artifact to open the executable cache. - The executable cache key names the pack by its package.json text, the manifest's identity (its text on darwin), and the link features and flavor. Together they fix which artifacts the selection names. - The cache selects the artifacts only when it needs their identities, in restore() and trace(). keep() checks the recorded entry, which lists them. - A build that does not keep or restore its executable selects the pack before code generation, as before. --- packages/compiler/src/native/compiler.ts | 46 ++++++------- .../src/native/executable-cache-open.test.ts | 21 ++++-- .../src/native/executable-cache.test.ts | 31 +++++++++ .../compiler/src/native/executable-cache.ts | 67 ++++++++++++++++--- 4 files changed, 125 insertions(+), 40 deletions(-) diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts index e919090a9..64964d94f 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -30,6 +30,7 @@ import { verifyNativeHelper, } from "../backend/native-tools.js"; import { + type NativeRuntimeSelection, selectNativeRuntimePack, runtimePackManifest, stageNativeRuntimeSelection, @@ -400,34 +401,33 @@ export class NativeCompiler { outputKind === "exe" ? join(inputDirectory, "program" + target.outputSuffixes.obj) : stagedOutput; - const executablePack = - outputKind === "exe" - ? selectNativeRuntimePack( - toolchain.runtimePackRoot, - target, - toolchain.compilerVersion, - features, - optimization, - "executable", - this.runtimePackStore(), - ) - : null; + let executablePack: NativeRuntimeSelection | null = null; + const selectPack = (): NativeRuntimeSelection => + (executablePack ??= selectNativeRuntimePack( + toolchain.runtimePackRoot, + target, + toolchain.compilerVersion, + features, + optimization, + "executable", + this.runtimePackStore(), + )); const executableCache = - executablePack === null - ? null - : openNativeExecutableCache( - this.cache, - toolchain, - digestLlvm, - options, - ffi, - executablePack, - ); + outputKind === "exe" + ? openNativeExecutableCache(this.cache, toolchain, digestLlvm, options, ffi, { + root: toolchain.runtimePackRoot, + features, + flavor: optimization, + select: selectPack, + }) + : null; // An unchanged rebuild keeps the executable the last build left. const kept = executableCache?.keep(output) ?? false; const restored = kept || (executableCache?.restore(stagedOutput) ?? false); timing(restored ? "executable-cache-hit" : "executable-cache-miss"); if (!restored) { + // A missing or invalid runtime pack fails before code generation. + if (outputKind === "exe") selectPack(); let programObjects = [object]; if (options.sanitize) { runNativeTool(sanitizerDriver(toolchain), [ @@ -496,7 +496,7 @@ export class NativeCompiler { }, }; } - const pack = executablePack!; + const pack = selectPack(); const runtime = options.sanitize ? buildSanitizedRuntime(toolchain, pack, join(stage, "runtime"), "executable", this.cache) : stageNativeRuntimeSelection(pack, join(stage, "runtime"), this.runtimePackStore()); diff --git a/packages/compiler/src/native/executable-cache-open.test.ts b/packages/compiler/src/native/executable-cache-open.test.ts index 09a8b9cc9..0c3b2832f 100644 --- a/packages/compiler/src/native/executable-cache-open.test.ts +++ b/packages/compiler/src/native/executable-cache-open.test.ts @@ -2,9 +2,10 @@ import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:f import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, expect, test } from "vitest"; +import type { NativeLinkFeatures } from "../backend/native-link-info.js"; import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js"; import { NativeCache } from "./cache.js"; -import { openNativeExecutableCache } from "./executable-cache.js"; +import { type ExecutableRuntimePack, openNativeExecutableCache } from "./executable-cache.js"; import type { NativeToolchain } from "./toolchain.js"; const directories: string[] = []; @@ -50,14 +51,20 @@ function fixture(platform: string, driver: string) { linkerArgs: [], dsymutil: "dsymutil", } as unknown as NativeToolchain; - const pack = { + const pack: ExecutableRuntimePack = { root: runtime, - licenses: [], - packageText: "{}", - manifestText: "{}", - selected: { runtime: [], archives: [] }, + features: {} as NativeLinkFeatures, flavor: "release", - } as unknown as NativeRuntimeSelection; + select: () => + ({ + root: runtime, + licenses: [], + packageText: "{}", + manifestText: "{}", + selected: { runtime: [], archives: [] }, + flavor: "release", + }) as unknown as NativeRuntimeSelection, + }; const open = () => openNativeExecutableCache( new NativeCache(join(root, "cache")), diff --git a/packages/compiler/src/native/executable-cache.test.ts b/packages/compiler/src/native/executable-cache.test.ts index 97f9a3571..8bec9916e 100644 --- a/packages/compiler/src/native/executable-cache.test.ts +++ b/packages/compiler/src/native/executable-cache.test.ts @@ -300,3 +300,34 @@ test("a Linux build whose output changed links again, and then stores the execut expect(open().restore(f.destination)).toBe(true); expect(readFileSync(f.destination)).toEqual(readFileSync(f.output)); }); + +test("a Linux build that keeps its output does not read the deferred inputs", () => { + const f = fixture(); + const artifact = join(f.root, "inputs", "artifact.o"); + writeFileSync(artifact, "runtime pack artifact"); + const deferred = vi.fn(() => [artifact]); + const open = () => + new NativeExecutableCache(f.cache, f.key, "clang", false, [f.runtime], true, deferred); + const first = open(); + expect(first.trace(["-o", f.output], f.stage)).toBe(true); + first.recordOutput(f.output); + expect(deferred).toHaveBeenCalledTimes(1); + const recorded = ( + JSON.parse(f.cache.read("kept", f.key)!.toString()) as { inputs: { path: string }[] } + ).inputs.map((saved) => saved.path); + expect(recorded).toContain(artifact); + // The unchanged rebuild checks the recorded identities, the artifact's + // among them, without asking for the deferred inputs. + expect(open().keep(f.output)).toBe(true); + expect(deferred).toHaveBeenCalledTimes(1); + // A replaced artifact misses. + writeFileSync(artifact, "another runtime pack artifact"); + expect(open().keep(f.output)).toBe(false); + expect(deferred).toHaveBeenCalledTimes(1); + // restore() adds the deferred inputs before it checks an entry. + const second = open(); + expect(second.trace(["-o", f.output], f.stage)).toBe(true); + second.publish(f.output); + expect(open().restore(f.destination)).toBe(true); + expect(deferred).toHaveBeenCalledTimes(3); +}); diff --git a/packages/compiler/src/native/executable-cache.ts b/packages/compiler/src/native/executable-cache.ts index 0ce638d54..bafd2c020 100644 --- a/packages/compiler/src/native/executable-cache.ts +++ b/packages/compiler/src/native/executable-cache.ts @@ -9,6 +9,8 @@ import { toolchainEnvironmentFingerprint, } from "../backend/toolchain-environment.js"; import { runNativeTool } from "../backend/native-tools.js"; +import type { NativeLinkFeatures } from "../backend/native-link-info.js"; +import type { NativeOptimization } from "../backend/optimization.js"; import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js"; import type { NativeToolchain } from "./toolchain.js"; import { contentDigest, NativeCache } from "./cache.js"; @@ -204,6 +206,7 @@ export class NativeExecutableCache { private traced = false; /** The inputs of the entry restore() used. */ private restoredInputs: InputIdentity[] | null = null; + private deferredAdded = false; constructor( private readonly cache: NativeCache, @@ -214,10 +217,25 @@ export class NativeExecutableCache { /** Linux: trace with one verbose link, keep an unchanged output, and * store the executable from a key's second link on. */ private readonly linux = false, + /** Inputs that the key determines, such as the runtime pack's selected + * artifacts. keep() does not read them, because the entry it checks + * lists them; restore() and trace() add their identities first. */ + private readonly deferredPaths: () => string[] = () => [], ) { this.inputs = [...new Set(paths)].sort().map(identity); } + private addDeferredInputs(): void { + if (this.deferredAdded) return; + this.deferredAdded = true; + const listed = new Set(this.inputs.map((input) => input.path)); + for (const path of this.deferredPaths()) + if (!listed.has(path)) { + listed.add(path); + this.inputs.push(identity(path)); + } + } + /** Whether `recorded`, the inputs of an entry, still match and cover this * build's inputs. */ private covers(recorded: InputIdentity[]): boolean { @@ -249,6 +267,7 @@ export class NativeExecutableCache { try { const metadata = this.cache.read("executable", this.key); if (metadata === null) return false; + this.addDeferredInputs(); const entry: unknown = JSON.parse(metadata.toString("utf8")); if (!validEntry(entry) || !this.covers(entry.inputs)) return false; if (this.debug !== (entry.symbols !== null)) return false; @@ -275,6 +294,7 @@ export class NativeExecutableCache { trace(args: string[], stage: string): boolean { this.traced = false; try { + this.addDeferredInputs(); const roots = [resolve(stage), realpathSync(stage)]; let driverInputs = this.linux ? [] @@ -342,13 +362,24 @@ export class NativeExecutableCache { } } +/** The runtime pack an executable links: its directory, and the link features + * and flavor that select its artifacts. `select` reads and validates the + * manifest; the cache calls it only when it needs the artifacts' identities, + * so an unchanged rebuild that keeps its output never reads the manifest. */ +export interface ExecutableRuntimePack { + root: string; + features: NativeLinkFeatures; + flavor: NativeOptimization; + select: () => NativeRuntimeSelection; +} + export function openNativeExecutableCache( cache: NativeCache | null, toolchain: NativeToolchain, llvmDigest: () => string, options: CompileRequestOptions, ffi: FfiProfile | null, - pack: NativeRuntimeSelection, + pack: ExecutableRuntimePack, ): NativeExecutableCache | null { // Match the established complete-cache contract: the default Apple driver // or a Linux clang driver, known runtime inputs, and no wrapper or @@ -394,11 +425,19 @@ export function openNativeExecutableCache( join(toolchain.helperPackageRoot, "package.json"), join(toolchain.runtimePackRoot, "package.json"), join(toolchain.runtimePackRoot, "runtime-pack.json"), - ...[...pack.selected.runtime, ...pack.selected.archives, ...pack.licenses].map((artifact) => - join(pack.root, artifact.path), - ), ]; - if (platform === "linux") paths.push(join(pack.root, "runtime-pack.json")); + const manifest = join(pack.root, "runtime-pack.json"); + if (platform === "linux") paths.push(manifest); + // The manifest's identity, the features and the flavor in the key fix + // which artifacts the selection names. + const artifacts = (): string[] => { + const selection = pack.select(); + return [ + ...selection.selected.runtime, + ...selection.selected.archives, + ...selection.licenses, + ].map((artifact) => join(selection.root, artifact.path)); + }; if (debug) { const dsymutil = commandPath(toolchain.dsymutil); if (realpathSync(dsymutil) !== "/usr/bin/dsymutil") return null; @@ -410,13 +449,13 @@ export function openNativeExecutableCache( llvm: llvmDigest(), options, toolchain, - runtimeIdentity: pack.packageText, + runtimeIdentity: readFileSync(join(pack.root, "package.json"), "utf8"), // On Linux the manifest's identity names it: restore checks that // identity again, as it does every other input. runtimeManifest: - platform === "darwin" - ? pack.manifestText - : identity(join(pack.root, "runtime-pack.json")), + platform === "darwin" ? readFileSync(manifest, "utf8") : identity(manifest), + runtimeFeatures: pack.features, + runtimeFlavor: pack.flavor, linker: identity(linker), effective: identity(effective), environment: toolchainEnvironmentFingerprint(), @@ -424,7 +463,15 @@ export function openNativeExecutableCache( path: process.env["PATH"] ?? null, }), ); - return new NativeExecutableCache(cache, key, linker, debug, paths, platform === "linux"); + return new NativeExecutableCache( + cache, + key, + linker, + debug, + paths, + platform === "linux", + artifacts, + ); } catch { return null; } From be7fb3723568a2631d8788f2cbef9dd9dda171d7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 00:16:42 -0500 Subject: [PATCH 170/273] Expect the get probe at the native-array length site in the emission test The length read in the keyed and length emission test is a get site, which now calls scr_js_get_probe. --- packages/compiler/test/js-ic-emission.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/test/js-ic-emission.test.ts b/packages/compiler/test/js-ic-emission.test.ts index 437ac34e4..15e8e92bc 100644 --- a/packages/compiler/test/js-ic-emission.test.ts +++ b/packages/compiler/test/js-ic-emission.test.ts @@ -170,7 +170,7 @@ console.log(lib.total([1, 2, 3])); expect(fn).toContain("@scr_js_keyed_probe("); // The length read answers from the native array's length. expect(fn).toContain("icg.rest"); - expect(fn).toContain("@scr_js_load_probe("); + expect(fn).toContain("@scr_js_get_probe("); } finally { await rm(dir, { recursive: true, force: true }); } From 478feb15cd860605215de22605ec4ee299bef182 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 00:26:55 -0500 Subject: [PATCH 171/273] Call the declared base method for super calls in JavaScript method values Since 76735a8d9 (#1975), a `super.m()` call in a JavaScript class method whose `this` is the call-site receiver reflected the base class's whole prototype and read `m` from it. An instance's member lookup first tests its property bag, which inherits from its class's prototype data, so once the base's prototype was reflected, `child[k]` for a method the subclass overrides found the base's method there: `value[name](arg)` on a subclass instance called the base method. While no code reads a prototype method by name, `super.m()` now calls the declaring class's receiver-independent method value with the receiver, as the method's [[HomeObject]] lookup answers, without reflecting the base prototype. Programs that read prototype methods by name keep the reflected lookup. --- .../src/frontend/lowering/lower-classes.ts | 37 +++++++++++++------ .../main.ts.json | 7 ++++ .../8302-class-super-method-values/lib.cjs | 33 +++++++++++++++++ .../8302-class-super-method-values/main.ts | 26 +++++++++++++ .../package.json | 1 + .../main.ts.json | 1 + 6 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8302-class-super-method-values/main.ts.json create mode 100644 tests/corpus/8302-class-super-method-values/lib.cjs create mode 100644 tests/corpus/8302-class-super-method-values/main.ts create mode 100644 tests/corpus/8302-class-super-method-values/package.json create mode 100644 tests/harness/corpus-costs/8302-class-super-method-values/main.ts.json diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 333e11ff8..b125734f7 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -194,7 +194,10 @@ import { classPrototypeData, reflectClassPrototype } from "./class-prototypes.js 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 { + reflectedClassMethodValue, + reflectedClassStaticMethodValue, +} from "./class-method-values.js"; import { identifierReadSymbol } from "./surfaces.js"; export function returnsOnlyThis(member: ts.MethodDeclaration): boolean { @@ -9560,19 +9563,31 @@ export function lowerSuperMethodCall( // method up on the base class's complete prototype, as `super` reads // [[HomeObject]].[[Prototype]], and calls it with that receiver. const dynamicThis = thisRef.type.kind === "dyn"; + // While no code reads a prototype method by name (which could replace it), + // that method is the declaring class's own: its receiver-independent value + // is called directly. Reflecting the base's whole prototype instead would + // put its members on the prototype chain of every subclass instance's + // property bag, ahead of the subclasses' own compiled overrides. + const declared = + dynamicThis && !lowerer.prototypeMethodAccesses.has(access.name.text) + ? reflectedClassMethodValue(lowerer, found.declarer, access.name.text) + : null; if (dynamicThis || lowerer.prototypeMethodAccesses.has(access.name.text)) { - const prototype = - dynamicThis && cls.base.decl + const prototype = declared + ? null + : dynamicThis && cls.base.decl ? reflectClassPrototype(lowerer, cls.base, loc, classValueRef(lowerer, cls.base, access)) : classPrototypeData(lowerer, cls.base, loc); - if (prototype) { - const callee: IrExpr = { - kind: "dynKeyGet", - value: prototype, - key: { kind: "strLit", value: access.name.text, type: STRING, loc }, - type: DYN, - loc, - }; + if (declared || prototype) { + const callee: IrExpr = declared + ? lowerer.coerceToExpected(declared, DYN) + : { + kind: "dynKeyGet", + value: prototype!, + key: { kind: "strLit", value: access.name.text, type: STRING, loc }, + type: DYN, + loc, + }; // Spread arguments are spread into the dynamic call, as in a dynamic method call. const spreads: { arg: number; what: string }[] = []; const args = call.arguments.nodes.map((arg, index) => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8302-class-super-method-values/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8302-class-super-method-values/main.ts.json new file mode 100644 index 000000000..24fe2decc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8302-class-super-method-values/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/8302-class-super-method-values/lib.cjs", + "/tests/corpus/8302-class-super-method-values/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8302-class-super-method-values/lib.cjs b/tests/corpus/8302-class-super-method-values/lib.cjs new file mode 100644 index 000000000..4a8b1640a --- /dev/null +++ b/tests/corpus/8302-class-super-method-values/lib.cjs @@ -0,0 +1,33 @@ +"use strict"; +// Subclasses whose methods call super, read and called through computed and +// literal keys, as values called with call(), and directly. +class Base { + constructor(n) { this.n = n; } + scale(k) { return this.n * k; } + label() { return "base:" + this.n; } + plain(k) { return k; } +} +class Child extends Base { + scale(k) { return super.scale(k) * 10; } + label() { return "child(" + super.label() + ")"; } + plain(k) { return k * 100; } +} +class Grandchild extends Child { + scale(k) { return super.scale(k) + 1; } +} +class Sibling extends Base { + scale(k) { return -k; } + plain(k) { return super.plain(k) + 0.5; } +} +exports.make = function (i) { + switch (i % 4) { + case 0: return new Base(i); + case 1: return new Child(i); + case 2: return new Grandchild(i); + default: return new Sibling(i); + } +}; +exports.computed = function (value, name, arg) { return value[name](arg); }; +exports.viaCall = function (value, name, arg) { const f = value[name]; return f.call(value, arg); }; +exports.literal = function (value, arg) { return [value["scale"](arg), value["plain"](arg), value["label"]()].join(" "); }; +exports.direct = function (value, arg) { return [value.scale(arg), value.plain(arg), value.label()].join(" "); }; diff --git a/tests/corpus/8302-class-super-method-values/main.ts b/tests/corpus/8302-class-super-method-values/main.ts new file mode 100644 index 000000000..1a7edbb49 --- /dev/null +++ b/tests/corpus/8302-class-super-method-values/main.ts @@ -0,0 +1,26 @@ +// Methods of JavaScript subclasses that call super, read from instances +// through computed keys, literal keys and as values, and called directly. +import { createRequire } from "node:module"; + +interface Lib { + make(i: number): unknown; + computed(value: unknown, name: string, arg: number): unknown; + viaCall(value: unknown, name: string, arg: number): unknown; + literal(value: unknown, arg: number): string; + direct(value: unknown, arg: number): string; +} +const require = createRequire(import.meta.url); +const lib: Lib = require("./lib.cjs"); + +for (let i = 0; i < 8; i++) { + const value = lib.make(i); + console.log( + i, + lib.computed(value, "scale", 3), + lib.computed(value, "plain", 3), + lib.computed(value, "label", 0), + lib.viaCall(value, "scale", 2), + lib.viaCall(value, "label", 0), + ); + console.log(i, lib.literal(value, 5), "|", lib.direct(value, 5)); +} diff --git a/tests/corpus/8302-class-super-method-values/package.json b/tests/corpus/8302-class-super-method-values/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/8302-class-super-method-values/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/harness/corpus-costs/8302-class-super-method-values/main.ts.json b/tests/harness/corpus-costs/8302-class-super-method-values/main.ts.json new file mode 100644 index 000000000..30437efb4 --- /dev/null +++ b/tests/harness/corpus-costs/8302-class-super-method-values/main.ts.json @@ -0,0 +1 @@ +{ "plain": 4797 } From e5ba0734680922fe58128355f71582515d12467f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 00:44:25 -0500 Subject: [PATCH 172/273] Parse JSON into discriminated unions in one pass `JSON.parse(text) as T` for a discriminated union of records took the checked-dynamic route: it parsed the text into a dynamic tree, matched and copied that tree into records, then freed it. The one-pass schema parser now builds these unions directly, as record members, array elements and whole documents. For an object, a shallow scan of its members finds the discriminant's value, which selects the one record arm that holds that literal, and the object is parsed with that arm's schema. An object with a missing, non-string or unknown discriminant, or any member the arm rejects, declines, and the checked route then gives the result or the error as before. The compiler admits unions whose record arms each name their string literals, every literal naming one arm, plus null and undefined arms. --- packages/compiler/src/backend/llvm/dyn.ts | 86 ++++++++++-- .../test/json-schema-parse-emission.test.ts | 58 +++++++- ...43-json-parse-discriminated-unions.ts.json | 6 + packages/runtime/src/scr_json.c | 129 ++++++++++++++++-- packages/runtime/src/scr_json.h | 32 ++++- .../8243-json-parse-discriminated-unions.ts | 119 ++++++++++++++++ 6 files changed, 396 insertions(+), 34 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8243-json-parse-discriminated-unions.ts.json create mode 100644 tests/corpus/8243-json-parse-discriminated-unions.ts diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 9a91db0ca..54ce1bece 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -1480,8 +1480,9 @@ export class LlDyn { * fields, and at least one unless an index signature collects the * members), record members typed as unions of those leaves, records and * arrays, null and undefined (optional and nullable members; see - * jsonUnionSupported), and index signatures whose value type is not a - * union. Everything else keeps the checked-dynamic route alone. */ + * jsonUnionSupported), discriminated unions of records anywhere (see + * jsonDiscriminatedSupported), and index signatures whose value type is + * not a union. Everything else keeps the checked-dynamic route alone. */ private jsonSchemaSupported(t: IrType, visiting: Set, member = false): boolean { switch (t.kind) { case "f64": @@ -1508,13 +1509,42 @@ export class LlDyn { (!index || this.jsonSchemaSupported(index, visiting)) ); } - case "union": + case "union": { + const def = this.host.unionsById.get(t.unionId); + if (def?.discriminant) return this.jsonDiscriminatedSupported(def, visiting); return member && this.jsonUnionSupported(t, visiting); + } default: return false; } } + /** A discriminated union the one-pass parser writes directly, as a record + * member, an array element or the whole document: record arms that each + * name the string literals their discriminant member holds, every + * literal naming one arm, plus null and undefined arms. An object then + * selects the one arm whose literal its discriminant member holds, the + * only arm the checked route's literal tests leave to match + * (unionArmsByDiscriminant); an object holding no such literal declines. */ + private jsonDiscriminatedSupported(def: IrUnionDef, visiting: Set): boolean { + const discriminant = def.discriminant!; + const literals = new Set(); + let records = 0; + const supported = def.arms.every((arm, tag) => { + if (arm.kind === "nullT" || arm.kind === "undefinedT") return true; + if (arm.kind !== "record") return false; + records++; + const guard = discriminant.cases.find((candidate) => candidate.tag === tag); + if (!guard || guard.values.length === 0) return false; + for (const value of guard.values) { + if (typeof value !== "string" || literals.has(value)) return false; + literals.add(value); + } + return this.jsonSchemaSupported(arm, visiting); + }); + return supported && records >= 2; + } + /** A union record member the one-pass parser writes directly: every arm * a number, boolean, string, null, undefined, or a supported record or * array, with at most one record and one array arm and no discriminant. @@ -1619,9 +1649,15 @@ export class LlDyn { body = `${S} 3, ${S} ${entries.length}, ptr ${fields}, ptr ${overflow.elem}, ptr @${mangleRecordNew(t.shapeId)}, ptr null, ptr ${vAdapters(host, t).release}, ptr null, ${S} ${overflow.offset}`; break; } - case "union": - body = `${S} 5, ${S} 0, ptr null, ptr null, ptr null, ptr null, ptr null, ptr @${this.jsonUnionSchema(t, name)}, ${S} 0`; + case "union": { + // A discriminated union can be a whole document, whose value the + // parser releases when text follows it. + const release = host.unionsById.get(t.unionId)?.discriminant + ? vAdapters(host, t).release + : "null"; + body = `${S} 5, ${S} 0, ptr null, ptr null, ptr null, ptr null, ptr ${release}, ptr @${this.jsonUnionSchema(t, name)}, ${S} 0`; break; + } default: throw new InternalCompilerError(`llvm emitter bug: JSON schema of ${t.kind}`); } @@ -1636,7 +1672,11 @@ export class LlDyn { /** The ScrJsonUnion descriptor of a union member (scr_json.h layout): * its slot representation, the arm each JSON value kind selects, and the * values the runtime stores for null and for a missing member. Boxed - * unions build their boxes through `_box`. */ + * unions build their boxes through `_box`. A discriminated union + * also lists its object arms by discriminant literal (`_cases`), + * and, because it can be an array element or a whole document, whose + * values are boxes unless the union is a pointer union, it keeps its null + * value and box whatever its field representation. */ private jsonUnionSchema(t: IrType & { kind: "union" }, name: string): string { const host = this.host; const S = host.sizeType; @@ -1655,10 +1695,11 @@ export class LlDyn { ); const nullTag = tags[0]!; const absentTag = tags[6]!; - const nullValue = !inline && nullTag >= 0 ? host.unitInstanceRef(t.unionId, nullTag) : "null"; - const absentValue = !inline && absentTag >= 0 ? host.absentInstanceRef(t.unionId) : "null"; + const values = !inline || def.discriminant !== undefined; + const nullValue = values && nullTag >= 0 ? host.unitInstanceRef(t.unionId, nullTag) : "null"; + const absentValue = values && absentTag >= 0 ? host.absentInstanceRef(t.unionId) : "null"; let box = "null"; - if (repr === 2) { + if (repr === 2 || (def.discriminant && !nullable)) { box = `@${name}_box`; const lines = [`define internal ptr ${box}(i32 %tag, i64 %bits) ${FN_ATTRS} {`, `entry:`]; const cases: string[] = []; @@ -1699,9 +1740,27 @@ export class LlDyn { ``, ); } + let cases = "null"; + if (def.discriminant) { + const discriminant = def.discriminant; + const caseTy = `{ ptr, ${S}, ${S}, ptr }`; + const entries = discriminant.cases.flatMap((guard) => + guard.values.map( + (value) => + `${caseTy} { ptr ${host.cstr(String(value))}, ${S} ${Buffer.byteLength(String(value), "utf8")}, ${S} ${guard.tag}, ptr @${this.jsonSchema(def.arms[guard.tag]!)} }`, + ), + ); + const table = `@${name}_case_table`; + cases = `@${name}_cases`; + this.defs.push( + `${table} = internal constant [${entries.length} x ${caseTy}] [${entries.join(", ")}]`, + `${cases} = internal constant { ptr, ${S}, ${S}, ptr } { ptr ${host.cstr(discriminant.field)}, ${S} ${Buffer.byteLength(discriminant.field, "utf8")}, ${S} ${entries.length}, ptr ${table} }`, + ``, + ); + } const descriptor = `${name}_union`; this.defs.push( - `@${descriptor} = internal constant { ${S}, [7 x ${S}], [7 x ptr], ptr, ptr, ptr } { ${S} ${repr}, [7 x ${S}] [${tags.map((tag) => `${S} ${tag}`).join(", ")}], [7 x ptr] [${arms.join(", ")}], ptr ${nullValue}, ptr ${absentValue}, ptr ${box} }`, + `@${descriptor} = internal constant { ${S}, [7 x ${S}], [7 x ptr], ptr, ptr, ptr, ptr } { ${S} ${repr}, [7 x ${S}] [${tags.map((tag) => `${S} ${tag}`).join(", ")}], [7 x ptr] [${arms.join(", ")}], ptr ${nullValue}, ptr ${absentValue}, ptr ${box}, ptr ${cases} }`, ``, ); return descriptor; @@ -1751,7 +1810,12 @@ export class LlDyn { * dynCheck builder — which reports the exact error. Returns +1, or null * with the pending flag set. */ jsonParseHelper(t: IrType): string | null { - if (t.kind !== "record" && t.kind !== "array") return null; + if ( + t.kind !== "record" && + t.kind !== "array" && + !(t.kind === "union" && this.host.unionsById.get(t.unionId)?.discriminant) + ) + return null; const key = typeKey(t); const existing = this.jsonParsers.get(key); if (existing) return existing; diff --git a/packages/compiler/test/json-schema-parse-emission.test.ts b/packages/compiler/test/json-schema-parse-emission.test.ts index b96c9a032..dd6bf52ae 100644 --- a/packages/compiler/test/json-schema-parse-emission.test.ts +++ b/packages/compiler/test/json-schema-parse-emission.test.ts @@ -58,13 +58,13 @@ console.log(orders.length, typed.sku, loose.name, mixed.length, pair[0], either. // record pointer (0), the inline scalar pair (1), and a tagged box built // through the schema's box function (2), with arm tags by JSON kind. expect(llvm).toMatch( - /@sc_jsch_\d+_union = internal constant \{ i64, \[7 x i64\], \[7 x ptr\], ptr, ptr, ptr \} \{ i64 0, /, + /@sc_jsch_\d+_union = internal constant \{ i64, \[7 x i64\], \[7 x ptr\], ptr, ptr, ptr, ptr \} \{ i64 0, /, ); expect(llvm).toMatch( - /@sc_jsch_\d+_union = internal constant \{ i64, \[7 x i64\], \[7 x ptr\], ptr, ptr, ptr \} \{ i64 1, /, + /@sc_jsch_\d+_union = internal constant \{ i64, \[7 x i64\], \[7 x ptr\], ptr, ptr, ptr, ptr \} \{ i64 1, /, ); expect(llvm).toMatch( - /@sc_jsch_\d+_union = internal constant \{ i64, \[7 x i64\], \[7 x ptr\], ptr, ptr, ptr \} \{ i64 2, .*ptr @sc_jsch_\d+_box \}/, + /@sc_jsch_\d+_union = internal constant \{ i64, \[7 x i64\], \[7 x ptr\], ptr, ptr, ptr, ptr \} \{ i64 2, .*ptr @sc_jsch_\d+_box, ptr null \}/, ); expect(llvm).toMatch(/^define internal ptr @sc_jsch_\d+_box\(i32 %tag, i64 %bits\)/m); // A pure dictionary has no declared fields: its index value schema and @@ -78,9 +78,59 @@ console.log(orders.length, typed.sku, loose.name, mixed.length, pair[0], either. const wasm = emitLlvmModule(mod, { pointerBits: 32, wasi: true }); expect(wasm).toContain("= internal constant { i32, i32, ptr, ptr, ptr, ptr, ptr, ptr, i32 }"); - expect(wasm).toContain("= internal constant { i32, [7 x i32], [7 x ptr], ptr, ptr, ptr }"); + expect(wasm).toContain("= internal constant { i32, [7 x i32], [7 x ptr], ptr, ptr, ptr, ptr }"); expect(wasm).toMatch(/\{ ptr, i32, i32, ptr \} \{ ptr @sc_cs_\d+, i32 3, i32 ptrtoint/); } finally { await rm(dir, { recursive: true, force: true }); } }); + +test("JSON.parse casts to discriminated unions fuse into the schema parser", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-json-schema-union-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile( + entry, + ` +type Event = + | { kind: "click"; x: number; y: number } + | { kind: "focus" | "blur"; target: string } + | { kind: "load"; ms: number; next: Event | null }; +type Expr = { kind: "num"; value: number } | { kind: "add"; items: Expr[] }; +type Loose = { kind: string; a: number } | { kind: string; b: string }; +const text = process.argv[2] ?? "[]"; +const events = JSON.parse(text) as Event[]; +const tree = JSON.parse(text) as Expr; +const loose = JSON.parse(text) as Loose[]; +console.log(events.length, tree.kind, loose.length); +`, + ); + 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 llvm = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + // Event[] and the whole-document Expr take the fused path; a union + // without literal discriminants keeps the checked-dynamic route alone. + expect(llvm.match(/^define internal ptr @sc_jp_\d+(?:\.i32)?\(ptr %text\)/gm)).toHaveLength(2); + expect(llvm.match(/call ptr @scr_json_parse\(ptr /g)).toHaveLength(3); + // Each literal names its arm's tag and record schema; an arm with two + // literals is listed under both. Event, `Event | null` and Expr each + // have a table. + const cases = [...llvm.matchAll(/^@sc_jsch_\d+_case_table = internal constant \[(\d+) x /gm)]; + expect(cases.map((m) => Number(m[1])).sort()).toEqual([2, 4, 4]); + expect(llvm).toMatch( + /^@sc_jsch_\d+_cases = internal constant \{ ptr, i64, i64, ptr \} \{ ptr @sc_cs_\d+, i64 4, i64 4, ptr @sc_jsch_\d+_case_table \}/m, + ); + // The union descriptors point at their cases, and a union schema that + // can be a whole document carries its release. + expect(llvm).toMatch( + /@sc_jsch_\d+_union = internal constant \{ i64, \[7 x i64\], \[7 x ptr\], ptr, ptr, ptr, ptr \} \{ .*, ptr @sc_jsch_\d+_cases \}/, + ); + expect(llvm).toMatch( + /= internal constant \{ i64, i64, ptr, ptr, ptr, ptr, ptr, ptr, i64 \} \{ i64 5, i64 0, ptr null, ptr null, ptr null, ptr null, ptr @\S+, ptr @sc_jsch_\d+_union, i64 0 \}/, + ); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8243-json-parse-discriminated-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8243-json-parse-discriminated-unions.ts.json new file mode 100644 index 000000000..96ef2084c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8243-json-parse-discriminated-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8243-json-parse-discriminated-unions.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index cfe3e8389..7539b3a1b 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -12304,7 +12304,8 @@ static bool scr_jschema_read(ScrJsonP *p, const ScrJsonSchema *s, ScrJsonSlot *o static void scr_jschema_drop(const ScrJsonSchema *s, ScrJsonSlot *v) { if (s->kind == SCR_JSCHEMA_STR) scr_str_release(v->ptr); - else if (s->kind == SCR_JSCHEMA_REC || s->kind == SCR_JSCHEMA_ARR) s->release(v->ptr); + else if (s->kind == SCR_JSCHEMA_REC || s->kind == SCR_JSCHEMA_ARR || s->kind == SCR_JSCHEMA_UNION) + s->release(v->ptr); } static bool scr_jschema_string_skip(ScrJsonP *p) { @@ -12418,14 +12419,79 @@ static void scr_jschema_inline_store(char *slot, size_t tag, uint64_t bits) { memcpy(slot, &pair, sizeof pair); } -/* A union member value written into its field slot. Each JSON value kind - * selects at most one arm (the compiler admits no two arms one kind could - * both match), so the checked route's first full match is that arm, and - * any other value declines. */ -static __attribute__((noinline)) bool scr_jschema_union(ScrJsonP *p, const ScrJsonUnion *u, - char *slot) { - scr_json_ws(p); - if (p->pos >= p->len) return false; +/* The string at p->pos (a quote), as a span into the text or, when it + * holds escapes, decoded into *decoded (released by the caller). */ +static bool scr_jschema_string_at(ScrJsonP *p, const char **span, size_t *span_len, + ScrStr **decoded) { + *decoded = NULL; + int r = scr_json_string_span(p, span, span_len); + if (r > 0) return true; + if (r < 0) return false; + *decoded = scr_json_string_slow(p, *span_len); + if (!*decoded) return false; + *span = (*decoded)->data; + *span_len = (*decoded)->len; + return true; +} + +/* The case of discriminated union `d` that the object at p->pos (its + * opening brace) selects: the one whose literal the object's discriminant + * member holds. A shallow scan of the members up to that one finds it and + * restores p->pos. NULL when the member is missing or not a string, when + * no case has its value, or when the text before it is malformed: the + * checked route then decides. A repeated discriminant member selects by + * its first value; the arm's record then declines the repeat. */ +static __attribute__((noinline)) const ScrJsonCase *scr_jschema_case(ScrJsonP *p, + const ScrJsonDiscriminant *d) { + const size_t start = p->pos, depth = p->depth; + const ScrJsonCase *found = NULL; + if (++p->depth > SCR_JSON_MAX_DEPTH) goto done; + p->pos++; /* '{' */ + for (;;) { + scr_json_ws(p); + if (p->pos >= p->len || p->s[p->pos] != '"') goto done; + const char *key; + size_t key_len; + ScrStr *decoded; + if (!scr_jschema_string_at(p, &key, &key_len, &decoded)) goto done; + bool discriminant = key_len == d->field_len && scr_key_equal(key, d->field, key_len); + scr_str_release(decoded); + scr_json_ws(p); + if (p->pos >= p->len || p->s[p->pos] != ':') goto done; + p->pos++; + if (discriminant) { + scr_json_ws(p); + if (p->pos >= p->len || p->s[p->pos] != '"') goto done; + const char *value; + size_t value_len; + if (!scr_jschema_string_at(p, &value, &value_len, &decoded)) goto done; + for (size_t i = 0; i < d->ncases; i++) { + const ScrJsonCase *c = &d->cases[i]; + if (c->value_len == value_len && memcmp(c->value, value, value_len) == 0) { + found = c; + break; + } + } + scr_str_release(decoded); + goto done; + } + if (!scr_jschema_skip(p)) goto done; + scr_json_ws(p); + if (p->pos >= p->len || p->s[p->pos] != ',') goto done; + p->pos++; + } +done: + p->pos = start; + p->depth = depth; + return found; +} + +/* A union value's arm tag and payload at p->pos (whitespace skipped): the + * arm its JSON value kind selects or, for an object of a discriminated + * union, its case's record. A null value reads no payload (*arm_out is + * SCR_JARM_NULL). False when the value declines. */ +static bool scr_jschema_union_payload(ScrJsonP *p, const ScrJsonUnion *u, size_t *tag, + size_t *arm_out, ScrJsonSlot *v) { char c = p->s[p->pos]; size_t arm; if (c == 'n') arm = SCR_JARM_NULL; @@ -12435,16 +12501,36 @@ static __attribute__((noinline)) bool scr_jschema_union(ScrJsonP *p, const ScrJs else if (c == 't' || c == 'f') arm = SCR_JARM_BOOL; else if (c == '-' || (c >= '0' && c <= '9')) arm = SCR_JARM_NUM; else return false; - size_t tag = u->tags[arm]; - if (tag == SCR_JSCHEMA_NO_ARM) return false; + *arm_out = arm; + if (arm == SCR_JARM_OBJ && u->disc) { + const ScrJsonCase *selected = scr_jschema_case(p, u->disc); + if (!selected) return false; + *tag = selected->tag; + return scr_jschema_read(p, selected->record, v); + } + *tag = u->tags[arm]; + if (*tag == SCR_JSCHEMA_NO_ARM) return false; + if (arm == SCR_JARM_NULL) return scr_json_lit(p, "null", 4); + return scr_jschema_read(p, u->arms[arm], v); +} + +/* A union member value written into its field slot. Each JSON value kind + * selects at most one arm, and an object of a discriminated union selects + * its case's arm (the compiler admits no two arms one value could both + * match), so the checked route's first full match is that arm, and any + * other value declines. */ +static __attribute__((noinline)) bool scr_jschema_union(ScrJsonP *p, const ScrJsonUnion *u, + char *slot) { + scr_json_ws(p); + if (p->pos >= p->len) return false; + size_t tag, arm; + ScrJsonSlot v; + if (!scr_jschema_union_payload(p, u, &tag, &arm, &v)) return false; if (arm == SCR_JARM_NULL) { - if (!scr_json_lit(p, "null", 4)) return false; if (u->repr == SCR_JUNION_INLINE) scr_jschema_inline_store(slot, tag, 0); else memcpy(slot, &u->null_value, sizeof u->null_value); /* immortal */ return true; } - ScrJsonSlot v; - if (!scr_jschema_read(p, u->arms[arm], &v)) return false; uint64_t bits = 0; if (arm == SCR_JARM_NUM) memcpy(&bits, &v.f, sizeof bits); else if (arm == SCR_JARM_BOOL) bits = v.b ? 1 : 0; @@ -12460,6 +12546,19 @@ static __attribute__((noinline)) bool scr_jschema_union(ScrJsonP *p, const ScrJs return true; } +/* A discriminated union value outside a record field: an array element or + * the whole document. A pointer union's value is the case's record itself; + * any other union's is a box. */ +static bool scr_jschema_union_value(ScrJsonP *p, const ScrJsonUnion *u, ScrJsonSlot *out) { + size_t tag, arm; + ScrJsonSlot v; + if (!scr_jschema_union_payload(p, u, &tag, &arm, &v)) return false; + if (arm == SCR_JARM_NULL) out->ptr = u->null_value; /* immortal */ + else if (u->repr == SCR_JUNION_NULLABLE) out->ptr = v.ptr; + else out->ptr = u->box((uint32_t)tag, (uint64_t)(uintptr_t)v.ptr); /* the record moves in */ + return true; +} + /* A declared member value written into its (zeroed) slot of `rec`. */ static inline __attribute__((always_inline)) bool scr_jschema_field(ScrJsonP *p, const ScrJsonSchemaField *f, @@ -12805,6 +12904,8 @@ static bool scr_jschema_read(ScrJsonP *p, const ScrJsonSchema *s, ScrJsonSlot *o p->depth--; return ok; } + case SCR_JSCHEMA_UNION: + return scr_jschema_union_value(p, s->uni, out); } return false; } diff --git a/packages/runtime/src/scr_json.h b/packages/runtime/src/scr_json.h index 8a4ecc62d..a8a59b60c 100644 --- a/packages/runtime/src/scr_json.h +++ b/packages/runtime/src/scr_json.h @@ -13,11 +13,14 @@ ScrDyn *scr_json_parse(ScrStr *text); /* Compiler-emitted description of a statically known JSON.parse target * (`JSON.parse(text) as T`). Constant data in the program image; the LLVM * backend mirrors this layout ({size, size, ptr, ptr, ptr, ptr, ptr, ptr, size}, - * {ptr, size, size, ptr} and {size, [7 x size], [7 x ptr], ptr, ptr, ptr}). - * Records list their declared fields (at most 64), each with its byte - * offset in the record struct; bool fields are one byte, f64 fields - * doubles, inline union fields a {uint32 tag, uint64 bits} pair, every - * other kind a +1 pointer. Unions appear only as record field types. A + * {ptr, size, size, ptr}, {size, [7 x size], [7 x ptr], ptr, ptr, ptr, ptr}, + * and for discriminated unions {ptr, size, size, ptr} with {ptr, size, size, + * ptr} cases). Records list their declared fields (at most 64), each with + * its byte offset in the record struct; bool fields are one byte, f64 + * fields doubles, inline union fields a {uint32 tag, uint64 bits} pair, + * every other kind a +1 pointer. Unions appear as record field types; + * discriminated unions also as array elements and whole documents, whose + * values are the union's box (or its pointer, for a pointer union). A * record with an index signature names its index value type in `elem` and * its overflow map's offset in `ovf_offset`. */ enum { @@ -57,6 +60,22 @@ typedef struct { size_t offset; const ScrJsonSchema *type; } ScrJsonSchemaField; +/* One string literal of a discriminated union's discriminant member and + * the record arm (canonical tag and schema) that holds it. */ +typedef struct { + const char *value; + size_t value_len; + size_t tag; + const ScrJsonSchema *record; +} ScrJsonCase; +/* A discriminated union's object arms by discriminant literal: every + * literal names one arm. */ +typedef struct { + const char *field; + size_t field_len; + size_t ncases; + const ScrJsonCase *cases; +} ScrJsonDiscriminant; typedef struct { size_t repr; size_t tags[SCR_JARM_COUNT]; /* canonical arm tag, or SCR_JSCHEMA_NO_ARM */ @@ -66,6 +85,9 @@ typedef struct { /* boxed: a +1 box of the value arm `tag`; bits hold its double, its * bool, or its +1 payload pointer (moved in). */ void *(*box)(uint32_t tag, uint64_t bits); + /* discriminated unions: the object arms; NULL for other unions, whose + * object arm is arms[SCR_JARM_OBJ] */ + const ScrJsonDiscriminant *disc; } ScrJsonUnion; struct ScrJsonSchema { size_t kind; diff --git a/tests/corpus/8243-json-parse-discriminated-unions.ts b/tests/corpus/8243-json-parse-discriminated-unions.ts new file mode 100644 index 000000000..df3fd6dd8 --- /dev/null +++ b/tests/corpus/8243-json-parse-discriminated-unions.ts @@ -0,0 +1,119 @@ +// JSON.parse cast to discriminated unions of records: arrays of them, +// whole documents, members (with null and optional arms) and recursive +// trees. The discriminant appears first, last or between other members, +// escaped, repeated, and beside undeclared members; values hold escapes +// and non-ASCII text. + +type Event = + | { kind: "click"; x: number; y: number } + | { kind: "focus" | "blur"; target: string } + | { key: string; kind: "key"; repeat: boolean } + | { kind: "load"; ms: number; next: Event | null; note?: string }; + +type Expr = + | { kind: "num"; value: number } + | { kind: "neg"; operand: Expr } + | { items: Expr[]; kind: "max" } + | { body: Expr; kind: "let"; name: string; value: Expr }; + +function describe(e: Event): string { + switch (e.kind) { + case "click": + return `click ${e.x},${e.y}`; + case "focus": + case "blur": + return `${e.kind} ${e.target}`; + case "key": + return `key ${e.key}${e.repeat ? " (repeat)" : ""}`; + case "load": + return `load ${e.ms}${e.note === undefined ? "" : ` "${e.note}"`}${e.next === null ? "" : ` then ${describe(e.next)}`}`; + } +} + +function evaluate(e: Expr, env: Map): number { + switch (e.kind) { + case "num": + return e.value; + case "neg": + return -evaluate(e.operand, env); + case "max": { + let best = -Infinity; + for (const item of e.items) best = Math.max(best, evaluate(item, env)); + return best; + } + case "let": { + const inner = new Map(env); + inner.set(e.name, evaluate(e.value, env)); + return evaluate(e.body, inner) + (inner.get(e.name) ?? 0); + } + } +} + +const events = JSON.parse(`[ + {"kind":"click","x":1,"y":2}, + {"x":3,"y":4,"kind":"click"}, + {"y":6,"kind":"click","x":5}, + {"kind":"focus","target":"input"}, + {"target":"button","kind":"blur"}, + {"key":"Enter","kind":"key","repeat":false}, + {"repeat":true,"key":"\\u00e9","kind":"key"}, + {"kind":"load","ms":12,"next":null}, + {"kind":"load","ms":7,"note":"caf\\u00e9 \\ud83d\\ude00","next":{"kind":"blur","target":"tab"}}, + {"next":{"kind":"load","ms":1,"next":{"kind":"click","x":0,"y":-1}},"ms":30,"kind":"load"}, + {"k\\u0069nd":"focus","target":"escaped key"}, + {"kind":"bl\\u0075r","target":"escaped value"}, + {"kind":"click","kind":"focus","target":"repeated"}, + {"kind":"click","x":8,"y":9,"extra":{"kind":"not an event","list":[1,[2]]}}, + { "kind" : "key" , "key" : "space" , "repeat" : true } +]`) as Event[]; +console.log(events.length); +for (const e of events) console.log(describe(e)); +console.log(JSON.stringify(events[12]), JSON.stringify(events[13])); + +const empty = JSON.parse("[]") as Event[]; +console.log(empty.length); + +const single = JSON.parse('{"target":"whole document","kind":"focus"}') as Event; +console.log(describe(single)); + +const tree = JSON.parse(`{ + "body": {"items": [{"kind":"num","value":4}, {"kind":"neg","operand":{"kind":"num","value":-9}}, {"items":[],"kind":"max"}], "kind": "max"}, + "kind": "let", + "name": "x", + "value": {"kind":"neg","operand":{"kind":"neg","operand":{"kind":"num","value":2.5}}} +}`) as Expr; +console.log(evaluate(tree, new Map())); + +const forest = JSON.parse( + `[${Array.from({ length: 50 }, (_, i) => + i % 3 === 0 + ? `{"kind":"num","value":${i}}` + : i % 3 === 1 + ? `{"operand":{"kind":"num","value":${i}},"kind":"neg"}` + : `{"items":[{"kind":"num","value":${i}},{"kind":"num","value":${50 - i}}],"kind":"max"}`, + ).join(",")}]`, +) as Expr[]; +let total = 0; +for (const t of forest) total += evaluate(t, new Map()); +console.log(forest.length, total); + +// Parsing the same text again builds separate values. +const again = JSON.parse('[{"kind":"num","value":1},{"kind":"num","value":2}]') as Expr[]; +const twice = JSON.parse('[{"kind":"num","value":1},{"kind":"num","value":2}]') as Expr[]; +console.log(again.length + twice.length, again[0] === twice[0]); + +// Malformed text throws Node's SyntaxError, including text that ends +// inside the members before the discriminant or inside its value. +for (const text of [ + '[{"kind":"click","x":1,"y":2},', + '[{"x":1,"kind"', + '[{"x":[1,2,"kind":"click"}]', + '[{"kind":"cl', + '[{"target":"a" "kind":"focus"}]', +]) { + try { + console.log((JSON.parse(text) as Event[]).length); + } catch (error) { + console.log(error instanceof SyntaxError, (error as Error).message); + } +} From a54e0811d0c22d6fb019b8aa776b15922bf37e3c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 01:04:31 -0500 Subject: [PATCH 173/273] Journal the Promise.all tuple and AsyncLocalStorage callback thunks --- .../compiler/src/backend/llvm/lib-dispatch.ts | 52 ++++++++++++------- .../compiler/src/backend/llvm/lib-process.ts | 7 +++ .../compiler/test/chunked-emission.test.ts | 35 ++++++++++++- 3 files changed, 73 insertions(+), 21 deletions(-) diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index e1f038093..8a98fb154 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -25,6 +25,7 @@ import { DYN_KIND } from "./dyn.js"; import { caBytes } from "./lib-abi.js"; import { emitInlineAtomicsOp } from "./expr-bytes.js"; import { regexLiteralCaptures } from "./regex-matchers.js"; +import { registerReplay } from "./emit-journal.js"; /** ScrDyn.v.typed_ref.type_key_len: the low 27 bits of its bit-field word. */ const TYPE_KEY_LEN_MASK = (1 << 27) - 1; @@ -1130,6 +1131,36 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { } } +/** The settlement adapter of Promise.all's tuple form for the builder + * `fnName`, which fulfills the result with the builder's tuple. */ +function allTupleAdapterFor(host: LlvmEmitterContext, fnName: string, tuple: IrType): string { + const key = `allTuple:${fnName}`; + let adapter = host.resolveThunks.get(key); + if (adapter !== undefined) return adapter; + adapter = `sc_all_tuple_${host.resolveThunks.size}`; + host.resolveThunks.set(key, adapter); + host.journal?.allocated(adapter, "allTupleAdapterFor", [fnName, tuple]); + const rc = vAdapters(host.shapeHost, tuple); + host.declare(`declare ptr @scr_promise_payload_ref(ptr)`); + host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); + host.resolveThunkDefs.push( + `define internal void @${adapter}(ptr %dst, ptr %src) ${FN_ATTRS} { ; Promise.all tuple`, + `entry:`, + ` %entries = call ptr @scr_promise_payload_ref(ptr %src) ; +1, moves into the builder`, + ` %tuple = call ptr @${host.callTarget(fnName)}(ptr %entries)`, + ` call void @scr_promise_fulfill_ref(ptr %dst, ptr %tuple, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, tuple)})`, + ` ret void`, + `}`, + ``, + ); + host.journal?.built(); + return adapter; +} + +registerReplay("allTupleAdapterFor", (host, args) => + allTupleAdapterFor(host, args[0] as string, args[1] as IrType), +); + /** Promise.all's tuple form (promise.allTuple): the combinator over the * entries stores each fulfilled entry promise, and a settlement adapter on * the combinator calls the builder with them and fulfills the result with @@ -1142,26 +1173,7 @@ function emitPromiseAllTuple(host: LlvmEmitterContext, e: LibCallExpr): LlValue throw new InternalCompilerError("llvm emitter bug: promise.allTuple shape"); const entriesT = e.args[0]!.type; const entries = host.emitExpr(e.args[0]!); - const tuple = e.type.inner; - const key = `allTuple:${build.fnName}`; - let adapter = host.resolveThunks.get(key); - if (adapter === undefined) { - adapter = `sc_all_tuple_${host.resolveThunks.size}`; - host.resolveThunks.set(key, adapter); - const rc = vAdapters(host.shapeHost, tuple); - host.declare(`declare ptr @scr_promise_payload_ref(ptr)`); - host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); - host.resolveThunkDefs.push( - `define internal void @${adapter}(ptr %dst, ptr %src) ${FN_ATTRS} { ; Promise.all tuple`, - `entry:`, - ` %entries = call ptr @scr_promise_payload_ref(ptr %src) ; +1, moves into the builder`, - ` %tuple = call ptr @${host.callTarget(build.fnName)}(ptr %entries)`, - ` call void @scr_promise_fulfill_ref(ptr %dst, ptr %tuple, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, tuple)})`, - ` ret void`, - `}`, - ``, - ); - } + const adapter = allTupleAdapterFor(host, build.fnName, e.type.inner); host.declare(`declare ptr @scr_promise_all(ptr, ptr, ptr)`); host.declare(`declare void @scr_promise_all_store_promise(ptr, double, ptr)`); host.declare(`declare ptr @scr_promise_new()`); diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 74ab34971..75d8038fc 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -6,6 +6,7 @@ import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js import { f64Lit } from "./common.js"; import { mockTimerSym } from "./lib-shared.js"; import { FN_ATTRS } from "./shapes.js"; +import { registerReplay } from "./emit-journal.js"; export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; @@ -498,6 +499,7 @@ function boundCallbackThunk(host: LlvmEmitterContext, cbT: IrType & { kind: "fun if (sym) return sym; sym = `sc_alsbound_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "boundCallbackThunk", [cbT]); host.declare("declare ptr @scr_als_bound_target(ptr)"); host.declare("declare ptr @scr_als_bound_enter(ptr)"); host.declare("declare void @scr_als_bound_leave(ptr, ptr)"); @@ -517,9 +519,14 @@ function boundCallbackThunk(host: LlvmEmitterContext, cbT: IrType & { kind: "fun `}`, ``, ); + host.journal?.built(); return sym; } +registerReplay("boundCallbackThunk", (host, args) => + boundCallbackThunk(host, args[0] as IrType & { kind: "func" }), +); + export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; if (e.fn === "process.forkTarget") { diff --git a/packages/compiler/test/chunked-emission.test.ts b/packages/compiler/test/chunked-emission.test.ts index 2aec1e49b..f4977f035 100644 --- a/packages/compiler/test/chunked-emission.test.ts +++ b/packages/compiler/test/chunked-emission.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; @@ -25,6 +25,7 @@ const PROGRAMS = [ "6305-json-discriminant-dispatch.ts", "7321-speculative-loop-bail-sites.ts", "7360-worker-typed-records.ts", + "7110-als-completion-callbacks.ts", "node24-text-utilities-coercion.js", ]; @@ -70,6 +71,38 @@ test("function bodies emitted in chunks merge into the text one emitter gives", } }, 600_000); +// Thunks a range creates are journaled, so the merging emitter defines +// them: here Promise.all's tuple adapter, requested by a function that is +// not async (async functions are emitted again when the merging emitter +// selects stackless bodies). +test("a Promise.all tuple adapter requested in a range is defined", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-chunked-tuple-")); + try { + const entry = join(dir, "main.ts"); + await writeFile( + entry, + [ + "function both(a: Promise, b: Promise): Promise<[number, string]> {", + " return Promise.all([a, b]);", + "}", + 'both(Promise.resolve(1), Promise.resolve("x")).then((v) => console.log(v[0], v[1]));', + "", + ].join("\n"), + ); + 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")); + const ir = await readFile(outPath, "utf8"); + const serial = text(emitLlvmModuleSource(deserializeModule(ir), {})); + expect(serial).toContain("define internal void @sc_all_tuple_0("); + const chunked = text(emitLlvmModuleSource(deserializeModule(ir), { functionChunks: 2 })); + expect(chunked === serial).toBe(true); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 60_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 () => { From 9d7f0182e5d670bb603e80cf23720b5c78d742a3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 01:04:55 -0500 Subject: [PATCH 174/273] Keep CommonJS wrapper names and redeclared exports out of namespace rewrites - A guarded declaration of a name the CommonJS wrapper defines (`var exports = exports || {}`, and `module`, `require`, `__filename`, `__dirname`, `arguments`) reads the wrapper's value, not undefined, so it keeps its text instead of becoming a fresh object. - The write scan counts destructuring assignments, and destructuring for-in/for-of targets, as writes, so a hoisted function that sets the namespace before its guarded declaration keeps the declaration as it is. - An exported `var` that a later declaration of the same name sets again is not settled at the export: importers would read the redeclared value, where Node copies the first one into module.exports. It stays refused (SC1090), as before. The npm case var-export-redeclared pins it. --- .../src/frontend/lowering/lower-stmts.ts | 8 +++-- .../npm-static-namespace-defaults.test.ts | 17 ++++++++++ .../frontend/npm-static-namespace-defaults.ts | 31 ++++++++++++++++--- .../cases/var-export-redeclared/main.ts.json | 6 ++++ .../npm/cases/var-export-redeclared/main.ts | 3 ++ .../node_modules/redeclared-ns/index.d.ts | 1 + .../node_modules/redeclared-ns/index.js | 5 +++ .../node_modules/redeclared-ns/package.json | 1 + .../cases/var-export-redeclared/package.json | 1 + tests/harness/npm.test.ts | 2 ++ 10 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/var-export-redeclared/main.ts.json create mode 100644 tests/fixtures/npm/cases/var-export-redeclared/main.ts create mode 100644 tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/index.d.ts create mode 100644 tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/index.js create mode 100644 tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/package.json create mode 100644 tests/fixtures/npm/cases/var-export-redeclared/package.json diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 851cae4c9..4366a04b0 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -12708,13 +12708,15 @@ function isAssertionInitializer(node: ts.Expression): boolean { return ts.isAsExpression(e) || ts.isTypeAssertion(e); } -/** A `var` (or `let`) whose initialized declaration precedes the export - * statement in the module's top-level statements and that nothing ever - * reassigns: exporting it by reference reads the value Node copies. The +/** A `var` (or `let`) whose one initialized declaration precedes the + * export statement in the module's top-level statements and that nothing + * ever reassigns or redeclares: exporting it by reference reads the value Node copies. The * legacy `var ns = ns || {}; …; module.exports = { ns }` shape. */ function settledBeforeExport(lowerer: Lowerer, sym: ts.Symbol, exportNode: ts.Node): boolean { const decl = sym.valueDeclaration; if (!decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return false; + // A redeclaration (`var ns = other` after the export) writes it too. + if (sym.declarations.some((d) => d !== decl)) return false; const stmt = decl.parent?.parent; if (!stmt || !ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent)) return false; let exportStmt: ts.Node = exportNode; diff --git a/packages/compiler/src/frontend/npm-static-namespace-defaults.test.ts b/packages/compiler/src/frontend/npm-static-namespace-defaults.test.ts index 93a776f7e..0fe7dc9a1 100644 --- a/packages/compiler/src/frontend/npm-static-namespace-defaults.test.ts +++ b/packages/compiler/src/frontend/npm-static-namespace-defaults.test.ts @@ -60,4 +60,21 @@ test("any other write, declaration or earlier mention keeps the text", () => { expect(rewrite("let ns = ns || {};\n")).toBeNull(); // Not the declared name on the left. expect(rewrite("var ns = other || {};\n")).toBeNull(); + // Written through a destructuring assignment, here by a hoisted function + // that runs before the declaration. + expect( + rewrite("init();\nvar ns = ns || {};\nfunction init() { [ns] = [{ a: 1 }]; }\n"), + ).toBeNull(); + expect( + rewrite("init();\nvar ns = ns || {};\nfunction init() { ({ ns } = { ns: { a: 1 } }); }\n"), + ).toBeNull(); + expect(rewrite("var ns = ns || {};\nfor ([ns] of [[{}]]);\n")).toBeNull(); +}); + +test("a name the CommonJS wrapper defines keeps the text", () => { + // `exports` is the module's export object here, not undefined. + const source = "var exports = exports || {};\nexports.answer = 42;\n"; + expect(rewrite(source)).toBeNull(); + expect(rewrite("var module = module || {};\n")).toBeNull(); + expect(rewrite("var require = require || null;\n")).toBeNull(); }); diff --git a/packages/compiler/src/frontend/npm-static-namespace-defaults.ts b/packages/compiler/src/frontend/npm-static-namespace-defaults.ts index 2e4bf046c..26a8e4cd0 100644 --- a/packages/compiler/src/frontend/npm-static-namespace-defaults.ts +++ b/packages/compiler/src/frontend/npm-static-namespace-defaults.ts @@ -6,8 +6,9 @@ * read in its own initializer sees `undefined` unless something writes the * variable first. When nothing else in the file ever writes it (no other * declaration, assignment or update of that name anywhere, no function or - * class of that name) and no earlier top-level statement mentions it, the - * initializer is its right operand: `var ns = {};`. Inference then types + * class of that name), no earlier top-level statement mentions it, and it + * is not a name a CommonJS module's wrapper defines (`exports`, `module`, + * ...), the initializer is its right operand: `var ns = {};`. Inference then types * the namespace from that literal and the members later statements add, * instead of `any` (the self-reference makes the checker give up). `??` * is the same. Anything else keeps the original text. */ @@ -15,6 +16,18 @@ import * as ts from "./ts7/syntax.js"; import { tokenStartOfNode } from "./ts7/node-functions.js"; import { applyEdits, unparen, type Edit } from "./npm-static-bundled-chunks.js"; +/** Names a CommonJS module's wrapper function defines before its first + * statement: its parameters and its arguments object. `var exports = + * exports || {}` keeps the module's export object, not a fresh one. */ +const COMMONJS_WRAPPER_NAMES = new Set([ + "exports", + "require", + "module", + "__filename", + "__dirname", + "arguments", +]); + /** A cheap probe: some `var X = X ||` or `?? ` may be present. */ export function mayHoldNamespaceDefault(source: string): boolean { return /\bvar\s+([A-Za-z_$][\w$]*)\s*=\s*\(?\s*\1\s*(\|\||\?\?)/.test(source); @@ -42,6 +55,7 @@ export function rewriteNamespaceDefaults(sf: ts.SourceFile): string | null { continue; const left = unparen(init.left); if (!ts.isIdentifier(left) || left.text !== name) continue; + if (COMMONJS_WRAPPER_NAMES.has(name)) continue; if (mentionedBefore(stmts, i, name) || writtenElsewhere(sf, name, decl)) continue; const right = init.right; edits.push({ @@ -82,7 +96,7 @@ function writtenElsewhere(sf: ts.SourceFile, name: string, decl: ts.VariableDecl if (written) return "stop"; if (n === decl.name) return undefined; if (ts.isBinaryExpression(n) && isAssignmentOperator(n.operatorToken.kind)) { - if (isName(n.left, name)) written = true; + if (assignsName(n.left, name)) written = true; } else if ( (ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) && (n.operator === ts.SyntaxKind.PlusPlusToken || @@ -108,7 +122,7 @@ function writtenElsewhere(sf: ts.SourceFile, name: string, decl: ts.VariableDecl } else if ( (ts.isForInStatement(n) || ts.isForOfStatement(n)) && !ts.isVariableDeclarationList(n.initializer) && - isName(n.initializer, name) + assignsName(n.initializer, name) ) { written = true; } @@ -128,6 +142,15 @@ function bindsName(binding: ts.Node, name: string): boolean { return found; } +/** Whether an assignment target may write `name`: the name itself, or a + * destructuring pattern that mentions it anywhere (over-approximated: a + * property key of that name counts too). */ +function assignsName(target: ts.Node, name: string): boolean { + const t = unparen(target as ts.Expression); + if (ts.isArrayLiteralExpression(t) || ts.isObjectLiteralExpression(t)) return bindsName(t, name); + return isName(t, name); +} + function isName(node: ts.Node, name: string): boolean { const n = unparen(node as ts.Expression); return ts.isIdentifier(n) && n.text === name; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/var-export-redeclared/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/var-export-redeclared/main.ts.json new file mode 100644 index 000000000..ccc1a3653 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/var-export-redeclared/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/var-export-redeclared/main.ts" + ], + "diags": [] +} diff --git a/tests/fixtures/npm/cases/var-export-redeclared/main.ts b/tests/fixtures/npm/cases/var-export-redeclared/main.ts new file mode 100644 index 000000000..23f014d29 --- /dev/null +++ b/tests/fixtures/npm/cases/var-export-redeclared/main.ts @@ -0,0 +1,3 @@ +// An importer sees the value module.exports holds, not the redeclared binding. +import { ns } from "redeclared-ns"; +console.log(ns.version); diff --git a/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/index.d.ts b/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/index.d.ts new file mode 100644 index 000000000..1db3d1444 --- /dev/null +++ b/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/index.d.ts @@ -0,0 +1 @@ +export declare const ns: { version: number }; diff --git a/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/index.js b/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/index.js new file mode 100644 index 000000000..45f29a690 --- /dev/null +++ b/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/index.js @@ -0,0 +1,5 @@ +// A namespace exported by reference, then redeclared with a new value: +// Node copies the first object into module.exports. +var ns = { version: 1 }; +module.exports = { ns }; +var ns = { version: 2 }; diff --git a/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/package.json b/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/package.json new file mode 100644 index 000000000..c18908fd8 --- /dev/null +++ b/tests/fixtures/npm/cases/var-export-redeclared/node_modules/redeclared-ns/package.json @@ -0,0 +1 @@ +{ "name": "redeclared-ns", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/var-export-redeclared/package.json b/tests/fixtures/npm/cases/var-export-redeclared/package.json new file mode 100644 index 000000000..023d0b3d9 --- /dev/null +++ b/tests/fixtures/npm/cases/var-export-redeclared/package.json @@ -0,0 +1 @@ +{ "name": "var-export-redeclared-case", "private": true, "type": "module" } diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 27b27b8e3..203e75a41 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -104,6 +104,8 @@ const KNOWN: Record = { "self-name": { build: "SC2013" }, "stream-shims": { run: "SC2020" }, "util-shims": { run: "SC2020" }, + // The package redeclares the var it exported: Node copies the first value. + "var-export-redeclared": { run: "SC1090" }, "web-streams": { run: "SC1090" }, "workspace-copied": { build: "SC2013" }, "workspace-unclean": { build: "SC2013" }, From 6b27ffbf08660c5b4cc1b8b69bea657664b9a436 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 01:25:03 -0500 Subject: [PATCH 175/273] Fetch only the syntax trees lowering reads --- .../compiler/src/frontend/lowering/argument-counts.ts | 3 +-- .../compiler/src/frontend/lowering/js-collection-types.ts | 4 ++-- packages/compiler/src/frontend/lowering/lower-classes.ts | 4 ++-- packages/compiler/src/frontend/lowering/lower-server.ts | 8 ++++---- packages/compiler/src/frontend/lowering/lowerer.ts | 4 +--- packages/compiler/src/frontend/lowering/symbol-fields.ts | 4 ++-- packages/compiler/src/frontend/program.ts | 5 ++--- packages/compiler/src/frontend/shape-inference/analyze.ts | 3 +-- 8 files changed, 15 insertions(+), 20 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/argument-counts.ts b/packages/compiler/src/frontend/lowering/argument-counts.ts index ec1753160..d79e7b43f 100644 --- a/packages/compiler/src/frontend/lowering/argument-counts.ts +++ b/packages/compiler/src/frontend/lowering/argument-counts.ts @@ -193,8 +193,7 @@ export function usedAsValue( const symbol = symbolAt(name); if (!symbol) return false; let found = false; - for (const sf of lowerer.program.getSourceFiles()) { - if (sf.isDeclarationFile) continue; + for (const sf of lowerer.program.getImplementationSourceFiles()) { ts.walkPreorder(sf, (n) => { if (found) return "stop"; if (ts.isTypeNode(n)) return "skip"; diff --git a/packages/compiler/src/frontend/lowering/js-collection-types.ts b/packages/compiler/src/frontend/lowering/js-collection-types.ts index 2be4e0dbd..1e3219276 100644 --- a/packages/compiler/src/frontend/lowering/js-collection-types.ts +++ b/packages/compiler/src/frontend/lowering/js-collection-types.ts @@ -91,8 +91,8 @@ function inferProgram(lowerer: Lowerer): Map { const out = new Map(); const carriers = carrierTypes(lowerer); if (carriers === null) return out; - for (const file of lowerer.program.getSourceFiles()) { - if (file.isDeclarationFile || !isJsSourceFile(file)) continue; + for (const file of lowerer.program.getImplementationSourceFiles()) { + if (!isJsSourceFile(file)) continue; for (const candidate of inferFile(lowerer, file)) { const name = candidate.kind === "map" diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index bb1f1714f..5f1f6a03d 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -7019,8 +7019,8 @@ function capturesEnclosingScope( * reflection: the member a runtime lookup finds can then differ. */ function memberAssigned(lowerer: Lowerer, member: ts.MethodDeclaration, name: string): boolean { let found = false; - for (const sf of lowerer.program.getSourceFiles()) { - if (sf.isDeclarationFile || found) continue; + for (const sf of lowerer.program.getImplementationSourceFiles()) { + if (found) continue; if (!sf.text.includes(name)) continue; ts.walkPreorder(sf, (node) => { if (found) return "skip"; diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 4121d13b4..7359b56a8 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -2317,8 +2317,8 @@ function lowerNetServerMethodCall( function socketEncodingReachable(lowerer: Lowerer): boolean { if (lowerer.socketEncodingReachable !== null) return lowerer.socketEncodingReachable; let reachable = false; - for (const file of lowerer.program.getSourceFiles()) { - if (file.isDeclarationFile || !file.text.includes("setEncoding")) continue; + for (const file of lowerer.program.getImplementationSourceFiles()) { + if (!file.text.includes("setEncoding")) continue; ts.walkPreorder(file, (node) => { if (reachable) return "skip"; if (!ts.isCallExpression(node)) return undefined; @@ -2349,8 +2349,8 @@ function socketEncodingReachable(lowerer: Lowerer): boolean { function unixListenReachable(lowerer: Lowerer): boolean { if (lowerer.unixListenReachable !== null) return lowerer.unixListenReachable; let reachable = false; - for (const file of lowerer.program.getSourceFiles()) { - if (file.isDeclarationFile || !file.text.includes("listen")) continue; + for (const file of lowerer.program.getImplementationSourceFiles()) { + if (!file.text.includes("listen")) continue; ts.walkPreorder(file, (node) => { if (reachable) return "skip"; if (!ts.isCallExpression(node) || node.arguments.nodes.length === 0) return undefined; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 15c0ef993..6b54b17bf 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3743,9 +3743,7 @@ export class Lowerer { * that comes back through a boundary cast keeps its identity. */ private declBoundaryMemo: boolean | null = null; get declBoundary(): boolean { - this.declBoundaryMemo ??= this.program - .getSourceFiles() - .some((sf) => isDeclBoundaryFile(sf.fileName)); + this.declBoundaryMemo ??= this.program.getSourceFileNames().some(isDeclBoundaryFile); return this.declBoundaryMemo; } /** Tracked classes' bag initializers over a typed `this` diff --git a/packages/compiler/src/frontend/lowering/symbol-fields.ts b/packages/compiler/src/frontend/lowering/symbol-fields.ts index cdfb9e1b8..958af5963 100644 --- a/packages/compiler/src/frontend/lowering/symbol-fields.ts +++ b/packages/compiler/src/frontend/lowering/symbol-fields.ts @@ -394,9 +394,9 @@ function tablePropertyWritten( } return false; }; - for (const sf of lowerer.program.getSourceFiles()) { + for (const sf of lowerer.program.getImplementationSourceFiles()) { if (written) break; - if (sf.isDeclarationFile || !sf.text.includes(name)) continue; + if (!sf.text.includes(name)) continue; ts.walkPreorder(sf, (node) => { if (written) return "stop"; let target: ts.Node | undefined; diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index aa6ddbfa7..00afcd696 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -1291,10 +1291,9 @@ export function loadProgram( * packages whose declarations the boundary projects from. */ function declBoundaryUserFiles(program: ts.Program, packages: readonly string[]): string[] { const out: string[] = []; - for (const sf of program.getSourceFiles()) { + for (const sf of program.getImplementationSourceFiles()) { const name = sf.fileName.split("\\").join("/"); - if (sf.isDeclarationFile || name.includes("/node_modules/") || !/\.[mc]?tsx?$/.test(name)) - continue; + if (name.includes("/node_modules/") || !/\.[mc]?tsx?$/.test(name)) continue; if (name.endsWith(".tsx")) continue; // JSX files: not projected yet const text = sf.text; if ( diff --git a/packages/compiler/src/frontend/shape-inference/analyze.ts b/packages/compiler/src/frontend/shape-inference/analyze.ts index 19d3cac22..c403c6ba8 100644 --- a/packages/compiler/src/frontend/shape-inference/analyze.ts +++ b/packages/compiler/src/frontend/shape-inference/analyze.ts @@ -325,8 +325,7 @@ class Analyzer { } run(): ShapeAnalysis { - for (const sf of this.program.getSourceFiles()) { - if (sf.isDeclarationFile) continue; + for (const sf of this.program.getImplementationSourceFiles()) { const name = sf.fileName.split("\\").join("/"); const inPackage = isPackageJsFile(sf); if (!inPackage && name.includes("/node_modules/")) continue; From db2cb5c99e767d1f3f3841965649f16a73a7efd8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 01:35:28 -0500 Subject: [PATCH 176/273] Split readline input in one scan and copy ASCII strings into buffers whole readline split redirected input byte by byte, twice per chunk (once to find whether the chunk ends a line, once to split it). Before splitting it copied each chunk out of its Buffer and into a scratch buffer, decoded it into a new string, and copied the unfinished line and the whole chunk into a joined buffer. Now: - one scan per chunk: memchr for a CR once, then LF to LF with memchr when the chunk has none, and the LF, CR and CRLF split otherwise; - the unfinished line joins only the first line of the chunk; - an ASCII chunk with no partial UTF-8 sequence held from the previous one is split where it is, and a chunk that is not shared memory is read from its own storage. Lines are still copied into their strings before the first is delivered. Buffer.from(string, 'latin1' | 'ascii') encoded character by character; an ASCII prefix, all of an ASCII string, is now copied with memcpy. Line events, their order and the CRLF delay are unchanged. Corpus 8344 covers lines joined across chunks, a line longer than a chunk, empty lines, mixed line endings and UTF-8 split across chunks. The new line-reader workload reads a 67 MB log through readline over a file stream. --- benchmarks/runtime/README.md | 1 + benchmarks/runtime/workloads/line-reader.json | 7 ++ benchmarks/runtime/workloads/line-reader.ts | 34 +++++++++ .../8344-readline-chunk-splitting.ts.json | 6 ++ packages/runtime/src/scr_bytes.c | 4 +- packages/runtime/src/scr_readline.c | 71 +++++++++++-------- scripts/bench-runtime.mjs | 10 +++ tests/corpus/8344-readline-chunk-splitting.ts | 41 +++++++++++ 8 files changed, 145 insertions(+), 29 deletions(-) create mode 100644 benchmarks/runtime/workloads/line-reader.json create mode 100644 benchmarks/runtime/workloads/line-reader.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8344-readline-chunk-splitting.ts.json create mode 100644 tests/corpus/8344-readline-chunk-splitting.ts diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 83cfcb831..520964cf3 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -20,6 +20,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation | | `json-unions` | cpu | `JSON.parse` into discriminated unions: an event log with eight kinds and nested expression trees with nine, evaluated after the cast | | `keyed-option-writes` | cpu | class methods whose names are also keys of option records written through element accesses: `enabled[name] = true` over a union of literal names and `totals["count"] = n` | +| `line-reader` | cpu | `readline` over a file stream: 'line' events for a 67 MB access log (every 50th line not ASCII), with a status count, a path tally and byte totals | | `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe | | `log-summary`, `inventory-report` | cpu | the multi-module applications from `benchmarks/builds`, on 300k-line inputs | | `number-maps` | cpu | number-keyed Map/Set: a dense-id table, sparse ids in many small maps, get/has/set mixes | diff --git a/benchmarks/runtime/workloads/line-reader.json b/benchmarks/runtime/workloads/line-reader.json new file mode 100644 index 000000000..85194f70c --- /dev/null +++ b/benchmarks/runtime/workloads/line-reader.json @@ -0,0 +1,7 @@ +{ + "entry": "workloads/line-reader.ts", + "input": "line-reader", + "args": [ + "{input}" + ] +} diff --git a/benchmarks/runtime/workloads/line-reader.ts b/benchmarks/runtime/workloads/line-reader.ts new file mode 100644 index 000000000..cbdf384bb --- /dev/null +++ b/benchmarks/runtime/workloads/line-reader.ts @@ -0,0 +1,34 @@ +// Line-by-line processing of a log file with readline over a file stream: +// 'line' events for about 67 MB of access-log lines, with light work per +// line (a status count, a path tally, byte totals). +import { createReadStream } from "node:fs"; +import { createInterface } from "node:readline"; + +const path = process.argv[2]!; +const statuses = new Map(); +const paths = new Map(); +let lines = 0; +let chars = 0; +let bytes = 0; +let longest = 0; + +const input = createInterface({ input: createReadStream(path), crlfDelay: Infinity }); +input.on("line", (line: string) => { + lines++; + chars += line.length; + if (line.length > longest) longest = line.length; + const quote = line.indexOf('"'); + const space = line.indexOf(" ", quote); + const path = line.slice(space + 1, line.indexOf("/", line.indexOf("/", space) + 1)); + paths.set(path, (paths.get(path) ?? 0) + 1); + const close = line.indexOf('"', space); + const status = line.slice(close + 2, close + 5); + statuses.set(status, (statuses.get(status) ?? 0) + 1); + bytes += Number(line.slice(close + 6, line.indexOf(" ", close + 6))); +}); +input.on("close", () => { + console.log(`${lines} lines, ${chars} characters, longest ${longest}`); + console.log(`bytes ${bytes}`); + for (const status of [...statuses.keys()].sort()) console.log(`status ${status}: ${statuses.get(status)}`); + for (const prefix of [...paths.keys()].sort()) console.log(`path ${prefix}: ${paths.get(prefix)}`); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8344-readline-chunk-splitting.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8344-readline-chunk-splitting.ts.json new file mode 100644 index 000000000..8c01e6b47 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8344-readline-chunk-splitting.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8344-readline-chunk-splitting.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index e6df837c8..6b8594c08 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -2722,7 +2722,9 @@ static ScrBytes *scr_bytes_encode_str(ScrBytesSink *sink, const ScrStr *s, const /* Node writes charCodeAt(i) & 0xFF for BOTH spellings — an astral * code point contributes its two surrogates' low bytes. */ ScrBytes *b = scr_bytes_conversion_storage_in(sink, n); - size_t o = 0, i = 0; + /* An ASCII prefix (all of an ASCII string) is its own encoding. */ + size_t o = s->ascii ? n : scr_bytes_ascii_prefix(in, n), i = o; + memcpy(b->data, in, o); while (i < n) { uint32_t cp = scr_bytes_next_cp(in, &i); if (cp > 0xffff) { diff --git a/packages/runtime/src/scr_readline.c b/packages/runtime/src/scr_readline.c index 1bc48bbe7..178d620c6 100644 --- a/packages/runtime/src/scr_readline.c +++ b/packages/runtime/src/scr_readline.c @@ -283,7 +283,23 @@ static void scr_rl_buf_append(ScrRl *rl, const char *p, size_t n) { rl->line_len += n; } -/* kNormalWrite over decoded UTF-8 text. */ +/* The end of the line starting at `i` in text without a CR: the next LF, + * or n when the text ends first. */ +static size_t scr_rl_next_lf(const char *p, size_t i, size_t n) { + const char *lf = memchr(p + i, '\n', n - i); + return lf ? (size_t)(lf - p) : n; +} + +/* The end of the line starting at `i`: the next LF or CR, or n. */ +static size_t scr_rl_next_break(const char *p, size_t i, size_t n) { + while (i < n && p[i] != '\n' && p[i] != '\r') i++; + return i; +} + +/* kNormalWrite over decoded UTF-8 text. The text is scanned once: LF-only + * text (no CR anywhere in it) moves from LF to LF with memchr, other text + * splits at LF, CR and CRLF. The unfinished line before the text only + * joins the first line, so the rest of the text is never copied whole. */ static void scr_rl_feed_text(ScrRl *rl, const char *p, size_t n) { if (rl->tty) { scr_rl_tty_ops->feed(rl, p, n); @@ -296,43 +312,31 @@ static void scr_rl_feed_text(ScrRl *rl, const char *p, size_t n) { } rl->saw_return_at = 0; } - bool ending = false; - for (size_t i = 0; i < n && !ending; i++) ending = p[i] == '\n' || p[i] == '\r'; - if (!ending) { + bool cr = n > 0 && memchr(p, '\r', n) != NULL; + size_t end = n == 0 ? 0 : cr ? scr_rl_next_break(p, 0, n) : scr_rl_next_lf(p, 0, n); + if (end == n) { if (n > 0) scr_rl_buf_append(rl, p, n); return; } - /* The unfinished line joins the new text; the scan runs over both. */ - char *text = p ? (char *)p : (char *)""; - size_t len = n; - char *joined = NULL; - if (rl->line_len > 0) { - joined = malloc(rl->line_len + n); - if (!joined) scr_rl_oom(); - memcpy(joined, rl->line_buf, rl->line_len); - memcpy(joined + rl->line_len, p, n); - text = joined; - len = rl->line_len + n; - } - rl->line_len = 0; - rl->saw_return_at = len > 0 && text[len - 1] == '\r' ? scr_now_ms() : 0; + rl->saw_return_at = p[n - 1] == '\r' ? scr_now_ms() : 0; /* Split first (the remainder becomes the new unfinished line before any * line is delivered, as in Node), then deliver in order. */ size_t n_lines = 0, cap_lines = 0; ScrStr **lines = NULL; size_t start = 0; - for (size_t i = 0; i < len; i++) { - size_t adv; - if (text[i] == '\n') adv = 1; - else if (text[i] == '\r') adv = i + 1 < len && text[i + 1] == '\n' ? 2 : 1; - else continue; + while (end < n) { lines = scr_rl_grow(lines, &cap_lines, n_lines + 1, sizeof *lines); - lines[n_lines++] = scr_str_new(text + start, i - start); - i += adv - 1; - start = i + 1; + if (n_lines == 0 && rl->line_len > 0) { + scr_rl_buf_append(rl, p, end); + lines[n_lines++] = scr_str_new(rl->line_buf, rl->line_len); + } else { + lines[n_lines++] = scr_str_new(p + start, end - start); + } + start = end + (p[end] == '\r' && end + 1 < n && p[end + 1] == '\n' ? 2 : 1); + end = start >= n ? n : cr ? scr_rl_next_break(p, start, n) : scr_rl_next_lf(p, start, n); } - if (start < len) scr_rl_buf_append(rl, text + start, len - start); - free(joined); + rl->line_len = 0; + if (start < n) scr_rl_buf_append(rl, p + start, n - start); for (size_t i = 0; i < n_lines; i++) { if (rl->closed || scr_exc_pending()) { scr_str_release(lines[i]); @@ -366,6 +370,11 @@ static void scr_rl_feed_bytes(ScrRl *rl, const char *p, size_t n) { rl->parked_len += n; return; } + /* ASCII with no held bytes decodes to itself: split it in place. */ + if (rl->n_held == 0 && scr_bytes_ascii_span(p, n) == n) { + scr_rl_feed_text(rl, p, n); + return; + } size_t total = rl->n_held + n; unsigned char *all = malloc(total ? total : 1); if (!all) scr_rl_oom(); @@ -401,6 +410,12 @@ static void scr_rl_feed_end(ScrRl *rl) { void scr_rl_input_bytes(double id, ScrBytes *chunk) { ScrRl *rl = scr_rl_find(id); if (!rl || rl->closed) return; + /* The splitter copies every line it keeps before it delivers one, so it + * can read a chunk's own storage; shared memory is copied out first. */ + if (!chunk->shared) { + scr_rl_feed_bytes(rl, (const char *)chunk->data, chunk->len); + return; + } char *p = malloc(chunk->len ? chunk->len : 1); if (!p) scr_rl_oom(); scr_bytes_read(chunk, 0, p, chunk->len); diff --git a/scripts/bench-runtime.mjs b/scripts/bench-runtime.mjs index b45e3ebdf..d89a4e759 100644 --- a/scripts/bench-runtime.mjs +++ b/scripts/bench-runtime.mjs @@ -180,6 +180,16 @@ function generateInput(kind) { ["sku-" + i, "category-" + (i % 17), "product " + i, i % 40, 199 + (i % 5000)].join("\t"), ).join("\n") + "\n", ); + } else if (kind === "line-reader") { + // About 67 MB of access-log lines; every 50th path is not ASCII. + writeFileSync( + path, + Array.from( + { length: 600_000 }, + (_, i) => + `10.${i % 251}.${i % 7}.${i % 13} - - [24/Oct/2026:${String(i % 24).padStart(2, "0")}:${String(i % 60).padStart(2, "0")}:00 +0000] "${i % 9 === 0 ? "POST" : "GET"} /${i % 50 === 0 ? "caf\u00e9" : "api"}/items/${i % 997}?page=${i % 31} HTTP/1.1" ${i % 23 === 0 ? 404 : 200} ${(i * 7919) % 50000} "scriptc-bench/1.0"`, + ).join("\n") + "\n", + ); } else if (kind === "cli-config") { writeFileSync(path, JSON.stringify(deployManifest(), null, 2) + "\n"); } else throw new Error(`unknown input kind ${kind}`); diff --git a/tests/corpus/8344-readline-chunk-splitting.ts b/tests/corpus/8344-readline-chunk-splitting.ts new file mode 100644 index 000000000..85eff8cdc --- /dev/null +++ b/tests/corpus/8344-readline-chunk-splitting.ts @@ -0,0 +1,41 @@ +// Line splitting over stream chunks: an unfinished line joined across many +// chunks, a line longer than a chunk, empty lines, LF-only +// chunks next to chunks with CR and CRLF, an ASCII chunk after a chunk that +// ends inside a UTF-8 sequence, and text written with rl.write(). Each line +// event shows the line's length and its first and last characters. +import { createInterface } from "node:readline"; +import { Readable } from "node:stream"; + +function show(tag: string, line: string): void { + console.log(tag, line.length, JSON.stringify(line.slice(0, 12)), JSON.stringify(line.slice(-6))); +} + +function feed(tag: string, chunks: (string | Buffer)[]): Promise { + return new Promise((resolve) => { + const src = new Readable({ read() {} }); + const rl = createInterface({ input: src, crlfDelay: Infinity }); + rl.on("line", (line: string) => show(tag, line)); + rl.on("close", () => { + console.log(tag, "closed"); + resolve(); + }); + for (const chunk of chunks) src.push(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk); + src.push(null); + }); +} + +const long = "x".repeat(70000) + "y"; +await feed("joined", ["par", "tial ", "line", " continues", "\nnext\n", "tail"]); +await feed("long", [long.slice(0, 30000), long.slice(30000, 65536), long.slice(65536) + "\nshort\n"]); +await feed("empty", ["\n\n", "a\n\n", "\n", "b"]); +await feed("mixed", ["lf only\nstill lf\n", "cr\rcrlf\r\nlf\n", "end\r", "\nafter\r", "\r\n", "done"]); +const euro = Buffer.from("€uro\n", "utf8"); +await feed("utf8", [euro.subarray(0, 2), Buffer.concat([euro.subarray(2), Buffer.from("plain ascii\nmore")])]); +await feed("utf8-lines", ["café one\nnaïve two\n", "ascii three\n", "中文 four"]); + +const writer = createInterface({ input: new Readable({ read() {} }), crlfDelay: Infinity }); +writer.on("line", (line: string) => show("write", line)); +writer.write("first part "); +writer.write("second part\nthird\r\nfourth\rfifth"); +writer.write("\n"); +writer.close(); From 3ef34770bd7a652169db35bc0d21b8934c43feef Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 01:41:31 -0500 Subject: [PATCH 177/273] Read optional elements of bindings that hold undefined as undefined `a?.[i]` on a binding whose type admits undefined, such as a `string[] | undefined` parameter, threw "undefined is not representable in the target union" when the binding held undefined. Element access replaced an identifier receiver whose storage has an undefined arm with a checked narrow to its present arm before the optional chain ran, so the chain saw a receiver that is never nullish and lowered `?.[` as a plain element read. Element access now leaves the receiver to the optional chain when the access carries `?.`; the chain tests the undefined arm and lowers the access again inside its guard. This applies to arrays, tuples and record keys. Property access (`a?.length`, `a?.at(0)`) already lowered through the chain. Corpus 8330 matches Node's output. --- .../compiler/src/frontend/lowering/lowerer.ts | 4 + ...tional-element-undefined-receivers.ts.json | 6 ++ ...30-optional-element-undefined-receivers.ts | 75 +++++++++++++++++++ ...tional-element-undefined-receivers.ts.json | 1 + 4 files changed, 86 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8330-optional-element-undefined-receivers.ts.json create mode 100644 tests/corpus/8330-optional-element-undefined-receivers.ts create mode 100644 tests/harness/corpus-costs/8330-optional-element-undefined-receivers.ts.json diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 15c0ef993..9065e691e 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -13145,6 +13145,10 @@ export class Lowerer { ) { return lowerElementAccess(this, expr); } + // `a?.[i]`: the optional chain tests the receiver's undefined arm and + // lowers this access again inside its guard. + if (expr.questionDotToken && !this.chainHandled.has(expr)) + return lowerElementAccess(this, expr); const receiver = this.runtimeOptionalIdentifierValue(expr.expression); const receiverValue: IrExpr | null = receiver && (receiver.present.kind === "array" || receiver.present.kind === "record") diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8330-optional-element-undefined-receivers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8330-optional-element-undefined-receivers.ts.json new file mode 100644 index 000000000..ef78d4391 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8330-optional-element-undefined-receivers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8330-optional-element-undefined-receivers.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8330-optional-element-undefined-receivers.ts b/tests/corpus/8330-optional-element-undefined-receivers.ts new file mode 100644 index 000000000..b9433e66c --- /dev/null +++ b/tests/corpus/8330-optional-element-undefined-receivers.ts @@ -0,0 +1,75 @@ +// Optional element access on bindings whose type admits undefined: an +// undefined receiver short-circuits to undefined without evaluating the +// key, and a present receiver reads the element as a plain access does. + +function first(kinds: string[] | undefined, i: number): string { + const kind = kinds?.[i]; + return kind === undefined ? "none" : String(kind); +} +console.log(first(undefined, 0)); +console.log(first(["a", "b"], 1)); +console.log(first(["a"], 5)); + +function orDefault(values: number[] | undefined, i: number): number { + return values?.[i] ?? -1; +} +console.log(orDefault(undefined, 0), orDefault([4, 5], 1), orDefault([4], 3)); + +function isMissing(flags: boolean[] | undefined, i: number): boolean { + return flags?.[i] === undefined; +} +console.log(isMissing(undefined, 0), isMissing([true], 0), isMissing([false], 2)); + +function head(pair: [string, number] | undefined): string { + return pair?.[0] ?? "empty"; +} +console.log(head(undefined), head(["x", 1])); + +function lookup(table: Record | undefined, key: string): number | undefined { + return table?.[key]; +} +console.log(lookup(undefined, "a"), lookup({ a: 1 }, "a"), lookup({ a: 1 }, "b")); + +function width(words: string[] | undefined, i: number): number { + return words?.[i]?.length ?? 0; +} +console.log(width(undefined, 0), width(["abc"], 0), width(["abc"], 1)); + +// The key is not evaluated when the receiver is undefined. +let keys = 0; +function nextKey(): number { + keys++; + return 0; +} +function counted(items: string[] | undefined): string | undefined { + return items?.[nextKey()]; +} +console.log(counted(undefined), keys); +console.log(counted(["k"]), keys); + +// Local bindings, including one reassigned between reads. +function maybe(present: boolean): number[] | undefined { + return present ? [7, 8] : undefined; +} +let current = maybe(false); +console.log(current?.[0]); +current = maybe(true); +console.log(current?.[1]); +current = maybe(false); +console.log(current?.[1]); + +function local(present: boolean): string { + const items: string[] | undefined = present ? ["p", "q"] : undefined; + const second = items?.[1]; + return second ?? "absent"; +} +console.log(local(true), local(false)); + +// A sparse array reads its holes as undefined. +const sparse: number[] = [1]; +sparse[3] = 4; +function at(values: number[] | undefined, i: number): string { + const value = values?.[i]; + return value === undefined ? "hole" : String(value); +} +console.log(at(sparse, 1), at(sparse, 3), at(undefined, 3)); diff --git a/tests/harness/corpus-costs/8330-optional-element-undefined-receivers.ts.json b/tests/harness/corpus-costs/8330-optional-element-undefined-receivers.ts.json new file mode 100644 index 000000000..662eb397f --- /dev/null +++ b/tests/harness/corpus-costs/8330-optional-element-undefined-receivers.ts.json @@ -0,0 +1 @@ +{ "plain": 2108 } From 8b0921243f87dd17f096eb030c52648b35a49c96 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 01:59:32 -0500 Subject: [PATCH 178/273] Compile large sanitized dev programs in shards --- packages/compiler/src/backend/library-pack.ts | 2 +- .../compiler/src/backend/llvm/split.test.ts | 132 ++++++- packages/compiler/src/backend/llvm/split.ts | 335 ++++++++++++++---- .../src/backend/native-toolchain.test.ts | 22 +- .../compiler/src/backend/native/contracts.ts | 4 +- .../compiler/src/backend/native/executable.ts | 18 +- .../compiler/src/backend/native/library.ts | 4 +- .../src/backend/native/program-shards.ts | 6 +- packages/compiler/src/index.ts | 11 +- packages/compiler/src/native/compiler.ts | 2 +- tests/harness/three.test.ts | 6 +- 11 files changed, 431 insertions(+), 111 deletions(-) diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index e7fa7a782..521f1d53e 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -153,7 +153,7 @@ export async function compilePackedLibrary( ...(await Promise.all( shards .slice(start, start + 4) - .map((shard, offset) => emit(shard.source, `shard-${start + offset}.o`)), + .map((shard, offset) => emit(shard.source(), `shard-${start + offset}.o`)), )), ); } diff --git a/packages/compiler/src/backend/llvm/split.test.ts b/packages/compiler/src/backend/llvm/split.test.ts index a49a11545..2e0cfa5a5 100644 --- a/packages/compiler/src/backend/llvm/split.test.ts +++ b/packages/compiler/src/backend/llvm/split.test.ts @@ -3,7 +3,7 @@ import { mkdtemp, readFile, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, expect, test } from "vitest"; -import { splitLlvmLibraryProgram, splitLlvmProgram } from "./split.js"; +import { splitLlvmLibraryProgram, splitLlvmProgram, splitLlvmProgramFile } from "./split.js"; import { LlvmDebugInfo } from "./debug-info.js"; import { VOID } from "../../ir/ir.js"; @@ -61,17 +61,17 @@ test("splits generated LLVM functions and promotes only private cross-shard defi expect(actual!.promotedSymbols).not.toContain("public_entry"); expect(actual!.publicSymbols).toEqual(["public_value", "public_entry"]); expect(actual!.shards[0]!.name).toBe("program-globals.ll"); - expect(actual!.shards[0]!.source).toContain("@hidden_value = hidden global i64 7"); + expect(actual!.shards[0]!.source()).toContain("@hidden_value = hidden global i64 7"); expect( actual!.shards .slice(1) - .every((shard) => shard.source.includes("@hidden_value = external hidden global i64")), + .every((shard) => shard.source().includes("@hidden_value = external hidden global i64")), ).toBe(true); expect( actual!.shards.some( (shard) => - !shard.source.includes("define hidden i64 @right() #0") && - shard.source.includes("declare hidden i64 @right() #0"), + !shard.source().includes("define hidden i64 @right() #0") && + shard.source().includes("declare hidden i64 @right() #0"), ), ).toBe(true); }); @@ -82,9 +82,115 @@ test("thread-local definitions and declarations preserve their storage model acr "@hidden_value = internal thread_local global i64 7", ); const split = splitLlvmProgram(tls, { minimumBytes: 0, targetBytes: 64 * 1024 })!; - expect(split.shards[0]!.source).toContain("@hidden_value = hidden thread_local global i64 7"); + expect(split.shards[0]!.source()).toContain("@hidden_value = hidden thread_local global i64 7"); for (const shard of split.shards.slice(1)) { - expect(shard.source).toContain("@hidden_value = external hidden thread_local global i64"); + expect(shard.source()).toContain("@hidden_value = external hidden thread_local global i64"); + } +}); + +const PADDING = Array.from( + { length: 80 }, + (_, i) => + `define internal i64 @pad_${i}() #0 {\nentry:\n ; ${"x".repeat(2048)}\n ret i64 ${i}\n}\n`, +).join("\n"); + +/** SAMPLE with more globals after @public_value and enough functions to + * split into several shards. */ +function withGlobals(globals: string, tail = ""): string { + return `${SAMPLE.replace( + "@public_value = constant i64 11", + `@public_value = constant i64 11\n${globals}`, + ).replace("define i64 @public_entry", `${PADDING}\ndefine i64 @public_entry`)}${tail}`; +} + +/** Writes each shard and compiles it with clang. */ +async function compileShards( + shards: readonly { name: string; source: () => string }[], +): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-llvm-split-shards-")); + scratch.push(dir); + for (const shard of shards) { + const source = join(dir, shard.name); + await writeFile(source, shard.source()); + execFileSync("clang", ["-Wno-override-module", "-c", source, "-o", `${source}.o`]); + } +} + +test("private unnamed_addr lookup tables are promoted like internal globals", async () => { + const split = splitLlvmProgram( + withGlobals('@table = private unnamed_addr constant [2 x i8] c"\\01\\02"'), + { minimumBytes: 0, targetBytes: 64 * 1024 }, + )!; + expect(split).not.toBeNull(); + const [globals, ...functions] = split.shards; + expect(globals!.source()).toContain('@table = hidden unnamed_addr constant [2 x i8] c"\\01\\02"'); + for (const shard of functions) + expect(shard.source()).toContain("@table = external hidden constant [2 x i8]"); + expect(split.promotedSymbols).toContain("table"); + await compileShards(split.shards); +}); + +test("aggregate initializers over several lines stay whole in the globals shard", async () => { + // The regex global's comment holds unbalanced brackets. + const split = splitLlvmProgram( + withGlobals(`@regex = internal global { ptr, i64 } { ptr @left, i64 0 } ; /(a|[b/ +@vt = internal constant [2 x { i64, ptr }] [ + { i64, ptr } { i64 1, ptr @left }, + { i64, ptr } { i64 2, ptr @right } +]`), + { minimumBytes: 0, targetBytes: 64 * 1024 }, + )!; + expect(split).not.toBeNull(); + const [globals, ...functions] = split.shards; + expect(globals!.source()).toContain( + "@vt = hidden constant [2 x { i64, ptr }] [\n { i64, ptr } { i64 1, ptr @left },\n { i64, ptr } { i64 2, ptr @right }\n]", + ); + for (const shard of functions) { + expect(shard.source()).toContain("@vt = external hidden constant [2 x { i64, ptr }]"); + expect(shard.source()).not.toContain("{ i64, ptr } { i64 1"); + } + expect(split.promotedSymbols).toEqual(expect.arrayContaining(["regex", "vt"])); + await compileShards(split.shards); +}); + +test("globals after the attributes and metadata join the globals shard", async () => { + const split = splitLlvmProgram(withGlobals("", "@flag = internal constant i1 true\n"), { + minimumBytes: 0, + targetBytes: 64 * 1024, + })!; + expect(split).not.toBeNull(); + const [globals, ...functions] = split.shards; + expect(globals!.source()).toContain("@flag = hidden constant i1 true"); + for (const shard of functions) + expect(shard.source()).toContain("@flag = external hidden constant i1"); + await compileShards(split.shards); +}); + +test("a module split from its file has the shards of the same module split in memory", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-llvm-split-file-")); + scratch.push(dir); + const options = { minimumBytes: 0, targetBytes: 64 * 1024 }; + const storage = withGlobals( + `@table = private unnamed_addr constant [2 x i8] c"\\01\\02" +@vt = internal constant [2 x { i64, ptr }] [ + { i64, ptr } { i64 1, ptr @left }, + { i64, ptr } { i64 2, ptr @right } +]`, + "@flag = internal constant i1 true\n", + ); + for (const [name, source] of [ + ["sample.ll", withGlobals("")], + ["storage.ll", storage], + ] as const) { + const path = join(dir, name); + await writeFile(path, source); + const fromFile = splitLlvmProgramFile(path, options)!; + expect(fromFile).not.toBeNull(); + const materialize = (split: NonNullable) => ({ + ...split, + shards: split.shards.map((shard) => ({ name: shard.name, source: shard.source() })), + }); + expect(materialize(fromFile)).toEqual(materialize(splitLlvmProgram(source, options)!)); } }); @@ -127,7 +233,7 @@ entry: for (const shard of split.shards) { const path = join(dir, shard.name); const object = `${path}.o`; - await writeFile(path, shard.source); + await writeFile(path, shard.source()); execFileSync("clang", ["-Wno-override-module", "-O2", "-c", path, "-o", object]); objects.push(object); } @@ -169,10 +275,10 @@ test("shared metadata bounds shard growth while preserving every definition", () expect(split).not.toBeNull(); expect(split!.shards.length).toBeGreaterThan(1); expect( - split!.shards.reduce((bytes, shard) => bytes + Buffer.byteLength(shard.source), 0), + split!.shards.reduce((bytes, shard) => bytes + Buffer.byteLength(shard.source()), 0), ).toBeLessThanOrEqual(2 * Buffer.byteLength(source)); const definitions = split!.shards.flatMap((shard) => - [...shard.source.matchAll(/^define (?:hidden )?i64 @(\w+)\(/gm)].map((match) => match[1]), + [...shard.source().matchAll(/^define (?:hidden )?i64 @(\w+)\(/gm)].map((match) => match[1]), ); expect(definitions.sort()).toEqual([...names, "left", "right", "public_entry"].sort()); expect(split!.publicSymbols).toEqual(["public_value", "public_entry"]); @@ -207,9 +313,9 @@ test("debug metadata stays on definitions when splitting LLVM modules", async () const dir = await mkdtemp(join(tmpdir(), "scriptc-debug-split-")); scratch.push(dir); for (const shard of split!.shards) { - expect(shard.source).not.toMatch(/^declare.*!dbg/m); + expect(shard.source()).not.toMatch(/^declare.*!dbg/m); const path = join(dir, shard.name); - await writeFile(path, shard.source); + await writeFile(path, shard.source()); const result = spawnSync("clang", ["-Wno-override-module", "-c", path, "-o", `${path}.o`], { encoding: "utf8", }); @@ -250,7 +356,7 @@ test("every shard compiles and its merged object exposes only canonical public d for (const shard of split.shards) { const source = join(dir, shard.name); const object = `${source}.o`; - await writeFile(source, shard.source); + await writeFile(source, shard.source()); execFileSync("clang", ["-Wno-override-module", "-c", source, "-o", object]); objects.push(object); } diff --git a/packages/compiler/src/backend/llvm/split.ts b/packages/compiler/src/backend/llvm/split.ts index 3fb41ce6b..aecef8df8 100644 --- a/packages/compiler/src/backend/llvm/split.ts +++ b/packages/compiler/src/backend/llvm/split.ts @@ -19,10 +19,15 @@ * canonical TU normally — cache optimization must never become correctness. */ +import { closeSync, openSync, readSync, statSync } from "node:fs"; + export interface LlvmProgramShard { /** Stable source identity used in object cache keys and diagnostics. */ name: string; - source: string; + /** Assembles the shard's LLVM text. A module split from a file reads its + * function bodies back from there on each call, so only the shards being + * written are in memory. */ + source: () => string; } export interface LlvmProgramSplit { @@ -54,12 +59,20 @@ const LIBRARY_TARGET_BYTES = 768 * 1024; const LIBRARY_MINIMUM_BYTES = 2 * 1024 * 1024; interface FunctionDef { - source: string; + /** UTF-8 length of the definition's text. */ + bytes: number; + /** The definition's text: its header (promoted to hidden linkage when + * internal), body and closing brace. A definition left in a file reads + * its body through `read`. */ + text: (read: RangeReader) => string; declaration: string; symbol: string; promoted: boolean; } +/** Reads the text between two byte offsets of the module file. */ +type RangeReader = (start: number, end: number) => string; + interface GlobalDef { line: string; declaration: string | null; @@ -72,50 +85,68 @@ function symbolOf(spelling: string): string | null { return match?.[1] ?? null; } -function functionDefAt( - lines: readonly string[], - start: number, -): { def: FunctionDef; end: number } | null { - const header = lines[start]!; +/** A definition's header line: the line its shard compiles, the + * declaration every other shard carries, and its linkage. */ +function functionHeader( + header: string, +): { line: string; declaration: string; symbol: string; promoted: boolean } | null { const match = /^define\s+(internal\s+)?(.+?)\s+(@[-$._A-Za-z][-$.\w]*)\((.*)\)(.*)\{(?:\s*;.*)?$/.exec( header, ); if (!match) return null; - let end = start + 1; - while (end < lines.length && lines[end] !== "}") end++; - if (end >= lines.length) return null; const symbol = symbolOf(match[3]!); if (symbol === null) return null; const promoted = match[1] !== undefined; const linkage = promoted ? "hidden " : ""; - const definitionHeader = promoted - ? header.replace(/^define internal /, "define hidden ") - : header; - const source = [definitionHeader, ...lines.slice(start + 1, end + 1)].join("\n"); + const line = promoted ? header.replace(/^define internal /, "define hidden ") : header; // Parameter names are legal on declarations. Preserve the emitted text so // attributes, zeroext, and varargs stay byte-exact. // A definition's DISubprogram cannot be attached to a declaration in a // different shard. Its owning definition retains the original metadata. const attrs = match[5]!.replace(/\s+!dbg !\d+/g, ""); const declaration = `declare ${linkage}${match[2]} ${match[3]}(${match[4]})${attrs}`.trimEnd(); - return { def: { source, declaration, symbol, promoted }, end }; + return { line, declaration, symbol, promoted }; +} + +function functionDefAt( + lines: readonly string[], + start: number, +): { def: FunctionDef; end: number } | null { + const header = functionHeader(lines[start]!); + if (header === null) return null; + let end = start + 1; + while (end < lines.length && lines[end] !== "}") end++; + if (end >= lines.length) return null; + const source = [header.line, ...lines.slice(start + 1, end + 1)].join("\n"); + return { + def: { + bytes: Buffer.byteLength(source), + text: () => source, + declaration: header.declaration, + symbol: header.symbol, + promoted: header.promoted, + }, + end, + }; } function globalDef(line: string): GlobalDef | null { const match = - /^(@[-$._A-Za-z][-$.\w]*)\s+=\s+(internal\s+)?(thread_local(?:\([^)]*\))?\s+)?(global|constant)\s+(.+)$/.exec( + /^(@[-$._A-Za-z][-$.\w]*)\s+=\s+((?:internal|private)\s+)?(thread_local(?:\([^)]*\))?\s+)?((?:local_)?unnamed_addr\s+)?(global|constant)\s+([\s\S]+)$/.exec( line, ); if (!match) return null; const symbol = symbolOf(match[1]!); if (symbol === null) return null; + // Internal and private definitions (such as private unnamed_addr lookup + // tables) are local to their module: shards reach them as hidden ones. const promoted = match[2] !== undefined; const threadLocal = match[3] === undefined ? "" : `${match[3].trim()} `; // The declaration needs only the complete type. Emitted initializers begin // after it; the generated LLVM subset uses first-class scalars/pointers or // balanced aggregate types, so one small scanner is sufficient. - const rest = match[5]!; + const rest = match[6]!; let depth = 0; let quote = false; let typeEnd = -1; @@ -133,13 +164,30 @@ function globalDef(line: string): GlobalDef | null { if (typeEnd < 0) return null; const type = rest.slice(0, typeEnd); return { - line: promoted ? line.replace(`${match[1]} = internal `, `${match[1]} = hidden `) : line, - declaration: `${match[1]} = external ${promoted ? "hidden " : ""}${threadLocal}${match[4]} ${type}`, + line: promoted ? line.replace(`${match[1]} = ${match[2]}`, `${match[1]} = hidden `) : line, + declaration: `${match[1]} = external ${promoted ? "hidden " : ""}${threadLocal}${match[5]} ${type}`, symbol, promoted, }; } +/** The change in aggregate nesting depth over one line of LLVM text, + * outside string constants and comments (a regex global's comment spells + * its source). */ +function bracketDepth(line: string): number { + let depth = 0; + let quote = false; + for (let i = 0; i < line.length; i++) { + const ch = line[i]!; + if (ch === '"') quote = !quote; + if (quote) continue; + if (ch === ";") break; + if (ch === "{" || ch === "[" || ch === "<" || ch === "(") depth++; + else if (ch === "}" || ch === "]" || ch === ">" || ch === ")") depth--; + } + return depth; +} + function stableBucket(symbol: string, count: number): number { let hash = 0x811c9dc5; for (let i = 0; i < symbol.length; i++) { @@ -154,7 +202,7 @@ function chunksOf( targetBytes: number, maximumBuckets: number, ): { bucket: number; functions: FunctionDef[] }[] { - const totalBytes = functions.reduce((sum, fn) => sum + Buffer.byteLength(fn.source) + 2, 0); + const totalBytes = functions.reduce((sum, fn) => sum + fn.bytes + 2, 0); // A power-of-two bucket count changes only when the program crosses a wide // size band. Ordinary edits therefore keep every function in the same // symbol-hash bucket; ceil(total/target) would reshuffle the whole program @@ -168,6 +216,74 @@ function chunksOf( ); } +/** A module's top-level forms, as the splitter classifies them. */ +interface ModuleParts { + preamble: string[]; + globals: GlobalDef[]; + functions: FunctionDef[]; + trailer: string[]; +} + +/** Classifies a module one line at a time. `define` lines go to + * `onFunction`, which takes the following lines through the definition's + * closing brace; every other line is offered to `line`. */ +class ModuleReader { + readonly parts: ModuleParts = { preamble: [], globals: [], functions: [], trailer: [] }; + private sawDefinition = false; + private inTrailer = false; + /** A global definition whose aggregate initializer continues over the + * following lines (vtable arrays list one element per line). */ + private pendingGlobal: { text: string; depth: number } | null = null; + failed = false; + + /** Whether a definition may start here (none may follow the trailer). */ + startFunction(): boolean { + if (this.inTrailer || this.pendingGlobal !== null) this.failed = true; + this.sawDefinition = true; + return !this.failed; + } + + line(line: string): void { + if (this.pendingGlobal !== null) { + this.pendingGlobal.text += "\n" + line; + this.pendingGlobal.depth += bracketDepth(line); + if (this.pendingGlobal.depth <= 0) { + const { text } = this.pendingGlobal; + this.pendingGlobal = null; + this.global(text); + } + return; + } + if (line.startsWith("@") && line.includes(" = ")) { + const depth = bracketDepth(line); + if (depth > 0) this.pendingGlobal = { text: line, depth }; + else this.global(line); + return; + } + if (line.startsWith("attributes ") || line.startsWith("!")) this.inTrailer = true; + if (this.inTrailer) this.parts.trailer.push(line); + else if (!this.sawDefinition || line.trim() !== "") this.parts.preamble.push(line); + } + + /** Globals may follow the attributes and metadata (the feature flags the + * link-features step appends); they are module storage like any other. */ + private global(definition: string): void { + const parsed = globalDef(definition); + if (parsed === null) { + // External declarations are already valid in every shard. + if (/^@.+\s+=\s+external\s+/.test(definition)) this.parts.preamble.push(definition); + else this.failed = true; + } else { + this.parts.globals.push(parsed); + this.sawDefinition = true; + } + } + + finish(): ModuleParts | null { + return this.failed || this.pendingGlobal !== null ? null : this.parts; + } +} + export function splitLlvmProgram( source: string, options: LlvmProgramSplitOptions = {}, @@ -175,42 +291,129 @@ export function splitLlvmProgram( const minimumBytes = options.minimumBytes ?? DEFAULT_MINIMUM_BYTES; const sourceBytes = Buffer.byteLength(source); if (sourceBytes < minimumBytes) return null; - const targetBytes = Math.max(64 * 1024, options.targetBytes ?? DEFAULT_TARGET_BYTES); const lines = source.split("\n"); - const preamble: string[] = []; - const globals: GlobalDef[] = []; - const functions: FunctionDef[] = []; - const trailer: string[] = []; - let sawDefinition = false; - let inTrailer = false; - for (let i = 0; i < lines.length; i++) { + const reader = new ModuleReader(); + for (let i = 0; i < lines.length && !reader.failed; i++) { const line = lines[i]!; if (line.startsWith("define ")) { - if (inTrailer) return null; + if (!reader.startFunction()) break; const parsed = functionDefAt(lines, i); if (parsed === null) return null; - functions.push(parsed.def); - sawDefinition = true; + reader.parts.functions.push(parsed.def); i = parsed.end; - continue; - } - if (line.startsWith("@") && line.includes(" = ")) { - if (inTrailer) return null; - const parsed = globalDef(line); - if (parsed === null) { - // External declarations are already valid in every shard. - if (/^@.+\s+=\s+external\s+/.test(line)) preamble.push(line); - else return null; - } else { - globals.push(parsed); - sawDefinition = true; - } - continue; - } - if (line.startsWith("attributes ") || line.startsWith("!")) inTrailer = true; - if (inTrailer) trailer.push(line); - else if (!sawDefinition || line.trim() !== "") preamble.push(line); + } else reader.line(line); } + const parts = reader.finish(); + return parts === null ? null : shardModule(parts, sourceBytes, options, null); +} + +/** The read size of splitLlvmProgramFile's scan. */ +const FILE_CHUNK_BYTES = 16 * 1024 * 1024; + +/** splitLlvmProgram over LLVM text already written to `path`, for modules + * too large to hold in memory more than once (or past the longest string the + * engine allows). Function bodies stay in the file: each shard's source + * reads its bodies when it is read, so only the shards being written at a + * time are in memory. */ +export function splitLlvmProgramFile( + path: string, + options: LlvmProgramSplitOptions = {}, +): LlvmProgramSplit | null { + const minimumBytes = options.minimumBytes ?? DEFAULT_MINIMUM_BYTES; + const sourceBytes = statSync(path).size; + if (sourceBytes < minimumBytes) return null; + const reader = new ModuleReader(); + // The definition being read: its header and where its body starts. + let current: { header: NonNullable>; body: number } | null = + null; + const onLine = (bytes: Buffer, start: number, end: number, offset: number): void => { + if (current !== null) { + // Only the closing brace ends a definition; its body is not decoded. + if (end - start === 1 && bytes[start] === 0x7d) { + const { header, body } = current; + const bodyEnd = offset + 1; + reader.parts.functions.push({ + bytes: Buffer.byteLength(header.line) + 1 + (bodyEnd - body), + text: (read) => `${header.line}\n${read(body, bodyEnd)}`, + declaration: header.declaration, + symbol: header.symbol, + promoted: header.promoted, + }); + current = null; + } + return; + } + const line = bytes.toString("utf8", start, end); + if (line.startsWith("define ")) { + const header = functionHeader(line); + if (header === null || !reader.startFunction()) reader.failed = true; + else current = { header, body: offset + (end - start) + 1 }; + } else reader.line(line); + }; + const fd = openSync(path, "r"); + try { + let carry: Buffer = Buffer.alloc(0); + let carryOffset = 0; + for (let position = 0; position < sourceBytes && !reader.failed;) { + const chunk = Buffer.allocUnsafe(Math.min(FILE_CHUNK_BYTES, sourceBytes - position)); + const read = readSync(fd, chunk, 0, chunk.length, position); + if (read === 0) break; + const data = + carry.length === 0 + ? chunk.subarray(0, read) + : Buffer.concat([carry, chunk.subarray(0, read)]); + const dataOffset = carry.length === 0 ? position : carryOffset; + position += read; + let lineStart = 0; + for (let newline = data.indexOf(10); newline >= 0; newline = data.indexOf(10, lineStart)) { + onLine(data, lineStart, newline, dataOffset + lineStart); + lineStart = newline + 1; + } + carry = Buffer.from(data.subarray(lineStart)); + carryOffset = dataOffset + lineStart; + } + // The text after the last newline is a line too (empty when the file + // ends with one), as String.prototype.split answers it. + if (!reader.failed) onLine(carry, 0, carry.length, carryOffset); + } finally { + closeSync(fd); + } + if (current !== null) return null; + const parts = reader.finish(); + return parts === null ? null : shardModule(parts, sourceBytes, options, path); +} + +/** Runs `use` with a reader of `path`'s byte ranges (or of nothing, for a + * module held in memory). */ +function withRangeReader(path: string | null, use: (read: RangeReader) => T): T { + if (path === null) + return use(() => { + throw new Error("an in-memory LLVM module has no file to read"); + }); + const fd = openSync(path, "r"); + try { + return use((start, end) => { + const bytes = Buffer.allocUnsafe(end - start); + for (let done = 0; done < bytes.length;) { + const read = readSync(fd, bytes, done, bytes.length - done, start + done); + if (read === 0) throw new Error(`${path} ended before byte ${end}`); + done += read; + } + return bytes.toString("utf8"); + }); + } finally { + closeSync(fd); + } +} + +function shardModule( + parts: ModuleParts, + sourceBytes: number, + options: LlvmProgramSplitOptions, + path: string | null, +): LlvmProgramSplit | null { + const { preamble, globals, functions, trailer } = parts; + const targetBytes = Math.max(64 * 1024, options.targetBytes ?? DEFAULT_TARGET_BYTES); if (functions.length < 2) return null; const globalDecls = globals.flatMap((global) => global.declaration === null ? [] : [global.declaration], @@ -230,16 +433,22 @@ export function splitLlvmProgram( const chunks = chunksOf(functions, targetBytes, 2 ** Math.floor(Math.log2(bucketLimit))); if (chunks.length < 2) return null; const tail = trailer.length === 0 ? "" : `\n${trailer.join("\n")}`; - const functionShards = chunks.map(({ bucket, functions: chunk }) => { + // A shard's source is assembled when it is read; a module split from a + // file reads its function bodies from there each time. + const functionShards = chunks.map(({ bucket, functions: chunk }): LlvmProgramShard => { const owned = new Set(chunk); - const declarations = functions.filter((fn) => !owned.has(fn)).map((fn) => fn.declaration); return { name: `program-f${bucket.toString().padStart(3, "0")}.ll`, - source: [ - [...preamble, ...globalDecls, ...declarations, ""].join("\n"), - ...chunk.map((fn) => fn.source), - tail, - ].join("\n"), + source: () => { + const declarations = functions.filter((fn) => !owned.has(fn)).map((fn) => fn.declaration); + return withRangeReader(path, (read) => + [ + [...preamble, ...globalDecls, ...declarations, ""].join("\n"), + ...chunk.map((fn) => fn.text(read)), + tail, + ].join("\n"), + ); + }, }; }); // Global initializer changes are common (literal text, generated tables) @@ -247,15 +456,15 @@ export function splitLlvmProgram( // all module storage in one dedicated shard that declares every function. const globalShard: LlvmProgramShard = { name: "program-globals.ll", - source: [ - [...preamble, ...functions.map((fn) => fn.declaration), ""].join("\n"), - ...globals.map((global) => global.line), - tail, - ].join("\n"), + source: () => + [ + [...preamble, ...functions.map((fn) => fn.declaration), ""].join("\n"), + ...globals.map((global) => global.line), + tail, + ].join("\n"), }; - const shards = [globalShard, ...functionShards]; return { - shards, + shards: [globalShard, ...functionShards], promotedSymbols: [ ...globals.filter((global) => global.promoted).map((global) => global.symbol), ...functions.filter((fn) => fn.promoted).map((fn) => fn.symbol), diff --git a/packages/compiler/src/backend/native-toolchain.test.ts b/packages/compiler/src/backend/native-toolchain.test.ts index fd7a5325f..f1bcde404 100644 --- a/packages/compiler/src/backend/native-toolchain.test.ts +++ b/packages/compiler/src/backend/native-toolchain.test.ts @@ -3983,12 +3983,12 @@ test.skipIf( programShards: [ { name: "program-f000.ll", - source: + source: () => "define i32 @main() {\nentry:\n ret i32 7\n}\ndeclare hidden i64 @other_value()\n", }, { name: "program-f001.ll", - source: + source: () => "declare i32 @main()\ndefine hidden i64 @other_value() {\nentry:\n ret i64 9\n}\n", }, ], @@ -4038,11 +4038,11 @@ test.skipIf(!nativeShardMergeAvailable)( const programShards = [ { name: "program-f000.ll", - source: "define i64 @foo() {\nentry:\n ret i64 1\n}\ndeclare i64 @bar()\n", + source: () => "define i64 @foo() {\nentry:\n ret i64 1\n}\ndeclare i64 @bar()\n", }, { name: "program-f001.ll", - source: "declare i64 @foo()\ndefine i64 @bar() {\nentry:\n ret i64 2\n}\n", + source: () => "declare i64 @foo()\ndefine i64 @bar() {\nentry:\n ret i64 2\n}\n", }, ]; try { @@ -4082,7 +4082,7 @@ test.skipIf(!nativeShardMergeAvailable)( // The canonical TU and ABI stay fixed, but the exact shard projection // changes. The merged-object tier must not reuse the previous layout. await build("bar", join(dir, "bar-commented.lib.a"), [ - { ...programShards[0]!, source: `; projection edit\n${programShards[0]!.source}` }, + { ...programShards[0]!, source: () => `; projection edit\n${programShards[0]!.source()}` }, programShards[1]!, ]); expect(await completeArtifacts(cacheRoot, "lib")).toHaveLength(3); @@ -4131,11 +4131,11 @@ test.skipIf( const programShards = [ { name: "program-f000.ll", - source: "define i64 @foo() {\nentry:\n ret i64 1\n}\ndeclare i64 @bar()\n", + source: () => "define i64 @foo() {\nentry:\n ret i64 1\n}\ndeclare i64 @bar()\n", }, { name: "program-f001.ll", - source: "declare i64 @foo()\ndefine i64 @bar() {\nentry:\n ret i64 2\n}\n", + source: () => "declare i64 @foo()\ndefine i64 @bar() {\nentry:\n ret i64 2\n}\n", }, ]; const ldWrapper = (generation: string): string => `#!/bin/sh @@ -4227,8 +4227,8 @@ test.skipIf( // If compilation reaches either shard, this build fails. Missing ld // (and objcopy on Linux) must select the valid canonical TU instead. programShards: [ - { name: "program-f000.ll", source: "not valid LLVM IR\n" }, - { name: "program-f001.ll", source: "also not valid LLVM IR\n" }, + { name: "program-f000.ll", source: () => "not valid LLVM IR\n" }, + { name: "program-f001.ll", source: () => "also not valid LLVM IR\n" }, ], programPublicSymbols: ["public_value"], outPath, @@ -4293,12 +4293,12 @@ test.skipIf( programShards: [ { name: "program-f000.ll", - source: + source: () => "define i64 @public_value() {\nentry:\n ret i64 7\n}\ndeclare i64 @other_value()\n", }, { name: "program-f001.ll", - source: + source: () => "declare i64 @public_value()\ndefine i64 @other_value() {\nentry:\n ret i64 9\n}\n", }, ], diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index ff1c7a3f6..e3f97710f 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -29,7 +29,7 @@ export interface CcOptions { windowsSubsystem?: WindowsSubsystem; /** Optional equivalent LLVM modules for dev compilation. Unsupported * targets or merge failures fall back to the canonical cPath TU. */ - programShards?: readonly { name: string; source: string }[]; + programShards?: readonly { name: string; source: () => string }[]; /** Canonical externally visible definitions retained while shard merging * demotes generated cross-shard linkage back to local symbols. */ programPublicSymbols?: readonly string[]; @@ -306,7 +306,7 @@ export interface LibArchiveOptions { * `programSource` remains the canonical TU/cache identity; these stable * shards compile independently and are relocatably merged into one program * object before archive assembly. */ - programShards?: readonly { name: string; source: string }[]; + programShards?: readonly { name: string; source: () => string }[]; /** Canonical externally visible definitions retained while the shard merge * demotes generated cross-shard linkage back to local symbols. */ programPublicSymbols?: readonly string[]; diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 71bec40e8..7f989a5e4 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -436,7 +436,7 @@ export async function compileCInternal( ? ["-gline-tables-only", ...(opts.cPath.endsWith(".ll") ? [] : ["-gno-column-info"])] : []; const { programShards, programPublicSymbols, programShardMergeIdentity } = - await selectProgramShards(driver, opts, optimization === "dev" && !sanitize); + await selectProgramShards(driver, opts, optimization === "dev"); // Mobile triples produce library archives, never standalone executables: // the executable-lane runtime (event loop, sockets, child processes) is // not verified on those device classes. compile() reports the SC3002 @@ -890,9 +890,15 @@ export async function compileCInternal( }; const buildExecutable = async ( runtimeInput: (path: string) => string, - build: { programPath?: string; outPath?: string; compilerVisibleSource?: string } = {}, + build: { + programPath?: string; + outPath?: string; + compilerVisibleSource?: string; + /** programPath is an object already (the merged program shards). */ + programObject?: boolean; + } = {}, ): Promise => { - if (!separateSanitizedProgram) { + if (!separateSanitizedProgram || build.programObject === true) { await runClang(buildArgs(runtimeInput, build)); return; } @@ -1471,7 +1477,7 @@ export async function compileCInternal( .update("\0") .update(shard.name) .update("\0") - .update(shard.source) + .update(shard.source()) .digest("hex"); return { ...shard, @@ -1485,7 +1491,7 @@ export async function compileCInternal( for (let i = 0; i < entries.length; i += shardWidth) { await Promise.all( entries.slice(i, i + shardWidth).map(async (entry) => { - await writeFile(entry.sourcePath, entry.source); + await writeFile(entry.sourcePath, entry.source()); if (await copyValidCachedFile(entry.cachePath, entry.staged)) return; entry.missed = true; await runClang([ @@ -1572,7 +1578,7 @@ export async function compileCInternal( (p) => objects?.get(p) ?? p, shardedProgramObject === null ? { programPath, outPath: privateOut, compilerVisibleSource: opts.cPath } - : { programPath: shardedProgramObject, outPath: privateOut }, + : { programPath: shardedProgramObject, outPath: privateOut, programObject: true }, ); if (darwinDebugSymbols) { // clang invokes dsymutil when compiling a source TU while linking. diff --git a/packages/compiler/src/backend/native/library.ts b/packages/compiler/src/backend/native/library.ts index cca706896..f0577f3ca 100644 --- a/packages/compiler/src/backend/native/library.ts +++ b/packages/compiler/src/backend/native/library.ts @@ -485,7 +485,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise .update("\0") .update(shard.name) .update("\0") - .update(shard.source) + .update(shard.source()) .digest("hex"); cachePath = join(persistentCache.root, "program-shard", key); } @@ -495,7 +495,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise for (let i = 0; i < shardEntries.length; i += shardWidth) { await Promise.all( shardEntries.slice(i, i + shardWidth).map(async (entry) => { - await writeFile(entry.sourcePath, entry.source); + await writeFile(entry.sourcePath, entry.source()); if ( entry.cachePath !== null && (await copyValidCachedFile(entry.cachePath, entry.staged)) diff --git a/packages/compiler/src/backend/native/program-shards.ts b/packages/compiler/src/backend/native/program-shards.ts index 978be3a97..1afd87f7b 100644 --- a/packages/compiler/src/backend/native/program-shards.ts +++ b/packages/compiler/src/backend/native/program-shards.ts @@ -7,7 +7,7 @@ import { rememberFingerprintDependencies } from "./dependency-files.js"; export function updateProgramShardCacheIdentity( hash: ReturnType, - shards: readonly { name: string; source: string }[] | undefined, + shards: readonly { name: string; source: () => string }[] | undefined, publicSymbols: readonly string[] | undefined, mergeIdentity: string | undefined, ): void { @@ -17,7 +17,7 @@ export function updateProgramShardCacheIdentity( } else { hash.update("\0"); for (const shard of shards) { - hash.update(shard.name).update("\0").update(shard.source).update("\0"); + hash.update(shard.name).update("\0").update(shard.source()).update("\0"); } } hash.update("\0program-public-symbols\0"); @@ -78,7 +78,7 @@ export async function resolveProgramShardMergeIdentity(driver: CcDriver): Promis } /** Library builds admit shards in both optimization modes; executable builds - * only admit unsanitized dev LLVM. Both use the same validated names, required + * admit dev LLVM, sanitized or not. Both use the same validated names, required * public-symbol projection, target support, and canonical-TU fallback. */ export async function selectProgramShards( driver: CcDriver, diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 74a61d892..17087d542 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -103,7 +103,11 @@ import { platformLinkerSupportsPersistentCache, resolvePlatformLinker, } from "./backend/linker.js"; -import { splitLlvmLibraryProgram, splitLlvmProgram } from "./backend/llvm/split.js"; +import { + splitLlvmLibraryProgram, + splitLlvmProgram, + splitLlvmProgramFile, +} from "./backend/llvm/split.js"; import { emitLibraryIdentityLines, replaceLibraryIdentity, @@ -566,8 +570,8 @@ async function compileExecutableNative( } const effectiveProgramSplit = programSplit ?? - (!programIsObject && features.optimization === "dev" && features.backend === "llvm" && !sanitize - ? splitLlvmProgram(await readFile(llvmPath, "utf8")) + (!programIsObject && features.optimization === "dev" && features.backend === "llvm" + ? splitLlvmProgramFile(llvmPath) : null); if (programIsObject) throw new InternalCompilerError("program objects must link against a precompiled runtime pack"); @@ -912,7 +916,6 @@ async function prepareExecutableInput( !useRuntimePack && backend === "llvm" && (opts.optimization ?? "release") === "dev" && - !(opts.sanitize ?? false) && typeof llvmSource === "string" ? splitLlvmProgram(llvmSource) : null; diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts index 64964d94f..8710d31b6 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -295,7 +295,7 @@ export class NativeCompiler { for (const [index, shard] of split.shards.entries()) { const shardInput = join(stage, `shard-${index}.ll`); const shardOutput = join(stage, `shard-${index}` + this.toolchain.target.outputSuffixes.obj); - writeFileSync(shardInput, shard.source); + writeFileSync(shardInput, shard.source()); this.emitObject(shardInput, shardOutput, source + "." + shard.name, optimization, "obj"); objects.push(shardOutput); } diff --git a/tests/harness/three.test.ts b/tests/harness/three.test.ts index 967c8654a..27fb86d1f 100644 --- a/tests/harness/three.test.ts +++ b/tests/harness/three.test.ts @@ -72,11 +72,7 @@ test.for(cases)( const previousTarget = process.env["SCRIPTC_TARGET"]; const previousCc = process.env["SCRIPTC_CC"]; const wasm = target === "wasm32-wasi"; - // The sanitized nodes program is one 1.14 GB LLVM module, whose clang - // compile needs about 16 GB, more than the CI runners have. It runs - // unsanitized until sanitized dev builds compile large programs in - // shards; every other fixture keeps the sanitizer. - const sanitize = !wasm && fixture !== "nodes" && process.env["SCRIPTC_SAN"] === "1"; + const sanitize = !wasm && process.env["SCRIPTC_SAN"] === "1"; try { if (wasm) { process.env["SCRIPTC_TARGET"] = target; From 05a6ec8b31dc5815e7694f64ba26afbe35cd72d3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 02:14:17 -0500 Subject: [PATCH 179/273] Treat s03's island timer as a race s03-load-in-immediate loads Node inside an immediate whose island code arms a 0 ms timer, while more immediates are pending. Whether that timer fires before the next check phase depends on how long the island call takes, which grows under ASan: Node itself prints both orders (17 and 3 in 20 runs), and a sanitized native run printed the rarer one more often, so the test failed whenever Node's runs missed it. Its native runs now have to produce the lines of one of Node's outputs, like the other race programs. --- tests/corpus-node-islands-order/s03-load-in-immediate/main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/corpus-node-islands-order/s03-load-in-immediate/main.ts b/tests/corpus-node-islands-order/s03-load-in-immediate/main.ts index abbe2e637..f4e453ab4 100644 --- a/tests/corpus-node-islands-order/s03-load-in-immediate/main.ts +++ b/tests/corpus-node-islands-order/s03-load-in-immediate/main.ts @@ -1,4 +1,5 @@ // @requires-libnode +// @order: race // Switch: Node loads inside an immediate while later immediates of the same check // phase, and timers, are pending. import { callSync } from "@scriptc/node"; From 0af32aff5a22a364b694bc6440edbf687ed6e1df Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 02:15:04 -0500 Subject: [PATCH 180/273] Compute dev builds' int32 slot analysis on a forked process --- .../compiler/src/backend/llvm/emit-module.ts | 8 +- packages/compiler/src/backend/llvm/emitter.ts | 73 ++++++++++++--- .../src/backend/llvm/forked-analysis.ts | 45 ++++++++++ packages/compiler/src/ir/int32-slots.ts | 76 ++++++++++++++++ packages/compiler/src/ir/node-index.ts | 30 +++++++ .../compiler/test/chunked-emission.test.ts | 6 +- .../test/int32-slots-transfer.test.ts | 88 +++++++++++++++++++ 7 files changed, 306 insertions(+), 20 deletions(-) create mode 100644 packages/compiler/src/backend/llvm/forked-analysis.ts create mode 100644 packages/compiler/test/int32-slots-transfer.test.ts diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index c6d3dbff4..dfdd95451 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -102,10 +102,10 @@ function emitFunctionsInChunks(emitter: LlEmitter, chunks: number): string[] { 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 { +/** Modules with fewer functions emit in one process, and compute their + * analyses in it: forking and merging would cost more than the workers save + * (SCRIPTC_EMIT_WORKER_FUNCTIONS lowers it, for tests). */ +export function workerMinFunctions(): number { const value = Number(process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] ?? "2000"); return Number.isFinite(value) && value > 0 ? value : 2000; } diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index a8c5d2c39..8049f8624 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -104,7 +104,7 @@ import { analyzeInt32Slots, dynamicFieldStores, type DynamicFieldStores, - type Int32Slots, + Int32Slots, } from "../../ir/int32-slots.js"; import { analyzeFinalFields, type FinalFields } from "../../ir/final-fields.js"; import { foldConstantGlobals } from "../../ir/constant-globals.js"; @@ -373,7 +373,9 @@ import { emitModuleParts, emitParts, helperDefs, + workerMinFunctions, } from "./emit-module.js"; +import { forkedAnalysis } from "./forked-analysis.js"; import { emitLoopCopies, emitSpeculativeWrite, @@ -1483,7 +1485,13 @@ export class LlEmitter { streamingSplitsEnabled = false; readonly initializerBindings: ReturnType; /** Whole-program int32 storage and parameter/result facts. */ - readonly int32Slots: Int32Slots; + get int32Slots(): Int32Slots { + return (this.int32SlotsValue ??= this.joinInt32Slots()); + } + private int32SlotsValue: Int32Slots | undefined; + /** The join of the int32 slot analysis a forked process computes + * (startInt32Slots). */ + private int32SlotsJoin: (() => Int32Slots) | undefined; /** The program has no runtime store into a checked-dynamic value * (dynamicFieldStores), so live views of classes and records never * commit back into their objects. Library builds keep their commits: @@ -1736,6 +1744,15 @@ export class LlEmitter { // One walk of every body, which the analyses below read instead of // walking the bodies themselves. this.nodeIndex = int32Analysis?.nodeIndex ?? new IrNodeIndex(mod); + this.initializerBindings = + int32Analysis?.initializerBindings ?? findInitializerBindings(mod, this.nodeIndex); + // One walk finds the dynamic field stores every analysis below consults. + const dynamicStores = + int32Analysis?.dynamicStores ?? dynamicFieldStores(mod, undefined, undefined, this.nodeIndex); + // Started before the other analyses, which may run while a forked + // process computes it (startInt32Slots). + if (int32Analysis !== null) this.int32SlotsValue = int32Analysis.slots; + else this.startInt32Slots(dynamicStores); this.stackTraces = observesStacks(mod, this.nodeIndex); this.nullableUnions = new NullableUnions( mod, @@ -1754,18 +1771,6 @@ export class LlEmitter { this.nullableUnions.armTypes(), ); this.constantNumericTables = findConstantNumericTables(mod); - this.initializerBindings = - int32Analysis?.initializerBindings ?? findInitializerBindings(mod, this.nodeIndex); - // One walk finds the dynamic field stores every analysis below consults. - const dynamicStores = - int32Analysis?.dynamicStores ?? dynamicFieldStores(mod, undefined, undefined, this.nodeIndex); - this.int32Slots = - int32Analysis?.slots ?? - analyzeInt32Slots(mod, { - dynamicStores, - initializerBindings: this.initializerBindings, - nodes: this.nodeIndex, - }); this.untypedIntegrity = mod.untypedIntegrity === true; this.liveViewsReadOnly = mod.lib === undefined && @@ -2211,6 +2216,46 @@ export class LlEmitter { ); // After the vtable slots: borrowing slots exclude their implementations. if (this.debug === null) this.analyzeBorrowedReturns(); + this.int32SlotsValue ??= this.joinInt32Slots(); + } + + /** Computes the int32 slot analysis, or starts it on a forked process. + * Dev builds read its result only near the end of the constructor + * (int32Abis is empty with debug info), so for a large module the + * process computes it while the constructor runs the other analyses. */ + private startInt32Slots(dynamicStores: DynamicFieldStores): void { + const mod = this.mod; + const nodes = this.nodeIndex; + const analyze = (): Int32Slots => + analyzeInt32Slots(mod, { + dynamicStores, + initializerBindings: this.initializerBindings, + nodes, + }); + const fork = this.targetOptions.forkPrimitives; + if ( + fork === undefined || + this.targetOptions.debugSources === undefined || + (this.targetOptions.functionWorkers ?? 1) < 2 || + mod.functions.length < workerMinFunctions() + ) { + this.int32SlotsValue = analyze(); + return; + } + this.int32SlotsJoin = forkedAnalysis( + fork, + analyze, + (slots) => slots.encode(nodes), + (text) => Int32Slots.decode(text, mod, nodes), + ); + } + + private joinInt32Slots(): Int32Slots { + const join = this.int32SlotsJoin; + if (join === undefined) + throw new InternalCompilerError("llvm emitter bug: int32 slots read before analysis"); + this.int32SlotsJoin = undefined; + return join(); } /** The borrowed parameters of a hierarchy's vtable slot (empty: owned). */ diff --git a/packages/compiler/src/backend/llvm/forked-analysis.ts b/packages/compiler/src/backend/llvm/forked-analysis.ts new file mode 100644 index 000000000..80ba35fac --- /dev/null +++ b/packages/compiler/src/backend/llvm/forked-analysis.ts @@ -0,0 +1,45 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { ForkPrimitives } from "./emitter.js"; + +/** Starts `compute` on a forked child process, which inherits this process + * as it is at the fork, and returns the join that answers its result. The + * child writes `encode`'s text to a file and exits; the join waits for it + * and decodes the text. When the fork fails, the child fails, or `encode` + * answers null, the join runs `compute` here instead, so the result does + * not depend on the child. Join once. */ +export function forkedAnalysis( + fork: ForkPrimitives, + compute: () => T, + encode: (value: T) => string | null, + decode: (text: string) => T, +): () => T { + const dir = mkdtempSync(join(tmpdir(), "scriptc-analysis-")); + const file = join(dir, "result.json"); + const pid = fork.fork(); + if (pid === 0) { + let code = 1; + try { + const text = encode(compute()); + if (text !== null) { + writeFileSync(file, text); + code = 0; + } + } catch { + // The parent computes the result itself. + } + fork.exit(code); + } + return () => { + let text: string | null = null; + try { + if (pid > 0 && fork.wait(pid) === 0) text = readFileSync(file, "utf8"); + } catch { + // The result is computed here. + } finally { + rmSync(dir, { recursive: true, force: true }); + } + return text === null ? compute() : decode(text); + }; +} diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 1616a2a77..35207f79b 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -352,6 +352,82 @@ export class Int32Slots { for (const params of this.paramFacts.values()) count += params.size; return count; } + + /** The analysis as JSON, for a process that shares this module and its + * node index (a forked one). Proven writes are numbered by their position + * in `nodes`. Null when a proven write is not in the index. */ + encode(nodes: IrNodeIndex): string | null { + const proven = nodes.expressionPositions(this.provenWrites); + if (proven === null) return null; + const data: Int32SlotsData = { + fields: [...this.fields], + familyOf: [...this.familyOf], + paramFacts: [...this.paramFacts].map(([fn, params]) => [fn, [...params]]), + returns: [...this.returns], + returnRanges: [...this.returnRanges], + boxes: [...this.boxes], + boxGroupOf: [...this.boxGroupOf], + provenWrites: proven, + called: [...this.called], + reasons: [...this.reasons], + pinned: [...this.pinned], + int32Locals: [...this.int32Locals], + int32Elements: [...this.int32Elements], + pinViolations: this.pinViolations, + nonNegative: [...this.nonNegative], + narrowedReads: this.narrowedReads, + int32Arrays: [...this.int32Arrays], + checkedInt32Arrays: [...this.checkedInt32Arrays], + }; + return JSON.stringify(data); + } + + /** The analysis `encode` wrote for `mod`, whose node index is `nodes`. */ + static decode(text: string, mod: IrModule, nodes: IrNodeIndex): Int32Slots { + const data = JSON.parse(text) as Int32SlotsData; + const slots = new Int32Slots(new Map((mod.classes ?? []).map((c) => [c.name, c]))); + for (const family of data.fields) slots.fields.add(family); + for (const [key, family] of data.familyOf) slots.familyOf.set(key, family); + for (const [fn, params] of data.paramFacts) slots.paramFacts.set(fn, new Map(params)); + for (const fn of data.returns) slots.returns.add(fn); + for (const [fn, range] of data.returnRanges) slots.returnRanges.set(fn, range); + for (const group of data.boxes) slots.boxes.add(group); + for (const [member, group] of data.boxGroupOf) slots.boxGroupOf.set(member, group); + for (const node of nodes.expressionsAt(data.provenWrites)) slots.provenWrites.add(node); + for (const fn of data.called) slots.called.add(fn); + for (const [key, why] of data.reasons) slots.reasons.set(key, why); + for (const key of data.pinned) slots.pinned.add(key); + for (const key of data.int32Locals) slots.int32Locals.add(key); + for (const key of data.int32Elements) slots.int32Elements.add(key); + slots.pinViolations.push(...data.pinViolations); + for (const key of data.nonNegative) slots.nonNegative.add(key); + slots.narrowedReads = data.narrowedReads; + for (const key of data.int32Arrays) slots.int32Arrays.add(key); + for (const key of data.checkedInt32Arrays) slots.checkedInt32Arrays.add(key); + return slots; + } +} + +/** Int32Slots as plain data (Int32Slots.encode). */ +interface Int32SlotsData { + fields: string[]; + familyOf: [string, string | null][]; + paramFacts: [string, [string, IntegerRange][]][]; + returns: string[]; + returnRanges: [string, IntegerRange][]; + boxes: string[]; + boxGroupOf: [string, string][]; + provenWrites: number[]; + called: string[]; + reasons: [string, string][]; + pinned: string[]; + int32Locals: string[]; + int32Elements: string[]; + pinViolations: string[]; + nonNegative: string[]; + narrowedReads: boolean; + int32Arrays: string[]; + checkedInt32Arrays: string[]; } const DYN_KINDS = new Set(["dyn", "jsval", "caught"]); diff --git a/packages/compiler/src/ir/node-index.ts b/packages/compiler/src/ir/node-index.ts index 2aac7df92..be1fa506a 100644 --- a/packages/compiler/src/ir/node-index.ts +++ b/packages/compiler/src/ir/node-index.ts @@ -122,6 +122,36 @@ export class IrNodeIndex { if (statementKinds.has(kind)) this.statements[code] = 1; } + /** The positions of the expressions `wanted` in the index, which an + * index of the same module in another process resolves with + * expressionsAt. Null when an expression is not recorded, or is recorded + * more than once. */ + expressionPositions(wanted: ReadonlySet): number[] | null { + const positions: number[] = []; + if (wanted.size === 0) return positions; + const found = new Set(); + const { nodes, codes, statements } = this; + for (let i = 0; i < nodes.length; i++) { + if (statements[codes[i]!] !== 0) continue; + const node = nodes[i]! as IrExpr; + if (!wanted.has(node)) continue; + if (found.has(node)) return null; + found.add(node); + positions.push(i); + } + return found.size === wanted.size ? positions : null; + } + + /** The expressions at `positions` (expressionPositions). */ + expressionsAt(positions: readonly number[]): IrExpr[] { + return positions.map((position) => { + const code = this.codes[position]; + if (code === undefined || this.statements[code] !== 0) + throw new InternalCompilerError(`no IR expression at index position ${position}`); + return this.nodes[position]! as IrExpr; + }); + } + /** A scan for the nodes of `kinds`, for eachNodeOfKinds. Pass a * constant list: scans are kept by the list's identity. */ scan(kinds: readonly IrNodeKind[]): IrKindScan { diff --git a/packages/compiler/test/chunked-emission.test.ts b/packages/compiler/test/chunked-emission.test.ts index f4977f035..6d2b2e06a 100644 --- a/packages/compiler/test/chunked-emission.test.ts +++ b/packages/compiler/test/chunked-emission.test.ts @@ -104,7 +104,9 @@ test("a Promise.all tuple adapter requested in a range is defined", async () => }, 60_000); // Debug metadata is numbered per emitter (debug-info.ts) and not journaled: -// a debug build emits its function bodies in one emitter. +// a debug build emits its function bodies in one emitter. Its one fork +// computes the int32 slot analysis; when the fork fails, the emitter +// computes it itself. 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"]; @@ -148,7 +150,7 @@ test("debug builds emit function bodies serially, with every metadata reference 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); + expect(forks).toBe(1); } finally { if (minFunctions === undefined) delete process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"]; else process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = minFunctions; diff --git a/packages/compiler/test/int32-slots-transfer.test.ts b/packages/compiler/test/int32-slots-transfer.test.ts new file mode 100644 index 000000000..3ea4f6a6a --- /dev/null +++ b/packages/compiler/test/int32-slots-transfer.test.ts @@ -0,0 +1,88 @@ +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 { findInitializerBindings } from "../src/ir/initializer-bindings.js"; +import { analyzeInt32Slots, dynamicFieldStores, Int32Slots } from "../src/ir/int32-slots.js"; +import type { IrExpr, IrModule } from "../src/ir/ir.js"; +import { IrNodeIndex } from "../src/ir/node-index.js"; +import { unshareNodes } from "../src/ir/unshare.js"; + +// Programs with int32 fields, parameters, results, captured boxes, contract +// pins, int32 arrays and disqualified candidates. +const PROGRAMS = [ + "4451-int32-field-storage.ts", + "4452-int32-field-dynamic-writes.ts", + "4453-int32-closed-locals.ts", + "4630-contract-int32.ts", + "4970-int32-call-abi.ts", + "5323-int32-nonnegative-slot-facts.ts", + "5332-int32-narrowed-slot-facts.ts", + "5930-contract-int32-arrays.ts", + "7326-int32-element-arrays.ts", + "7580-int32-arrays-across-calls.ts", +]; + +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"); +} + +/** The analysis LlEmitter makes of `mod`, with its node index. */ +function analyze(mod: IrModule): { nodes: IrNodeIndex; slots: Int32Slots } { + const nodes = new IrNodeIndex(mod); + const slots = analyzeInt32Slots(mod, { + nodes, + dynamicStores: dynamicFieldStores(mod, undefined, undefined, nodes), + initializerBindings: findInitializerBindings(mod, nodes), + }); + return { nodes, slots }; +} + +// A forked process holds its own copy of the module: the encoded analysis +// names nodes by index position, which resolve in any copy of the module. +test("the int32 slot analysis decodes against another copy of its module", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-transfer-")); + try { + let provenWrites = 0; + for (const program of PROGRAMS) { + const ir = await lower(program, dir); + const first = analyze(unshareNodes(deserializeModule(ir))); + const copy = unshareNodes(deserializeModule(ir)); + const second = analyze(copy); + const text = first.slots.encode(first.nodes); + expect(text, program).not.toBeNull(); + const decoded = Int32Slots.decode(text!, copy, second.nodes); + expect(decoded, program).toEqual(second.slots); + for (const node of decoded.provenWrites) + expect(second.slots.provenWrites.has(node), program).toBe(true); + provenWrites += decoded.provenWrites.size; + } + expect(provenWrites).toBeGreaterThan(0); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 120_000); + +test("an analysis whose proven writes the index does not record does not encode", () => { + const loc = { file: "test.ts", start: 0, end: 0 }; + const mod: IrModule = { + irVersion: 17, + sourceFile: "test.ts", + entry: "%main", + functions: [ + { loc, name: "%main", params: [], locals: [], returnType: { kind: "void" }, body: [] }, + ], + }; + const { nodes, slots } = analyze(mod); + expect(slots.encode(nodes)).not.toBeNull(); + const elsewhere: IrExpr = { loc, kind: "numLit", value: 1, type: { kind: "f64" } }; + slots.provenWrites.add(elsewhere); + expect(nodes.expressionPositions(slots.provenWrites)).toBeNull(); + expect(slots.encode(nodes)).toBeNull(); +}); From ad26bed418b3e976d4b6168cc75049e278bea26b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 02:29:47 -0500 Subject: [PATCH 181/273] List line-reader among the benchmark workloads that link the dynamic tier The workload reads its file through readline over fs.createReadStream. The stream and readline runtime units define dynamic-tier symbols on their own: a program that only reads a file with createReadStream links the same 32, as log-summary and inventory-report do. --- scripts/zero-cost-compare.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index b23f61a0a..7ae33d64a 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -418,6 +418,7 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/typed-array-scan-workers", "bench/log-summary", "bench/inventory-report", + "bench/line-reader", "bench/json-unions", "bench/throw-chains", ]); From f176e868bf3f27b0056191d703ed2ee9f7e007d6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 02:32:44 -0500 Subject: [PATCH 182/273] Prove int32 array candidates in one analysis that revises dropped ones selectInt32Arrays proposed int32 array candidates, analyzed the marked module, and when a candidate dropped, marked the module again without it and analyzed the result from scratch, until a round kept every candidate it marked. On tsc-ts the first analysis drops 6 of 8 candidates, and a second full analysis confirms the other 2. The analysis now revises the module as candidates drop: a candidate whose element stores fail (and that keeps no checked stores) has its functions replaced by the module's functions marked without it, which call the element readers again, and the fixpoint goes on over the revised functions. The selection takes the module of the functions the analysis ended with when marking the module for the candidates it kept proposes exactly those candidates, in those functions, and the emitter takes over the analysis with that module's node index, initializer bindings and dynamic stores. A revision that changes anything else in a function, such as a call of a function no other call reaches, ends the analysis, and selection falls back to rounds. SCRIPTC_INT32_VERIFY=1 also runs the rounds and throws unless the selected module and every fact of its analysis are the same. --- packages/compiler/src/backend/llvm/emitter.ts | 236 ++++++++++++++---- packages/compiler/src/ir/int32-arrays.ts | 169 +++++++++++-- packages/compiler/src/ir/int32-slots.ts | 196 ++++++++++++++- .../test/int32-element-array-emission.test.ts | 58 ++++- 4 files changed, 581 insertions(+), 78 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index a8c5d2c39..329ec4771 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -17,6 +17,7 @@ import { import { youngNurseryRequested } from "../young-nursery.js"; import { llvmBytes as llBytes, wtf8Bytes } from "../literals.js"; import { InternalCompilerError } from "../../errors.js"; +import { serializeModuleParts } from "../../ir/serialize.js"; import { createHash } from "node:crypto"; import { endsWithJump, @@ -88,6 +89,8 @@ import { fillAllocationLength, int32ArrayKey, selectInt32Arrays, + type Int32ArrayProof, + type Int32ArrayRevision, } from "../../ir/int32-arrays.js"; import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js"; import { @@ -103,6 +106,7 @@ import { resolveSelfCaptures } from "../../ir/self-captures.js"; import { analyzeInt32Slots, dynamicFieldStores, + Int32RevisionConflict, type DynamicFieldStores, type Int32Slots, } from "../../ir/int32-slots.js"; @@ -552,15 +556,60 @@ export interface Int32SlotAnalysis { readonly slots: Int32Slots; } -/** The int32 slot analysis of `mod`, with the inputs LlEmitter gives it. */ -function analyzeModuleInt32Slots(mod: IrModule): Int32SlotAnalysis { +/** The int32 slot analysis of `mod`, with the inputs LlEmitter gives it. + * With `revise`, the analysis revises the functions of candidates that drop + * (Int32SlotInputs.revise), and `mod` is the module it started from. */ +function analyzeModuleInt32Slots( + mod: IrModule, + revise?: (fnName: string, dropped: ReadonlySet) => IrFunction, +): Int32SlotAnalysis { const nodeIndex = new IrNodeIndex(mod); const initializerBindings = findInitializerBindings(mod, nodeIndex); const dynamicStores = dynamicFieldStores(mod, undefined, undefined, nodeIndex); - const slots = analyzeInt32Slots(mod, { dynamicStores, initializerBindings, nodes: nodeIndex }); + const slots = analyzeInt32Slots(mod, { + dynamicStores, + initializerBindings, + nodes: nodeIndex, + ...(revise === undefined ? {} : { revise }), + }); return { mod, nodeIndex, initializerBindings, dynamicStores, slots }; } +/** The candidates an analysis keeps: int32 arrays and checked ones. */ +function keptInt32Arrays(analysis: Int32SlotAnalysis): ReadonlySet { + return new Set([...analysis.slots.int32Arrays, ...analysis.slots.checkedInt32Arrays]); +} + +/** An analysis that revised functions (analyzeModuleInt32Slots' `revise`) + * as the analysis of `mod`, the module of the functions it ended with: the + * node index of `mod`, and its initializer bindings and dynamic stores, + * which must be the ones the analysis read. Null when they differ. */ +function revisedAnalysis(analysis: Int32SlotAnalysis, mod: IrModule): Int32SlotAnalysis | null { + const nodeIndex = new IrNodeIndex(mod); + const initializerBindings = findInitializerBindings(mod, nodeIndex); + const dynamicStores = dynamicFieldStores(mod, undefined, undefined, nodeIndex); + const read = analysis.initializerBindings; + if (initializerBindings.size !== read.size) return null; + for (const [name, globals] of initializerBindings) { + const other = read.get(name); + if (other?.length !== globals.length || globals.some((g, i) => other[i] !== g)) return null; + } + const stores = analysis.dynamicStores; + if ( + dynamicStores.all !== stores.all || + dynamicStores.names.size !== stores.names.size || + [...dynamicStores.names].some((name) => !stores.names.has(name)) + ) + return null; + return { + mod, + nodeIndex, + initializerBindings, + dynamicStores, + slots: analysis.slots, + }; +} + /** The functions whose calls the backend may emit as calls of their * result-slot entries (record-results.ts). */ function resultEntryTargets(mod: IrModule): ReadonlySet { @@ -575,57 +624,127 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter { // Facts are keyed by node identity: emit the same module whether it was // lowered in memory or read back from its serialized form. const mod = unshareNodes(input); - // Each round of selectInt32Arrays analyzes the module it marks; the - // emitter of the module it returns reuses that round's analysis. - const rounds: { last: Int32SlotAnalysis | null } = { last: null }; - const proven = (marked: IrModule): ReadonlySet => { - const analysis = analyzeModuleInt32Slots(marked); - rounds.last = analysis; - return new Set([...analysis.slots.int32Arrays, ...analysis.slots.checkedInt32Arrays]); + // Each proof selectInt32Arrays asks for analyzes a marked module; the + // emitter of the module it returns reuses the last one. A proof that + // revised functions analyzed the module of the functions it ended with. + const rounds: { + last: Int32SlotAnalysis | null; + functions: readonly IrFunction[] | null; + revising: boolean; + } = { last: null, functions: null, revising: true }; + const proven = (marked: IrModule, revise: Int32ArrayRevision | null): Int32ArrayProof | null => { + rounds.functions = null; + if (revise === null || !rounds.revising) { + rounds.last = analyzeModuleInt32Slots(marked); + return { kept: keptInt32Arrays(rounds.last), functions: null }; + } + const latest = new Map(); + try { + rounds.last = analyzeModuleInt32Slots(marked, (fnName, dropped) => { + const fn = revise(fnName, dropped); + latest.set(fnName, fn); + return fn; + }); + } catch (error) { + if (!(error instanceof Int32RevisionConflict)) throw error; + rounds.last = null; + return null; + } + if (latest.size === 0) return { kept: keptInt32Arrays(rounds.last), functions: null }; + rounds.functions = marked.functions.map((fn) => latest.get(fn.name) ?? fn); + return { kept: keptInt32Arrays(rounds.last), functions: rounds.functions }; }; // Keep source storage intact for debugger inspection in dev builds. - const emitted = - options.debugSources === undefined - ? selectInt32Arrays( - versionParameters( - addRecordResultEntries( - scalarizeLiteralResults( - scalarizeNumericRecords( - specializeCallbackCalls( - specializeNumericCalls( - pairRowSums( - inlineExpressionFunctions( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads( - freezeInitOnlyGlobals( - holdCharLocalsAsUnits( - dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)), - ), - ), - ), - ), + if (options.debugSources !== undefined) return new LlEmitter(mod, options, null, null); + const passed = versionParameters( + addRecordResultEntries( + scalarizeLiteralResults( + scalarizeNumericRecords( + specializeCallbackCalls( + specializeNumericCalls( + pairRowSums( + inlineExpressionFunctions( + foldConstantGlobals( + copyStableCaptures( + resolveSelfCaptures( + hoistFinalFieldReads( + freezeInitOnlyGlobals( + holdCharLocalsAsUnits( + dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)), ), ), ), ), - mangleFunction, ), - mangleFunction, ), ), ), + mangleFunction, ), + mangleFunction, ), - proven, - resultEntryTargets, - ) - : mod; - const prepared = rounds.last; + ), + ), + ), + ); + let emitted = selectInt32Arrays(passed, proven, resultEntryTargets); + let prepared = rounds.last; + // A proof that revised functions analyzed the module of the functions it + // ended with, which is the selected module unless selection fell back to + // rounds: then the selected module was proved by its own round. + if (prepared !== null && rounds.functions !== null && prepared.mod !== emitted) { + const functions = rounds.functions; + prepared = + emitted !== passed && + emitted.functions.length === functions.length && + emitted.functions.every((fn, i) => fn === functions[i]) + ? revisedAnalysis(prepared, emitted) + : null; + if (prepared === null && emitted !== passed) { + rounds.revising = false; + emitted = selectInt32Arrays(passed, proven, resultEntryTargets); + prepared = rounds.last; + } + } + if (process.env["SCRIPTC_INT32_VERIFY"] === "1") + verifyInt32Selection(passed, emitted, prepared?.mod === emitted ? prepared : null); return new LlEmitter(emitted, options, null, prepared?.mod === emitted ? prepared : null); } +/** SCRIPTC_INT32_VERIFY=1: throws unless the selection with revisions + * (`emitted`, and the analysis the emitter takes over) is the one rounds + * without revisions give: the same module, and the same facts. */ +function verifyInt32Selection( + passed: IrModule, + emitted: IrModule, + analysis: Int32SlotAnalysis | null, +): void { + let last: Int32SlotAnalysis | null = null; + const reference = selectInt32Arrays( + passed, + (marked) => { + last = analyzeModuleInt32Slots(marked); + return { kept: keptInt32Arrays(last), functions: null }; + }, + resultEntryTargets, + ); + const text = (mod: IrModule): string => serializeModuleParts(mod).join(""); + if (text(reference) !== text(emitted)) + throw new InternalCompilerError("int32 selection: the revised module differs from the rounds'"); + const facts = (mod: IrModule, prepared: Int32SlotAnalysis | null): string => + (prepared?.mod === mod ? prepared : analyzeModuleInt32Slots(mod)).slots.summary(mod); + const expected = facts(reference, last); + const actual = facts(emitted, analysis); + if (expected !== actual) { + const want = expected.split("\n"); + const got = actual.split("\n"); + const line = want.findIndex((entry, i) => entry !== got[i]); + throw new InternalCompilerError( + `int32 selection: the revised analysis differs from the rounds': ${want[line]?.slice(0, 400)} != ${got[line]?.slice(0, 400)}`, + ); + } +} + /** Emits the module's parts, emitting it again with the functions an * UnwindAbiViolation names on the pending-flag ABI. */ function emitModuleText(input: IrModule, options: LlvmTargetOptions): string[] { @@ -1438,7 +1557,12 @@ export class LlEmitter { /** Preorder intervals of the runtime error classes under THIS module's * class-forest numbering (main() stamps scr_error_vts with them, exactly * like the runtime ABI’s errorVtStampLines). */ - readonly errorIntervals: { kind: number; pre: number; post: number; lib: string }[] = []; + readonly errorIntervals: { + kind: number; + pre: number; + post: number; + lib: string; + }[] = []; /** The runtime emitter vtable's preorder interval, when the program * touches node:events (the class def rides the module exactly then) — * main() stamps scr_emitter_vt with it (emitterVtStampLines, ported). */ @@ -1447,7 +1571,12 @@ export class LlEmitter { * ported): the defs ride every emitter-touching module (the frontend * collects the whole emitter-rooted tree), and scr_stream.c links on * the same predicate, so the stamps always have their globals. */ - readonly streamIntervals: { vt: string; pre: number; post: number; lib: string }[] = []; + readonly streamIntervals: { + vt: string; + pre: number; + post: number; + lib: string; + }[] = []; // ── per-function state (reset in emitFunction) ───────────────────────── B = new BlockBuilder(); @@ -1514,7 +1643,12 @@ export class LlEmitter { /** 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 } } + { + 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 @@ -1597,7 +1731,11 @@ export class LlEmitter { * (the fast copy) or as double arrays (an ordinary copy entered only * after they widened), as `function\0local` keys, and where each of the * fast copy's writes resumes in the third copy. */ - int32Kinds: { int32: Set; wide: Set; resume: Map } | null = null; + int32Kinds: { + int32: Set; + wide: Set; + resume: Map; + } | null = null; /** The resume label of the fast-copy write being emitted. */ int32KindResume: string | null = null; /** The function being emitted, for speculative globals. */ @@ -1671,7 +1809,12 @@ export class LlEmitter { * releases frames/scopes down to the recorded depths and branches to * `label` (the catch, or the exception-path finally) instead of * returning out of the function. Entering a try emits no code. */ - tryStack: { label: string; used: boolean; frameDepth: number; scopeDepth: number }[] = []; + tryStack: { + label: string; + used: boolean; + frameDepth: number; + scopeDepth: number; + }[] = []; /** Return type of the function being emitted — the unwind path returns * a dummy of this type (never read: callers check the flag first). */ currentReturnType: IrType = VOID; @@ -7150,7 +7293,10 @@ export class LlEmitter { return childDataDynThunkFor(this, chunk); } - emitterFixedAdapter(cbT: IrType & { kind: "func" }): { fn: string; shim: string } { + emitterFixedAdapter(cbT: IrType & { kind: "func" }): { + fn: string; + shim: string; + } { return emitterFixedAdapter(this, cbT); } diff --git a/packages/compiler/src/ir/int32-arrays.ts b/packages/compiler/src/ir/int32-arrays.ts index 353755c05..0677b2ad1 100644 --- a/packages/compiler/src/ir/int32-arrays.ts +++ b/packages/compiler/src/ir/int32-arrays.ts @@ -805,6 +805,41 @@ function rewriteCandidates( return { ...fn, body, locals: locals.filter((local) => !popLocals.has(local.id)) }; } +/** A module marked for its int32 array candidates: the candidates of + * each function (int32ArrayCandidates) and the element readers the marking + * inlines. */ +interface Int32ArrayMarking { + readonly mod: IrModule; + readonly readers: ReadonlyMap; + readonly candidates: ReadonlyMap>; +} + +/** markInt32ArrayCandidates with the candidates it marked; null when it + * marks none. */ +function markCandidates( + mod: IrModule, + keep: ReadonlySet | null, + redirected: ReadonlySet, +): Int32ArrayMarking | null { + if (mod.lib !== undefined) return null; + const readers = new Map(); + for (const fn of mod.functions) if (isElementReader(fn)) readers.set(fn.name, fn); + const all = moduleInt32ArrayCandidates(mod, new Set(readers.keys()), keep, redirected); + if (all.size === 0) return null; + const keys = new Set(); + const functions = mod.functions.map((fn) => { + const candidates = all.get(fn.name); + if (!candidates) return fn; + for (const id of candidates) keys.add(int32ArrayKey(fn.name, id)); + return rewriteCandidates(fn, candidates, readers); + }); + return { + mod: { ...mod, functions, int32ArrayCandidates: [...keys] }, + readers, + candidates: all, + }; +} + /** Propose the module's int32 array candidates (int32ArrayKey keys in * `int32ArrayCandidates`), only those in `keep` when given, and rewrite * their functions. */ @@ -813,42 +848,126 @@ export function markInt32ArrayCandidates( keep: ReadonlySet | null = null, redirected: ReadonlySet = new Set(), ): IrModule { - if (mod.lib !== undefined) return mod; - const readers = new Map(); - for (const fn of mod.functions) if (isElementReader(fn)) readers.set(fn.name, fn); - const all = moduleInt32ArrayCandidates(mod, new Set(readers.keys()), keep, redirected); - if (all.size === 0) return mod; - const keys = new Set(); - const functions = mod.functions.map((fn) => { - const candidates = all.get(fn.name); - if (!candidates) return fn; - for (const id of candidates) keys.add(int32ArrayKey(fn.name, id)); - return rewriteCandidates(fn, candidates, readers); - }); - return { ...mod, functions, int32ArrayCandidates: [...keys] }; + return markCandidates(mod, keep, redirected)?.mod ?? mod; +} + +/** Revises a function of a module marked for int32 array candidates when + * some of them drop: the function marked for its candidates outside + * `dropped` (int32ArrayKey keys), which is the unmarked function when none + * is left. */ +export type Int32ArrayRevision = (fnName: string, dropped: ReadonlySet) => IrFunction; + +/** What a proof of a marked module (selectInt32Arrays' `prove`) keeps. */ +export interface Int32ArrayProof { + /** The candidates it keeps. */ + readonly kept: ReadonlySet; + /** The functions of the module it proved, in module order, when it + * revised some through the Int32ArrayRevision it was given; otherwise + * null, for the marked module's own. */ + readonly functions: readonly IrFunction[] | null; } /** The module the backend emits: rewritten for exactly the candidates the - * int32 slot analysis (`prove`, returning its int32Arrays) keeps, so a - * module without int32 arrays is `mod` itself. A dropped candidate's - * rewrite is undone by marking again without it, until every candidate - * marked is kept. `redirected` names the functions some of whose calls the + * int32 slot analysis (`prove`) keeps, so a module without int32 arrays is + * `mod` itself. `redirected` names the functions some of whose calls the * backend emits as calls of another function (a result-slot entry): their - * parameters keep doubles. */ + * parameters keep doubles. + * + * The proof of the marked module gets an Int32ArrayRevision: as candidates + * drop, it revises their functions and goes on proving the revised module, + * so one analysis ends with the module marked for the candidates it keeps. + * That module is the answer when marking for those candidates again proposes + * exactly them, in the functions the proof revised. A proof that cannot + * follow a revision answers null; then each round marks the module, proves + * it without revisions, and marks again without the dropped candidates, + * until a round keeps every candidate it marks (four rounds at most). */ export function selectInt32Arrays( mod: IrModule, - prove: (marked: IrModule) => ReadonlySet, + prove: (marked: IrModule, revise: Int32ArrayRevision | null) => Int32ArrayProof | null, redirected: (mod: IrModule) => ReadonlySet = () => new Set(), ): IrModule { - let keep: ReadonlySet | null = null; const others = redirected(mod); - for (let round = 0; round < 4; round++) { + const marking = markCandidates(mod, null, others); + if (marking === null) return mod; + const original = new Map(mod.functions.map((fn) => [fn.name, fn])); + const markedFunctions = new Map(marking.mod.functions.map((fn) => [fn.name, fn])); + // Each function's latest revision and the candidates it is marked for. + const revisions = new Map }>(); + const revise: Int32ArrayRevision = (fnName, dropped) => { + const candidates = marking.candidates.get(fnName) ?? new Set(); + const ids = new Set(); + for (const id of candidates) if (!dropped.has(int32ArrayKey(fnName, id))) ids.add(id); + const latest = revisions.get(fnName); + if (latest !== undefined && latest.ids.size === ids.size) return latest.fn; + if (latest === undefined && ids.size === candidates.size) return markedFunctions.get(fnName)!; + const fn = + ids.size === 0 + ? original.get(fnName)! + : rewriteCandidates(original.get(fnName)!, ids, marking.readers); + revisions.set(fnName, { fn, ids }); + return fn; + }; + const proof = prove(marking.mod, revise); + const proposed = marking.mod.int32ArrayCandidates ?? []; + let keep: ReadonlySet | null = null; + let round = 0; + if (proof !== null && proof.functions === null) { + // The marked module itself was proved: the first round. + if (proposed.every((key) => proof.kept.has(key))) return marking.mod; + keep = new Set(proposed.filter((key) => proof.kept.has(key))); + round = 1; + } else if (proof !== null) { + const settled = settleRevisions(mod, marking, proof, revisions, others); + if (settled !== null) return settled; + } + for (; round < 4; round++) { const marked = markInt32ArrayCandidates(mod, keep, others); if (marked === mod) return mod; - const kept = prove(marked); - const proposed = marked.int32ArrayCandidates ?? []; - if (proposed.every((key) => kept.has(key))) return marked; - keep = new Set(proposed.filter((key) => kept.has(key))); + const kept = prove(marked, null)!.kept; + const roundProposed = marked.int32ArrayCandidates ?? []; + if (roundProposed.every((key) => kept.has(key))) return marked; + keep = new Set(roundProposed.filter((key) => kept.has(key))); } return mod; } + +/** The module marked for the candidates a revising proof kept, built from + * the functions it proved: null unless marking `mod` for those candidates + * proposes, in each function, exactly the candidates its proved function is + * marked for. `mod` itself when it proposes none. */ +function settleRevisions( + mod: IrModule, + marking: Int32ArrayMarking, + proof: Int32ArrayProof, + revisions: ReadonlyMap }>, + redirected: ReadonlySet, +): IrModule | null { + const kept = proof.kept; + const functions = proof.functions!; + if (functions.length !== mod.functions.length) return null; + const all = moduleInt32ArrayCandidates( + mod, + new Set(marking.readers.keys()), + new Set([...(marking.mod.int32ArrayCandidates ?? [])].filter((key) => kept.has(key))), + redirected, + ); + const keys = new Set(); + for (let i = 0; i < mod.functions.length; i++) { + const fn = mod.functions[i]!; + const proved = functions[i]!; + const candidates = all.get(fn.name); + const revision = revisions.get(fn.name); + const marked = revision?.ids ?? marking.candidates.get(fn.name) ?? new Set(); + if ((candidates?.size ?? 0) !== marked.size) return null; + if (candidates !== undefined) + for (const id of candidates) { + if (!marked.has(id)) return null; + keys.add(int32ArrayKey(fn.name, id)); + } + const expected = + revision?.fn ?? (marking.candidates.has(fn.name) ? marking.mod.functions[i]! : fn); + if (proved !== expected) return null; + } + if (all.size === 0) return mod; + return { ...mod, functions: [...functions], int32ArrayCandidates: [...keys] }; +} diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 1616a2a77..af1ba36ac 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -27,7 +27,7 @@ import { int32ArraySlot, int32ArrayStores, } from "./int32-arrays.js"; -import { everyExprChild, everyStmtChild, everyTypeChild } from "./traverse.js"; +import { everyExprChild, everyStmtChild, everyStmtList, everyTypeChild } from "./traverse.js"; /** The storage slot an array-valued expression reads, as an * IrContracts.int32Elements key: a local or parameter @@ -352,6 +352,47 @@ export class Int32Slots { for (const params of this.paramFacts.values()) count += params.size; return count; } + + /** Every fact, in a canonical order, with each proven write as its + * position in `mod` (function index, then walk order): two analyses of + * equal modules hold the same facts when their summaries are equal. */ + summary(mod: IrModule): string { + const sorted = (values: Iterable): string => [...values].sort().join(","); + const lines = [ + `fields ${sorted(this.fields)}`, + `params ${sorted([...this.paramFacts].flatMap(([fn, params]) => [...params].map(([id, r]) => `${fn}\0${id}=${r.min}:${r.max}`)))}`, + `returns ${sorted(this.returns)}`, + `returnRanges ${sorted([...this.returnRanges].map(([fn, r]) => `${fn}=${r.min}:${r.max}`))}`, + `boxes ${sorted(this.boxes)}`, + `boxGroups ${sorted([...this.boxGroupOf].map(([member, group]) => `${member}=${group}`))}`, + `called ${sorted(this.called)}`, + `pinned ${sorted(this.pinned)}`, + `int32Locals ${sorted(this.int32Locals)}`, + `int32Elements ${sorted(this.int32Elements)}`, + `nonNegative ${sorted(this.nonNegative)}`, + `pinViolations ${sorted(this.pinViolations)}`, + `narrowedReads ${this.narrowedReads}`, + `int32Arrays ${sorted(this.int32Arrays)}`, + `checkedInt32Arrays ${sorted(this.checkedInt32Arrays)}`, + ]; + const positions: string[] = []; + mod.functions.forEach((fn, f) => { + let at = 0; + everyStmtList(fn.body, { + expr: (e) => { + if (this.provenWrites.has(e)) positions.push(`${f}:${at}`); + at++; + return true; + }, + stmt: () => { + at++; + return true; + }, + }); + }); + lines.push(`provenWrites ${this.provenWrites.size} ${positions.join(",")}`); + return lines.join("\n"); + } } const DYN_KINDS = new Set(["dyn", "jsval", "caught"]); @@ -833,6 +874,95 @@ export interface Int32SlotInputs { initializerBindings?: ReadonlyMap; /** The module's node index, read instead of walking the bodies. */ nodes?: IrNodeIndex; + /** Revises a function of a module marked for int32 array candidates when + * candidates drop (int32-arrays.ts Int32ArrayRevision): the function + * marked without the `dropped` candidates. With it, a candidate that drops + * (and keeps no checked stores) takes its sites back to the element-reader + * calls the marking inlined, and the fixpoint goes on over the revised + * functions, so its result is the analysis of the module marked for the + * candidates it keeps. A revision that changes more of a function than + * those sites throws Int32RevisionConflict. */ + revise?: (fnName: string, dropped: ReadonlySet) => IrFunction; +} + +/** A revision (Int32SlotInputs.revise) changed a function in a way the + * fixpoint cannot follow: a call of a function no other call reaches, or + * any change besides element-reader calls taking the place of their + * inlined reads. The module needs an analysis of its own. */ +export class Int32RevisionConflict extends Error {} + +/** What the analysis's setup reads from a function body, besides direct + * calls: a revision must leave all of it unchanged. */ +function revisionStructure(fn: IrFunction): { + calls: { callee: string; args: number }[]; + rest: string; +} { + const calls: { callee: string; args: number }[] = []; + const rest: string[] = []; + const boxed = new Set(fn.locals.filter((local) => local.boxed).map((local) => local.id)); + const expr = (e: IrExpr): boolean => { + const referenced = exprFunctionReference(e); + if (referenced !== undefined) rest.push(`ref:${referenced}`); + switch (e.kind) { + case "call": + calls.push({ callee: e.callee, args: e.args.length }); + break; + case "new": + rest.push(`new:${e.className}:${e.args.length}`); + break; + case "varRef": + if (boxed.has(e.localId)) rest.push(`read:${e.localId}`); + break; + case "assignExpr": + case "incDec": + rest.push(`${e.kind}:${e.localId}`); + break; + case "fieldGet": + case "fieldIncDec": + rest.push(`${e.kind}:${e.className}\0${e.field}`); + break; + case "closure": + rest.push(`closure:${e.fnName}:${e.captures.join(",")}`); + break; + case "classRef": + rest.push(`classRef:${e.className}:${(e.captures ?? []).join(",")}`); + break; + case "virtualCall": + rest.push(`virtualCall:${e.className}.${e.method}`); + break; + case "instanceOfValue": + rest.push(`instanceOf:${e.classValue.kind}`); + break; + } + if (BOUNDARY_KINDS.has(e.kind)) rest.push(`boundary:${e.kind}`); + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + switch (s.kind) { + case "forOf": + rest.push(`forOf:${s.localId}`); + break; + case "tryCatch": + if (s.catchLocalId) rest.push(`catch:${s.catchLocalId}`); + break; + case "assign": + case "varDecl": + if (boxed.has(s.localId)) rest.push(`${s.kind}:${s.localId}`); + break; + case "throw": + rest.push("throw"); + break; + } + return everyStmtChild(s, expr, stmt); + }; + for (const s of fn.body) stmt(s); + rest.sort(); + for (const local of fn.locals) + if (local.boxed || local.tdz) + rest.push(`local:${local.id}:${local.boxed === true}:${local.tdz === true}`); + for (const capture of fn.captures ?? []) rest.push(`capture:${capture.localId}`); + for (const capture of fn.classCaptures ?? []) rest.push(`classCapture:${capture.localId}`); + return { calls, rest: rest.join("\n") }; } export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): Int32Slots { @@ -1204,6 +1334,36 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): }); return found; }; + // Candidates dropped without checked stores, and the revision of the + // functions that hold them (Int32SlotInputs.revise). + const dropped = new Set(); + const revise = (names: ReadonlySet): void => { + for (const name of names) { + const previous = functions.get(name)!; + const next = inputs.revise!(name, dropped); + if (next === previous) continue; + const before = revisionStructure(previous); + const after = revisionStructure(next); + if (before.rest !== after.rest) throw new Int32RevisionConflict(name); + // The revision adds the element-reader calls back: each must be of a + // function other calls already reach, with its arity. + const counts = new Map(); + for (const { callee } of before.calls) counts.set(callee, (counts.get(callee) ?? 0) + 1); + for (const { callee, args } of after.calls) { + const left = counts.get(callee) ?? 0; + if (left > 0) { + counts.set(callee, left - 1); + continue; + } + const target = functions.get(callee); + if (!called.has(callee) || target === undefined || target.params.length !== args) + throw new Int32RevisionConflict(name); + reads(`ret:${callee}`, name); + } + for (const left of counts.values()) if (left > 0) throw new Int32RevisionConflict(name); + functions.set(name, next); + } + }; const provenByFunction = new Map(); // Each function's parameter and result writes under its latest analysis: // at the fixpoint, the ranges of every write under the final facts. @@ -1248,6 +1408,8 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): for (const id of globalArrays) both.add(id); arrays = both; } + // Functions whose candidates this visit drops, revised after it. + const revised = new Set(); if (arrays && arrayGroups) { const requeue = (name: string): void => { if (queued.has(name)) return; @@ -1282,9 +1444,39 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): checkableInt32Allocations(fn, id) ) slots.checkedInt32Arrays.add(key); - else for (const member of group) reason(`array:${member}`, "an element store is not int32"); + else { + for (const member of group) reason(`array:${member}`, "an element store is not int32"); + if (inputs.revise !== undefined) { + // The revised module no longer marks the group: its functions + // leave the candidate sets the visits below consult. + for (const member of group) { + dropped.add(member); + if (member.startsWith("\0")) { + globalArrays.delete(member.slice(1)); + const names = arrayGroups.globalReaders.get(member); + if (names) for (const name of names) revised.add(name); + } else { + const cut = member.indexOf("\0"); + const owner = member.slice(0, cut); + const ids = arrayCandidates.get(owner); + ids?.delete(member.slice(cut + 1)); + if (ids?.size === 0) arrayCandidates.delete(owner); + revised.add(owner); + } + } + globalArrayFunctions.clear(); + for (const [member, names] of arrayGroups.globalReaders) + if (!dropped.has(member)) for (const name of names) globalArrayFunctions.add(name); + } + } } } + if (revised.size > 0) { + revise(revised); + // A revised body is analyzed again (it was queued with its readers); + // the writes of the body it replaces no longer count. + if (functions.get(fn.name) !== fn) continue; + } // An int32 written into a slot still assumed non-negative: widen the // assumption when the value may be negative, and reanalyze its readers. const settle = (slot: string, e: IrExpr): void => { diff --git a/packages/compiler/test/int32-element-array-emission.test.ts b/packages/compiler/test/int32-element-array-emission.test.ts index 605f64e04..fccf06973 100644 --- a/packages/compiler/test/int32-element-array-emission.test.ts +++ b/packages/compiler/test/int32-element-array-emission.test.ts @@ -3,8 +3,10 @@ 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 type { IrFunction, IrModule } from "../src/ir/ir.js"; import { markInt32ArrayCandidates, selectInt32Arrays } from "../src/ir/int32-arrays.js"; +import { analyzeInt32Slots, type Int32Slots } from "../src/ir/int32-slots.js"; +import { serializeModule } from "../src/ir/serialize.js"; async function emitLlvm(source: string): Promise { const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-elements-")); @@ -210,12 +212,12 @@ test("only the candidates the proof keeps are rewritten", async () => { // An array that reaches `join` is never proposed. expect(proposed.some((key) => key.startsWith("joined\0"))).toBe(false); // Without a kept candidate, the module is the input itself. - expect(selectInt32Arrays(mod, () => new Set())).toBe(mod); + expect(selectInt32Arrays(mod, () => ({ kept: new Set(), functions: null }))).toBe(mod); const kept = new Set(["compose\0p.0", "compose\0q.0"]); - const selected = selectInt32Arrays( - mod, - (marked) => new Set((marked.int32ArrayCandidates ?? []).filter((key) => kept.has(key))), - ); + const selected = selectInt32Arrays(mod, (marked) => ({ + kept: new Set((marked.int32ArrayCandidates ?? []).filter((key) => kept.has(key))), + functions: null, + })); expect(selected.int32ArrayCandidates).toEqual([...kept]); for (const fn of selected.functions) if (fn.name !== "compose") expect(fn).toBe(mod.functions.find((f) => f.name === fn.name)); @@ -259,3 +261,47 @@ test("arrays passed to helpers and held in module globals get 4-byte slots", asy expect(functions(llvm, "mean")).not.toMatch(/getelementptr inbounds i32, ptr/); expect(llvm).toMatch(/@scr_arr_new(_small)?\(i32 7/); }); + +/** The module selectInt32Arrays selects, the analysis of its last proof, + * and how many proofs it asked for: with `revising`, each proof revises + * the functions of candidates that drop. */ +function selection( + mod: IrModule, + revising: boolean, +): { selected: IrModule; slots: Int32Slots; proofs: number } { + let slots: Int32Slots | null = null; + let proofs = 0; + const selected = selectInt32Arrays(mod, (marked, revise) => { + proofs++; + const latest = new Map(); + slots = analyzeInt32Slots( + marked, + revising && revise !== null + ? { + revise: (fnName, dropped) => { + const fn = revise(fnName, dropped); + latest.set(fnName, fn); + return fn; + }, + } + : {}, + ); + const kept = new Set([...slots.int32Arrays, ...slots.checkedInt32Arrays]); + if (latest.size === 0) return { kept, functions: null }; + return { kept, functions: marked.functions.map((fn) => latest.get(fn.name) ?? fn) }; + }); + return { selected, slots: slots!, proofs }; +} + +test("one proof that revises dropped candidates selects what rounds of proofs select", async () => { + for (const program of [source, sharedSource]) { + const mod = await lowerIr(program); + const rounds = selection(mod, false); + const revised = selection(mod, true); + // The rounds drop a candidate and prove the module again. + expect(rounds.proofs).toBeGreaterThan(1); + expect(revised.proofs).toBe(1); + expect(serializeModule(revised.selected, true)).toBe(serializeModule(rounds.selected, true)); + expect(revised.slots.summary(revised.selected)).toBe(rounds.slots.summary(rounds.selected)); + } +}); From 12bbb9bde6069414b66a363696bad7d2bfc986b3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 03:00:30 -0500 Subject: [PATCH 183/273] Keep a throttler's pending-call storage within its live calls A queue that never emptied kept the slots of calls already taken, so its storage grew with lifetime traffic even when maxPending bounded the live calls. Before growing, the queue now moves its live calls to the front. --- packages/runtime/src/scr_util_timing.c | 7 +++++++ tests/corpus/7711-util-throttle.cjs | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/runtime/src/scr_util_timing.c b/packages/runtime/src/scr_util_timing.c index a8a700d47..13651f5ff 100644 --- a/packages/runtime/src/scr_util_timing.c +++ b/packages/runtime/src/scr_util_timing.c @@ -303,6 +303,13 @@ static size_t ut_pending_count(const UtState *s) { } static void ut_push(UtState *s, ScrPromise *promise /* moves in */, ScrDyn **args /* moves in */, size_t argc) { + /* Calls already taken free their slots, so a queue that never empties + * keeps its storage within twice its live calls. */ + if (s->len == s->cap && s->head > 0) { + s->len -= s->head; + memmove(s->calls, s->calls + s->head, s->len * sizeof *s->calls); + s->head = 0; + } if (s->len == s->cap) { size_t cap = s->cap ? s->cap * 2 : 4; UtCall *grown = realloc(s->calls, cap * sizeof *grown); diff --git a/tests/corpus/7711-util-throttle.cjs b/tests/corpus/7711-util-throttle.cjs index d786535ef..f776e7174 100644 --- a/tests/corpus/7711-util-throttle.cjs +++ b/tests/corpus/7711-util-throttle.cjs @@ -35,6 +35,18 @@ async function main() { console.log(c.activeCount, c.pendingCount); console.log(await ca, await cb, order, c.activeCount); + // A queue that never empties while calls are taken and others arrive. + const q = util.throttle(async (x) => { await null; return x; }, 1000, 1000, { concurrency: 1 }); + const queued = []; + for (let i = 0; i < 5; i++) queued.push(q(i)); + const results = []; + for (let i = 5; i < 25; i++) { + results.push(await queued[i - 5]); + queued.push(q(i)); + } + for (let i = 20; i < 25; i++) results.push(await queued[i]); + console.log(results.join(","), q.pendingCount, q.activeCount); + // A throwing fn rejects that call only. const e = util.throttle((x) => { if (x === 2) throw new RangeError("two"); return x; }, 5, 10); await settle("e1", e(1)); From 975270cef5a59ffe33821c8abe2be257dc141b24 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 03:02:53 -0500 Subject: [PATCH 184/273] Decode base64 eight characters at a time Buffer.from(text, "base64" | "base64url") decoded each four-character group with four lookups and a sign test, then stored its three bytes one at a time. The decoder now first takes complete groups two at a time: each character's value comes from a table that holds it already shifted into place within its group's 24 bits, a single bit test covers all eight characters, and each group's three bytes go out in one four-byte store whose last byte the next store overwrites. A group with any other character (padding, whitespace, an invalid character) takes the previous path, and the two-group step resumes after it, so line-wrapped text keeps most of the gain. The destination already holds two spare bytes, which cover the extra store byte. --- .../corpus/8006-buffer-base64-decode.ts.json | 6 ++ packages/runtime/src/scr_bytes.c | 66 ++++++++++++++++--- tests/corpus/8006-buffer-base64-decode.ts | 66 +++++++++++++++++++ .../8006-buffer-base64-decode.ts.json | 1 + 4 files changed, 129 insertions(+), 10 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8006-buffer-base64-decode.ts.json create mode 100644 tests/corpus/8006-buffer-base64-decode.ts create mode 100644 tests/harness/corpus-costs/8006-buffer-base64-decode.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8006-buffer-base64-decode.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8006-buffer-base64-decode.ts.json new file mode 100644 index 000000000..135fe4af6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8006-buffer-base64-decode.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8006-buffer-base64-decode.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 6b8594c08..31716604e 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -2672,20 +2672,66 @@ static ScrBytes *scr_bytes_conversion_storage(size_t capacity) { return scr_bytes_conversion_storage_in(NULL, capacity); } +/* scr_b64_values, each value shifted into its place in a four-character + * group's 24 bits; a character outside both alphabets sets bit 24. */ +#define SCR_B64_VALUE(c) \ + ((c) >= 'A' && (c) <= 'Z' ? (c) - 'A' \ + : (c) >= 'a' && (c) <= 'z' ? (c) - 'a' + 26 \ + : (c) >= '0' && (c) <= '9' ? (c) - '0' + 52 \ + : (c) == '+' || (c) == '-' ? 62 \ + : (c) == '/' || (c) == '_' ? 63 \ + : -1) +#define SCR_B64_AT(c, shift) \ + (SCR_B64_VALUE(c) < 0 ? UINT32_C(0x1000000) : (uint32_t)SCR_B64_VALUE(c) << (shift)) +#define SCR_B64_4(c, s) SCR_B64_AT(c, s), SCR_B64_AT(c + 1, s), SCR_B64_AT(c + 2, s), SCR_B64_AT(c + 3, s) +#define SCR_B64_16(c, s) SCR_B64_4(c, s), SCR_B64_4(c + 4, s), SCR_B64_4(c + 8, s), SCR_B64_4(c + 12, s) +#define SCR_B64_64(c, s) SCR_B64_16(c, s), SCR_B64_16(c + 16, s), SCR_B64_16(c + 32, s), SCR_B64_16(c + 48, s) +#define SCR_B64_256(s) SCR_B64_64(0, s), SCR_B64_64(64, s), SCR_B64_64(128, s), SCR_B64_64(192, s) +static const uint32_t scr_b64_shifted[4][256] = { + {SCR_B64_256(18)}, {SCR_B64_256(12)}, {SCR_B64_256(6)}, {SCR_B64_256(0)}}; + +/* One group's 24 bits as three bytes, in a four-byte store whose last byte + * the next store overwrites (the destination holds two spare bytes). */ +static inline void scr_b64_put_group(uint8_t *out, uint32_t bits) { +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + uint32_t word = __builtin_bswap32(bits << 8); +#else + uint32_t word = bits << 8; +#endif + memcpy(out, &word, 4); +} + static ScrBytes *scr_bytes_decode_b64(ScrBytesSink *sink, const uint8_t *in, size_t n) { ScrBytes *b = scr_bytes_conversion_storage_in(sink, n / 4 * 3 + 2); + /* A local copy: a byte store could otherwise change b->data. */ + uint8_t *out = b->data; + const uint32_t(*t)[256] = scr_b64_shifted; size_t i = 0, o = 0; unsigned acc = 0, have = 0; while (i < n) { + /* Complete groups, two per step: each character's shifted value comes + * from a table and one bit test covers all eight. The second store + * ends at o + 6, within the destination while eight characters remain. */ + if (have == 0) { + while (n - i >= 8) { + uint32_t first = t[0][in[i]] | t[1][in[i + 1]] | t[2][in[i + 2]] | t[3][in[i + 3]]; + uint32_t second = t[0][in[i + 4]] | t[1][in[i + 5]] | t[2][in[i + 6]] | t[3][in[i + 7]]; + if ((first | second) & UINT32_C(0x1000000)) break; + scr_b64_put_group(out + o, first); + scr_b64_put_group(out + o + 3, second); + o += 6; + i += 8; + } + } /* Common complete groups need neither per-character branches nor state. */ if (have == 0 && n - i >= 4) { int a = scr_b64_values[in[i]], c = scr_b64_values[in[i + 1]]; int d = scr_b64_values[in[i + 2]], e = scr_b64_values[in[i + 3]]; if ((a | c | d | e) >= 0) { unsigned v = ((unsigned)a << 18) | ((unsigned)c << 12) | ((unsigned)d << 6) | (unsigned)e; - b->data[o++] = (uint8_t)(v >> 16); - b->data[o++] = (uint8_t)(v >> 8); - b->data[o++] = (uint8_t)v; + out[o++] = (uint8_t)(v >> 16); + out[o++] = (uint8_t)(v >> 8); + out[o++] = (uint8_t)v; i += 4; continue; } @@ -2696,17 +2742,17 @@ static ScrBytes *scr_bytes_decode_b64(ScrBytesSink *sink, const uint8_t *in, siz if (v < 0) continue; acc = (acc << 6) | (unsigned)v; if (++have == 4) { - b->data[o++] = (uint8_t)(acc >> 16); - b->data[o++] = (uint8_t)(acc >> 8); - b->data[o++] = (uint8_t)acc; + out[o++] = (uint8_t)(acc >> 16); + out[o++] = (uint8_t)(acc >> 8); + out[o++] = (uint8_t)acc; acc = 0; have = 0; } } - if (have == 2) b->data[o++] = (uint8_t)(acc >> 4); + if (have == 2) out[o++] = (uint8_t)(acc >> 4); else if (have == 3) { - b->data[o++] = (uint8_t)(acc >> 10); - b->data[o++] = (uint8_t)(acc >> 2); + out[o++] = (uint8_t)(acc >> 10); + out[o++] = (uint8_t)(acc >> 2); } b->len = o; return b; @@ -2764,7 +2810,7 @@ static ScrBytes *scr_bytes_encode_str(ScrBytesSink *sink, const ScrStr *s, const /* Node interprets encoded text as the low byte of each UTF-16 unit. * ASCII can borrow storage; non-ASCII must preserve that truncation. */ ScrBytes *narrowed = NULL; - if (scr_bytes_ascii_prefix(in, n) != n) { + if (!scr_str_known_ascii(s) && scr_bytes_ascii_prefix(in, n) != n) { ScrStr *latin1 = scr_str_new("latin1", 6); narrowed = scr_bytes_encode_str(NULL, s, latin1); scr_str_release(latin1); diff --git a/tests/corpus/8006-buffer-base64-decode.ts b/tests/corpus/8006-buffer-base64-decode.ts new file mode 100644 index 000000000..3d7b20303 --- /dev/null +++ b/tests/corpus/8006-buffer-base64-decode.ts @@ -0,0 +1,66 @@ +// Buffer.from(text, "base64" | "base64url") over complete eight-character +// blocks and the text around them: every length from 0 to 40, an invalid +// character at each position of a block, padding and text after it, both +// alphabets, line breaks every 76 characters, sizes around the pool +// threshold, and characters past U+00FF. + +let seed = 7; +function next(): number { + seed = (seed * 1103515245 + 12345) & 0x7fffffff; + return seed >>> 8; +} +function bytes(size: number): Buffer { + const buf = Buffer.alloc(size); + for (let i = 0; i < size; i++) buf[i] = next() & 255; + return buf; +} + +const rows: string[] = []; +for (let size = 0; size <= 30; size++) { + const buf = bytes(size); + const text = buf.toString("base64"); + const url = buf.toString("base64url"); + rows.push( + `${size}:${text.length}:${Buffer.from(text, "base64").equals(buf)}:${Buffer.from(url, "base64url").equals(buf)}:${Buffer.from(url, "base64").equals(buf)}`, + ); +} +console.log(rows.join(" ")); + +const block = Buffer.from("0123456789abcdefghijklmnopqrstuv").toString("base64"); +for (let at = 0; at < 16; at++) { + for (const bad of [" ", "\n", "*", "=", "é", "中"]) { + const text = block.slice(0, at) + bad + block.slice(at); + console.log(at, JSON.stringify(bad), Buffer.from(text, "base64").toString("hex")); + } +} + +for (const text of [ + "QUJDREVGR0g=", + "QUJDREVGR0", + "QUJDREVGR0g=QUJD", + "QUJD=REVGR0hJ", + "QUJDREVGR0hJSktM", + "QUJDREVGR0hJSktMTQ", + "QUJDREVGR0hJSktMTQ==", + "--__--__", + "++//++//", + "+-/_+-/_+-/_", + "Q U J D R E V G R 0 h J", + "QUJD\tREVG\r\nR0hJ", + "=QUJDREVGR0hJ", + "", +]) { + console.log(JSON.stringify(text), Buffer.from(text, "base64").toString("hex"), Buffer.from(text, "base64url").length); +} + +for (const size of [4095, 4096, 4097, 6144, 6145, 70000]) { + const buf = bytes(size); + const text = buf.toString("base64"); + const wrapped = text.replace(/.{76}/g, "$&\n"); + const decoded = Buffer.from(text, "base64"); + const unwrapped = Buffer.from(wrapped, "base64"); + console.log(size, decoded.length, decoded.equals(buf), unwrapped.equals(buf), Buffer.byteLength(text, "base64")); +} + +const wide = "QUJDREVGR0hJİSktM"; +console.log(Buffer.from(wide, "base64").toString("hex"), Buffer.from("ŁłQUJD", "base64").toString("hex")); diff --git a/tests/harness/corpus-costs/8006-buffer-base64-decode.ts.json b/tests/harness/corpus-costs/8006-buffer-base64-decode.ts.json new file mode 100644 index 000000000..88f85d0aa --- /dev/null +++ b/tests/harness/corpus-costs/8006-buffer-base64-decode.ts.json @@ -0,0 +1 @@ +{ "plain": 3995, "san": 2236 } From 493785a12166c34e1a85343de0cd6804ed4d7788 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 03:20:44 -0500 Subject: [PATCH 185/273] Test switch labels in copied callbacks that return early A lone array callback whose body has an early return is copied into an exit block, and its statements are renamed by a separate path from the single-return case. The new test covers labeled switches, a nested labeled switch and breaks to both labels in map and some callbacks that return early. Both calls run as loops, and the IR validator accepts every renamed break. --- .../test/array-pipeline-fusion.test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/compiler/test/array-pipeline-fusion.test.ts b/packages/compiler/test/array-pipeline-fusion.test.ts index ba814105f..e41e378a3 100644 --- a/packages/compiler/test/array-pipeline-fusion.test.ts +++ b/packages/compiler/test/array-pipeline-fusion.test.ts @@ -213,3 +213,39 @@ console.log(out.join(",")); ${postlude}`); expect(helperCalls(init(mod))).toEqual(["%arr.loop"]); }); + +test("a copied callback with early returns keeps its switch labels", async () => { + const mod = await lowerSource(`${prelude} +const out = rows.map((r) => { + let n = 0; + outer: switch (r.id % 4) { + case 0: + return -1; + case 1: + n = 10; + break outer; + default: + inner: switch (r.value % 2) { + case 0: + n = r.value; + break outer; + default: + break inner; + } + n = 1; + } + return n; +}); +const hit = rows.some((r) => { + pick: switch (r.id) { + case 3: + break pick; + default: + return false; + } + return true; +}); +console.log(out.join(","), hit); +${postlude}`); + expect(helperCalls(init(mod))).toEqual(["%arr.loop", "%arr.loop"]); +}); From 2b11f14a041c49b64e304db16abdd9e1d830f7d8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 03:21:25 -0500 Subject: [PATCH 186/273] Test that typed-array element descriptors are read through their getters Object.defineProperty on a typed-array index reads its descriptor with ToPropertyDescriptor (scr_dyn_descriptor_fields_read): every field once, in specification order, through its getter, and a primitive descriptor throws a TypeError. 7753 now also checks that the stored value is the value getter's result, read once; that a getter-produced writable: false is refused after the fields are read in order; and that null, booleans, BigInts and symbols are refused as descriptors. --- .../corpus/7753-typed-array-named-properties.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/corpus/7753-typed-array-named-properties.js b/tests/corpus/7753-typed-array-named-properties.js index 449e78ac7..e37181a5e 100644 --- a/tests/corpus/7753-typed-array-named-properties.js +++ b/tests/corpus/7753-typed-array-named-properties.js @@ -27,6 +27,23 @@ function expandos(TA) { const got = []; Object.defineProperty(a, "1", { get value() { got.push("value"); return 6; }, get enumerable() { got.push("enumerable"); return true; } }); console.log(a[1], got); + // Each field is read once, through its getter; the stored value is the + // getter's result, and a getter-produced `writable: false` is refused. + let valueReads = 0; + Object.defineProperty(a, "1", { get value() { valueReads++; return 40 + valueReads; } }); + console.log(a[1], valueReads); + const order = []; + try { + Object.defineProperty(a, "1", { + get writable() { order.push("writable"); return false; }, + get value() { order.push("value"); return 2; }, + get configurable() { order.push("configurable"); return true; }, + }); + } catch (e) { console.log(e.constructor.name, e.message); } + console.log(a[1], order); + for (const bad of [null, true, 1n, Symbol("d")]) { + try { Object.defineProperty(a, "1", bad); } catch (e) { console.log(e.constructor.name); } + } for (const bad of [5, "x", undefined]) { try { Object.defineProperty(a, "0", bad); } catch (e) { console.log(e.constructor.name); } } From 342b770cf20a8b1ae124c5053b174ab18281177f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 03:25:37 -0500 Subject: [PATCH 187/273] Keep a Map.get result owned when its key can replace the map's holder The receiver of a borrowed Map.get is read before its key. When the key's evaluation replaces the field or local that holds the map (`this.m.get(this.key())` where key() assigns this.m), the read runs on the old map, which only the statement still holds, and the borrowed value outlives it. A borrow now also requires the read itself, receiver and key included, to preserve reference edges; otherwise the read keeps its own reference. Corpus 8181 and the emission test cover a key method that replaces the field. --- .../src/backend/llvm/map-read-lifetimes.ts | 7 +++++-- .../test/map-get-borrow-emission.test.ts | 17 ++++++++++++++++- tests/corpus/8181-map-get-borrows.ts | 19 ++++++++++++++++++- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts index 3371a7f70..d5d286674 100644 --- a/packages/compiler/src/backend/llvm/map-read-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/map-read-lifetimes.ts @@ -186,8 +186,10 @@ function heldReceiver(expr: IrExpr): boolean { * branches and joins them; any other statement that names the local is * judged whole. A later pass of an enclosing loop declares the local again, * so only the rest of its own statement list matters. The map must be held - * by a local or a field (heldReceiver), and locals declared from the local - * count as its uses. An update such as + * by a local or a field (heldReceiver), the read itself must preserve + * reference edges (a key whose evaluation replaces the receiver's field + * leaves the read on a map the statement releases), and locals declared + * from the local count as its uses. An update such as * `const c = m.get(k); if (c === undefined) m.set(k, v); else c.n++` keeps * the borrow: the write runs only on the branch that does not read `c`. */ export function findMapReadBorrows( @@ -264,6 +266,7 @@ export function findMapReadBorrows( stmt.kind === "varDecl" && stmt.init?.kind === "mapIntrinsic" && heldReceiver(stmt.init.receiver) && + effects.preserves(stmt.init) && candidate(stmt) && walk(list.slice(index + 1), new Set([stmt.localId]), false) !== null ) diff --git a/packages/compiler/test/map-get-borrow-emission.test.ts b/packages/compiler/test/map-get-borrow-emission.test.ts index eba430453..4e3c91766 100644 --- a/packages/compiler/test/map-get-borrow-emission.test.ts +++ b/packages/compiler/test/map-get-borrow-emission.test.ts @@ -49,9 +49,20 @@ function temporary(keys: number[]): number { } return total; } +class Holder { + m = new Map([[1, new Counter()]]); + key(): number { + this.m = new Map(); + return 1; + } + read(): number { + const held = this.m.get(this.key()); + return held === undefined ? 0 : held.num; + } +} const numbers = new Map(); count(numbers, [1, 2, 1]); -console.log(named(new Map([["a", new Counter()]]), "a"), replaced(numbers), aliased(numbers), temporary([1, 2])); +console.log(named(new Map([["a", new Counter()]]), "a"), replaced(numbers), aliased(numbers), temporary([1, 2]), new Holder().read()); `, ); const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); @@ -72,6 +83,10 @@ console.log(named(new Map([["a", new Counter()]]), "a"), replaced(numbers), alia // does a read from a map that is released after the declaration. for (const name of ["replaced", "aliased", "temporary"]) expect(fn(name)).toContain("@sc_retain_Counter"); + // A key whose evaluation replaces the receiver's field leaves the read + // on the old map, which the statement releases: the read keeps its own + // reference. + expect(fn("_x25_Holder_read")).toContain("@sc_retain_Counter"); } finally { await rm(dir, { recursive: true, force: true }); } diff --git a/tests/corpus/8181-map-get-borrows.ts b/tests/corpus/8181-map-get-borrows.ts index 5df7ad226..1a6c71667 100644 --- a/tests/corpus/8181-map-get-borrows.ts +++ b/tests/corpus/8181-map-get-borrows.ts @@ -2,7 +2,8 @@ // release the entry's value: an update on the absent branch, a write, delete // or clear before a later read, a call that may change the map, loops, // string keys, values that are stored, returned or compared, a local declared -// from the read, and a read from a temporary map. +// from the read, a read from a temporary map, and a key that replaces the +// field holding the map. class Counter { num: number; label: string; @@ -153,3 +154,19 @@ function fromTemporary(): string { return held === undefined ? "none" : `${held.num}:${held.label}:${filler.length}`; } console.log("temporary", fromTemporary()); + +// A key whose evaluation replaces the field that holds the map: the read +// uses the old map, which only the statement still holds. +class Holder { + m = new Map([[1, new Counter(9, "nine")]]); + key(): number { + this.m = new Map(); + return 1; + } + read(): string { + const held = this.m.get(this.key()); + const filler = [new Counter(10, "filler"), new Counter(11, "filler")]; + return held === undefined ? "none" : `${held.num}:${held.label}:${filler.length}:${this.m.size}`; + } +} +console.log("replaced by key", new Holder().read()); From 9cb9750f92c6bd3daeb4d1c97ff888367073d9d9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 03:34:33 -0500 Subject: [PATCH 188/273] Set a trusted build aside while a getter runs outside a visit session A getter or proxy trap that runs during a trusted build outside any visit session now runs with the build's state set aside, as inside a session. A conversion the getter starts, such as a recursive union that opens and ends its own session, no longer clears the mark that keeps the rest of the outer build from trusting its earlier match. --- packages/runtime/src/scr_json.c | 22 ++++++++---------- tests/harness/dyncheck.test.ts | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b0f4c99c4..9f10368cc 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -10932,9 +10932,9 @@ static void scr_dyn_visit_exit(void) { } /* Getters and proxy traps run user code, which may start conversions of - * its own and catch their failures. It runs with the session set aside, - * so its conversions use their own tables and leave the session's - * entries alone. */ + * its own and catch their failures. It runs with the session and any + * trusted build set aside, so its conversions use their own tables and + * leave the session's entries and the trusted build's state alone. */ typedef struct { size_t depth; ScrDynVisitSet matching, built, viewed; @@ -10977,12 +10977,10 @@ static void scr_dyn_visit_resume(const ScrDynVisitState *saved) { scr_dyn_session_ran_code = true; } -/* Calls a getter for a member read, outside any visit session. */ +/* Calls a getter for a member read, outside any visit session or trusted + * build. */ static ScrDyn *scr_dyn_visit_call_getter(ScrDyn *getter) { - if (scr_dyn_visit_depth == 0) { - if (scr_dyn_trusted) scr_dyn_session_ran_code = true; - return scr_dyn_call(getter, NULL, 0, "getter"); - } + if (scr_dyn_visit_depth == 0 && !scr_dyn_trusted) return scr_dyn_call(getter, NULL, 0, "getter"); ScrDynVisitState saved; scr_dyn_visit_suspend(&saved); ScrDyn *result = scr_dyn_call(getter, NULL, 0, "getter"); @@ -10990,12 +10988,10 @@ static ScrDyn *scr_dyn_visit_call_getter(ScrDyn *getter) { return result; } -/* A proxy's get trap for a member read, outside any visit session. */ +/* A proxy's get trap for a member read, outside any visit session or + * trusted build. */ static ScrDyn *scr_dyn_visit_proxy_get(const ScrDyn *d, ScrStr *name) { - if (scr_dyn_visit_depth == 0) { - if (scr_dyn_trusted) scr_dyn_session_ran_code = true; - return scr_dyn_proxy_get(d, name); - } + if (scr_dyn_visit_depth == 0 && !scr_dyn_trusted) return scr_dyn_proxy_get(d, name); ScrDynVisitState saved; scr_dyn_visit_suspend(&saved); ScrDyn *result = scr_dyn_proxy_get(d, name); diff --git a/tests/harness/dyncheck.test.ts b/tests/harness/dyncheck.test.ts index f38d8237d..8dde55167 100644 --- a/tests/harness/dyncheck.test.ts +++ b/tests/harness/dyncheck.test.ts @@ -686,6 +686,46 @@ console.log(JSON.stringify(plain)); ]); }); + test("nested unions: a getter that converts a recursive union keeps a non-recursive build untrusted", async () => { + // The getter converts a recursive union, which opens and ends a visit + // session of its own, before it answers. The outer build still matches + // the getter's second answer in full, and the failure names that union. + const r = await compileAndRun( + "flat-discriminated-getter-session", + `type Tree = { kind: "leaf"; n: number } | { kind: "node"; left: Tree; right: Tree }; +type Inner = { tag: "x"; n: number } | { tag: "y"; s: string }; +type Outer = { kind: "a"; inner: Inner } | { kind: "b"; other: number }; +const v = JSON.parse('{"kind":"a"}') as unknown; +let calls = 0; +let leaves = 0; +Object.defineProperty(v, "inner", { + enumerable: true, + get() { + calls++; + const t = JSON.parse('{"kind":"node","left":{"kind":"leaf","n":1},"right":{"kind":"leaf","n":2}}') as Tree; + if (t.kind === "node") leaves += 2; + return calls === 1 ? JSON.parse('{"tag":"x","n":1}') : JSON.parse('{"tag":"x","n":"bad"}'); + }, +}); +try { + const o = v as Outer; + console.log("ok", JSON.stringify(o)); +} catch (err) { + console.log((err as Error).message); +} +console.log("getter calls", calls, "leaves", leaves); +const plain = JSON.parse('{"kind":"a","inner":{"tag":"y","s":"z"}}') as Outer; +console.log(JSON.stringify(plain)); +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout.trimEnd().split("\n")).toEqual([ + "expected object | object at $.inner, got object", + "getter calls 2 leaves 4", + '{"kind":"a","inner":{"tag":"y","s":"z"}}', + ]); + }); + test("array where a record is expected names the mismatch at the root", async () => { const r = await compileAndRun( "root-kind", From 1e799cb60abe10a04db4b2395bbeef8abe40ef55 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 03:40:30 -0500 Subject: [PATCH 189/273] Answer net.Socket write() with Node's backpressure boolean, and fire 'drain' A TCP socket's write() answered nothing in typed code and true through untyped values, and a 'drain' listener never fired. Node 24 answers false when the bytes the write leaves unsent reach the socket's 65536-byte high-water mark (writeOrBuffer computes it after the socket writes), sets needDrain, and emits 'drain' when the write buffer empties; writes to an ended or destroyed socket answer false. The runtime's write entry points now return that answer, record needDrain, and fire the socket's 'drain' listeners from the flush that empties the buffer, unless the write half is ending or the socket is gone. writableLength reads the unsent bytes and writableNeedDrain the flag. on('drain') and once('drain') register on the typed path and through untyped values, and socket.write() may be used as a value. The fallback node:net declarations type write() as boolean, add the 'drain' event and both properties. RUNTIME_ABI_VERSION goes from 43 to 44: the socket write entry points return a boolean. --- packages/cli/test/native-link-info.test.ts | 4 +- .../ambient/scriptc-node-fallback.d.ts | 19 +++- .../compiler/src/backend/llvm/lib-network.ts | 2 + .../compiler/src/backend/llvm/lib-shared.ts | 2 + packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../src/frontend/lowering/lower-server.ts | 59 +++++++++--- .../compiler/src/ir/builtin-signatures.ts | 20 +++- .../test/native-codegen-integration.test.ts | 4 +- .../7515-net-socket-backpressure.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_net.c | 94 ++++++++++++++++--- packages/runtime/src/scr_net.h | 12 ++- tests/corpus/7515-net-socket-backpressure.ts | 47 ++++++++++ .../7515-net-socket-backpressure.ts.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- 20 files changed, 238 insertions(+), 50 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7515-net-socket-backpressure.ts.json create mode 100644 tests/corpus/7515-net-socket-backpressure.ts create mode 100644 tests/harness/corpus-costs/7515-net-socket-backpressure.ts.json diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index b4b11fa95..ef07c838d 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: 43, marker: "scr_runtime_abi_v43" }, + runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" }, 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_v43"); + expect(error?.stderr).toContain("scr_runtime_abi_v44"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 1924baece..463808a1a 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -5322,10 +5322,10 @@ declare module "node:zlib" { * with the real port discoverable as address().port (the composed read * is the lowered form — bare address() has no lowering); close([cb]) * firing once connections drain; socket write/end/destroy/pipe and the - * data/end/close/error/connect events (on and once — listeners take at - * most the declared parameter). write/end return void here (Node returns - * the backpressure boolean / the socket); on/once return void (chaining - * is fenced). connect's host is a numeric IP or "localhost" (pinned to + * data/end/close/error/connect/drain events (on and once — listeners + * take at most the declared parameter). write returns Node's + * backpressure boolean; end returns void here (Node returns the socket); + * on/once return void (chaining is fenced). connect's host is a numeric IP or "localhost" (pinned to * 127.0.0.1 — SEMANTICS.md); every failure is the async 'error' event, * and an 'error' with no listener exits 1 like an unhandled EventEmitter * 'error'. */ @@ -5357,7 +5357,13 @@ declare module "net" { readonly bytesWritten: number; /* write's callback runs once the data is flushed, with null; end's * once the socket finishes. */ - write(data: string | Uint8Array, callback?: (err?: Error | null) => void): void; + /* false when the bytes this write left unsent reach the 65536-byte + * high-water mark, as in Node; 'drain' fires when they are all sent. */ + write(data: string | Uint8Array, callback?: (err?: Error | null) => void): boolean; + /* the bytes written and not yet sent */ + readonly writableLength: number; + /* a write answered false and 'drain' has not fired yet */ + readonly writableNeedDrain: boolean; end(callback?: () => void): void; end(data: string | Uint8Array, callback?: () => void): void; destroy(): void; @@ -5405,6 +5411,7 @@ declare module "net" { | "connect" | "ready" | "timeout" + | "drain" | "readable" | "finish" | "secureConnect" @@ -5429,6 +5436,7 @@ declare module "net" { | "connect" | "ready" | "timeout" + | "drain" | "readable" | "finish" | "secureConnect" @@ -5452,6 +5460,7 @@ declare module "net" { | "connect" | "ready" | "timeout" + | "drain" | "readable" | "finish" | "secureConnect" diff --git a/packages/compiler/src/backend/llvm/lib-network.ts b/packages/compiler/src/backend/llvm/lib-network.ts index d49bbde14..35eb885cd 100644 --- a/packages/compiler/src/backend/llvm/lib-network.ts +++ b/packages/compiler/src/backend/llvm/lib-network.ts @@ -309,6 +309,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) e.fn === "net.sockOnClose" || e.fn === "net.sockOnConnect" || e.fn === "net.sockOnTimeout" || + e.fn === "net.sockOnDrain" || e.fn === "net.sockOnReadable" || e.fn === "net.sockOnReady" || e.fn === "http.reqOnEnd" || @@ -329,6 +330,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) "net.sockOnClose": "scr_net_sock_on_close", "net.sockOnConnect": "scr_net_sock_on_connect", "net.sockOnTimeout": "scr_net_sock_on_timeout", + "net.sockOnDrain": "scr_net_sock_on_drain", "net.sockOnReadable": "scr_net_sock_on_readable", "net.sockOnReady": "scr_net_sock_on_ready", "http.reqOnEnd": "scr_http_req_on_end", diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index def2dd6bf..2548b75d5 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1337,6 +1337,8 @@ export const LIB_FN_SYMS: Record = { "net.sockResetAndDestroy": "scr_net_sock_reset_and_destroy", "net.sockAddress": "scr_net_sock_address", "net.sockBytesWritten": "scr_net_sock_bytes_written", + "net.sockWritableLength": "scr_net_sock_writable_length", + "net.sockWritableNeedDrain": "scr_net_sock_writable_need_drain", "net.sockAddrField": "scr_net_sock_addr_field", "net.sockBytesRead": "scr_net_sock_bytes_read", "net.sockConnecting": "scr_net_sock_connecting", diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index d8e256349..7ac0877eb 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 = 43 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v43" as const; +export const RUNTIME_ABI_VERSION = 44 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v44" 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 5da2b8e59..650e7c543 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: 43, marker: "scr_runtime_abi_v43" }, + runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" }, 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 a87fabd2e..443c22d9e 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -129,7 +129,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" }, + runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 7359b56a8..614d6a206 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -2425,7 +2425,9 @@ function lowerNetSocketMethodCall( }; } if (name === "write" || name === "end") { - requireStatementPosition(lowerer, call, `socket.${name}(...)`); + // write() answers Node's backpressure boolean; end() stays a statement + if (name === "end") requireStatementPosition(lowerer, call, `socket.${name}(...)`); + const resultT = name === "write" ? BOOL : VOID; // write(chunk, encoding) — the two-argument encoding form: 'buffer' // beside a string chunk is Writable's encoding check (a synchronous // ERR_UNKNOWN_ENCODING TypeError, "Unknown encoding: buffer" — the @@ -2451,7 +2453,7 @@ function lowerNetSocketMethodCall( const socket = coerceToHandle(lowerer, access.expression, NETSOCKET_T); const chunk = lowerer.lowerExpr(args[0]!); const fn: IrLibFn = chunk.type.kind === "string" ? "net.sockWrite" : "net.sockWriteBytes"; - return { kind: "libCall", fn, args: [socket, chunk], type: VOID, loc }; + return { kind: "libCall", fn, args: [socket, chunk], type: BOOL, loc }; } } } @@ -2510,7 +2512,7 @@ function lowerNetSocketMethodCall( NARROW_DATA_HINT, ); } - main = { kind: "libCall", fn, args: [receiver(), data], type: VOID, loc }; + main = { kind: "libCall", fn, args: [receiver(), data], type: resultT, loc }; } const cb = lowerer.lowerExpr(last); let register: IrExpr; @@ -2531,8 +2533,24 @@ function lowerNetSocketMethodCall( loc, }; } - stmts.push({ kind: "exprStmt", expr: main, loc }); - return { kind: "seqExpr", stmts, result: register, type: VOID, loc }; + if (resultT.kind === "void") { + stmts.push({ kind: "exprStmt", expr: main, loc }); + return { kind: "seqExpr", stmts, result: register, type: VOID, loc }; + } + // write(data, cb): the data is written, the callback registered, and + // the write's answer is the value + const okLocal = lowerer.declareHiddenLocal("%sockWriteOk", BOOL); + stmts.push( + { kind: "varDecl", localId: okLocal.id, init: main, loc }, + { kind: "exprStmt", expr: register, loc }, + ); + return { + kind: "seqExpr", + stmts, + result: { kind: "varRef", localId: okLocal.id, type: BOOL, loc }, + type: BOOL, + loc, + }; } const maxArgs = name === "write" ? 1 : 1; const minArgs = name === "write" ? 1 : 0; @@ -2552,17 +2570,17 @@ function lowerNetSocketMethodCall( const data = lowerer.lowerExpr(args[0]!); if (data.type.kind === "string") { const fn: IrLibFn = name === "write" ? "net.sockWrite" : "net.sockEndStr"; - return { kind: "libCall", fn, args: [receiver, data], type: VOID, loc }; + return { kind: "libCall", fn, args: [receiver, data], type: resultT, loc }; } if (data.type.kind === "bytes" && data.type.elem === "u8") { const fn: IrLibFn = name === "write" ? "net.sockWriteBytes" : "net.sockEndBytes"; - return { kind: "libCall", fn, args: [receiver, data], type: VOID, loc }; + return { kind: "libCall", fn, args: [receiver, data], type: resultT, loc }; } if (data.type.kind === "dyn") { // An untyped JS payload (a helper's parameter): the runtime // dispatches STR/BYTES and throws Node's chunk TypeError otherwise. const fn: IrLibFn = name === "write" ? "net.sockWriteDyn" : "net.sockEndDyn"; - return { kind: "libCall", fn, args: [receiver, data], type: VOID, loc }; + return { kind: "libCall", fn, args: [receiver, data], type: resultT, loc }; } lowerer.noLowering( `${name} of '${lowerer.fmt(data.type)}' data`, @@ -2944,6 +2962,7 @@ function lowerNetSocketMethodCall( event === "connect" || event === "ready" || event === "timeout" || + event === "drain" || event === "readable" ) { const { cb } = lowerCallbackArg( @@ -2962,11 +2981,13 @@ function lowerNetSocketMethodCall( ? "net.sockOnClose" : event === "timeout" ? "net.sockOnTimeout" - : event === "readable" - ? "net.sockOnReadable" - : event === "ready" - ? "net.sockOnReady" - : "net.sockOnConnect"; + : event === "drain" + ? "net.sockOnDrain" + : event === "readable" + ? "net.sockOnReadable" + : event === "ready" + ? "net.sockOnReady" + : "net.sockOnConnect"; return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc }; } if (event === "lookup") { @@ -3957,6 +3978,18 @@ export function lowerServerProperty( const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T); return { kind: "libCall", fn: "net.sockBytesWritten", args: [receiver], type: F64, loc }; } + if ( + recvKind === "netSocket" && + lowerer.isStdlibMember(expr) && + (expr.name.text === "writableLength" || expr.name.text === "writableNeedDrain") + ) { + // The write buffer's unsent bytes, and whether a write answered false + // and 'drain' has not fired yet. + const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T); + return expr.name.text === "writableLength" + ? { kind: "libCall", fn: "net.sockWritableLength", args: [receiver], type: F64, loc } + : { kind: "libCall", fn: "net.sockWritableNeedDrain", args: [receiver], type: BOOL, loc }; + } if (recvKind === "netSocket" && lowerer.isStdlibMember(expr) && expr.name.text === "readable") { // true until the read half is done (peer FIN / destroy). const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T); diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index e768f257f..7183b72a1 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1543,15 +1543,18 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * true } and a callback taking an address string or list, then dials * the answers in order. MAY THROW (host validation, a lookup throw). */ "net.connectLookupDyn": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, - "net.sockWrite": { argTypes: [NETSOCKET_T, STRING], result: VOID }, - "net.sockWriteBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID }, + /** socket.write: Node's Writable answer — false when the bytes the write + * left unsent reach the 65536-byte high-water mark ('drain' follows), + * and on an ended or closed socket. */ + "net.sockWrite": { argTypes: [NETSOCKET_T, STRING], result: BOOL }, + "net.sockWriteBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: BOOL }, "net.sockEnd": { argTypes: [NETSOCKET_T], result: VOID }, "net.sockEndStr": { argTypes: [NETSOCKET_T, STRING], result: VOID }, "net.sockEndBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID }, /** write/end with a CHECKED-DYNAMIC chunk (an untyped JS payload into a * typed socket): the runtime dispatches STR/BYTES and throws Node's * ERR_INVALID_ARG_TYPE chunk TypeError on any other kind. */ - "net.sockWriteDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, + "net.sockWriteDyn": { argTypes: [NETSOCKET_T, DYN], result: BOOL }, "net.sockEndDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, "net.sockDestroy": { argTypes: [NETSOCKET_T], result: VOID }, "net.sockPipe": { argTypes: [NETSOCKET_T, NETSOCKET_T], result: VOID }, @@ -2009,6 +2012,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID, }, + /** on('drain') / once('drain'): fires after a write answered false, once + * the write buffer empties. */ + "net.sockOnDrain": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, "net.sockRemoteAddress": { argTypes: [NETSOCKET_T], result: VOID }, /** socket.encrypted — `boolean | undefined`: the true arm iff the * socket carries a TLS transport (Node types `encrypted: true` on @@ -2135,6 +2144,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** socket.address(): { address, family, port } or {} as a dyn. */ "net.sockAddress": { argTypes: [NETSOCKET_T], result: DYN }, "net.sockBytesWritten": { argTypes: [NETSOCKET_T], result: F64 }, + /** socket.writableLength: the bytes written and not yet sent. */ + "net.sockWritableLength": { argTypes: [NETSOCKET_T], result: F64 }, + /** socket.writableNeedDrain: a write answered false and 'drain' has not + * fired, with the write half open. */ + "net.sockWritableNeedDrain": { argTypes: [NETSOCKET_T], result: BOOL }, /** remotePort (0), remoteFamily (1), localAddress (2), localPort (3), * localFamily (4): dyn values, undefined when unknown. */ "net.sockAddrField": { argTypes: [NETSOCKET_T, F64], result: DYN }, diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 8e8439749..dc3f94e6c 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_v43"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v44"); 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_v43"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v44"); }); 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/7515-net-socket-backpressure.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7515-net-socket-backpressure.ts.json new file mode 100644 index 000000000..16f6edb8d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7515-net-socket-backpressure.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7515-net-socket-backpressure.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 84c3b83b6..7858c9782 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -366,7 +366,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" }, + runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 545c6651b..b770d81b6 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -469,7 +469,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" }, + runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 2f7c91b7d..ad49e4f6f 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_v43(void) {} +void scr_runtime_abi_v44(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 5d6d80778..8e50ec0ca 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_v43(void); +void scr_runtime_abi_v44(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_net.c b/packages/runtime/src/scr_net.c index bd1233978..91e8461b7 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -565,6 +565,11 @@ struct ScrNetSocket { ScrNetLs lookup_ls; struct ScrNetLookupEv *lookup_evs; ScrNetLs data_ls, end_ls, close_ls, err_ls, conn_ls, timeout_ls, readable_ls; + /* Writable backpressure: a user write answered false (the bytes it left + * unsent reached SCR_NET_WRITE_HWM), so 'drain' fires once the write + * buffer empties */ + ScrNetLs drain_ls; + bool need_drain; /* Flow control (pause()/resume()): user_paused holds reads OFF even * with consumers (kernel/TCP backpressure is the buffer); flowing * counts as a consumer even with no 'data' listener @@ -747,6 +752,7 @@ void scr_net_sock_release(ScrNetSocket *s) { scr_net_ls_drop(&s->conn_ls); scr_net_ls_drop(&s->timeout_ls); scr_net_ls_drop(&s->readable_ls); + scr_net_ls_drop(&s->drain_ls); scr_net_ls_drop(&s->finish_ls); scr_net_ls_drop(&s->wcb_ls); scr_net_ls_drop(&s->ready_ls); @@ -1103,6 +1109,40 @@ static void scr_net_sock_buffer(ScrNetSocket *s, const char *data, size_t len) { s->wlen += len; } +/* Node's net.Socket writableHighWaterMark (the Writable default, + * getDefaultHighWaterMark()). */ +#define SCR_NET_WRITE_HWM 65536 + +/* 'drain' once a false write's bytes are all out: Node's afterWrite emits + * it when needDrain is set and nothing is pending, unless the stream is + * ending or destroyed. */ +static void scr_net_sock_maybe_drain(ScrNetSocket *s) { + if (!s->need_drain || s->wlen > s->whead) return; + if (s->wr_ending || s->close_emitted || s->fd < 0) return; + s->need_drain = false; + scr_net_fire0_this(&s->drain_ls, s, SCR_DYNH_NET_SOCKET); +} + +static void scr_net_sock_write_raw(ScrNetSocket *s, const char *data, size_t len); + +/* A user write: Writable.write's answer, as Node 24's writeOrBuffer + * computes it after the socket writes — the bytes the write path left + * unsent below the high-water mark — with needDrain set when it is false. + * A write the kernel takes at once answers true whatever its size; the + * buffered bytes of a false one drain from the flush. A write to an ended + * or closed socket answers false. */ +static bool scr_net_sock_user_write(ScrNetSocket *s, const char *data, size_t len) { + ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s; + bool pre_dial = w->fd < 0 && (w->dial_deferred || w->lookup_wait) && !w->wr_ending && + !w->close_emitted; + bool live = pre_dial || !(w->fd < 0 || w->wr_ending || w->close_emitted); + scr_net_sock_write_raw(s, data, len); + if (!live) return false; + bool ok = w->wlen - w->whead < SCR_NET_WRITE_HWM; + if (!ok) w->need_drain = true; + return ok; +} + /* The write path shared by write()/end(data)/pipe delivery. Ignored on a * dead or ended write half (Node fires 'error' there; this slice drops — * SEMANTICS.md documents the bound). */ @@ -1252,6 +1292,9 @@ static void scr_net_sock_flush(ScrNetSocket *s) { s->whead = s->wlen = 0; scr_net_sock_update_write(s); scr_net_sock_maybe_finish_write(s); + /* Node's afterWrite for a write the socket completed later: 'drain' + * now, unless the write half is ending or the socket is gone */ + scr_net_sock_maybe_drain(s); } } @@ -3150,16 +3193,40 @@ void scr_net_lookup_answer(ScrClosure *self, bool has_err, ScrStr *msg /*borrowe /* ── the socket surface ──────────────────────────────────────────────── */ -void scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/) { +bool scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/) { char *owned; const char *bytes = scr_str_utf8(data, &owned); - scr_net_sock_write_raw(s, bytes, data->len); + bool ok = scr_net_sock_user_write(s, bytes, data->len); scr_str_utf8_end(owned); + return ok; } -void scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) { +bool scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) { SCR_BYTES_SNAPSHOT(data); - scr_net_sock_write_raw(s, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem))); + return scr_net_sock_user_write(s, (const char *)data->data, + (data->len * scr_bytes_elem_size(data->elem))); +} + +/* socket.writableLength: the bytes written and not yet sent. */ +double scr_net_sock_writable_length(ScrNetSocket *s) { + ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s; + return (double)(w->wlen - w->whead); +} + +/* socket.writableNeedDrain: a write answered false and 'drain' has not + * fired, on a socket whose write half is open. */ +bool scr_net_sock_writable_need_drain(ScrNetSocket *s) { + ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s; + return w->need_drain && !w->wr_ending && !w->close_emitted && w->fd >= 0; +} + +/* on('drain') / once('drain'). */ +void scr_net_sock_on_drain(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) { + if (s->close_emitted) { + scr_closure_release(cb); + return; + } + scr_net_ls_add(&s->drain_ls, cb, NULL, once); } void scr_net_sock_end(ScrNetSocket *s) { @@ -5860,6 +5927,7 @@ static void scr_net_cleanup_atexit(void) { scr_net_ls_drop(&s->conn_ls); scr_net_ls_drop(&s->timeout_ls); scr_net_ls_drop(&s->readable_ls); + scr_net_ls_drop(&s->drain_ls); scr_net_ls_drop(&s->lookup_ls); /* Tear down the protocol layer's parser NOW: an in-flight http * exchange (a handler that threw before res.end()) holds req/res, @@ -5984,8 +6052,7 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho /* fires once when the FIN goes out — once either way */ scr_net_sock_on_finish(s, scr_dyn_listener_closure0(cb)); } else if (scr_net_dynh_name_is(name, "drain")) { - /* This surface never backpressures (write answers true) — an - * accepted, never-fired registration is the consistent answer. */ + scr_net_sock_on_drain(s, scr_dyn_listener_closure0(cb), once); } else if (scr_net_dynh_tls != NULL && name->kind == SCR_DYN_STR && scr_net_dynh_tls->on(s, name->v.str->data, cb, once)) { /* a TLS event name ('secureConnect'), handled by the TLS unit */ @@ -6008,13 +6075,13 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", chunk); return NULL; } - if (chunk->kind == SCR_DYN_STR) scr_net_sock_write_str(s, chunk->v.str); - else scr_net_sock_write_bytes(s, chunk->v.bytes); + bool ok = chunk->kind == SCR_DYN_STR ? scr_net_sock_write_str(s, chunk->v.str) + : scr_net_sock_write_bytes(s, chunk->v.bytes); if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) { /* write(chunk, cb): fires when the buffer drains (sweep-deferred) */ scr_net_sock_on_write_flush(s, scr_dyn_listener_closure0(args[1])); } - return scr_dyn_new_bool(true); /* backpressure is not modeled (SEMANTICS.md) */ + return scr_dyn_new_bool(ok); } if (strcmp(method, "end") == 0) { const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined(); @@ -6500,10 +6567,11 @@ void scr_net_dyn_install(void) { * STR/BYTES dispatch onto the typed entries; undefined/null end()s plain * (Node accepts them); anything else throws Node's ERR_INVALID_ARG_TYPE * chunk TypeError. Both borrowed. */ -void scr_net_sock_write_dynv(ScrNetSocket *s, const ScrDyn *d) { - if (d->kind == SCR_DYN_STR) scr_net_sock_write_str(s, d->v.str); - else if (d->kind == SCR_DYN_BYTES) scr_net_sock_write_bytes(s, d->v.bytes); - else scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", d); +bool scr_net_sock_write_dynv(ScrNetSocket *s, const ScrDyn *d) { + if (d->kind == SCR_DYN_STR) return scr_net_sock_write_str(s, d->v.str); + if (d->kind == SCR_DYN_BYTES) return scr_net_sock_write_bytes(s, d->v.bytes); + scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", d); + return false; } void scr_net_sock_end_dynv(ScrNetSocket *s, const ScrDyn *d) { diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index 95490aaa5..6a41e26d8 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -227,15 +227,21 @@ ScrNetSocket *scr_net_connect_lookup(double port, ScrStr *host /*borrowed*/, ScrClosure *lookup /*moves*/, void *answer_fn); /* +1 */ void scr_net_lookup_answer(ScrClosure *self, bool has_err, ScrStr *msg /*borrowed, nullable*/, ScrArr *ips /*borrowed, nullable*/); -void scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/); -void scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/); +/* write(): Node's Writable answer — false when the bytes the write left + * unsent reach the 65536-byte high-water mark, after which 'drain' fires + * when the write buffer empties; false on an ended or closed socket. */ +bool scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/); +bool scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/); +double scr_net_sock_writable_length(ScrNetSocket *s); +bool scr_net_sock_writable_need_drain(ScrNetSocket *s); +void scr_net_sock_on_drain(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once); void scr_net_sock_end(ScrNetSocket *s); void scr_net_sock_end_str(ScrNetSocket *s, ScrStr *data /*borrowed*/); void scr_net_sock_end_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/); /* Checked-dynamic chunk forms (an untyped JS payload into a typed * socket): STR/BYTES dispatch; end() takes undefined/null as no chunk; * anything else throws Node's ERR_INVALID_ARG_TYPE chunk TypeError. */ -void scr_net_sock_write_dynv(ScrNetSocket *s, const ScrDyn *d /*borrowed*/); +bool scr_net_sock_write_dynv(ScrNetSocket *s, const ScrDyn *d /*borrowed*/); void scr_net_sock_end_dynv(ScrNetSocket *s, const ScrDyn *d /*borrowed*/); void scr_net_sock_destroy(ScrNetSocket *s); /* Flow control (pause/resume — see the struct's flag comments), the diff --git a/tests/corpus/7515-net-socket-backpressure.ts b/tests/corpus/7515-net-socket-backpressure.ts new file mode 100644 index 000000000..a265d3f77 --- /dev/null +++ b/tests/corpus/7515-net-socket-backpressure.ts @@ -0,0 +1,47 @@ +// net.Socket write() backpressure, as in Node 24: write answers false when +// the bytes it leaves unsent reach the 65536-byte high-water mark, which a +// write made before the connection does, writableNeedDrain stays true +// until 'drain', and 'drain' fires once the buffered bytes are sent. An +// echo client that waits for 'drain' when write answers false moves every +// byte. +import { connect, createServer, Socket } from "node:net"; + +const CHUNK = 65536; +const TOTAL = 64 * CHUNK; +const chunk = Buffer.alloc(CHUNK, 120); + +const server = createServer((socket: Socket) => { + socket.pipe(socket); +}); + +server.listen(0, "127.0.0.1", () => { + const addr = server.address(); + const port = typeof addr === "object" && addr !== null ? addr.port : 0; + const client = connect(port, "127.0.0.1"); + console.log("small write before connect", client.write("hi"), client.writableLength); + console.log("large write before connect", client.write(Buffer.alloc(70000, 121)), client.writableLength); + console.log("need drain", client.writableNeedDrain); + let sent = 70002; + let received = 0; + const pump = (): void => { + while (sent < TOTAL) { + sent += CHUNK; + if (!client.write(chunk)) { + client.once("drain", pump); + return; + } + } + client.end(); + }; + client.once("drain", () => { + console.log("drain fired, need drain", client.writableNeedDrain, "pending", client.writableLength); + pump(); + }); + client.on("data", (data: Buffer) => { + received += data.length; + }); + client.on("end", () => { + console.log("received", received, "of", sent); + server.close(); + }); +}); diff --git a/tests/harness/corpus-costs/7515-net-socket-backpressure.ts.json b/tests/harness/corpus-costs/7515-net-socket-backpressure.ts.json new file mode 100644 index 000000000..feabbe802 --- /dev/null +++ b/tests/harness/corpus-costs/7515-net-socket-backpressure.ts.json @@ -0,0 +1 @@ +{ "plain": 2800, "san": 4200 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 8470ce04e..f8d901ee8 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -110,7 +110,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" }, + runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From e38f7c329d4119ef3629e5eed802cbe6f8176d57 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 03:56:48 -0500 Subject: [PATCH 190/273] Speculate floor-mods and products by powers of two as masks and shifts In a speculative integer loop, `v - Math.floor(v / 2^n) * 2^n` computed the floor division as an arithmetic shift, multiplied it back by 2^n with overflow and -0 checks, and subtracted with range checks. For a safe integer v and n in [0, 52] the result is the floor-mod, which is v's low n bits: the fast copy now computes it as `v & (2^n - 1)`, which lies in [0, 2^n) and needs no check. A product with a power of two (a literal or `Math.pow(2, n)`) becomes a shift, with one test that the other factor's magnitude is at most (2^53 - 1) >> n; its zero result is +0. --- .../src/backend/llvm/speculative-integers.ts | 62 +++++++++++++ .../test/speculative-loop-emission.test.ts | 24 +++++ .../8244-speculative-floor-mod-powers.ts.json | 6 ++ .../8244-speculative-floor-mod-powers.ts | 88 +++++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8244-speculative-floor-mod-powers.ts.json create mode 100644 tests/corpus/8244-speculative-floor-mod-powers.ts diff --git a/packages/compiler/src/backend/llvm/speculative-integers.ts b/packages/compiler/src/backend/llvm/speculative-integers.ts index 07e5e3721..971bd00cb 100644 --- a/packages/compiler/src/backend/llvm/speculative-integers.ts +++ b/packages/compiler/src/backend/llvm/speculative-integers.ts @@ -187,6 +187,37 @@ function exactPowerOfTwo(value: number): number | null { return k >= 0 && k <= 52 && 2 ** k === value ? k : null; } +/** The same speculated operand written twice: a local, a literal, or a + * power of two of the same exponent. */ +function sameOperand(a: IrExpr, b: IrExpr): boolean { + if (a.kind === "varRef") return b.kind === "varRef" && a.localId === b.localId; + if (a.kind === "numLit") return b.kind === "numLit" && Object.is(a.value, b.value); + return ( + a.kind === "libCall" && + b.kind === "libCall" && + a.fn === "math.pow" && + b.fn === "math.pow" && + powerOfTwo(a) && + powerOfTwo(b) && + sameOperand(a.args[1]!, b.args[1]!) + ); +} + +/** `a - Math.floor(a / p) * p` (either factor order) with p a power of two + * and `a` a local or literal: the floor-mod of `a` by p. */ +function floorModPowerOfTwo(e: IrExpr): { value: IrExpr; power: IrExpr } | null { + if (e.kind !== "bin" || e.op !== "-" || e.right.kind !== "bin" || e.right.op !== "*") return null; + const { left, right } = e.right; + const [floor, power] = + left.kind === "libCall" && left.fn === "math.floor" ? [left, right] : [right, left]; + if (floor.kind !== "libCall" || floor.fn !== "math.floor" || floor.args.length !== 1) return null; + const quotient = floor.args[0]!; + if (quotient.kind !== "bin" || quotient.op !== "/") return null; + if (!powerOfTwo(power) || !sameOperand(quotient.right, power)) return null; + if (e.left.kind !== "varRef" && e.left.kind !== "numLit") return null; + return sameOperand(quotient.left, e.left) ? { value: e.left, power } : null; +} + /** Whether `e` is a power of two speculation can shift by: * `Math.pow(2, n)` or a literal 2^k with k in [0, 52]. */ function powerOfTwo(e: IrExpr): boolean { @@ -950,6 +981,37 @@ export function emitSpeculativeInteger( } case "bin": { if (e.op !== "+" && e.op !== "-" && e.op !== "*" && e.op !== "%") break; + // The floor-mod of a safe integer by 2^n (n in [0, 52]) is its low n + // bits, also for a negative value: in [0, 2^n), and never -0. + const mod = floorModPowerOfTwo(e); + if (mod) { + const value = emitSpeculativeInteger(host, mod.value, bail, reads, integral); + const n = exponentOf(host, mod.power, bail, reads, integral); + const mask = B.tmp(), + low = B.tmp(); + B.line(`${mask} = sub i64 ${shiftOne(host, n)}, 1`); + B.line(`${low} = and i64 ${value}, ${mask}`); + return low; + } + // A product with 2^n shifts: it is safe iff the other factor's + // magnitude is at most (2^53 - 1) >> n, and a zero product of a safe + // integer and a positive power is +0. + if (e.op === "*" && (powerOfTwo(e.right) || powerOfTwo(e.left))) { + const [factor, power] = powerOfTwo(e.right) ? [e.left, e.right] : [e.right, e.left]; + const value = emitSpeculativeInteger(host, factor, bail, reads, integral); + const n = exponentOf(host, power, bail, reads, integral); + const limit = B.tmp(), + magnitude = B.tmp(), + outside = B.tmp(), + product = B.tmp(); + B.line(`${limit} = ashr i64 ${SAFE}, ${n}`); + host.declare("declare i64 @llvm.abs.i64(i64, i1)"); + B.line(`${magnitude} = call i64 @llvm.abs.i64(i64 ${value}, i1 true)`); + B.line(`${outside} = icmp ugt i64 ${magnitude}, ${limit}`); + bail(outside); + B.line(`${product} = shl nsw i64 ${value}, ${n}`); + return product; + } const a = emitSpeculativeInteger(host, e.left, bail, reads, integral); const b = emitSpeculativeInteger(host, e.right, bail, reads, integral); const result = B.tmp(); diff --git a/packages/compiler/test/speculative-loop-emission.test.ts b/packages/compiler/test/speculative-loop-emission.test.ts index 748fd50c5..d26a0c96c 100644 --- a/packages/compiler/test/speculative-loop-emission.test.ts +++ b/packages/compiler/test/speculative-loop-emission.test.ts @@ -159,3 +159,27 @@ if (isMainThread) { expect(order.slice(slow, fast).some((l) => l.startsWith("spec.fast"))).toBe(true); expect(order.slice(fast).some((l) => l.startsWith("spec.fast"))).toBe(true); }); + +test("a floor-mod by a power of two speculates as a mask and a product by one as a shift", async () => { + const llvm = await emitLlvm(` +function trim(value: number, bits: number): number { + return value - Math.floor(value / Math.pow(2, bits)) * Math.pow(2, bits); +} +function key(bytes: Uint8Array, bits: number): number { + let k = 0; + for (let i = 0; i < bytes.length; i++) k = trim(k, bits) * 4 + bytes[i]!; + return k; +} +console.log(key(new Uint8Array([1, 2, 3, 0]), 6)); +`); + const fn = /^define internal double @sc_bf_\w*key[\w.]*\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + const fast = fn.slice(fn.search(/^spec\.fast\d+:/m)); + expect(fast.length).toBeLessThan(fn.length); + // trim(k, bits): the low `bits` bits; * 4: a shift by two. + expect(fast).toMatch(/= sub i64 %\S+, 1\n\s+%\S+ = and i64 %\S+, %\S+\n/); + expect(fast).toMatch(/= shl nsw i64 %\S+, 2\n/); + // No floor division of k (an arithmetic shift of it) and no checked + // multiply remain. + expect(fast).not.toMatch(/ashr i64 %/); + expect(fast).not.toContain("@llvm.smul.with.overflow.i64"); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8244-speculative-floor-mod-powers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8244-speculative-floor-mod-powers.ts.json new file mode 100644 index 000000000..65e7ccb38 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8244-speculative-floor-mod-powers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8244-speculative-floor-mod-powers.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8244-speculative-floor-mod-powers.ts b/tests/corpus/8244-speculative-floor-mod-powers.ts new file mode 100644 index 000000000..db28dcde0 --- /dev/null +++ b/tests/corpus/8244-speculative-floor-mod-powers.ts @@ -0,0 +1,88 @@ +// Integer loops whose writes take a floor-mod by a power of two +// (`v - Math.floor(v / 2^n) * 2^n`) or multiply by one: negative and zero +// values, exponents 0, 52 and above, literal powers, both factor orders, +// fractional values and results past 2^53, which leave the integer copy. + +function trim(value: number, bits: number): number { + return value - Math.floor(value / Math.pow(2, bits)) * Math.pow(2, bits); +} + +function low3(value: number): number { + return value - Math.floor(value / 8) * 8; +} + +function rolling(bytes: Uint8Array, bits: number): number { + let key = 0; + let sum = 0; + for (let i = 0; i < bytes.length; i++) { + key = trim(key, bits) * 4 + bytes[i]!; + sum = (sum + key) % 1000003; + } + return key * 7 + sum; +} + +function stepped(start: number, step: number, bits: number, count: number): number { + let v = start; + let h = 0; + for (let i = 0; i < count; i++) { + v = trim(v, bits) * 3 - step; + h = (h * 31 + v) % 999983; + } + return h * 1000 + v; +} + +function literal(start: number, count: number): number { + let v = start; + let h = 0; + for (let i = 0; i < count; i++) { + v = low3(v) * 16 - i; + h = (h * 17 + v) % 65521; + } + return h * 100 + v; +} + +function scaled(start: number, bits: number, count: number): number { + let v = start; + let h = 0; + for (let i = 0; i < count; i++) { + v = Math.pow(2, bits) * v + 1; + h = (h * 7 + v) % 10007; + } + return h + v; +} + +function shifted(start: number, count: number): number { + let v = start; + let h = 0; + for (let i = 0; i < count; i++) { + v = v * 1024 - i; + h = (h * 3 + v) % 101; + } + return h + v; +} + +const bytes = new Uint8Array(4000); +for (let i = 0; i < bytes.length; i++) bytes[i] = (i * 7 + (i >> 3)) & 3; + +for (const bits of [0, 1, 2, 10, 22, 34, 50, 52, 53, 60]) console.log("rolling", bits, rolling(bytes, bits)); +for (const [start, step, bits] of [ + [0, 1, 3], + [-5, 3, 2], + [-1, -1, 0], + [123456789, 7, 20], + [-9007199254740991, 1, 52], + [2.5, 1, 4], + [7, 0.5, 3], +] as const) + console.log("stepped", start, step, bits, stepped(start, step, bits, 50)); +for (const start of [0, 13, -13, 4096, -7.25]) console.log("literal", start, literal(start, 40)); +for (const [start, bits] of [ + [1, 3], + [-2, 5], + [0, 52], + [3, 20], + [-1, 0], +] as const) + console.log("scaled", start, bits, scaled(start, bits, 6)); +for (const start of [0, 1, -1, 3, 1e6]) console.log("shifted", start, shifted(start, 7)); +console.log(Object.is(stepped(0, 0, 3, 5), 0), Object.is(scaled(0, 2, 3), 21 + 1)); From 45171ea02cb01c5d8b2683a7195623464d8d8bcc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 03:59:10 -0500 Subject: [PATCH 191/273] Use a built-in typed-array constructor as its own species before the species symbol exists A typed array whose constructor property is set to another built-in typed-array constructor now creates that kind from map, filter, slice and subarray when the program has not evaluated Symbol.species. --- packages/runtime/src/scr_json.c | 7 ++++-- .../8321-typed-array-constructor-kind.js | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/corpus/8321-typed-array-constructor-kind.js diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index f06080ef6..6e0ca43c0 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -5345,9 +5345,12 @@ static ScrDyn *scr_bytes_species(ScrDyn *recv, bool *failed) { *failed = true; return NULL; } - ScrDyn *species = scr_species_symbol ? scr_dyn_symbol_key_get(constructor, scr_species_symbol, true) : NULL; + /* Before the program creates the species symbol, no constructor can have + * a Symbol.species property of its own. */ + ScrDyn *species = scr_species_symbol ? scr_dyn_symbol_key_get(constructor, scr_species_symbol, true) + : scr_dyn_retain(scr_dyn_undefined()); if (species && species->kind == SCR_DYN_UNDEF && scr_bytes_ctor_elem(constructor) >= 0 && - !scr_bytes_ctor_own_species(constructor)) { + (!scr_species_symbol || !scr_bytes_ctor_own_species(constructor))) { /* A built-in typed-array constructor inherits %TypedArray%'s species * getter, which returns the constructor itself. */ scr_dyn_release(species); diff --git a/tests/corpus/8321-typed-array-constructor-kind.js b/tests/corpus/8321-typed-array-constructor-kind.js new file mode 100644 index 000000000..277128a93 --- /dev/null +++ b/tests/corpus/8321-typed-array-constructor-kind.js @@ -0,0 +1,23 @@ +// A built-in typed-array constructor set as a view's `constructor` is the +// species of map, filter, slice and subarray when the program never names +// the species symbol. +function kinds(TA, Other) { + const a = new TA([1, 2, 3, 4]); + a.constructor = Other; + const m = a.map((v) => v * 1.5), f = a.filter((v) => v > 1), s = a.slice(1, 3); + console.log(m, f, s, m.constructor === Other, f.constructor === Other, s.constructor === Other); + try { + const sub = a.subarray(0, 2); + console.log(sub, sub.constructor === Other); + } catch (err) { + console.log(err.constructor.name); + } + a.constructor = TA; + console.log(a.map((v) => v / 2).constructor === TA); + a.constructor = {}; + console.log(a.slice().constructor === TA); +} +kinds(Uint8Array, Float64Array); +kinds(Int16Array, Uint8Array); +kinds(Float32Array, Int32Array); +kinds(Float64Array, Uint8ClampedArray); From 0d4df6ff6202ae01094be725890c38c3493ee8d6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 04:14:58 -0500 Subject: [PATCH 192/273] Skip fork discovery in programs that never name node:child_process forkTargetPaths asked the checker for the symbol of every identifier callee in every implementation file, on each root-discovery pass and twice more in preflight, to find calls of child_process.fork. A call is a fork only through a binding whose import or alias chain names the module in a module specifier, or through a '#' specifier a package.json "imports" field maps. namesChildProcess reads every program file once per program, outside Node's type declarations and scriptc's own declaration files, which declare the module and alias it only as node:child_process. Implementation files are read from their syntax trees, which discovery fetches anyway; other files as the host serves them to the server (the new Ts7Program.getSourceText), without fetching their syntax trees. When no file holds "child_process" in its text, an escape or line continuation that spells it, or an "imports" entry that names it, forkTargetPaths answers no targets without querying the checker. The discovered fork targets are unchanged. --- .../compiler/src/frontend/fork-target.test.ts | 53 +++++++++++++ packages/compiler/src/frontend/fork-target.ts | 77 ++++++++++++++++++- .../compiler/src/frontend/program-analysis.ts | 3 + packages/compiler/src/frontend/resolve.ts | 6 ++ .../compiler/src/frontend/ts7/program-host.ts | 52 ++++++++----- 5 files changed, 172 insertions(+), 19 deletions(-) diff --git a/packages/compiler/src/frontend/fork-target.test.ts b/packages/compiler/src/frontend/fork-target.test.ts index 5e56cea5d..ffe01ee47 100644 --- a/packages/compiler/src/frontend/fork-target.test.ts +++ b/packages/compiler/src/frontend/fork-target.test.ts @@ -65,3 +65,56 @@ test("fork discovery batches only candidate bindings and preserves aliases and s rmSync(directory, { recursive: true, force: true }); } }); + +/** forkTargetPaths over the entry of a program made of `files`, and + * whether it asked the checker for any binding. */ +function discover(files: Record): { targets: string[]; queried: boolean } { + const directory = mkdtempSync(join(tmpdir(), "scriptc-fork-gate-")); + for (const [name, text] of Object.entries(files)) writeFileSync(join(directory, name), text); + const entry = join(directory, "main.ts"); + const host = new Ts7Host((options) => new Ts7Api(options), { cwd: directory }); + const program = host.createProgram([entry, fallbackDtsPath()], { + noLib: true, + types: [], + module: ModuleKind.ESNext, + moduleResolution: ModuleResolutionKind.Bundler, + }); + const batch = vi.spyOn(program.getTypeChecker(), "prefetchSymbolNodesExact"); + try { + const targets = forkTargetPaths(program, [program.getSourceFile(entry)!]).map((path) => + path.slice(directory.length + 1), + ); + return { targets, queried: batch.mock.calls.length > 0 }; + } finally { + batch.mockRestore(); + program.dispose(); + host.close(); + rmSync(directory, { recursive: true, force: true }); + } +} + +test("fork discovery asks the checker nothing when no file names node:child_process", () => { + expect( + discover({ + "main.ts": + 'function fork(url: URL) { return url; }\nfork(new URL("./child.ts", import.meta.url));\n', + }), + ).toEqual({ targets: [], queried: false }); +}); + +test("fork discovery reads a module specifier spelled with escapes", () => { + expect( + discover({ + "main.ts": + 'import { fork } from "node:child\\u005fprocess";\nfork(new URL("./child.ts", import.meta.url));\n', + }), + ).toEqual({ targets: ["child.ts"], queried: true }); +}); + +test("fork discovery reads a package imports entry that maps node:child_process", () => { + const result = discover({ + "package.json": JSON.stringify({ type: "module", imports: { "#cp": "node:child_process" } }), + "main.ts": 'import { fork } from "#cp";\nfork(new URL("./child.ts", import.meta.url));\n', + }); + expect(result.queried).toBe(true); +}); diff --git a/packages/compiler/src/frontend/fork-target.ts b/packages/compiler/src/frontend/fork-target.ts index 38c8f5f07..93fc7c244 100644 --- a/packages/compiler/src/frontend/fork-target.ts +++ b/packages/compiler/src/frontend/fork-target.ts @@ -1,7 +1,14 @@ import { resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import * as ts from "./ts7/adapter.js"; -import { fallbackDtsPath, isNodeTypesPath, tsgoPath } from "./dts-paths.js"; +import { + ambientDtsPath, + fallbackDtsPath, + isNodeTypesPath, + overridesDtsPath, + tsgoPath, +} from "./dts-paths.js"; +import { nearestPackageImports } from "./resolve.js"; import { canonicalBuiltinModule } from "./builtin-modules.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; @@ -292,9 +299,77 @@ export function forkCallModulePath(program: ts.Program, call: ts.CallExpression) return staticForkModulePath(program, call.arguments.nodes[0]); } +/** The ways a string or template literal can spell one character of a + * module name: the character, \xHH, \uHHHH, \u{H…}, a legacy octal escape, + * or the character after a backslash where that is not another escape. */ +function spelledCharacter(ch: string): string { + const code = ch.charCodeAt(0); + const hex = code.toString(16); + const forms = [ + ch, + "\\\\x" + hex.padStart(2, "0"), + "\\\\u" + hex.padStart(4, "0"), + "\\\\u\\{0*" + hex + "\\}", + "\\\\" + code.toString(8), + ]; + if (!"bfnrtvux0123456789".includes(ch)) forms.push("\\\\" + ch); + return "(?:" + forms.join("|") + ")"; +} + +/** "child_process" with escapes and line continuations in any of its + * characters (case-insensitive, a superset). */ +const SPELLED_CHILD_PROCESS = new RegExp( + [..."child_process"].map(spelledCharacter).join("(?:\\\\(?:\\r\\n|[\\n\\r\\u2028\\u2029]))*"), + "i", +); + +/** Whether a call in the program can reach node:child_process's fork. A + * fork binding is an import or alias whose chain names the module in a + * module specifier (a string), or a '#' specifier the nearest package.json + * "imports" field maps. Node's type declarations and scriptc's own declare + * the module, and alias it only as node:child_process, so they are not + * read; any other file that names the module in a string holds + * "child_process" in its text, or an escape that spells it. */ +function namesChildProcess(program: ts.Program): boolean { + const known = program.analysis.namesChildProcess; + if (known !== null) return known; + const own = new Set([ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]); + // Implementation files are already fetched; any other file is read as + // the host serves it, without fetching its syntax tree. + const texts = new Map(); + for (const sourceFile of program.getImplementationSourceFiles()) + texts.set(tsgoPath(sourceFile.fileName), sourceFile.text); + const scopes = new Set(); + let names = false; + for (const fileName of program.getSourceFileNames()) { + const path = tsgoPath(fileName); + if (isNodeTypesPath(fileName) || own.has(path)) continue; + const text = texts.get(path) ?? program.getSourceText(fileName) ?? ""; + if ( + text.includes("child_process") || + (text.includes("\\") && SPELLED_CHILD_PROCESS.test(text)) + ) { + names = true; + break; + } + const imports = nearestPackageImports(fileName); + if (imports === undefined || scopes.has(imports)) continue; + scopes.add(imports); + if (JSON.stringify(imports).includes("child_process")) { + names = true; + break; + } + } + program.analysis.namesChildProcess = names; + return names; +} + /** Source-order static fork targets in the supplied files, deduplicated by * resolved path. */ export function forkTargetPaths(program: ts.Program, files: readonly ts.SourceFile[]): string[] { + // Without a binding of node:child_process no call is a fork, and the + // checker queries below are skipped. + if (!namesChildProcess(program)) return []; const targets: string[] = []; const seen = new Set(); for (const sourceFile of files) { diff --git a/packages/compiler/src/frontend/program-analysis.ts b/packages/compiler/src/frontend/program-analysis.ts index d4ec9fa98..475942908 100644 --- a/packages/compiler/src/frontend/program-analysis.ts +++ b/packages/compiler/src/frontend/program-analysis.ts @@ -25,6 +25,8 @@ export class ProgramAnalysis { /** The modules of every import cycle the exact initialization rule admitted. */ readonly exactCycleMembers = new Set(); cycleEarlyBindings: EarlyBindingsMemo | null = null; + /** Whether some program file can name node:child_process (namesChildProcess). */ + namesChildProcess: boolean | null = null; moduleEarlyBindings: EarlyBindingsMemo | null = null; /** Top-level require declarations: the first reference to their bindings * that code running before the require can reach (requireEarlyReference). */ @@ -46,6 +48,7 @@ export class ProgramAnalysis { this.moduleSourceCandidates.clear(); this.exactCycleMembers.clear(); this.cycleEarlyBindings = null; + this.namesChildProcess = null; this.moduleEarlyBindings = null; this.requireEarlyReferences.clear(); this.requireFirstRunnable.clear(); diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts index 3b32dad3a..5b035b6cb 100644 --- a/packages/compiler/src/frontend/resolve.ts +++ b/packages/compiler/src/frontend/resolve.ts @@ -562,6 +562,12 @@ export function nearestPkgJsonPath(fromFile: string): string | null { return dir === null ? null : join(dir, "package.json"); } +/** The nearest package scope's "imports" field (undefined without one). */ +export function nearestPackageImports(fromFile: string): unknown { + const dir = nearestPkgDir(dirname(resolve(fromFile))); + return dir === null ? undefined : pkgJsonOf(dir)?.imports; +} + /** The target the nearest package scope's "imports" field maps a '#' * specifier to under Node's ESM runtime conditions: a './' path inside the * package or another package's specifier. Null when no entry maps it. */ diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index e66d598d9..84cbe2474 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -152,33 +152,37 @@ export class Ts7Host { private readonly virtualChanges = new Map(); private readonly closingProjects = new Set(); private readonly api: Ts7Api; + /** The text the server reads for a file (its readFile hook). */ + private readonly readSource: (fileName: string) => string | null; private closed = false; constructor(createApi: Ts7ApiFactory, options?: Ts7HostOptions) { const virtualFiles = this.virtualFiles; const shadow = options?.fsShadow ?? null; + // A virtual file, a shadow's replacement, a package shim's declaration + // entry or the file on disk; null when shadowed out of existence or + // missing. + this.readSource = (fileName) => { + const virtual = virtualFiles.get(tsgoPath(fileName)); + if (virtual !== undefined) return stripSourceBom(virtual); + if (shadow !== null) { + if (shadow.hideFile(fileName)) return null; + const replacement = shadow.readFile(fileName); + if (replacement !== undefined) return stripSourceBom(replacement); + } + // A package scriptc implements itself (package-shims.ts): its + // installed declaration entry re-exports the shim module, in every + // program the host creates. + const shim = packageShimDeclarationText(fileName); + if (shim !== undefined) return shim; + const source = trackedReadFile(fileName); + return source === null ? null : stripSourceBom(source); + }; this.api = createApi({ cwd: options?.cwd ?? process.cwd(), collectTiming: options?.collectTiming ?? false, fs: { - // string => virtual (or shadowed) hit; null => shadowed out of - // existence; undefined => real-FS fallthrough. - readFile: (fileName) => { - const virtual = virtualFiles.get(tsgoPath(fileName)); - if (virtual !== undefined) return stripSourceBom(virtual); - if (shadow !== null) { - if (shadow.hideFile(fileName)) return null; - const replacement = shadow.readFile(fileName); - if (replacement !== undefined) return stripSourceBom(replacement); - } - // A package scriptc implements itself (package-shims.ts): its - // installed declaration entry re-exports the shim module, in every - // program the host creates. - const shim = packageShimDeclarationText(fileName); - if (shim !== undefined) return shim; - const source = trackedReadFile(fileName); - return source === null ? null : stripSourceBom(source); - }, + readFile: this.readSource, fileExists: (fileName) => { if (virtualFiles.has(tsgoPath(fileName))) return true; if (shadow !== null && shadow.hideFile(fileName)) return false; @@ -193,6 +197,12 @@ export class Ts7Host { }); } + /** The text this host serves for a file (null when it serves none), + * without asking the server for its syntax tree. */ + sourceText(fileName: string): string | null { + return this.readSource(fileName); + } + /** Registers an in-memory file served to tsgo by the virtual-FS hooks. */ addVirtualFile(path: string, content: string): void { this.ensureOpen(); @@ -315,6 +325,12 @@ export class Ts7Program { return this.project.program.getSourceFile(resolve(fileName)); } + /** A file's text as the host serves it, without fetching its syntax + * tree (Ts7Host.sourceText). */ + getSourceText(fileName: string): string | null { + return this.host.sourceText(fileName); + } + getSourceFileNames(): readonly string[] { this.snapshot.ensureActive(); return (this.sourceFileNamesCache ??= this.project.program.getSourceFileNames()); From 080fe5e5e0f3e2660c58a8dd6b64a3ede63827d6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 04:16:32 -0500 Subject: [PATCH 193/273] Record the TypeScript order baseline for the typed-array constructor kind fixture --- .../tests/corpus/8321-typed-array-constructor-kind.js.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8321-typed-array-constructor-kind.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8321-typed-array-constructor-kind.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8321-typed-array-constructor-kind.js.json new file mode 100644 index 000000000..6da151987 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8321-typed-array-constructor-kind.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8321-typed-array-constructor-kind.js" + ], + "diags": [] +} From 534c08dd2f30fa2ce5611fdbfcce20598a8f8f77 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 04:34:24 -0500 Subject: [PATCH 194/273] Pack spread tuple fields together with other spreads into optional-element rests A call that spread a fixed tuple and also an array, a Set or another iterable into a `(T | undefined)[]` rest parameter took the general rest path, which converts every value to T, so an undefined tuple field threw ("undefined is not representable in the target union"). Such calls now pack every item in argument order through lowerArraySpreadItems, which takes values already lowered (the tuple's fields) beside argument nodes and keeps undefined values as present undefined elements. A tuple element typed only `undefined`, as in `...["b", undefined]`, has a `null | undefined` field. Stored into an array that keeps undefined elements it is now evaluated and stored as undefined, where it was an internal compiler error; the checker admits it there only as undefined. --- .../src/frontend/lowering/call-arguments.ts | 16 ++++-- .../lowering/containers/array-construction.ts | 53 +++++++++++++++++-- .../8004-optional-element-conversions.ts | 9 ++++ 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index a139bfc64..79e891d4f 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -407,10 +407,18 @@ export function completeArgs( // A `(T | undefined)[]` rest (stored as T elements) packs undefined // arguments as present undefined elements. const restNodes = sources.slice(restAt); - if (restType.optionalElements === true && !restNodes.some(isIr)) { - out.push( - lowerArraySpreadItems(lowerer, restNodes as ts.Expression[], restType.elem, restType, loc), - ); + // Plain arguments and spreads, also together with spread tuple fields + // (pre-lowered): each item, in argument order. + if ( + restType.optionalElements === true && + (!restNodes.some(isIr) || restNodes.some((a) => !isIr(a) && ts.isSpreadElement(a))) + ) { + const items: (ts.Expression | IrExpr)[] = []; + for (const a of restNodes) { + if (isIr(a)) items.push(a.ir); + else items.push(a); + } + out.push(lowerArraySpreadItems(lowerer, items, restType.elem, restType, loc)); return out; } // Tuple-spread values (pre-lowered) and plain arguments pack the same diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 25a98c8a6..63e2e6e07 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -4,6 +4,7 @@ import { generatorDrain } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { liftedFreshSpread } from "../coercions/fresh-spread.js"; import { checkedIterableSpread } from "../checked-iterable-spread.js"; +import { AstNode } from "../../ts7/ast-node.js"; import { InternalCompilerError } from "../../../errors.js"; import { dynUndefinedExpr, @@ -27,7 +28,9 @@ import { type IrType, STRING, type SrcLoc, + UNDEFINED_T, arrayOf, + isUnitType, canConvertToDyn, funcOf, isRefCounted, @@ -57,6 +60,34 @@ import { } from "./callback-arguments.js"; import { classGeneratorIterable } from "../class-iterables.js"; +/** A value whose type admits only null and undefined, stored into an array + * that keeps undefined elements: the checker admits it there only when its + * type is `undefined` (a tuple element typed `undefined` maps to such a + * union), so it is evaluated and stored as undefined. */ +function undefinedForNullish( + lowerer: Lowerer, + value: IrExpr, + arrType: IrType & { kind: "array" }, +): IrExpr { + if (arrType.optionalElements !== true || value.type.kind !== "union") return value; + const arms = lowerer.unions.get(value.type.unionId)?.arms ?? []; + if (arms.length === 0 || !arms.every(isUnitType)) return value; + const loc = value.loc; + const optional: IrType = { + kind: "union", + unionId: lowerer.unions.intern([arrType.elem, UNDEFINED_T]), + }; + const missing = lowerer.wrappedUndefined(optional, loc); + if (!missing) return value; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: value, loc }], + result: missing, + type: optional, + loc, + }; +} + /** Build the argument array for a mutating/copying operation while keeping * the receiver's scalar payload ABI. A missing read is represented as the * present UNDEFINED state in this temporary, so push/unshift and toSpliced @@ -92,7 +123,8 @@ export function lowerArrayValueItems( loc, }, ]; - for (const value of values) { + for (const item of values) { + const value = undefinedForNullish(lowerer, item, arrType); const stored = lowerer.runtimeOptionalWidening(value.type, elem) !== null || optionalElementValue(lowerer, value, elem) @@ -103,16 +135,21 @@ export function lowerArrayValueItems( return { kind: "seqExpr", stmts: body, result: outRef, type: arrType, loc }; } +/** The items of an array built from argument nodes (a spread copies its + * elements in) and values already lowered (a spread tuple's fields), in + * order. Each value is stored when its turn comes. */ export function lowerArraySpreadItems( lowerer: Lowerer, - nodes: readonly ts.Expression[], + nodes: readonly (ts.Expression | IrExpr)[], elem: IrType, arrType: IrType & { kind: "array" }, loc: SrcLoc, ): IrExpr { const out = lowerer.declareHiddenLocal("%arrayItems", arrType); const outRef = varRef(out.id, arrType, loc); - const plainItems = nodes.filter((node) => !ts.isSpreadElement(node)).length; + const plainItems = nodes.filter( + (node) => !(node instanceof AstNode) || !ts.isSpreadElement(node), + ).length; const length = (): IrExpr => ({ kind: "arrIntrinsic", method: "length", @@ -136,7 +173,15 @@ export function lowerArraySpreadItems( }, ]; for (const node of nodes) { - if (ts.isSpreadElement(node)) { + if (!(node instanceof AstNode)) { + const value = undefinedForNullish(lowerer, node, arrType); + const stored = + lowerer.runtimeOptionalWidening(value.type, elem) !== null || + optionalElementValue(lowerer, value, elem) + ? value + : lowerer.coerceToExpected(value, elem); + body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc)); + } else if (ts.isSpreadElement(node)) { let source = lowerer.lowerExpr(node.expression); if ( source.type.kind === "set" && diff --git a/tests/corpus/8004-optional-element-conversions.ts b/tests/corpus/8004-optional-element-conversions.ts index 07178e9b0..9a4ac55ac 100644 --- a/tests/corpus/8004-optional-element-conversions.ts +++ b/tests/corpus/8004-optional-element-conversions.ts @@ -49,6 +49,15 @@ function swap(pair: [string, string | undefined]): string { return "v"; } console.log(label("z", ...swapped, swap(swapped), ...swapped)); +// Tuple spreads together with array and Set spreads keep undefined +// elements, and every item is read in argument order. +const mixedPair: [boolean, boolean | undefined] = [true, undefined]; +const mixedMore: (boolean | undefined)[] = [false, undefined]; +console.log(flags(...mixedPair, ...mixedMore), flags(...mixedMore, ...mixedPair), flags(true, ...mixedPair, ...mixedMore, undefined)); +const mixedChanging: [boolean, boolean | undefined] = [true, undefined]; +console.log(flags(...mixedChanging, ...mixedMore, flip(mixedChanging, true), ...mixedChanging)); +const mixedWords: [string, string | undefined] = ["a", undefined]; +console.log(label("z", ...["b", undefined, "c"], ...mixedWords), label("y", ...mixedWords, ...new Set(["s", undefined]))); function pick(start: (number | undefined)[] | string[], other: (number | undefined)[] | string[]): string { let a = start; From 50c46bb9de2dfe5ee43604ba5bf0ced4f7bdb7b0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 04:50:20 -0500 Subject: [PATCH 195/273] Count socket writes as Node's state.length, and read write state from untyped code A socket's write() answer and writableLength counted the bytes left unsent. Node counts each write that is not completely sent in full, so a large write the kernel takes in part still counts its whole length. The socket now keeps its buffered writes in order with their lengths and completes each one when its last byte is sent; write() answers false when those reach the 65536-byte high-water mark. write() now checks whether the write half is open after the send, so a send that fails with EPIPE or ECONNRESET and closes the socket answers false. writableNeedDrain also reads true while a custom lookup or a deferred dial is pending, since writes then buffer until the socket connects. Untyped code reads writableLength and writableNeedDrain, and writableHighWaterMark and readableHighWaterMark read 65536, Node's default, instead of 16384. --- packages/runtime/src/scr_net.c | 103 +++++++++--- tests/corpus/7515-net-socket-backpressure.ts | 157 +++++++++++++++---- 2 files changed, 201 insertions(+), 59 deletions(-) diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 91e8461b7..0fe06cedc 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -516,6 +516,16 @@ struct ScrNetSocket { /* write buffer: [whead, wlen) of wbuf is unsent */ char *wbuf; size_t whead, wlen, wcap; + /* Node's writable state.length: each write with bytes in the buffer + * counts in full until its last byte is sent (a partly sent write + * included). wq[wq_head, wq_n) holds those writes in order, each with + * the offset where its bytes end, counted from wbase, the offset of + * wbuf[0] in everything buffered since the buffer last emptied. */ + struct ScrNetPendingWrite { + size_t end, len; + } *wq; + size_t wq_head, wq_n, wq_cap; + size_t wbase, wpending; /* receive buffer: [rhead, rlen) of rbuf is arrived-but-unconsumed — * fed by paused-mode reads ('readable' consumers) and unshift; drained * FIRST by every consumer (data listeners, the parser attach, the TLS @@ -770,6 +780,7 @@ void scr_net_sock_release(ScrNetSocket *s) { if (s->lookup_evs != NULL) scr_net_lookup_ops->free(s); scr_net_tunnel_free(s->tunnel); free(s->wbuf); + free(s->wq); free(s->rbuf); if (s->pipe_dst) scr_net_sock_release(s->pipe_dst); if (s->conn_pending) scr_net_server_release(s->conn_pending); @@ -1088,13 +1099,34 @@ static void scr_net_sock_maybe_finish_write(ScrNetSocket *s) { else scr_net_sock_update_read(s); /* the drain-to-EOF mode arms here */ } +/* The write buffer emptied (or its bytes moved elsewhere): every pending + * write is complete. */ +static void scr_net_sock_wq_clear(ScrNetSocket *s) { + s->whead = s->wlen = 0; + s->wq_head = s->wq_n = 0; + s->wbase = 0; + s->wpending = 0; +} + +/* Completes the pending writes whose last byte the flush has sent. */ +static void scr_net_sock_wq_settle(ScrNetSocket *s) { + size_t sent_to = s->wbase + s->whead; + while (s->wq_head < s->wq_n && s->wq[s->wq_head].end <= sent_to) { + s->wpending -= s->wq[s->wq_head].len; + s->wq_head++; + } + if (s->wq_head == s->wq_n) s->wq_head = s->wq_n = 0; +} + /* Append bytes to the write buffer (post-immediate-write remainder, or - * everything while connecting/buffered). */ -static void scr_net_sock_buffer(ScrNetSocket *s, const char *data, size_t len) { + * everything while connecting/buffered). `counted` is the length of the + * write they belong to, which counts in full until they are sent. */ +static void scr_net_sock_buffer(ScrNetSocket *s, const char *data, size_t len, size_t counted) { if (len == 0) return; /* compact the consumed head first */ if (s->whead > 0) { memmove(s->wbuf, s->wbuf + s->whead, s->wlen - s->whead); + s->wbase += s->whead; s->wlen -= s->whead; s->whead = 0; } @@ -1107,6 +1139,15 @@ static void scr_net_sock_buffer(ScrNetSocket *s, const char *data, size_t len) { } memcpy(s->wbuf + s->wlen, data, len); s->wlen += len; + if (s->wq_n == s->wq_cap) { + s->wq_cap = s->wq_cap ? s->wq_cap * 2 : 8; + s->wq = realloc(s->wq, s->wq_cap * sizeof *s->wq); + if (!s->wq) scr_net_oom(); + } + s->wq[s->wq_n].end = s->wbase + s->wlen; + s->wq[s->wq_n].len = counted; + s->wq_n++; + s->wpending += counted; } /* Node's net.Socket writableHighWaterMark (the Writable default, @@ -1125,20 +1166,27 @@ static void scr_net_sock_maybe_drain(ScrNetSocket *s) { static void scr_net_sock_write_raw(ScrNetSocket *s, const char *data, size_t len); +/* The write half takes writes: not ending or closed, and connected, + * connecting, or waiting to dial (the agent's deferred dial, the caller + * lookup), which buffers them until the connection is established. */ +static bool scr_net_sock_write_open(ScrNetSocket *w) { + if (w->wr_ending || w->close_emitted) return false; + return w->fd >= 0 || w->dial_deferred || w->lookup_wait; +} + /* A user write: Writable.write's answer, as Node 24's writeOrBuffer - * computes it after the socket writes — the bytes the write path left - * unsent below the high-water mark — with needDrain set when it is false. - * A write the kernel takes at once answers true whatever its size; the - * buffered bytes of a false one drain from the flush. A write to an ended - * or closed socket answers false. */ + * computes it after the socket writes. Each write with bytes still + * unsent counts in full (wpending, Node's state.length), so the answer is + * false when the pending writes reach the high-water mark, and needDrain + * is set. A write the kernel takes at once answers true whatever its + * size. A write to an ended or closed socket answers false, and so does + * one whose send fails and closes the socket. */ static bool scr_net_sock_user_write(ScrNetSocket *s, const char *data, size_t len) { ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s; - bool pre_dial = w->fd < 0 && (w->dial_deferred || w->lookup_wait) && !w->wr_ending && - !w->close_emitted; - bool live = pre_dial || !(w->fd < 0 || w->wr_ending || w->close_emitted); + bool open = scr_net_sock_write_open(w); scr_net_sock_write_raw(s, data, len); - if (!live) return false; - bool ok = w->wlen - w->whead < SCR_NET_WRITE_HWM; + if (!open || !scr_net_sock_write_open(w)) return false; + bool ok = w->wpending < SCR_NET_WRITE_HWM; if (!ok) w->need_drain = true; return ok; } @@ -1176,7 +1224,7 @@ static void scr_net_sock_write_raw(ScrNetSocket *s, const char *data, size_t len (s->tops && !s->t_est)) { /* mid-connect and mid-handshake bytes buffer; the flush after * 'connect'/establishment sends them */ - scr_net_sock_buffer(s, data, len); + scr_net_sock_buffer(s, data, len, len); scr_net_sock_update_write(s); return; } @@ -1195,7 +1243,7 @@ static void scr_net_sock_write_raw(ScrNetSocket *s, const char *data, size_t len } } if ((size_t)sent < len) { - scr_net_sock_buffer(s, data + sent, len - (size_t)sent); + scr_net_sock_buffer(s, data + sent, len - (size_t)sent, len); scr_net_sock_update_write(s); } } @@ -1260,7 +1308,7 @@ void scr_net_sock_writev_native(ScrNetSocket *s, const ScrNetSlice *slices, size skip -= len; continue; } - scr_net_sock_buffer(s, (const char *)iov[i].iov_base + skip, len - skip); + scr_net_sock_buffer(s, (const char *)iov[i].iov_base + skip, len - skip, len); skip = 0; } scr_net_sock_update_write(s); @@ -1287,9 +1335,10 @@ static void scr_net_sock_flush(ScrNetSocket *s) { } if (sent > 0) scr_net_sock_touch(s); s->whead += (size_t)sent; + scr_net_sock_wq_settle(s); } if (s->wlen == s->whead) { - s->whead = s->wlen = 0; + scr_net_sock_wq_clear(s); scr_net_sock_update_write(s); scr_net_sock_maybe_finish_write(s); /* Node's afterWrite for a write the socket completed later: 'drain' @@ -2778,7 +2827,7 @@ ScrNetSocket *scr_net_connect_deferred(double port, ScrStr *host /*borrowed, nul * for a slot takes a pooled socket instead of dialing. */ void scr_net_sock_move_writes(ScrNetSocket *from, ScrNetSocket *to) { if (from->wlen > from->whead) scr_net_sock_write_native(to, from->wbuf + from->whead, from->wlen - from->whead); - from->whead = from->wlen = 0; + scr_net_sock_wq_clear(from); } void scr_net_sock_dial_start(ScrNetSocket *s) { @@ -3207,17 +3256,19 @@ bool scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) { (data->len * scr_bytes_elem_size(data->elem))); } -/* socket.writableLength: the bytes written and not yet sent. */ +/* socket.writableLength: Node's state.length, the length of every write + * not yet completely sent. */ double scr_net_sock_writable_length(ScrNetSocket *s) { ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s; - return (double)(w->wlen - w->whead); + return (double)w->wpending; } /* socket.writableNeedDrain: a write answered false and 'drain' has not - * fired, on a socket whose write half is open. */ + * fired, on a socket whose write half is open (including one still + * waiting to dial). */ bool scr_net_sock_writable_need_drain(ScrNetSocket *s) { ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s; - return w->need_drain && !w->wr_ending && !w->close_emitted && w->fd >= 0; + return w->need_drain && scr_net_sock_write_open(w); } /* on('drain') / once('drain'). */ @@ -5331,7 +5382,7 @@ ScrNetSocket *scr_net_sock_adopt(ScrNetSocket *raw) { tun->send_only = true; t->tunnel = tun; } - raw->whead = raw->wlen = 0; + scr_net_sock_wq_clear(raw); raw->adopted_by = scr_net_sock_retain(t); t->adopted_from = scr_net_sock_retain(raw); scr_net_sock_register(t); @@ -6260,10 +6311,12 @@ static ScrDyn *scr_net_dynh_sock_get(void *h, const char *key, size_t key_len) { } } if (strcmp(key, "readable") == 0) return scr_dyn_new_bool(scr_net_sock_readable(s)); + if (strcmp(key, "writableLength") == 0) return scr_dyn_new_num(scr_net_sock_writable_length(s)); + if (strcmp(key, "writableNeedDrain") == 0) + return scr_dyn_new_bool(scr_net_sock_writable_need_drain(s)); if (strcmp(key, "writableHighWaterMark") == 0 || strcmp(key, "readableHighWaterMark") == 0) { - /* Node's default stream highWaterMark — a constant here (backpressure - * is not modeled; SEMANTICS.md) */ - return scr_dyn_new_num(16384); + /* Node's default stream highWaterMark (getDefaultHighWaterMark()) */ + return scr_dyn_new_num(SCR_NET_WRITE_HWM); } if (scr_net_dynh_tls != NULL) { ScrDyn *out = NULL; diff --git a/tests/corpus/7515-net-socket-backpressure.ts b/tests/corpus/7515-net-socket-backpressure.ts index a265d3f77..260726a15 100644 --- a/tests/corpus/7515-net-socket-backpressure.ts +++ b/tests/corpus/7515-net-socket-backpressure.ts @@ -1,47 +1,136 @@ // net.Socket write() backpressure, as in Node 24: write answers false when -// the bytes it leaves unsent reach the 65536-byte high-water mark, which a -// write made before the connection does, writableNeedDrain stays true -// until 'drain', and 'drain' fires once the buffered bytes are sent. An -// echo client that waits for 'drain' when write answers false moves every -// byte. +// the writes not yet completely sent reach the 65536-byte high-water mark, +// each counting in full, which a write made before the connection does; +// writableNeedDrain stays true until 'drain', and 'drain' fires once the +// buffered bytes are sent. The same holds while a custom lookup is pending, +// writableLength counts a large write the kernel took in part in full, +// untyped code reads the same properties, and a write whose send fails +// after the peer reset the connection answers false. An echo client that +// waits for 'drain' when write answers false moves every byte. import { connect, createServer, Socket } from "node:net"; +import type { LookupAddress, LookupAllOptions, LookupOneOptions } from "node:dns"; const CHUNK = 65536; const TOTAL = 64 * CHUNK; const chunk = Buffer.alloc(CHUNK, 120); +function untyped(socket: any): string { + return `${socket.writableLength} ${socket.writableNeedDrain} ${socket.writableHighWaterMark}`; +} + +function echo(port: number): Promise { + return new Promise((resolve) => { + const client = connect(port, "127.0.0.1"); + console.log("small write before connect", client.write("hi"), client.writableLength); + console.log("large write before connect", client.write(Buffer.alloc(70000, 121)), client.writableLength); + console.log("need drain", client.writableNeedDrain, "untyped", untyped(client)); + let sent = 70002; + let received = 0; + const pump = (): void => { + while (sent < TOTAL) { + sent += CHUNK; + if (!client.write(chunk)) { + client.once("drain", pump); + return; + } + } + client.end(); + }; + client.once("drain", () => { + console.log("drain fired, need drain", client.writableNeedDrain, "pending", client.writableLength); + pump(); + }); + client.on("data", (data: Buffer) => { + received += data.length; + }); + client.on("end", () => { + console.log("received", received, "of", sent); + resolve(); + }); + }); +} + +function lookupPending(port: number): Promise { + return new Promise((resolve) => { + const client = connect({ + port, + host: "localhost", + lookup: ( + _host: string, + _options: LookupOneOptions | LookupAllOptions | number | undefined, + callback: (err: NodeJS.ErrnoException | null, address: string | LookupAddress[], family?: number) => void, + ) => { + const answer: LookupAddress[] = [{ address: "127.0.0.1", family: 4 }]; + setTimeout(() => callback(null, answer), 20); + }, + }); + console.log("lookup write", client.write(Buffer.alloc(70000, 97))); + console.log("lookup state", client.writableLength, client.writableNeedDrain, "untyped", untyped(client)); + client.once("drain", () => { + console.log("lookup drain", client.writableLength, client.writableNeedDrain); + client.destroy(); + resolve(); + }); + }); +} + +function largeWrite(port: number): Promise { + return new Promise((resolve) => { + const client = connect(port, "127.0.0.1", () => { + const size = 16 * 1024 * 1024; + console.log("large write", client.write(Buffer.alloc(size, 98))); + console.log("large state", client.writableLength === size, client.writableNeedDrain); + client.once("drain", () => { + console.log("large drain", client.writableLength, client.writableNeedDrain); + client.destroy(); + resolve(); + }); + setTimeout(() => { + if (paused !== null) paused.resume(); + }, 30); + }); + }); +} + +function afterReset(port: number): Promise { + return new Promise((resolve) => { + const client = connect(port, "127.0.0.1", () => { + client.pause(); + setTimeout(() => { + console.log("first write after the peer closed", client.write("a")); + setTimeout(() => { + console.log("write after reset", client.write("b")); + }, 50); + }, 50); + }); + client.on("error", () => {}); + client.on("close", () => { + console.log("closed, destroyed", client.destroyed); + resolve(); + }); + }); +} + +let paused: Socket | null = null; +let mode = "echo"; const server = createServer((socket: Socket) => { - socket.pipe(socket); + if (mode === "echo") socket.pipe(socket); + else if (mode === "drain") socket.resume(); + else if (mode === "pause") { + socket.pause(); + paused = socket; + } else socket.destroy(); }); -server.listen(0, "127.0.0.1", () => { +server.listen(0, "127.0.0.1", async () => { const addr = server.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; - const client = connect(port, "127.0.0.1"); - console.log("small write before connect", client.write("hi"), client.writableLength); - console.log("large write before connect", client.write(Buffer.alloc(70000, 121)), client.writableLength); - console.log("need drain", client.writableNeedDrain); - let sent = 70002; - let received = 0; - const pump = (): void => { - while (sent < TOTAL) { - sent += CHUNK; - if (!client.write(chunk)) { - client.once("drain", pump); - return; - } - } - client.end(); - }; - client.once("drain", () => { - console.log("drain fired, need drain", client.writableNeedDrain, "pending", client.writableLength); - pump(); - }); - client.on("data", (data: Buffer) => { - received += data.length; - }); - client.on("end", () => { - console.log("received", received, "of", sent); - server.close(); - }); + await echo(port); + mode = "drain"; + await lookupPending(port); + mode = "pause"; + await largeWrite(port); + mode = "end"; + await afterReset(port); + server.close(); }); From 256fbdba20804c8a478f7cb0f77090d6df6dd5a8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 04:54:28 -0500 Subject: [PATCH 196/273] Keep the helper for callbacks that read a local class's captures A lone array callback in a local class's method could read a binding the method reaches through its class's captures. Those bindings are shared by every method of the class, so another method can assign one while the loop runs, but the copied loop read the value once when the call started. stableTarget now counts a method's class captures as inherited, so such a callback keeps its helper and reads the current value on every call. --- .../lowering/containers/array-pipelines.ts | 7 +++++-- .../compiler/test/array-pipeline-fusion.test.ts | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index 7c2e07b1c..197af2a9e 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -950,11 +950,14 @@ class PipelineFusion { * nothing assigns after their declaration: `fn`'s own locals and * parameters, written neither by `fn` nor by any closure that shares * them, and not by the callback body. A binding a local class captures - * counts as written, since its methods may assign it. The callback must + * counts as written, since its methods may assign it; so does a binding + * a method reads through its class's captures. The callback must * not observe the ambient receiver (AmbientReceiverReaders). */ private stableTarget(fn: IrFunction): InlineTarget { const locals = new Map(fn.locals.map((l) => [l.id, l])); - const inherited = new Set((fn.captures ?? []).map((c) => c.localId)); + const inherited = new Set( + [...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId), + ); const written = this.written.get(fn.name); const classCaptured = this.classCaptured(fn); return (arg) => { diff --git a/packages/compiler/test/array-pipeline-fusion.test.ts b/packages/compiler/test/array-pipeline-fusion.test.ts index e41e378a3..2c0c138ce 100644 --- a/packages/compiler/test/array-pipeline-fusion.test.ts +++ b/packages/compiler/test/array-pipeline-fusion.test.ts @@ -249,3 +249,20 @@ console.log(out.join(","), hit); ${postlude}`); expect(helperCalls(init(mod))).toEqual(["%arr.loop", "%arr.loop"]); }); + +test("a lone call in a local class's method keeps its helper for the class's captures", async () => { + const mod = await lowerSource(`${prelude} +let k = 1; +class Box { + set(v: number): void { k = v; } + go(): number[] { + const b = new Box(); + return rows.map((r) => { b.set(r.id); return r.value + k; }); + } +} +console.log(new Box().go().join(",")); +${postlude}`); + const go = mod.functions.find((f) => f.name.endsWith(".Box.go")); + expect(go).toBeDefined(); + expect(helperCalls(go!)).toEqual(["%arr.map"]); +}); From f8b1f9ffc36ade3f470c4e6d98d7625f5124caff Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 05:02:40 -0500 Subject: [PATCH 197/273] Keep a spread tuple alive until all its fields are packed into the rest A spread tuple's first field carries the statements that evaluate the tuple into a hidden local, and its later fields read that local. When lowerArraySpreadItems stored the first field as one item's expression, the local's frame ended with that store, so a tuple returned by a call was released before its later fields were read (a use-after-free under SCRIPTC_SAN=1). The field's statements now join the packing sequence at the item's position, whose frame lasts until every item is stored. --- .../frontend/lowering/containers/array-construction.ts | 10 +++++++++- tests/corpus/8004-optional-element-conversions.ts | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 63e2e6e07..5029abc36 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -174,7 +174,15 @@ export function lowerArraySpreadItems( ]; for (const node of nodes) { if (!(node instanceof AstNode)) { - const value = undefinedForNullish(lowerer, node, arrType); + // A spread tuple's first field declares the tuple's storage, which + // the later fields read: its statements join this sequence, whose + // frame lasts until every item is stored. + let item = node; + if (item.kind === "seqExpr") { + body.push(...item.stmts); + item = item.result; + } + const value = undefinedForNullish(lowerer, item, arrType); const stored = lowerer.runtimeOptionalWidening(value.type, elem) !== null || optionalElementValue(lowerer, value, elem) diff --git a/tests/corpus/8004-optional-element-conversions.ts b/tests/corpus/8004-optional-element-conversions.ts index 9a4ac55ac..a592a4f32 100644 --- a/tests/corpus/8004-optional-element-conversions.ts +++ b/tests/corpus/8004-optional-element-conversions.ts @@ -58,6 +58,14 @@ const mixedChanging: [boolean, boolean | undefined] = [true, undefined]; console.log(flags(...mixedChanging, ...mixedMore, flip(mixedChanging, true), ...mixedChanging)); const mixedWords: [string, string | undefined] = ["a", undefined]; console.log(label("z", ...["b", undefined, "c"], ...mixedWords), label("y", ...mixedWords, ...new Set(["s", undefined]))); +// A tuple returned by a call keeps its string fields alive until every +// field is packed beside the other spreads. +function makeTuple(n: number): [string, string | undefined, string] { + return ["p" + n.toString(16).repeat(4), n % 2 ? undefined : "q" + n, "r".repeat((n % 7) + 3) + n]; +} +for (let i = 0; i < 3; i++) { + console.log(label("z", ...["m", undefined], ...makeTuple(i), ...mixedWords), label("y", ...makeTuple(i + 10), ...new Set(["s" + i]))); +} function pick(start: (number | undefined)[] | string[], other: (number | undefined)[] | string[]): string { let a = start; From cbc062a35f84e7a7099419e4d5b8a852e37c2e90 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 05:03:04 -0500 Subject: [PATCH 198/273] Skip NULL children and key-cache walks when disposing dynamic values Disposing a checked-dynamic node released each entry's getter and setter and the node's prototype and symbol tables through scr_dyn_release, though almost all of them are NULL: in a zod validation loop, over half of all dyn releases. Dispose now tests each child first. A pooled object also walked all its slots again on reuse, to take the key bytes it kept back from the pool's key budget. It now records that total when it is pooled, and reuse subtracts it. --- packages/runtime/src/scr_dyn.h | 7 ++++- packages/runtime/src/scr_json.c | 51 +++++++++++++++++++-------------- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index d6b05be71..f90fa1838 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -480,7 +480,12 @@ struct ScrDyn { * 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 */ + union { + struct ScrDynRegion *region; /* see ScrDynRegion */ + /* While the node is pooled: the key bytes it keeps for reuse, + * counted in the pool's key budget (scr_dyn_obj_drop_keys). */ + size_t cached_key_bytes; + }; /* The dynamic tier's hidden class (scr_js_object.c): the transition * tree node naming this object's key sequence, or NULL while it has * none. Only programs with dynamic-tier code read or maintain it: diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 1738ba848..bfc63270f 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -846,15 +846,21 @@ static SCR_TL size_t scr_dyn_cached_key_bytes; #define SCR_DYN_KEY_CACHE_LENGTH 64 #endif -static void scr_dyn_obj_drop_keys(ScrDyn *d, bool cache) { +/* Returns the key bytes it kept for reuse, which it added to the budget; + * the pooled node records them (v.obj.cached_key_bytes) so its reuse takes + * them back without walking its slots. */ +static size_t scr_dyn_obj_drop_keys(ScrDyn *d, bool cache) { + size_t kept = 0; for (size_t i = 0; i < d->v.obj.cap; i++) { ScrDynEntry *entry = &d->v.obj.entries[i]; if (!entry->key) continue; #ifndef SCR_RC_AUDIT + size_t bytes = sizeof(ScrDynKey) + entry->key_len + 1; if (cache && d->v.obj.cap <= SCR_DYN_KEY_CACHE_SLOTS && entry->key_len <= SCR_DYN_KEY_CACHE_LENGTH && - sizeof(ScrDynKey) + entry->key_len + 1 <= SCR_DYN_KEY_CACHE_MAX - scr_dyn_cached_key_bytes) { - scr_dyn_cached_key_bytes += sizeof(ScrDynKey) + entry->key_len + 1; + bytes <= SCR_DYN_KEY_CACHE_MAX - scr_dyn_cached_key_bytes) { + scr_dyn_cached_key_bytes += bytes; + kept += bytes; continue; } #else @@ -864,6 +870,7 @@ static void scr_dyn_obj_drop_keys(ScrDyn *d, bool cache) { entry->key = NULL; entry->key_len = 0; } + return kept; } /* Primitive kinds cannot acquire properties or change kind while live, so @@ -1054,15 +1061,11 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) { d->v.arr.region = NULL; } else if (kind == SCR_DYN_OBJ) { d->v.obj.len = 0; /* cap/entries preserved */ + /* The kept keys leave the pool's budget with the node. */ + scr_dyn_cached_key_bytes -= d->v.obj.cached_key_bytes; 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++) { - ScrDynEntry *entry = &d->v.obj.entries[i]; - if (entry->key) scr_dyn_cached_key_bytes -= sizeof(ScrDynKey) + entry->key_len + 1; - } - } d->v.obj.source_identity = NULL; d->v.obj.source_access = NULL; } else if (kind != SCR_DYN_NUM && kind != SCR_DYN_STR) { @@ -1128,6 +1131,12 @@ static inline void scr_dyn_weak_dispose(ScrDyn *d) { if (scr_weak_observing) scr_weak_disposed(d); } +/* A disposed node's child: most getters, setters, prototypes and symbol + * tables are NULL, and the test costs less than the call. */ +static inline void scr_dyn_release_child(ScrDyn *child) { + if (child) scr_dyn_release(child); +} + static void scr_dyn_dispose(ScrDyn *d, bool collected) { #ifndef SCR_RC_AUDIT /* A capsule with nothing but its referent and adapters: the common end @@ -1149,9 +1158,9 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { d->v.obj.index = NULL; } if (d->kind == SCR_DYN_ARR || d->kind == SCR_DYN_OBJ) scr_dyn_region_leave(d); - if (!collected) scr_dyn_release(d->prototype); - if (!collected) scr_dyn_release(d->symbol_properties); - if (!collected) scr_dyn_release(d->symbol_keys); + if (!collected) scr_dyn_release_child(d->prototype); + if (!collected) scr_dyn_release_child(d->symbol_properties); + if (!collected) scr_dyn_release_child(d->symbol_keys); /* Pooled nodes keep all three NULL (scr_dyn_leaf_alloc relies on it). */ d->prototype = d->symbol_properties = d->symbol_keys = NULL; switch (d->kind) { @@ -1171,17 +1180,17 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { free(d->v.arr.presence); d->v.arr.presence = NULL; if (!collected) { - for (size_t i = 0; i < d->v.arr.len; i++) scr_dyn_release(d->v.arr.items[i]); - scr_dyn_release(d->v.arr.properties); + for (size_t i = 0; i < d->v.arr.len; i++) scr_dyn_release_child(d->v.arr.items[i]); + scr_dyn_release_child(d->v.arr.properties); } break; case SCR_DYN_OBJ: for (size_t i = 0; i < d->v.obj.len; i++) { ScrDyn *value = d->v.obj.entries[i].value; - if (!collected || !d->v.obj.entries[i].value_traced) scr_dyn_release(value); + if (!collected || !d->v.obj.entries[i].value_traced) scr_dyn_release_child(value); if (!collected) { - scr_dyn_release(d->v.obj.entries[i].getter); - scr_dyn_release(d->v.obj.entries[i].setter); + scr_dyn_release_child(d->v.obj.entries[i].getter); + scr_dyn_release_child(d->v.obj.entries[i].setter); } d->v.obj.entries[i].value = NULL; d->v.obj.entries[i].getter = NULL; @@ -1207,7 +1216,7 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { case SCR_DYN_TYPED_REF: scr_typed_ref_forget(d); - if (!collected) scr_dyn_release(d->v.typed_ref.materialized); + if (!collected) scr_dyn_release_child(d->v.typed_ref.materialized); while (d->v.typed_ref.casts) { ScrDynTypedCast *cast = d->v.typed_ref.casts; d->v.typed_ref.casts = cast->next; @@ -1218,8 +1227,8 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { break; case SCR_DYN_PROXY: if (!collected) { - scr_dyn_release(d->v.proxy.target); - scr_dyn_release(d->v.proxy.handler); + scr_dyn_release_child(d->v.proxy.target); + scr_dyn_release_child(d->v.proxy.handler); } break; default: @@ -1230,7 +1239,7 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { #endif #ifndef SCR_RC_AUDIT if (!collected && scr_dyn_free_count < SCR_DYN_FREE_MAX) { - if (d->kind == SCR_DYN_OBJ) scr_dyn_obj_drop_keys(d, true); + if (d->kind == SCR_DYN_OBJ) d->v.obj.cached_key_bytes = scr_dyn_obj_drop_keys(d, true); /* Pooled blocks retain their headers and count toward the bounded * allocated heap, but never remain in a candidate buffer. */ scr_dyn_weak_dispose(d); From 9abf35f6a2e999944ee503c545000f9033cf632a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 05:23:35 -0500 Subject: [PATCH 199/273] Build the large compiler tables on first use The native compiler ran four large module initializers at every startup: the builtin call signatures (LIB_FN_SIGS), the Node 24 fetch compatibility profile, and the builtin module function and ambient surface tables. Together their init functions are about 1.3 MB of code, and --version and warm cache-hit builds paged them in and built the tables without reading them. Each table is now built by a function on its first call and kept in a module variable: - libFnSignatures() replaces LIB_FN_SIGS - node24FetchCompatProfile() and staticFetchMembers() replace NODE24_FETCH_COMPAT_PROFILE and the STATIC_* member sets - builtinModuleFns() and ambientSurfaceFns() replace BUILTIN_MODULE_FNS and AMBIENT_SURFACE_FNS The @scriptc/compiler entry point keeps LIB_FN_SIGS and NODE24_FETCH_COMPAT_PROFILE as deprecated constants built from the new functions when the entry module loads. The native compiler does not load that module. The surface manifest's coverage notes read the fetch target through the profile function, so building the manifest module no longer builds the profile. The table contents are unchanged. --- packages/compiler/src/compat/fetch-profile.ts | 1511 +-- .../compiler/src/coverage/surface-manifest.ts | 53 +- .../src/frontend/lowering/lib-boundary.ts | 4 +- .../frontend/lowering/lower-builtin-values.ts | 6 +- .../src/frontend/lowering/lower-stdlib.ts | 40 +- .../src/frontend/lowering/surfaces.ts | 3116 ++--- packages/compiler/src/index.ts | 12 +- .../compiler/src/ir/builtin-signatures.ts | 10139 ++++++++-------- packages/compiler/src/ir/ir.ts | 2 +- packages/compiler/src/ir/validate.ts | 4 +- packages/compiler/src/library/fence-eval.ts | 12 +- packages/compiler/test/compat-exports.test.ts | 12 + packages/compiler/test/node-pin.test.ts | 4 +- tests/harness/README.md | 2 +- tests/harness/code-quality.test.ts | 2 +- tests/harness/fetch-conformance-program.ts | 2 +- tests/harness/fetch-conformance.test.ts | 4 +- tests/harness/surface-manifest.test.ts | 6 +- 18 files changed, 7525 insertions(+), 7406 deletions(-) create mode 100644 packages/compiler/test/compat-exports.test.ts diff --git a/packages/compiler/src/compat/fetch-profile.ts b/packages/compiler/src/compat/fetch-profile.ts index ff7cedb22..03d261aff 100644 --- a/packages/compiler/src/compat/fetch-profile.ts +++ b/packages/compiler/src/compat/fetch-profile.ts @@ -155,494 +155,104 @@ const typedInterfaceUnsupported = "typed source has no compiler bridge for this const metadataExclusion = "WebIDL brand metadata is observable reflection, not an executable compatibility operation"; -export const NODE24_FETCH_COMPAT_PROFILE = { - schemaVersion: 1, - target: { - node: "24.22.0", - undici: "7.29.1", - }, - requestInit: [ - { - id: "stdlib.fetch.request-init.method", - name: "RequestInit.method", - conversion: "WebIDL ByteString after all call arguments evaluate", - evidence: [fixture("static-coercion")], +function buildFetchCompatProfile() { + return { + schemaVersion: 1, + target: { + node: "24.22.0", + undici: "7.29.1", }, - { - id: "stdlib.fetch.request-init.headers", - name: "RequestInit.headers", - conversion: "Headers, record, or sequence-of-pairs snapshot", - evidence: [fixture("static"), fixture("static-coercion")], - }, - { - id: "stdlib.fetch.request-init.body", - name: "RequestInit.body", - conversion: "string, Uint8Array, ReadableStream, or null", - evidence: [fixture("static"), fixture("static-stream"), fixture("static-coercion")], - }, - { - id: "stdlib.fetch.request-init.duplex", - name: "RequestInit.duplex", - conversion: "WebIDL enum; 'half' required for streaming bodies", - evidence: [fixture("static-stream"), fixture("static-coercion")], - }, - { - id: "stdlib.fetch.request-init.redirect", - name: "RequestInit.redirect", - conversion: "WebIDL enum: follow, error, or manual", - evidence: [fixture("static"), fixture("static-coercion")], - }, - { - id: "stdlib.fetch.request-init.signal", - name: "RequestInit.signal", - conversion: "native AbortSignal handle or absent", - evidence: [fixture("static"), fixture("static-abort-throw")], - }, - ], - responseInit: [ - { - id: "stdlib.response-init.headers", - name: "ResponseInit.headers", - conversion: "Headers, record, or sequence-of-pairs snapshot", - evidence: [fixture("static")], - }, - { - id: "stdlib.response-init.status", - name: "ResponseInit.status", - conversion: "WebIDL unsigned-short conversion followed by the 200–599 range check", - evidence: [fixture("static")], - }, - { - id: "stdlib.response-init.statusText", - name: "ResponseInit.statusText", - conversion: "WebIDL ByteString with HTTP reason-phrase validation", - evidence: [fixture("static")], - }, - ], - members: { - responseReads: [ - "ok", - "status", - "statusText", - "url", - "redirected", - "headers", - "body", - "bodyUsed", - ], - responseCalls: ["json", "text", "bytes"], - readableStreamReads: ["locked"], - readableStreamCalls: ["cancel", "getReader"], - }, - operations: [ - { - id: "stdlib.headers.constructor", - name: "Headers constructor", - kind: "constructor", - facets: ["webidl-conversion", "mutation", "identity"], - scope: "Record and iterable header pairs, including copying native Headers", - evidence: [fixture("static-request")], - }, - { - id: "stdlib.request.constructor", - name: "Request constructor", - kind: "constructor", - facets: ["webidl-conversion", "body-consumption", "error-shape"], - scope: - "Absolute URLs with method, headers, body, duplex, redirect, and signal; other RequestInit members retain explicit fences", - evidence: [fixture("static-request")], - }, - ...[ - "method", - "url", - "headers", - "destination", - "referrer", - "referrerPolicy", - "mode", - "credentials", - "cache", - "redirect", - "integrity", - "keepalive", - "isReloadNavigation", - "isHistoryNavigation", - "signal", - "body", - "bodyUsed", - "duplex", - ].map((member): FetchCompatOperation => ({ - id: `stdlib.request.${member}`, - name: `Request.${member}`, - kind: "property", - facets: ["property-read", "state-machine"], - scope: "Reads on native Requests within the constructor's supported RequestInit subset", - evidence: [fixture("static-request")], - })), - ...["arrayBuffer", "text", "json", "bytes"].map((member): FetchCompatOperation => ({ - id: `stdlib.request.${member}`, - name: `Request.${member}`, - kind: "method", - facets: ["body-consumption", "promise-settlement", "state-machine", "error-shape"], - evidence: [fixture("static-request")], - })), - { - id: "stdlib.response.constructor", - name: "Response constructor", - kind: "constructor", - facets: ["webidl-conversion", "body-consumption", "state-machine", "error-shape"], - scope: - "BodyInit is string, Uint8Array/Buffer, ReadableStream, null/undefined, or a checked-dynamic value that follows the supported WebIDL string conversion; ResponseInit is headers/status/statusText", - evidence: [fixture("static")], - }, - { - id: "stdlib.fetch", - name: "fetch", - kind: "function", - facets: [ - "argument-evaluation", - "webidl-conversion", - "surplus-arguments", - "promise-settlement", - "transport", - "error-shape", - ], - evidence: [fixture("static"), fixture("static-coercion"), fixture("static-network-error")], - }, - { - id: "stdlib.abort-controller.constructor", - name: "AbortController constructor", - kind: "constructor", - facets: ["argument-evaluation", "identity", "state-machine", "surplus-arguments"], - evidence: [fixture("static-controller")], - }, - { - id: "stdlib.abort-controller.signal", - name: "AbortController.signal", - kind: "property", - facets: ["identity", "property-read", "state-machine"], - evidence: [fixture("static-controller")], - }, - { - id: "stdlib.abort-controller.abort", - name: "AbortController.abort", - kind: "method", - facets: ["callback-order", "identity", "liveness", "state-machine", "surplus-arguments"], - evidence: [fixture("static-controller")], - }, - { - id: "stdlib.abort-signal.abort", - name: "AbortSignal.abort", - kind: "static-method", - facets: ["identity", "liveness", "surplus-arguments", "property-read"], - evidence: [generated("webidl-operations"), fixture("static-stream-this")], - }, - { - id: "stdlib.abort-signal.any", - name: "AbortSignal.any", - kind: "static-method", - facets: ["webidl-conversion", "missing-arguments", "surplus-arguments", "property-read"], - evidence: [generated("webidl-operations"), fixture("static-stream-this")], - }, - { - id: "stdlib.abort-signal.timeout", - name: "AbortSignal.timeout", - kind: "static-method", - facets: ["webidl-conversion", "missing-arguments", "surplus-arguments", "error-shape"], - evidence: [generated("webidl-operations"), fixture("static-stream-this")], - }, - { - id: "stdlib.abort-signal.aborted", - name: "AbortSignal.aborted", - kind: "property", - facets: ["property-read"], - evidence: [generated("webidl-operations")], - }, - { - id: "stdlib.abort-signal.reason", - name: "AbortSignal.reason", - kind: "property", - facets: ["identity", "liveness", "property-read"], - evidence: [generated("webidl-operations"), fixture("static-stream-this")], - }, - { - id: "stdlib.abort-signal.onabort", - name: "AbortSignal.onabort", - kind: "property", - facets: ["callback-order", "callback-this", "mutation", "property-read"], - evidence: [generated("abort-events"), fixture("static-listener-this")], - }, - { - id: "stdlib.abort-signal.throw-if-aborted", - name: "AbortSignal.throwIfAborted", - kind: "method", - facets: ["identity", "error-shape"], - evidence: [generated("webidl-operations"), fixture("static-abort-throw")], - }, - { - id: "stdlib.abort-signal.add-event-listener", - name: "AbortSignal.addEventListener", - kind: "method", - facets: ["webidl-conversion", "identity", "callback-order", "callback-this"], - evidence: [ - generated("abort-events"), - fixture("static-listener-this"), - fixture("static-listener-noncallable"), - ], - }, - { - id: "stdlib.abort-signal.remove-event-listener", - name: "AbortSignal.removeEventListener", - kind: "method", - facets: ["webidl-conversion", "identity", "callback-order"], - evidence: [generated("abort-events"), fixture("static-stream-this")], - }, - { - id: "stdlib.abort-signal.dispatch-event", - name: "AbortSignal.dispatchEvent", - kind: "method", - facets: ["callback-order", "callback-this", "error-shape"], - evidence: [fixture("static-dispatch-throw")], - }, - { - id: "stdlib.readable-stream.constructor", - name: "ReadableStream constructor", - kind: "constructor", - facets: ["webidl-conversion", "callback-this", "callback-order", "state-machine"], - evidence: [generated("stream-traces"), fixture("static-stream-this")], - }, - { - id: "stdlib.readable-stream.from", - name: "ReadableStream.from", - kind: "static-method", - facets: ["webidl-conversion", "missing-arguments", "surplus-arguments", "liveness"], - evidence: [generated("webidl-operations"), generated("stream-traces")], - }, - { - id: "stdlib.readable-stream.locked", - name: "ReadableStream.locked", - kind: "property", - facets: ["property-read", "state-machine"], - evidence: [generated("stream-traces")], - }, - { - id: "stdlib.readable-stream.cancel", - name: "ReadableStream.cancel", - kind: "method", - facets: ["identity", "promise-settlement", "state-machine"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream.get-reader", - name: "ReadableStream.getReader", - kind: "method", - facets: ["webidl-conversion", "identity", "state-machine", "error-shape"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream-default-reader.closed", - name: "ReadableStreamDefaultReader.closed", - kind: "property", - facets: ["promise-settlement", "property-read", "state-machine"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream-default-reader.read", - name: "ReadableStreamDefaultReader.read", - kind: "method", - facets: ["identity", "promise-settlement", "state-machine"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream-default-reader.cancel", - name: "ReadableStreamDefaultReader.cancel", - kind: "method", - facets: ["identity", "promise-settlement", "state-machine"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream-default-reader.release-lock", - name: "ReadableStreamDefaultReader.releaseLock", - kind: "method", - facets: ["promise-settlement", "state-machine", "error-shape"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream-default-controller.desired-size", - name: "ReadableStreamDefaultController.desiredSize", - kind: "property", - facets: ["property-read", "state-machine"], - evidence: [generated("stream-traces")], - }, - { - id: "stdlib.readable-stream-default-controller.enqueue", - name: "ReadableStreamDefaultController.enqueue", - kind: "method", - facets: ["identity", "liveness", "state-machine", "error-shape"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream-default-controller.close", - name: "ReadableStreamDefaultController.close", - kind: "method", - facets: ["state-machine", "error-shape"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - { - id: "stdlib.readable-stream-default-controller.error", - name: "ReadableStreamDefaultController.error", - kind: "method", - facets: ["identity", "promise-settlement", "state-machine"], - evidence: [generated("stream-traces"), fixture("static-stream")], - }, - ...["append", "delete", "get", "getSetCookie", "has", "set", "forEach"].map( - (member): FetchCompatOperation => ({ - id: `stdlib.headers.${member}`, - name: `Headers.${member}`, - kind: "method", - facets: - member === "forEach" - ? ["callback-order", "callback-this", "mutation"] - : member === "get" || member === "has" - ? ["webidl-conversion", "missing-arguments", "property-read"] - : ["webidl-conversion", "mutation", "error-shape"], + requestInit: [ + { + id: "stdlib.fetch.request-init.method", + name: "RequestInit.method", + conversion: "WebIDL ByteString after all call arguments evaluate", + evidence: [fixture("static-coercion")], + }, + { + id: "stdlib.fetch.request-init.headers", + name: "RequestInit.headers", + conversion: "Headers, record, or sequence-of-pairs snapshot", evidence: [fixture("static"), fixture("static-coercion")], - }), - ), - ...["ok", "status", "statusText", "url", "redirected", "headers", "body", "bodyUsed"].map( - (member): FetchCompatOperation => ({ - id: `stdlib.response.${member}`, - name: `Response.${member}`, - kind: "property", - facets: ["property-read"], - evidence: [fixture("static")], - }), - ), - { - id: "stdlib.response.static.json", - name: "Response.json", - kind: "static-method", - facets: ["webidl-conversion", "error-shape"], - scope: - "data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor", - evidence: [fixture("static-response-json")], - }, - ...["json", "text", "bytes", "arrayBuffer"].map((member): FetchCompatOperation => ({ - id: `stdlib.response.${member}`, - name: `Response.${member}`, - kind: "method", - facets: ["body-consumption", "promise-settlement", "state-machine", "error-shape"], - evidence: - member === "arrayBuffer" - ? [fixture("static")] - : [fixture("static"), fixture("static-stream")], - })), - ], - inventory: { - interfaces: [ - "AbortController", - "AbortSignal", - "Headers", - "Request", - "Response", - "ReadableStream", - "ReadableStreamDefaultReader", - "ReadableStreamDefaultController", + }, + { + id: "stdlib.fetch.request-init.body", + name: "RequestInit.body", + conversion: "string, Uint8Array, ReadableStream, or null", + evidence: [fixture("static"), fixture("static-stream"), fixture("static-coercion")], + }, + { + id: "stdlib.fetch.request-init.duplex", + name: "RequestInit.duplex", + conversion: "WebIDL enum; 'half' required for streaming bodies", + evidence: [fixture("static-stream"), fixture("static-coercion")], + }, + { + id: "stdlib.fetch.request-init.redirect", + name: "RequestInit.redirect", + conversion: "WebIDL enum: follow, error, or manual", + evidence: [fixture("static"), fixture("static-coercion")], + }, + { + id: "stdlib.fetch.request-init.signal", + name: "RequestInit.signal", + conversion: "native AbortSignal handle or absent", + evidence: [fixture("static"), fixture("static-abort-throw")], + }, ], - entries: [ - staticEntry("stdlib.fetch", "globalThis", "fetch", "global"), - - staticEntry( - "stdlib.abort-controller.constructor", - "AbortController", - "constructor", - "constructor", - ), - staticEntry("stdlib.abort-controller.signal", "AbortController", "signal", "prototype"), - staticEntry("stdlib.abort-controller.abort", "AbortController", "abort", "prototype"), - outOfScopeEntry( - "stdlib.abort-controller.symbol.toStringTag", - "AbortController", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), - - unsupportedEntry( - "stdlib.abort-signal.constructor", - "AbortSignal", - "constructor", - "constructor", - "Node exposes the interface object but constructing it throws; the compiler does not preserve that constructor behavior", - ), - staticEntry("stdlib.abort-signal.abort", "AbortSignal", "abort", "static"), - staticEntry("stdlib.abort-signal.timeout", "AbortSignal", "timeout", "static"), - staticEntry("stdlib.abort-signal.any", "AbortSignal", "any", "static"), - staticEntry("stdlib.abort-signal.aborted", "AbortSignal", "aborted", "prototype"), - staticEntry("stdlib.abort-signal.reason", "AbortSignal", "reason", "prototype"), - staticEntry( - "stdlib.abort-signal.throw-if-aborted", - "AbortSignal", - "throwIfAborted", - "prototype", - ), - staticEntry("stdlib.abort-signal.onabort", "AbortSignal", "onabort", "prototype"), - staticEntry( - "stdlib.abort-signal.add-event-listener", - "AbortSignal", - "addEventListener", - "prototype-inherited", - ), - staticEntry( - "stdlib.abort-signal.remove-event-listener", - "AbortSignal", - "removeEventListener", - "prototype-inherited", - ), - staticEntry( - "stdlib.abort-signal.dispatch-event", - "AbortSignal", - "dispatchEvent", - "prototype-inherited", - ), - outOfScopeEntry( - "stdlib.abort-signal.symbol.toStringTag", - "AbortSignal", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), - - staticEntry("stdlib.headers.constructor", "Headers", "constructor", "constructor"), - ...["append", "delete", "get", "has", "set", "getSetCookie"].map((member) => - staticEntry(`stdlib.headers.${member}`, "Headers", member, "prototype"), - ), - ...["keys", "values", "entries"].map((member) => - unsupportedEntry( - `stdlib.headers.${member}`, - "Headers", - member, - "prototype", - "native Headers iteration does not yet expose a static iterator handle", - ), - ), - staticEntry("stdlib.headers.forEach", "Headers", "forEach", "prototype"), - unsupportedEntry( - "stdlib.headers.symbol.iterator", - "Headers", - "[Symbol.iterator]", - "prototype-symbol", - "native Headers iteration does not expose an engine-free iterator handle", - ), - outOfScopeEntry( - "stdlib.headers.symbol.toStringTag", - "Headers", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), - - staticEntry("stdlib.request.constructor", "Request", "constructor", "constructor"), + responseInit: [ + { + id: "stdlib.response-init.headers", + name: "ResponseInit.headers", + conversion: "Headers, record, or sequence-of-pairs snapshot", + evidence: [fixture("static")], + }, + { + id: "stdlib.response-init.status", + name: "ResponseInit.status", + conversion: "WebIDL unsigned-short conversion followed by the 200–599 range check", + evidence: [fixture("static")], + }, + { + id: "stdlib.response-init.statusText", + name: "ResponseInit.statusText", + conversion: "WebIDL ByteString with HTTP reason-phrase validation", + evidence: [fixture("static")], + }, + ], + members: { + responseReads: [ + "ok", + "status", + "statusText", + "url", + "redirected", + "headers", + "body", + "bodyUsed", + ], + responseCalls: ["json", "text", "bytes"], + readableStreamReads: ["locked"], + readableStreamCalls: ["cancel", "getReader"], + }, + operations: [ + { + id: "stdlib.headers.constructor", + name: "Headers constructor", + kind: "constructor", + facets: ["webidl-conversion", "mutation", "identity"], + scope: "Record and iterable header pairs, including copying native Headers", + evidence: [fixture("static-request")], + }, + { + id: "stdlib.request.constructor", + name: "Request constructor", + kind: "constructor", + facets: ["webidl-conversion", "body-consumption", "error-shape"], + scope: + "Absolute URLs with method, headers, body, duplex, redirect, and signal; other RequestInit members retain explicit fences", + evidence: [fixture("static-request")], + }, ...[ "method", "url", @@ -662,276 +272,695 @@ export const NODE24_FETCH_COMPAT_PROFILE = { "body", "bodyUsed", "duplex", - "clone", - "blob", - "arrayBuffer", - "text", - "json", - "formData", - "bytes", - "attribute", - ].map((member) => - ["clone", "blob", "formData", "attribute"].includes(member) - ? unsupportedEntry( - `stdlib.request.${member}`, - "Request", - member, - "prototype", - typedInterfaceUnsupported, - ) - : staticEntry(`stdlib.request.${member}`, "Request", member, "prototype"), + ].map((member): FetchCompatOperation => ({ + id: `stdlib.request.${member}`, + name: `Request.${member}`, + kind: "property", + facets: ["property-read", "state-machine"], + scope: "Reads on native Requests within the constructor's supported RequestInit subset", + evidence: [fixture("static-request")], + })), + ...["arrayBuffer", "text", "json", "bytes"].map((member): FetchCompatOperation => ({ + id: `stdlib.request.${member}`, + name: `Request.${member}`, + kind: "method", + facets: ["body-consumption", "promise-settlement", "state-machine", "error-shape"], + evidence: [fixture("static-request")], + })), + { + id: "stdlib.response.constructor", + name: "Response constructor", + kind: "constructor", + facets: ["webidl-conversion", "body-consumption", "state-machine", "error-shape"], + scope: + "BodyInit is string, Uint8Array/Buffer, ReadableStream, null/undefined, or a checked-dynamic value that follows the supported WebIDL string conversion; ResponseInit is headers/status/statusText", + evidence: [fixture("static")], + }, + { + id: "stdlib.fetch", + name: "fetch", + kind: "function", + facets: [ + "argument-evaluation", + "webidl-conversion", + "surplus-arguments", + "promise-settlement", + "transport", + "error-shape", + ], + evidence: [fixture("static"), fixture("static-coercion"), fixture("static-network-error")], + }, + { + id: "stdlib.abort-controller.constructor", + name: "AbortController constructor", + kind: "constructor", + facets: ["argument-evaluation", "identity", "state-machine", "surplus-arguments"], + evidence: [fixture("static-controller")], + }, + { + id: "stdlib.abort-controller.signal", + name: "AbortController.signal", + kind: "property", + facets: ["identity", "property-read", "state-machine"], + evidence: [fixture("static-controller")], + }, + { + id: "stdlib.abort-controller.abort", + name: "AbortController.abort", + kind: "method", + facets: ["callback-order", "identity", "liveness", "state-machine", "surplus-arguments"], + evidence: [fixture("static-controller")], + }, + { + id: "stdlib.abort-signal.abort", + name: "AbortSignal.abort", + kind: "static-method", + facets: ["identity", "liveness", "surplus-arguments", "property-read"], + evidence: [generated("webidl-operations"), fixture("static-stream-this")], + }, + { + id: "stdlib.abort-signal.any", + name: "AbortSignal.any", + kind: "static-method", + facets: ["webidl-conversion", "missing-arguments", "surplus-arguments", "property-read"], + evidence: [generated("webidl-operations"), fixture("static-stream-this")], + }, + { + id: "stdlib.abort-signal.timeout", + name: "AbortSignal.timeout", + kind: "static-method", + facets: ["webidl-conversion", "missing-arguments", "surplus-arguments", "error-shape"], + evidence: [generated("webidl-operations"), fixture("static-stream-this")], + }, + { + id: "stdlib.abort-signal.aborted", + name: "AbortSignal.aborted", + kind: "property", + facets: ["property-read"], + evidence: [generated("webidl-operations")], + }, + { + id: "stdlib.abort-signal.reason", + name: "AbortSignal.reason", + kind: "property", + facets: ["identity", "liveness", "property-read"], + evidence: [generated("webidl-operations"), fixture("static-stream-this")], + }, + { + id: "stdlib.abort-signal.onabort", + name: "AbortSignal.onabort", + kind: "property", + facets: ["callback-order", "callback-this", "mutation", "property-read"], + evidence: [generated("abort-events"), fixture("static-listener-this")], + }, + { + id: "stdlib.abort-signal.throw-if-aborted", + name: "AbortSignal.throwIfAborted", + kind: "method", + facets: ["identity", "error-shape"], + evidence: [generated("webidl-operations"), fixture("static-abort-throw")], + }, + { + id: "stdlib.abort-signal.add-event-listener", + name: "AbortSignal.addEventListener", + kind: "method", + facets: ["webidl-conversion", "identity", "callback-order", "callback-this"], + evidence: [ + generated("abort-events"), + fixture("static-listener-this"), + fixture("static-listener-noncallable"), + ], + }, + { + id: "stdlib.abort-signal.remove-event-listener", + name: "AbortSignal.removeEventListener", + kind: "method", + facets: ["webidl-conversion", "identity", "callback-order"], + evidence: [generated("abort-events"), fixture("static-stream-this")], + }, + { + id: "stdlib.abort-signal.dispatch-event", + name: "AbortSignal.dispatchEvent", + kind: "method", + facets: ["callback-order", "callback-this", "error-shape"], + evidence: [fixture("static-dispatch-throw")], + }, + { + id: "stdlib.readable-stream.constructor", + name: "ReadableStream constructor", + kind: "constructor", + facets: ["webidl-conversion", "callback-this", "callback-order", "state-machine"], + evidence: [generated("stream-traces"), fixture("static-stream-this")], + }, + { + id: "stdlib.readable-stream.from", + name: "ReadableStream.from", + kind: "static-method", + facets: ["webidl-conversion", "missing-arguments", "surplus-arguments", "liveness"], + evidence: [generated("webidl-operations"), generated("stream-traces")], + }, + { + id: "stdlib.readable-stream.locked", + name: "ReadableStream.locked", + kind: "property", + facets: ["property-read", "state-machine"], + evidence: [generated("stream-traces")], + }, + { + id: "stdlib.readable-stream.cancel", + name: "ReadableStream.cancel", + kind: "method", + facets: ["identity", "promise-settlement", "state-machine"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream.get-reader", + name: "ReadableStream.getReader", + kind: "method", + facets: ["webidl-conversion", "identity", "state-machine", "error-shape"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream-default-reader.closed", + name: "ReadableStreamDefaultReader.closed", + kind: "property", + facets: ["promise-settlement", "property-read", "state-machine"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream-default-reader.read", + name: "ReadableStreamDefaultReader.read", + kind: "method", + facets: ["identity", "promise-settlement", "state-machine"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream-default-reader.cancel", + name: "ReadableStreamDefaultReader.cancel", + kind: "method", + facets: ["identity", "promise-settlement", "state-machine"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream-default-reader.release-lock", + name: "ReadableStreamDefaultReader.releaseLock", + kind: "method", + facets: ["promise-settlement", "state-machine", "error-shape"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream-default-controller.desired-size", + name: "ReadableStreamDefaultController.desiredSize", + kind: "property", + facets: ["property-read", "state-machine"], + evidence: [generated("stream-traces")], + }, + { + id: "stdlib.readable-stream-default-controller.enqueue", + name: "ReadableStreamDefaultController.enqueue", + kind: "method", + facets: ["identity", "liveness", "state-machine", "error-shape"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream-default-controller.close", + name: "ReadableStreamDefaultController.close", + kind: "method", + facets: ["state-machine", "error-shape"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + { + id: "stdlib.readable-stream-default-controller.error", + name: "ReadableStreamDefaultController.error", + kind: "method", + facets: ["identity", "promise-settlement", "state-machine"], + evidence: [generated("stream-traces"), fixture("static-stream")], + }, + ...["append", "delete", "get", "getSetCookie", "has", "set", "forEach"].map( + (member): FetchCompatOperation => ({ + id: `stdlib.headers.${member}`, + name: `Headers.${member}`, + kind: "method", + facets: + member === "forEach" + ? ["callback-order", "callback-this", "mutation"] + : member === "get" || member === "has" + ? ["webidl-conversion", "missing-arguments", "property-read"] + : ["webidl-conversion", "mutation", "error-shape"], + evidence: [fixture("static"), fixture("static-coercion")], + }), ), - outOfScopeEntry( - "stdlib.request.symbol.toStringTag", + ...["ok", "status", "statusText", "url", "redirected", "headers", "body", "bodyUsed"].map( + (member): FetchCompatOperation => ({ + id: `stdlib.response.${member}`, + name: `Response.${member}`, + kind: "property", + facets: ["property-read"], + evidence: [fixture("static")], + }), + ), + { + id: "stdlib.response.static.json", + name: "Response.json", + kind: "static-method", + facets: ["webidl-conversion", "error-shape"], + scope: + "data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor", + evidence: [fixture("static-response-json")], + }, + ...["json", "text", "bytes", "arrayBuffer"].map((member): FetchCompatOperation => ({ + id: `stdlib.response.${member}`, + name: `Response.${member}`, + kind: "method", + facets: ["body-consumption", "promise-settlement", "state-machine", "error-shape"], + evidence: + member === "arrayBuffer" + ? [fixture("static")] + : [fixture("static"), fixture("static-stream")], + })), + ], + inventory: { + interfaces: [ + "AbortController", + "AbortSignal", + "Headers", "Request", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), - - staticEntry("stdlib.response.constructor", "Response", "constructor", "constructor"), - staticEntry("stdlib.response.static.json", "Response", "json", "static"), - ...["error", "redirect"].map((member) => - unsupportedEntry( - `stdlib.response.static.${member}`, - "Response", - member, - "static", - "Response static constructor-object operations have no compiler lowering", - ), - ), - unsupportedEntry("stdlib.response.type", "Response", "type", "prototype", widerMemberFence), - ...["url", "redirected", "status", "ok", "statusText", "headers", "body", "bodyUsed"].map( - (member) => staticEntry(`stdlib.response.${member}`, "Response", member, "prototype"), - ), - staticEntry("stdlib.response.arrayBuffer", "Response", "arrayBuffer", "prototype"), - ...["clone", "blob", "formData"].map((member) => - unsupportedEntry( - `stdlib.response.${member}`, - "Response", - member, - "prototype", - "the native fetch runtime does not implement this Response operation", - ), - ), - ...["text", "json"].map((member) => - staticEntry(`stdlib.response.${member}`, "Response", member, "prototype"), - ), - staticEntry("stdlib.response.bytes", "Response", "bytes", "prototype"), - outOfScopeEntry( - "stdlib.response.symbol.toStringTag", "Response", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), - - staticEntry( - "stdlib.readable-stream.constructor", "ReadableStream", - "constructor", - "constructor", - ), - staticEntry("stdlib.readable-stream.from", "ReadableStream", "from", "static"), - staticEntry("stdlib.readable-stream.locked", "ReadableStream", "locked", "prototype"), - staticEntry("stdlib.readable-stream.cancel", "ReadableStream", "cancel", "prototype"), - staticEntry("stdlib.readable-stream.get-reader", "ReadableStream", "getReader", "prototype"), - ...["pipeThrough", "values"].map((member) => - unsupportedEntry( - `stdlib.readable-stream.${member}`, - "ReadableStream", - member, - "prototype", - member === "values" - ? "direct for-await consumption compiles statically; storing or driving the first-class iterator handle is unsupported" - : "the wider Web Streams graph is outside the native readable-stream slice", + "ReadableStreamDefaultReader", + "ReadableStreamDefaultController", + ], + entries: [ + staticEntry("stdlib.fetch", "globalThis", "fetch", "global"), + + staticEntry( + "stdlib.abort-controller.constructor", + "AbortController", + "constructor", + "constructor", ), - ), - ...["pipeTo", "tee"].map((member) => - unsupportedEntry( - `stdlib.readable-stream.${member}`, - "ReadableStream", - member, - "prototype", - "this operation is outside the native readable-stream implementation", + staticEntry("stdlib.abort-controller.signal", "AbortController", "signal", "prototype"), + staticEntry("stdlib.abort-controller.abort", "AbortController", "abort", "prototype"), + outOfScopeEntry( + "stdlib.abort-controller.symbol.toStringTag", + "AbortController", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, ), - ), - unsupportedEntry( - "stdlib.readable-stream.symbol.asyncIterator", - "ReadableStream", - "[Symbol.asyncIterator]", - "prototype-symbol", - "direct for-await consumption compiles statically; explicit symbol access and stored iterator handles have no compiler lowering", - ), - outOfScopeEntry( - "stdlib.readable-stream.symbol.toStringTag", - "ReadableStream", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), - unsupportedEntry( - "stdlib.readable-stream-default-reader.constructor", - "ReadableStreamDefaultReader", - "constructor", - "constructor", - "reader handles come from ReadableStream.getReader(); the constructor object is unsupported", - ), - staticEntry( - "stdlib.readable-stream-default-reader.read", - "ReadableStreamDefaultReader", - "read", - "prototype", - ), - staticEntry( - "stdlib.readable-stream-default-reader.release-lock", - "ReadableStreamDefaultReader", - "releaseLock", - "prototype", - ), - staticEntry( - "stdlib.readable-stream-default-reader.closed", - "ReadableStreamDefaultReader", - "closed", - "prototype", - ), - staticEntry( - "stdlib.readable-stream-default-reader.cancel", - "ReadableStreamDefaultReader", - "cancel", - "prototype", - ), - outOfScopeEntry( - "stdlib.readable-stream-default-reader.symbol.toStringTag", - "ReadableStreamDefaultReader", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), + unsupportedEntry( + "stdlib.abort-signal.constructor", + "AbortSignal", + "constructor", + "constructor", + "Node exposes the interface object but constructing it throws; the compiler does not preserve that constructor behavior", + ), + staticEntry("stdlib.abort-signal.abort", "AbortSignal", "abort", "static"), + staticEntry("stdlib.abort-signal.timeout", "AbortSignal", "timeout", "static"), + staticEntry("stdlib.abort-signal.any", "AbortSignal", "any", "static"), + staticEntry("stdlib.abort-signal.aborted", "AbortSignal", "aborted", "prototype"), + staticEntry("stdlib.abort-signal.reason", "AbortSignal", "reason", "prototype"), + staticEntry( + "stdlib.abort-signal.throw-if-aborted", + "AbortSignal", + "throwIfAborted", + "prototype", + ), + staticEntry("stdlib.abort-signal.onabort", "AbortSignal", "onabort", "prototype"), + staticEntry( + "stdlib.abort-signal.add-event-listener", + "AbortSignal", + "addEventListener", + "prototype-inherited", + ), + staticEntry( + "stdlib.abort-signal.remove-event-listener", + "AbortSignal", + "removeEventListener", + "prototype-inherited", + ), + staticEntry( + "stdlib.abort-signal.dispatch-event", + "AbortSignal", + "dispatchEvent", + "prototype-inherited", + ), + outOfScopeEntry( + "stdlib.abort-signal.symbol.toStringTag", + "AbortSignal", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, + ), - unsupportedEntry( - "stdlib.readable-stream-default-controller.constructor", - "ReadableStreamDefaultController", - "constructor", - "constructor", - "controller handles come from underlying-source callbacks; the constructor object is unsupported", - ), - staticEntry( - "stdlib.readable-stream-default-controller.desired-size", - "ReadableStreamDefaultController", - "desiredSize", - "prototype", - ), - staticEntry( - "stdlib.readable-stream-default-controller.close", - "ReadableStreamDefaultController", - "close", - "prototype", - ), - staticEntry( - "stdlib.readable-stream-default-controller.enqueue", - "ReadableStreamDefaultController", - "enqueue", - "prototype", - ), - staticEntry( - "stdlib.readable-stream-default-controller.error", - "ReadableStreamDefaultController", - "error", - "prototype", - ), - outOfScopeEntry( - "stdlib.readable-stream-default-controller.symbol.toStringTag", - "ReadableStreamDefaultController", - "[Symbol.toStringTag]", - "prototype-symbol", - metadataExclusion, - ), + staticEntry("stdlib.headers.constructor", "Headers", "constructor", "constructor"), + ...["append", "delete", "get", "has", "set", "getSetCookie"].map((member) => + staticEntry(`stdlib.headers.${member}`, "Headers", member, "prototype"), + ), + ...["keys", "values", "entries"].map((member) => + unsupportedEntry( + `stdlib.headers.${member}`, + "Headers", + member, + "prototype", + "native Headers iteration does not yet expose a static iterator handle", + ), + ), + staticEntry("stdlib.headers.forEach", "Headers", "forEach", "prototype"), + unsupportedEntry( + "stdlib.headers.symbol.iterator", + "Headers", + "[Symbol.iterator]", + "prototype-symbol", + "native Headers iteration does not expose an engine-free iterator handle", + ), + outOfScopeEntry( + "stdlib.headers.symbol.toStringTag", + "Headers", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, + ), - ...[ - "body", - "cache", - "credentials", - "dispatcher", - "duplex", - "headers", - "integrity", - "keepalive", - "method", - "mode", - "priority", - "redirect", - "referrer", - "referrerPolicy", - "signal", - "window", - ].map((member) => { - const id = `stdlib.fetch.request-init.${member}`; - const supported = new Set(["body", "duplex", "headers", "method", "redirect", "signal"]); - return supported.has(member) - ? staticEntry(id, "RequestInit", member, "dictionary") - : member === "dispatcher" + staticEntry("stdlib.request.constructor", "Request", "constructor", "constructor"), + ...[ + "method", + "url", + "headers", + "destination", + "referrer", + "referrerPolicy", + "mode", + "credentials", + "cache", + "redirect", + "integrity", + "keepalive", + "isReloadNavigation", + "isHistoryNavigation", + "signal", + "body", + "bodyUsed", + "duplex", + "clone", + "blob", + "arrayBuffer", + "text", + "json", + "formData", + "bytes", + "attribute", + ].map((member) => + ["clone", "blob", "formData", "attribute"].includes(member) ? unsupportedEntry( - id, - "RequestInit", + `stdlib.request.${member}`, + "Request", member, - "dictionary", - "custom dispatchers are unsupported; native environment-proxy routing uses NODE_USE_ENV_PROXY", + "prototype", + typedInterfaceUnsupported, ) - : unsupportedEntry( - id, - "RequestInit", - member, - "dictionary", - "the compiler does not preserve this RequestInit member's conversion or transport behavior", - ); - }), - ...["headers", "status", "statusText"].map((member) => - staticEntry(`stdlib.response-init.${member}`, "ResponseInit", member, "dictionary"), - ), - ], - excludedInterfaces: [ - { - name: "Blob/File/FormData bodies", - reason: - "fetch request and response bodies do not accept or produce Blob, File, or FormData values", - }, - { - name: "WritableStream/TransformStream", - reason: - "native support covers readable fetch bodies and the identity `new TransformStream()`; the WritableStream constructor, transformers, and general Web Streams graphs are unsupported", - }, - { - name: "Readable byte/BYOB streams", - reason: - "the native stream projection currently supports default readers and controllers only", - }, - { - name: "EventSource/WebSocket", - reason: "these are separate protocol clients, not fetch request/response compatibility", - }, - ], - }, -} satisfies FetchCompatProfile; + : staticEntry(`stdlib.request.${member}`, "Request", member, "prototype"), + ), + outOfScopeEntry( + "stdlib.request.symbol.toStringTag", + "Request", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, + ), -export const STATIC_RESPONSE_READS = new Set(NODE24_FETCH_COMPAT_PROFILE.members.responseReads); + staticEntry("stdlib.response.constructor", "Response", "constructor", "constructor"), + staticEntry("stdlib.response.static.json", "Response", "json", "static"), + ...["error", "redirect"].map((member) => + unsupportedEntry( + `stdlib.response.static.${member}`, + "Response", + member, + "static", + "Response static constructor-object operations have no compiler lowering", + ), + ), + unsupportedEntry("stdlib.response.type", "Response", "type", "prototype", widerMemberFence), + ...["url", "redirected", "status", "ok", "statusText", "headers", "body", "bodyUsed"].map( + (member) => staticEntry(`stdlib.response.${member}`, "Response", member, "prototype"), + ), + staticEntry("stdlib.response.arrayBuffer", "Response", "arrayBuffer", "prototype"), + ...["clone", "blob", "formData"].map((member) => + unsupportedEntry( + `stdlib.response.${member}`, + "Response", + member, + "prototype", + "the native fetch runtime does not implement this Response operation", + ), + ), + ...["text", "json"].map((member) => + staticEntry(`stdlib.response.${member}`, "Response", member, "prototype"), + ), + staticEntry("stdlib.response.bytes", "Response", "bytes", "prototype"), + outOfScopeEntry( + "stdlib.response.symbol.toStringTag", + "Response", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, + ), -export const STATIC_RESPONSE_CALLS = new Set(NODE24_FETCH_COMPAT_PROFILE.members.responseCalls); + staticEntry( + "stdlib.readable-stream.constructor", + "ReadableStream", + "constructor", + "constructor", + ), + staticEntry("stdlib.readable-stream.from", "ReadableStream", "from", "static"), + staticEntry("stdlib.readable-stream.locked", "ReadableStream", "locked", "prototype"), + staticEntry("stdlib.readable-stream.cancel", "ReadableStream", "cancel", "prototype"), + staticEntry( + "stdlib.readable-stream.get-reader", + "ReadableStream", + "getReader", + "prototype", + ), + ...["pipeThrough", "values"].map((member) => + unsupportedEntry( + `stdlib.readable-stream.${member}`, + "ReadableStream", + member, + "prototype", + member === "values" + ? "direct for-await consumption compiles statically; storing or driving the first-class iterator handle is unsupported" + : "the wider Web Streams graph is outside the native readable-stream slice", + ), + ), + ...["pipeTo", "tee"].map((member) => + unsupportedEntry( + `stdlib.readable-stream.${member}`, + "ReadableStream", + member, + "prototype", + "this operation is outside the native readable-stream implementation", + ), + ), + unsupportedEntry( + "stdlib.readable-stream.symbol.asyncIterator", + "ReadableStream", + "[Symbol.asyncIterator]", + "prototype-symbol", + "direct for-await consumption compiles statically; explicit symbol access and stored iterator handles have no compiler lowering", + ), + outOfScopeEntry( + "stdlib.readable-stream.symbol.toStringTag", + "ReadableStream", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, + ), -export const STATIC_HEADERS_CALLS = new Set( - NODE24_FETCH_COMPAT_PROFILE.inventory.entries - .filter( - (entry) => - entry.owner === "Headers" && entry.placement === "prototype" && entry.status === "static", - ) - .map((entry) => entry.member), -); + unsupportedEntry( + "stdlib.readable-stream-default-reader.constructor", + "ReadableStreamDefaultReader", + "constructor", + "constructor", + "reader handles come from ReadableStream.getReader(); the constructor object is unsupported", + ), + staticEntry( + "stdlib.readable-stream-default-reader.read", + "ReadableStreamDefaultReader", + "read", + "prototype", + ), + staticEntry( + "stdlib.readable-stream-default-reader.release-lock", + "ReadableStreamDefaultReader", + "releaseLock", + "prototype", + ), + staticEntry( + "stdlib.readable-stream-default-reader.closed", + "ReadableStreamDefaultReader", + "closed", + "prototype", + ), + staticEntry( + "stdlib.readable-stream-default-reader.cancel", + "ReadableStreamDefaultReader", + "cancel", + "prototype", + ), + outOfScopeEntry( + "stdlib.readable-stream-default-reader.symbol.toStringTag", + "ReadableStreamDefaultReader", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, + ), -export const STATIC_READABLE_STREAM_READS = new Set( - NODE24_FETCH_COMPAT_PROFILE.members.readableStreamReads, -); + unsupportedEntry( + "stdlib.readable-stream-default-controller.constructor", + "ReadableStreamDefaultController", + "constructor", + "constructor", + "controller handles come from underlying-source callbacks; the constructor object is unsupported", + ), + staticEntry( + "stdlib.readable-stream-default-controller.desired-size", + "ReadableStreamDefaultController", + "desiredSize", + "prototype", + ), + staticEntry( + "stdlib.readable-stream-default-controller.close", + "ReadableStreamDefaultController", + "close", + "prototype", + ), + staticEntry( + "stdlib.readable-stream-default-controller.enqueue", + "ReadableStreamDefaultController", + "enqueue", + "prototype", + ), + staticEntry( + "stdlib.readable-stream-default-controller.error", + "ReadableStreamDefaultController", + "error", + "prototype", + ), + outOfScopeEntry( + "stdlib.readable-stream-default-controller.symbol.toStringTag", + "ReadableStreamDefaultController", + "[Symbol.toStringTag]", + "prototype-symbol", + metadataExclusion, + ), -export const STATIC_READABLE_STREAM_CALLS = new Set( - NODE24_FETCH_COMPAT_PROFILE.members.readableStreamCalls, -); + ...[ + "body", + "cache", + "credentials", + "dispatcher", + "duplex", + "headers", + "integrity", + "keepalive", + "method", + "mode", + "priority", + "redirect", + "referrer", + "referrerPolicy", + "signal", + "window", + ].map((member) => { + const id = `stdlib.fetch.request-init.${member}`; + const supported = new Set(["body", "duplex", "headers", "method", "redirect", "signal"]); + return supported.has(member) + ? staticEntry(id, "RequestInit", member, "dictionary") + : member === "dispatcher" + ? unsupportedEntry( + id, + "RequestInit", + member, + "dictionary", + "custom dispatchers are unsupported; native environment-proxy routing uses NODE_USE_ENV_PROXY", + ) + : unsupportedEntry( + id, + "RequestInit", + member, + "dictionary", + "the compiler does not preserve this RequestInit member's conversion or transport behavior", + ); + }), + ...["headers", "status", "statusText"].map((member) => + staticEntry(`stdlib.response-init.${member}`, "ResponseInit", member, "dictionary"), + ), + ], + excludedInterfaces: [ + { + name: "Blob/File/FormData bodies", + reason: + "fetch request and response bodies do not accept or produce Blob, File, or FormData values", + }, + { + name: "WritableStream/TransformStream", + reason: + "native support covers readable fetch bodies and the identity `new TransformStream()`; the WritableStream constructor, transformers, and general Web Streams graphs are unsupported", + }, + { + name: "Readable byte/BYOB streams", + reason: + "the native stream projection currently supports default readers and controllers only", + }, + { + name: "EventSource/WebSocket", + reason: "these are separate protocol clients, not fetch request/response compatibility", + }, + ], + }, + } satisfies FetchCompatProfile; +} + +let fetchCompatProfile: ReturnType | undefined; + +/** The Node 24 fetch compatibility profile. It is built on the first call, + * so runs that do not lower a module do not construct it. */ +export function node24FetchCompatProfile(): ReturnType { + fetchCompatProfile ??= buildFetchCompatProfile(); + return fetchCompatProfile; +} + +/** The members the native static fetch handles project. */ +export interface StaticFetchMembers { + responseReads: ReadonlySet; + responseCalls: ReadonlySet; + headersCalls: ReadonlySet; + readableStreamReads: ReadonlySet; + readableStreamCalls: ReadonlySet; +} + +let staticFetchMemberSets: StaticFetchMembers | undefined; + +/** The profile's static members as sets, built on the first call. */ +export function staticFetchMembers(): StaticFetchMembers { + if (staticFetchMemberSets !== undefined) return staticFetchMemberSets; + const profile = node24FetchCompatProfile(); + staticFetchMemberSets = { + responseReads: new Set(profile.members.responseReads), + responseCalls: new Set(profile.members.responseCalls), + headersCalls: new Set( + profile.inventory.entries + .filter( + (entry) => + entry.owner === "Headers" && + entry.placement === "prototype" && + entry.status === "static", + ) + .map((entry) => entry.member), + ), + readableStreamReads: new Set(profile.members.readableStreamReads), + readableStreamCalls: new Set(profile.members.readableStreamCalls), + }; + return staticFetchMemberSets; +} diff --git a/packages/compiler/src/coverage/surface-manifest.ts b/packages/compiler/src/coverage/surface-manifest.ts index 9867a5617..b87a8065d 100644 --- a/packages/compiler/src/coverage/surface-manifest.ts +++ b/packages/compiler/src/coverage/surface-manifest.ts @@ -39,14 +39,14 @@ import { InternalCompilerError } from "../errors.js"; * byte-deterministic: entries sort by id, keys are emitted in one fixed * order, and the output carries no timestamps or absolute paths. */ import { FENCE_CODES, UNSUPPORTED, webAssemblyDiag } from "../diagnostics/diagnostic.js"; -import { NODE24_FETCH_COMPAT_PROFILE } from "../compat/fetch-profile.js"; +import { node24FetchCompatProfile } from "../compat/fetch-profile.js"; import { SUPPORTED_BUILTIN_MODULES, SUPPORTED_NODE_MODULES } from "../frontend/builtin-modules.js"; import { - AMBIENT_SURFACE_FNS, + ambientSurfaceFns, ARRAY_METHODS, BUILTIN_MODULE_CONSTS, BUILTIN_MODULE_FENCE_HINTS, - BUILTIN_MODULE_FNS, + builtinModuleFns, COLLECTION_ITERATOR_METHODS, COMPOUND_ASSIGN_OPS, STDLIB_SURFACE, @@ -88,18 +88,21 @@ export const MANIFEST_SCHEMA_VERSION = 2; /** The manifest's own scope statement: what the projection covers and — * just as load-bearing — what it deliberately leaves out, so absence is * never read as a support claim in either direction. */ -const COVERAGE_NOTES: string[] = [ - "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.", - "The ambient-nondeterminism and ambient-authority surfaces the library sidecar's determinism attestation scans ARE projected even where they lower through dedicated code paths — the Date compositions (stdlib.date.*), perf_hooks' performance.now, and the process global's ambient reads and authority calls (node-builtin.process.*) — so a determinism fence can name every surface the attestation demotes on.", - "stdlib and node-builtin member entries name surface whose LOWERED call forms are constrained (arity, argument shapes); declared call forms outside the lowered set are refused per site, with code SC2020 for standard-library and node-builtin surface.", - "Entries with status 'unsupported' describe where the named code is raised: forms of the construct outside the supported subset are refused with that code — not that every form of the named feature is refused. Supported forms appear as their own static entries where a table projects them.", - `The engine-free fetch projection targets Node ${NODE24_FETCH_COMPAT_PROFILE.target.node} with bundled Undici ${NODE24_FETCH_COMPAT_PROFILE.target.undici}. Each projected row names the differential evidence that guards it; changing the pinned Node or Undici version is an explicit profile update.`, - "The fetch profile also contains a runtime-reflected census of the selected fetch, abort, Headers, and readable-stream interfaces plus RequestInit/ResponseInit dictionary reads. Static and unsupported census rows are projected here; its explicitly out-of-scope metadata rows and adjacent-interface exclusions remain in the profile so absence is deliberate rather than ambiguous.", - "Process-level diagnostic codes are not surface entries: SC0001-SC0004 are preflight gates, SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.", - "Entry statuses are projected for the desktop targets. The mobile targets (aarch64-apple-ios, aarch64-apple-ios-simulator, aarch64-linux-android) compile library-mode archives only: the library-admissible surface (what SC4005's async_free requirement and the library link set admit) is supported there, the executable lane refuses those triples with SC3002, and no entry outside the library-admissible surface carries a mobile support claim. iOS archives build for iOS 15.0 on darwin hosts; Android archives build against NDK API level 26.", - "No scheduling metadata is published; entry ids are the stable diff keys across releases.", -]; +function coverageNotes(): string[] { + const fetchTarget = node24FetchCompatProfile().target; + return [ + "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.", + "The ambient-nondeterminism and ambient-authority surfaces the library sidecar's determinism attestation scans ARE projected even where they lower through dedicated code paths — the Date compositions (stdlib.date.*), perf_hooks' performance.now, and the process global's ambient reads and authority calls (node-builtin.process.*) — so a determinism fence can name every surface the attestation demotes on.", + "stdlib and node-builtin member entries name surface whose LOWERED call forms are constrained (arity, argument shapes); declared call forms outside the lowered set are refused per site, with code SC2020 for standard-library and node-builtin surface.", + "Entries with status 'unsupported' describe where the named code is raised: forms of the construct outside the supported subset are refused with that code — not that every form of the named feature is refused. Supported forms appear as their own static entries where a table projects them.", + `The engine-free fetch projection targets Node ${fetchTarget.node} with bundled Undici ${fetchTarget.undici}. Each projected row names the differential evidence that guards it; changing the pinned Node or Undici version is an explicit profile update.`, + "The fetch profile also contains a runtime-reflected census of the selected fetch, abort, Headers, and readable-stream interfaces plus RequestInit/ResponseInit dictionary reads. Static and unsupported census rows are projected here; its explicitly out-of-scope metadata rows and adjacent-interface exclusions remain in the profile so absence is deliberate rather than ambiguous.", + "Process-level diagnostic codes are not surface entries: SC0001-SC0004 are preflight gates, SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.", + "Entry statuses are projected for the desktop targets. The mobile targets (aarch64-apple-ios, aarch64-apple-ios-simulator, aarch64-linux-android) compile library-mode archives only: the library-admissible surface (what SC4005's async_free requirement and the library link set admit) is supported there, the executable lane refuses those triples with SC3002, and no entry outside the library-admissible surface carries a mobile support claim. iOS archives build for iOS 15.0 on darwin hosts; Android archives build against NDK API level 26.", + "No scheduling metadata is published; entry ids are the stable diff keys across releases.", + ]; +} /** Stable kebab id segment from prose: quotes dropped, the parenthetical * tail dropped, non-alphanumeric runs collapsed to one dash. */ @@ -410,7 +413,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes : "recognized module (bare and node:-prefixed specifiers)", }); } - for (const [mod, members] of Object.entries(BUILTIN_MODULE_FNS)) { + for (const [mod, members] of Object.entries(builtinModuleFns())) { for (const member of Object.keys(members!)) { add({ id: `${moduleId(mod)}.${member}`, @@ -459,10 +462,9 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes } // ── fetch/Web platform: one explicit, versioned compatibility profile ─ - const fetchTarget = - `Node ${NODE24_FETCH_COMPAT_PROFILE.target.node} / ` + - `Undici ${NODE24_FETCH_COMPAT_PROFILE.target.undici}`; - for (const operation of NODE24_FETCH_COMPAT_PROFILE.operations) { + const fetchProfile = node24FetchCompatProfile(); + const fetchTarget = `Node ${fetchProfile.target.node} / Undici ${fetchProfile.target.undici}`; + for (const operation of fetchProfile.operations) { const evidence = operation.evidence.map((item) => item.generated !== undefined ? `generated:${item.generated}` : `fixture:${item.fixture!}`, ); @@ -478,10 +480,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes `differential evidence: ${evidence.join(", ")}`, }); } - for (const options of [ - NODE24_FETCH_COMPAT_PROFILE.requestInit, - NODE24_FETCH_COMPAT_PROFILE.responseInit, - ]) { + for (const options of [fetchProfile.requestInit, fetchProfile.responseInit]) { for (const option of options) { const evidence = option.evidence.map((item) => item.generated !== undefined ? `generated:${item.generated}` : `fixture:${item.fixture!}`, @@ -497,7 +496,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes }); } } - for (const row of NODE24_FETCH_COMPAT_PROFILE.inventory.entries) { + for (const row of fetchProfile.inventory.entries) { if (row.status !== "unsupported") continue; if (row.code === undefined || row.reason === undefined) { throw new InternalCompilerError(`non-static fetch inventory row '${row.id}' is incomplete`); @@ -524,7 +523,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes // Date/perf_hooks/process rows from the one ambient table — each entry // is fenceable by construction (its row carries the reach-witnessing // libCall spellings the fence detector uses). - for (const row of AMBIENT_SURFACE_FNS) { + for (const row of ambientSurfaceFns()) { add({ id: row.id, kind: row.kind, @@ -578,7 +577,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes return { schemaVersion: MANIFEST_SCHEMA_VERSION, compilerVersion, - coverage: COVERAGE_NOTES, + coverage: coverageNotes(), entries, }; } diff --git a/packages/compiler/src/frontend/lowering/lib-boundary.ts b/packages/compiler/src/frontend/lowering/lib-boundary.ts index 5be10e997..8a264d346 100644 --- a/packages/compiler/src/frontend/lowering/lib-boundary.ts +++ b/packages/compiler/src/frontend/lowering/lib-boundary.ts @@ -57,7 +57,7 @@ import { isUnitType, typeEquals, } from "../../ir/ir.js"; -import { LIB_FN_SIGS } from "../../ir/builtin-signatures.js"; +import { libFnSignatures } from "../../ir/builtin-signatures.js"; import { REGEX_INTRINSIC_SIGS, STR_INTRINSIC_SIGS } from "../../ir/intrinsic-signatures.js"; import { unionMismatchDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js"; @@ -162,7 +162,7 @@ function enforceExprBoundary(lowerer: Lowerer, node: IrExpr): void { const kind = node.kind; if (kind === "libCall") { const e = node; - const sig = LIB_FN_SIGS[e.fn]; + const sig = libFnSignatures()[e.fn]; if (!sig) return; e.args.forEach((a, i) => { const want = sig.argTypes[i]; diff --git a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts index e5ebc6d8e..d59b234d3 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts @@ -16,7 +16,7 @@ import { strLit, varRef } from "../../ir/build.js"; import type { Lowerer } from "./lowerer.js"; import { BUILTIN_MODULE_CONSTS, - BUILTIN_MODULE_FNS, + builtinModuleFns, OBJECT_CALLABLE_VALUES, builtinConstLit, builtinModuleConstOf, @@ -430,7 +430,7 @@ export function pathModuleValue(lowerer: Lowerer, module: string, loc: SrcLoc): const add = (member: string, value: IrExpr): void => { fields.push({ key: strLit(member, loc), value: box(value) }); }; - for (const member of Object.keys(BUILTIN_MODULE_FNS[module] ?? {})) { + for (const member of Object.keys(builtinModuleFns()[module] ?? {})) { const value = lowerer.lowerBuiltinCallableValue({ module, member }, loc); if (value) add(member, value); } @@ -512,7 +512,7 @@ function moduleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr { loc, }); }; - for (const member of Object.keys(BUILTIN_MODULE_FNS[module] ?? {})) { + for (const member of Object.keys(builtinModuleFns()[module] ?? {})) { const callable = lowerer.lowerBuiltinCallableValue({ module, member }, loc); if (callable) add(member, callable); } diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index fc0fd338f..fe748bb93 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -35,14 +35,7 @@ import { esmNamedImportLinkCrash, isCjsJsFile, locOf, npmPackageNameOf } from ". import { foldedStringKeyOf, lowerDynObjectLiteral } from "./expressions/object-literals.js"; import { noteEventListeners } from "./lower-event-target.js"; import { PoisonError, newFnCtx, own } from "./lowerer.js"; -import { - NODE24_FETCH_COMPAT_PROFILE, - STATIC_HEADERS_CALLS, - STATIC_READABLE_STREAM_CALLS, - STATIC_READABLE_STREAM_READS, - STATIC_RESPONSE_CALLS, - STATIC_RESPONSE_READS, -} from "../../compat/fetch-profile.js"; +import { node24FetchCompatProfile, staticFetchMembers } from "../../compat/fetch-profile.js"; import { classGeneratorIterable } from "./class-iterables.js"; import { generatorDrain } from "./iterator-adapters.js"; @@ -434,7 +427,7 @@ function fenceRequestInitObject( : contextual ? [contextual] : []; - const rows = NODE24_FETCH_COMPAT_PROFILE.inventory.entries.filter( + const rows = node24FetchCompatProfile().inventory.entries.filter( (entry) => entry.owner === "RequestInit" && entry.placement === "dictionary", ); const shadowed = new Set(); @@ -837,7 +830,7 @@ function fetchInventoryStatus( placement: "static" | "prototype" | "prototype-symbol", ): "static" | "unsupported" | "out-of-scope" | null { return ( - NODE24_FETCH_COMPAT_PROFILE.inventory.entries.find( + node24FetchCompatProfile().inventory.entries.find( (entry) => entry.owner === owner && entry.member === member && entry.placement === placement, )?.status ?? null ); @@ -848,7 +841,7 @@ function fetchInventoryReason( member: string, placement: "static" | "prototype" | "prototype-symbol", ): string | undefined { - return NODE24_FETCH_COMPAT_PROFILE.inventory.entries.find( + return node24FetchCompatProfile().inventory.entries.find( (entry) => entry.owner === owner && entry.member === member && entry.placement === placement, )?.reason; } @@ -951,8 +944,9 @@ export function fenceStaticResponseMember( member.startsWith("[Symbol.") ? "prototype-symbol" : "prototype", ); + const projected = staticFetchMembers(); const supported = (member: string) => - use === "call" ? STATIC_RESPONSE_CALLS.has(member) : STATIC_RESPONSE_READS.has(member); + use === "call" ? projected.responseCalls.has(member) : projected.responseReads.has(member); if (members.every(supported)) return null; const member = members.find((candidate) => !supported(candidate))!; if (status(member) === "unsupported") { @@ -993,12 +987,9 @@ export function fenceStaticHeadersMember( member.startsWith("[Symbol.") ? "prototype-symbol" : "prototype", ); - if (use === "call" && members.every((member) => STATIC_HEADERS_CALLS.has(member))) { - return null; - } - const member = members.find( - (candidate) => use !== "call" || !STATIC_HEADERS_CALLS.has(candidate), - )!; + const headersCalls = staticFetchMembers().headersCalls; + if (use === "call" && members.every((member) => headersCalls.has(member))) return null; + const member = members.find((candidate) => use !== "call" || !headersCalls.has(candidate))!; if (status(member) === "unsupported") { lowerer.noLowering( `Headers.${member}`, @@ -1108,7 +1099,7 @@ function fetchInterfaceInventoryOwner( const symbol = [type.getSymbol(), type.getAliasSymbol()].find( (candidate) => candidate !== undefined && - NODE24_FETCH_COMPAT_PROFILE.inventory.interfaces.includes(ts.symbolName(candidate)) && + node24FetchCompatProfile().inventory.interfaces.includes(ts.symbolName(candidate)) && lowerer.isStdlibSymbol(candidate), ); return symbol ? { owner: ts.symbolName(symbol), symbol } : null; @@ -1123,7 +1114,7 @@ function fenceFetchObjectMembers( if (!resolved) return; const { owner, symbol } = resolved; for (const { member, node } of members) { - const row = NODE24_FETCH_COMPAT_PROFILE.inventory.entries.find( + const row = node24FetchCompatProfile().inventory.entries.find( (entry) => entry.owner === owner && entry.member === member && @@ -1136,7 +1127,7 @@ function fenceFetchObjectMembers( // Static data properties retain the ordinary checked-dynamic keyed-read // lowering. Only a method extraction loses the receiver that its direct // call bridge would preserve; non-static rows keep their inventory fence. - const operation = NODE24_FETCH_COMPAT_PROFILE.operations.find( + const operation = node24FetchCompatProfile().operations.find( (candidate) => candidate.id === row.id, ); if (row.status === "static" && operation?.kind === "property") { @@ -1235,7 +1226,7 @@ export function lowerFetchElementMethodCall( return null; } const rows = members.map((member) => - NODE24_FETCH_COMPAT_PROFILE.inventory.entries.find( + node24FetchCompatProfile().inventory.entries.find( (entry) => entry.owner === resolved.owner && entry.member === member && @@ -1397,10 +1388,11 @@ export function fenceStaticReadableStreamMember( member.startsWith("[Symbol.") ? "prototype-symbol" : "prototype", ); + const projected = staticFetchMembers(); const supported = (member: string) => use === "call" - ? STATIC_READABLE_STREAM_CALLS.has(member) - : STATIC_READABLE_STREAM_READS.has(member); + ? projected.readableStreamCalls.has(member) + : projected.readableStreamReads.has(member); if (members.every(supported)) return null; const member = members.find((candidate) => !supported(candidate))!; if (status(member) === "unsupported") { diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 0303d34d7..b5051d79d 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1129,921 +1129,940 @@ export const UTIL_TYPES_KEYS: readonly string[] = [ "isCryptoKey", ]; -export const BUILTIN_MODULE_FNS: Record< - string, - Record | undefined -> = { - fs: { - readFileSync: { fn: "fs.readFileSync", params: [STRING, STRING], result: STRING }, - // The exact-API subset is first-class. A fixed native ABI is not enough: - // rows whose Node function has behavior-bearing trailing options remain - // call-only, or function-width adaptation could discard those options - // without a diagnostic before entering the fixed runtime call. - writeFileSync: { fn: "fs.writeFileSync", params: [STRING, STRING], result: VOID }, - appendFileSync: { fn: "fs.appendFileSync", params: [STRING, STRING], result: VOID }, - existsSync: { - fn: "fs.existsSync", - params: [STRING], - result: BOOL, - valueParams: exactValueParams(STRING), - }, - mkdirSync: { fn: "fs.mkdirSync", params: [STRING], result: VOID }, - rmSync: { fn: "fs.rmSync", params: [STRING], result: VOID }, - rmdirSync: { fn: "fs.rmdirSync", params: [STRING], result: VOID }, - readdirSync: { fn: "fs.readdirSync", params: [STRING], result: arrayOf(STRING) }, - // Pattern and options are checked by the runtime (lowerFsGlobSyncCall). - globSync: { fn: "fs.globSync", params: [DYN, DYN], result: arrayOf(STRING) }, - statSync: { fn: "fs.statSync", params: [STRING], result: STATS_T }, - mkdtempSync: { fn: "fs.mkdtempSync", params: [STRING], result: STRING }, - // accessSync's omitted mode completes to 0 (F_OK) in the special case - // in lowerBuiltinModuleCall — string `defaults` can't spell a number. - accessSync: { fn: "fs.accessSync", params: [STRING, F64], result: VOID }, - unlinkSync: { - fn: "fs.unlinkSync", - params: [STRING], - result: VOID, - valueParams: exactValueParams(STRING), - }, - chmodSync: { - fn: "fs.chmodSync", - params: [STRING, F64], - result: VOID, - valueParams: exactValueParams(STRING, F64), - }, - chownSync: { - fn: "fs.chownSync", - params: [STRING, F64, F64], - result: VOID, - valueParams: exactValueParams(STRING, F64, F64), - }, - lchownSync: { - fn: "fs.lchownSync", - params: [STRING, F64, F64], - result: VOID, - valueParams: exactValueParams(STRING, F64, F64), - }, - fchownSync: { - fn: "fs.fchownSync", - params: [F64, F64, F64], - result: VOID, - valueParams: exactValueParams(F64, F64, F64), - }, - // truncateSync's omitted len completes to 0 in lowerBuiltinModuleCall, - // like accessSync's mode. - truncateSync: { fn: "fs.truncateSync", params: [STRING, F64], result: VOID }, - // The 2-argument form only: Node's mode flags (COPYFILE_EXCL, ...) - // land on the arity fence. - copyFileSync: { fn: "fs.copyFileSync", params: [STRING, STRING], result: VOID }, - // The callback form is special-cased in lowerBuiltinModuleCall: its - // Error | null parameter is program-shaped and needs an emitted - // adapter. The row still projects the static surface and routes the - // dispatch. - rename: { fn: "fs.renameCb", params: [], result: VOID }, - renameSync: { - fn: "fs.renameSync", - params: [STRING, STRING], - result: VOID, - valueParams: exactValueParams(STRING, STRING), - }, - // statSync's no-follow sibling; stats.isSymbolicLink answers what the - // follow-free snapshot saw. - // opendirSync's options (a literal { bufferSize, encoding: "utf8" }) - // lower in lowerBuiltinModuleCall; the row routes the dispatch. - opendirSync: { fn: "fs.opendirSync", params: [STRING, F64], result: DIR_T }, - // createReadStream/createWriteStream lower in lowerBuiltinModuleCall - // (path and options cross as dyn values); the rows route the dispatch. - createReadStream: { - fn: "fs.createReadStream", - params: [DYN, DYN], - result: { kind: "object", className: "%Readable" }, - }, - createWriteStream: { - fn: "fs.createWriteStream", - params: [DYN, DYN], - result: { kind: "object", className: "%Writable" }, - }, - lstatSync: { fn: "fs.lstatSync", params: [STRING], result: STATS_T }, - fstatSync: { - fn: "fs.fstatSync", - params: [F64], - result: STATS_T, - valueParams: exactValueParams(F64), - }, - fchmodSync: { - fn: "fs.fchmodSync", - params: [F64, F64], - result: VOID, - valueParams: exactValueParams(F64, F64), - }, - fsyncSync: { - fn: "fs.fsyncSync", - params: [F64], - result: VOID, - valueParams: exactValueParams(F64), - }, - fdatasyncSync: { - fn: "fs.fdatasyncSync", - params: [F64], - result: VOID, - valueParams: exactValueParams(F64), - }, - ftruncateSync: { fn: "fs.ftruncateSync", params: [F64, F64], result: VOID }, - utimesSync: { fn: "fs.utimesSync", params: [DYN, DYN, DYN], result: VOID }, - futimesSync: { fn: "fs.futimesSync", params: [DYN, DYN, DYN], result: VOID }, - lutimesSync: { fn: "fs.lutimesSync", params: [DYN, DYN, DYN], result: VOID }, - readvSync: { fn: "fs.readvSync", params: [F64, arrayOf(BYTES_U8), F64], result: F64 }, - writevSync: { fn: "fs.writevSync", params: [F64, arrayOf(BYTES_U8), F64], result: F64 }, - linkSync: { - fn: "fs.linkSync", - params: [DYN, DYN], - result: VOID, - valueParams: exactValueParams(DYN, DYN), - }, - // Options and paths are checked by the runtime (scr_fs_cp_begin). - cpSync: { - fn: "fs.cpSync", - params: [DYN, DYN, DYN], - result: VOID, - valueParams: [ - ...exactValueParams(DYN, DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - symlinkSync: { - fn: "fs.symlinkSync", - params: [DYN, DYN, DYN], - result: VOID, - valueParams: [ - ...exactValueParams(DYN, DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - readlinkSync: { - fn: "fs.readlinkSync", - params: [DYN, DYN], - result: DYN, - valueParams: [ - ...exactValueParams(DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - statfsSync: { - fn: "fs.statfsSync", - params: [DYN, DYN], - result: DYN, - valueParams: [ - ...exactValueParams(DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - // realpath(3) — Node's realpathSync (failures spell syscall "lstat", - // Node's own message shape). - realpathSync: { fn: "fs.realpathSync", params: [STRING], result: STRING }, - "realpathSync.native": { fn: "fs.realpathNativeSync", params: [STRING], result: STRING }, - // The fd pair behind spawn's fd-stdio form (the daemon-log idiom: - // openSync(logPath, "a") → spawn stdio ["ignore", fd, fd] → - // closeSync). String flags use the established two-argument path; - // inline numeric O_* expressions route through openNumericSync, with - // an optional creation mode as the third argument. - openSync: { fn: "fs.openSync", params: [STRING, STRING], result: F64 }, - // The buffer forms (fd, buffer, offset, length[, position]). The - // lowering completes an omitted or literal-null position to -1 (the - // runtime's current-offset sentinel); numeric positions read without - // advancing the fd. The options-object and bigint forms still fence. - readSync: { fn: "fs.readSync", params: [F64, BYTES_U8, F64, F64, F64], result: F64 }, - // Entirely special-cased: the classic Buffer window and utf8 string - // overloads lower to separate fixed-width runtime ABIs. As with - // readSync, -1 is the current-offset sentinel. - writeSync: { fn: "fs.writeSync", params: [F64, BYTES_U8, F64, F64, F64], result: F64 }, - closeSync: { - fn: "fs.closeSync", - params: [F64], - result: VOID, - valueParams: exactValueParams(F64), - }, - // Entirely special-cased (lowerFsWatchCall — the callback needs an - // adapter per listener shape); this entry only routes the dispatch. - watch: { fn: "fs.watch", params: [], result: VOID }, - }, - "fs/promises": { - // The sync operations behind already-settled promises: failures - // REJECT (catchable at the await) instead of throwing. readFile is - // utf8-fenced exactly like readFileSync (same special case below). - readFile: { - fn: "fsp.readFile", - params: [STRING, STRING], - result: { kind: "promise", inner: STRING }, - }, - writeFile: { - fn: "fsp.writeFile", - params: [STRING, STRING], - result: { kind: "promise", inner: VOID }, - }, - mkdir: { fn: "fsp.mkdir", params: [STRING], result: { kind: "promise", inner: VOID } }, - readdir: { - fn: "fsp.readdir", - params: [STRING], - result: { kind: "promise", inner: arrayOf(STRING) }, - }, - rm: { fn: "fsp.rm", params: [STRING], result: { kind: "promise", inner: VOID } }, - // The async iterator lowers only as a for await head (lowerFspGlobCall). - glob: { fn: "fsp.globStrings", params: [DYN, DYN], result: arrayOf(STRING) }, - stat: { fn: "fsp.stat", params: [STRING], result: { kind: "promise", inner: STATS_T } }, - realpath: { fn: "fsp.realpath", params: [STRING], result: { kind: "promise", inner: STRING } }, - lstat: { fn: "fsp.lstat", params: [STRING], result: { kind: "promise", inner: STATS_T } }, - opendir: { - fn: "fsp.opendir", - params: [STRING, F64], - result: { kind: "promise", inner: DIR_T }, - }, - unlink: { - fn: "fsp.unlink", - params: [STRING], - result: { kind: "promise", inner: VOID }, - valueParams: exactValueParams(STRING), - }, - chmod: { - fn: "fsp.chmod", - params: [STRING, F64], - result: { kind: "promise", inner: VOID }, - valueParams: exactValueParams(STRING, F64), - }, - chown: { - fn: "fsp.chown", - params: [STRING, F64, F64], - result: { kind: "promise", inner: VOID }, - valueParams: exactValueParams(STRING, F64, F64), - }, - lchown: { - fn: "fsp.lchown", - params: [STRING, F64, F64], - result: { kind: "promise", inner: VOID }, - valueParams: exactValueParams(STRING, F64, F64), - }, - // access' omitted mode and truncate's omitted len complete to 0 in - // lowerBuiltinModuleCall, like their sync spellings. - access: { fn: "fsp.access", params: [STRING, F64], result: { kind: "promise", inner: VOID } }, - truncate: { - fn: "fsp.truncate", - params: [STRING, F64], - result: { kind: "promise", inner: VOID }, - }, - // String data; Uint8Array/Buffer data routes to fsp.appendFileBytes in - // lowerBuiltinModuleCall. - appendFile: { - fn: "fsp.appendFile", - params: [STRING, STRING], - result: { kind: "promise", inner: VOID }, - }, - // The 2-argument form only, like copyFileSync. - copyFile: { - fn: "fsp.copyFile", - params: [STRING, STRING], - result: { kind: "promise", inner: VOID }, - }, - mkdtemp: { fn: "fsp.mkdtemp", params: [STRING], result: { kind: "promise", inner: STRING } }, - rmdir: { fn: "fsp.rmdir", params: [STRING], result: { kind: "promise", inner: VOID } }, - link: { - fn: "fsp.link", - params: [DYN, DYN], - result: { kind: "promise", inner: VOID }, - valueParams: exactValueParams(DYN, DYN), - }, - cp: { - fn: "fsp.cp", - params: [DYN, DYN, DYN], - result: { kind: "promise", inner: VOID }, - valueParams: [ - ...exactValueParams(DYN, DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - symlink: { - fn: "fsp.symlink", - params: [DYN, DYN, DYN], - result: { kind: "promise", inner: VOID }, - valueParams: [ - ...exactValueParams(DYN, DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - readlink: { - fn: "fsp.readlink", - params: [DYN, DYN], - result: { kind: "promise", inner: DYN }, - valueParams: [ - ...exactValueParams(DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - statfs: { - fn: "fsp.statfs", - params: [DYN, DYN], - result: { kind: "promise", inner: DYN }, - valueParams: [ - ...exactValueParams(DYN), - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - utimes: { fn: "fsp.utimes", params: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, - lutimes: { - fn: "fsp.lutimes", - params: [DYN, DYN, DYN], - result: { kind: "promise", inner: VOID }, - }, - rename: { - fn: "fsp.rename", - params: [STRING, STRING], - result: { kind: "promise", inner: VOID }, - valueParams: exactValueParams(STRING, STRING), - }, - // open's optional flags/mode completion is special-cased in - // lowerBuiltinModuleCall; this row routes all import spellings and - // gives coverage the static member. - open: { - fn: "fsp.open", - params: [STRING, STRING, F64], - result: { kind: "promise", inner: FILEHANDLE_T }, - valueParams: [ - { mode: "required", type: STRING }, - { mode: "optional", type: STRING, defaultValue: "r" }, - { mode: "optional", type: F64, defaultValue: 0o666 }, - ], - }, - }, - // The bare module's POSIX-target binding; a win32 target rebinds it to - // the win32 table (builtinModuleFnsOf — Node on Windows IS path.win32). - path: PATH_MODULE_FNS, - // Real Node modules ("node:path/posix" resolves), and the receivers of - // `path.posix.` / `path.win32.` through the namespace - // chain (builtinNamespaceModuleOf composes "path" + ".posix" to these - // keys). Both namespaces carry the FULL surface on every platform, - // Node's own rule: each answers its own platform's semantics anywhere. - "path/posix": PATH_MODULE_FNS, - "path/win32": PATH_WIN32_MODULE_FNS, - os: { - // One platform implementation: os.platform() === process.platform. - platform: { fn: "process.platform", params: [], result: STRING, valueParams: [] }, - homedir: { fn: "os.homedir", params: [], result: STRING, valueParams: [] }, - tmpdir: { fn: "os.tmpdir", params: [], result: STRING, valueParams: [] }, - // uname(2)'s release field — Node's own implementation. - release: { fn: "os.release", params: [], result: STRING, valueParams: [] }, - // uname(2)'s sysname field ("Darwin", "Linux", "Windows_NT") — the - // libFn/runtime pair predates this entry (the portless surface used - // them); this row makes the MODULE call reach them (test/common's - // isAIX/isIBMi getters are the canonical callers). - type: { fn: "os.type", params: [], result: STRING, valueParams: [] }, - // Total physical memory in bytes — same predating-pair story. - totalmem: { fn: "os.totalmem", params: [], result: F64, valueParams: [] }, - // uv_available_parallelism: the CPUs this process may use (affinity, - // then cgroup quota on Linux), at least 1. - availableParallelism: { - fn: "os.availableParallelism", - params: [], - result: F64, - valueParams: [], - }, - // os.arch() IS process.arch, like platform(). - arch: { fn: "process.arch", params: [], result: STRING, valueParams: [] }, - hostname: { fn: "os.hostname", params: [], result: STRING, valueParams: [] }, - // uv_os_uname's machine and version fields, read once at module load - // in Node (they never change while a process runs). - machine: { fn: "os.machine", params: [], result: STRING, valueParams: [] }, - version: { fn: "os.version", params: [], result: STRING, valueParams: [] }, - endianness: { fn: "os.endianness", params: [], result: STRING, valueParams: [] }, - freemem: { fn: "os.freemem", params: [], result: F64, valueParams: [] }, - uptime: { fn: "os.uptime", params: [], result: F64, valueParams: [] }, - loadavg: { fn: "os.loadavg", params: [], result: arrayOf(F64), valueParams: [] }, - // An omitted (or undefined) pid is 0, the current process - // (lowerOsGetPriorityCall completes it). - getPriority: { - fn: "os.getPriority", - params: [F64], - result: F64, - valueParams: [{ mode: "optional", type: F64, defaultValue: 0 }], - }, - // A one-argument call is setPriority(0, priority) - // (lowerOsSetPriorityCall completes it). - setPriority: { fn: "os.setPriority", params: [F64, F64], result: VOID }, - // Special-cased (lowerOsCpusCall): the call site's CpuInfo[] assembles - // from one uv_cpu_info snapshot. - cpus: { fn: "os.cpusTake", params: [], result: VOID }, - // Entirely special-cased (lowerOsNetworkInterfacesCall): the result is - // the call site's mapped Dict shape, verified - // structurally there — this entry only routes the dispatch. - networkInterfaces: { fn: "os.networkInterfaces", params: [], result: VOID }, - // Special-cased too (lowerOsUserInfoCall): the call site's mapped - // UserInfo record assembles field-by-field from scalar libCalls. - userInfo: { fn: "os.userName", params: [], result: VOID }, - }, - crypto: { - randomUUID: { - fn: "crypto.randomUUID", - params: [], - result: STRING, - // As a value (uuid's `export default { randomUUID }`), the optional - // options argument goes through the validating entry point. - valueFn: "crypto.randomUUIDOpts", - valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], - }, - // A real Buffer result. The composed randomBytes(n).toString(enc) - // form KEEPS its one-libCall lowering (lowerCryptoComposedCall runs - // first and the Buffer never materializes there); this entry covers - // the bare calls and non-composed uses. - randomBytes: { - fn: "crypto.randomBytes", - params: [F64], - result: BYTES_U8, - valueParams: exactValueParams(F64), - }, - // The crypto utility surface is special-cased for overloads and - // string/Buffer splits in lowerCryptoModuleCall. These rows carry the - // canonical signatures for manifest projection and fallback dispatch. - createHash: { fn: "crypto.hashNew", params: [STRING], result: CRYPTOHASH_T }, - createHmac: { fn: "crypto.hmacNewStr", params: [STRING, STRING], result: CRYPTOHMAC_T }, - hash: { fn: "crypto.hashDigestStr", params: [STRING, STRING, STRING], result: STRING }, - timingSafeEqual: { fn: "crypto.timingSafeEqual", params: [BYTES_U8, BYTES_U8], result: BOOL }, - randomFillSync: { fn: "crypto.randomFill", params: [BYTES_U8, F64, F64], result: BYTES_U8 }, - getRandomValues: { fn: "crypto.getRandomValues", params: [BYTES_U8], result: BYTES_U8 }, - randomInt: { fn: "crypto.randomInt", params: [F64, F64], result: F64 }, - randomFill: { fn: "crypto.randomFillCb", params: [], result: VOID }, - randomUUIDv7: { - fn: "crypto.randomUUIDv7", - params: [DYN], - result: STRING, - valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], - }, - 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 }, - diffieHellman: { fn: "crypto.diffieHellman", params: [], result: DYN }, - pbkdf2Sync: { - fn: "crypto.pbkdf2", - params: [BYTES_U8, BYTES_U8, F64, F64, STRING], - result: BYTES_U8, - }, - pbkdf2: { fn: "crypto.pbkdf2Cb", params: [], result: VOID }, - hkdfSync: { - fn: "crypto.hkdf", - params: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64], - result: DYN, - }, - hkdf: { fn: "crypto.hkdfCb", params: [], result: VOID }, - scryptSync: { fn: "crypto.scrypt", params: [BYTES_U8, BYTES_U8, F64, DYN], result: BYTES_U8 }, - scrypt: { fn: "crypto.scryptCb", params: [], result: VOID }, - // Checked-dynamic arguments; the result is a native Cipheriv or - // Decipheriv handle (lowerCryptoModuleCall). - createCipheriv: { fn: "crypto.cipherNew", params: [BOOL, DYN, DYN, DYN, DYN], result: DYN }, - createDecipheriv: { fn: "crypto.cipherNew", params: [BOOL, DYN, DYN, DYN, DYN], result: DYN }, - // 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 }, - // Prime checks and generation (lowerPrimeCall): checked-dynamic - // arguments, results converted to the call site's type. - checkPrimeSync: { fn: "crypto.checkPrimeSync", params: [], result: DYN }, - // Checked-dynamic arguments, the result converted to the call site's - // type (lowerPrimeCall). - getCipherInfo: { fn: "crypto.getCipherInfo", params: [], result: DYN }, - checkPrime: { fn: "crypto.checkPrimeCb", params: [], result: VOID }, - generatePrimeSync: { fn: "crypto.generatePrimeSync", params: [], result: DYN }, - generatePrime: { fn: "crypto.generatePrimeCb", params: [], result: VOID }, - }, - zlib: { - // The default-options convenience family is special-cased in - // lowerBuiltinModuleCall: string inputs UTF-8 encode, callback forms - // carry program-shaped closures, and explicit options fence per site. - // native-toolchain.ts links libz only when these appear on the IR. - deflate: { fn: "zlib.deflateCb", params: [], result: VOID }, - deflateSync: { fn: "zlib.deflateSync", params: [BYTES_U8], result: BYTES_U8 }, - inflate: { fn: "zlib.inflateCb", params: [], result: VOID }, - inflateSync: { fn: "zlib.inflateSync", params: [BYTES_U8], result: BYTES_U8 }, - deflateRaw: { fn: "zlib.deflateRawCb", params: [], result: VOID }, - deflateRawSync: { fn: "zlib.deflateRawSync", params: [BYTES_U8], result: BYTES_U8 }, - inflateRaw: { fn: "zlib.inflateRawCb", params: [], result: VOID }, - inflateRawSync: { fn: "zlib.inflateRawSync", params: [BYTES_U8], result: BYTES_U8 }, - gzip: { fn: "zlib.gzipCb", params: [], result: VOID }, - gzipSync: { fn: "zlib.gzipSync", params: [BYTES_U8], result: BYTES_U8 }, - gunzip: { fn: "zlib.gunzipCb", params: [], result: VOID }, - gunzipSync: { fn: "zlib.gunzipSync", params: [BYTES_U8], result: BYTES_U8 }, - unzip: { fn: "zlib.unzipCb", params: [], result: VOID }, - unzipSync: { fn: "zlib.unzipSync", params: [BYTES_U8], result: BYTES_U8 }, - crc32: { fn: "zlib.crc32", params: [], result: F64 }, - // Brotli (special-cased in lowerZlibModuleCall): every form takes the - // options entries with Node's BROTLI_ENCODE or BROTLI_DECODE mode. - brotliCompress: { fn: "zlib.cbOpts", params: [], result: VOID }, - brotliCompressSync: { fn: "zlib.syncOpts", params: [], result: BYTES_U8 }, - brotliDecompress: { fn: "zlib.cbOpts", params: [], result: VOID }, - brotliDecompressSync: { fn: "zlib.syncOpts", params: [], result: BYTES_U8 }, - // The stream factories (special-cased in lowerZlibModuleCall): each - // answers a runtime Transform stream over the zlib engine. - createDeflate: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - createInflate: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - createGzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - createGunzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - createDeflateRaw: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - createInflateRaw: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - createUnzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - createBrotliCompress: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - createBrotliDecompress: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, - }, - url: { - fileURLToPath: { - fn: "url.fileURLToPathOptions", - params: [DYN, DYN], - result: STRING, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - fileURLToPathBuffer: { - fn: "url.fileURLToPathBuffer", - params: [DYN, DYN], - result: BYTES_U8, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - pathToFileURL: { - fn: "url.pathToFileURLChecked", - params: [DYN, DYN], - result: URL_T, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - // The legacy API and the domain pair (lowerLegacyUrlCall): arguments - // cross as dyn values for Node's own validators; url.parse() answers - // the checked-dynamic Url instance. - parse: { fn: "url.legacyParse", params: [DYN, DYN, DYN], result: DYN }, - format: { fn: "url.legacyFormat", params: [DYN, DYN], result: STRING }, - resolve: { fn: "url.legacyResolve", params: [DYN, DYN], result: STRING }, - domainToASCII: { fn: "url.domainToASCII", params: [DYN], result: STRING }, - domainToUnicode: { fn: "url.domainToUnicode", params: [DYN], result: STRING }, - urlToHttpOptions: { fn: "url.toHttpOptions", params: [DYN], result: DYN }, - }, - child_process: { - // spawnSync's and spawn's call completions are entirely special-cased - // (an omitted args list completes to an empty string[]; spawnSync - // accepts its documented literal options, while spawn supports the - // stdio/detached/env/cwd/windowsHide/shell subset — see - // lowerBuiltinModuleCall); the - // entries carry the canonical shapes. - spawnSync: { fn: "cp.spawnSync", params: [STRING, arrayOf(STRING)], result: SPAWNRES_T }, - spawn: { fn: "cp.spawn", params: [STRING, arrayOf(STRING)], result: CHILD_T }, - fork: { fn: "cp.fork", params: [STRING, arrayOf(STRING)], result: CHILD_T }, - // execFileSync(file, args?, options?) and execSync(command, options?) - // share the cp.execSync runtime entry (execSync sets the shell flag); - // both call completions are special-cased in lowerBuiltinModuleCall. - // The entries carry canonical shapes for the coverage/fence machinery. - execFileSync: { fn: "cp.execSync", params: [STRING, arrayOf(STRING)], result: STRING }, - execSync: { fn: "cp.execSync", params: [STRING], result: STRING }, - }, - // node:util lowers through dedicated paths: promisify - // only in the const-binding-over-execFile shape — recognized in - // lowerVarDecl / collectGlobals BEFORE any call lowering runs — and - // inspect/format/formatWithOptions plus parseArgs through the util spoke - // (lower-inspect.ts: per-type synthesized traversal helpers, - // compile-time format strings, checked-dynamic config/result for - // parseArgs), which both dispatch paths try before the generic table. - // parseArgs is tabled as well so the generated surface manifest records - // the dedicated static member; the spoke owns its optional arity and - // call-site-specific result type before this canonical signature is used. - util: { - styleText: { - fn: "util.styleText", - params: [DYN, DYN, DYN], - result: STRING, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - isDeepStrictEqual: { - fn: "util.isDeepStrictEqual", - params: [DYN, DYN, DYN], - result: BOOL, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - parseArgs: { fn: "util.parseArgs", params: [DYN], result: DYN }, - parseEnv: { fn: "util.parseEnv", params: [DYN], result: DYN }, - getSystemErrorName: { - fn: "util.getSystemErrorName", - params: [DYN], - result: STRING, - valueParams: exactValueParams(DYN), - }, - getSystemErrorMessage: { - fn: "util.getSystemErrorMessage", - params: [DYN], - result: STRING, - valueParams: exactValueParams(DYN), - }, - getSystemErrorMap: { fn: "util.systemErrorEntries", params: [], result: DYN }, - stripVTControlCharacters: { - fn: "util.stripVTControlCharacters", - params: [DYN], - result: STRING, - valueParams: exactValueParams(DYN), - }, - toUSVString: { - fn: "util.toUSVString", - params: [DYN], - result: STRING, - valueParams: exactValueParams(DYN), - }, - // The function helpers take checked-dynamic arguments and answer native - // dyn functions, which a static function slot checks when it receives - // one (lowerUtilCallableCall in the util spoke). - inherits: { - fn: "util.inherits", - params: [DYN, DYN], - result: VOID, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "required", type: DYN }, - ], - }, - deprecate: { - fn: "util.deprecate", - params: [DYN, DYN, DYN, DYN], - result: DYN, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - callbackify: { - fn: "util.callbackify", - params: [DYN], - result: DYN, - valueParams: [{ mode: "required", type: DYN }], - }, - debuglog: { - fn: "util.debuglog", - params: [DYN, DYN], - result: DYN, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - debug: { - fn: "util.debuglog", - params: [DYN, DYN], - result: DYN, - valueParams: [ - { mode: "required", type: DYN }, - { mode: "optional", type: DYN, defaultValue: undefined }, - ], - }, - }, - // The util/types spoke supplies the probe name to the shared brand check. - "util/types": { - isAnyArrayBuffer: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isArrayBuffer: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isArrayBufferView: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isDataView: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isTypedArray: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isUint8Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isUint8ClampedArray: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isUint16Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isUint32Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isInt8Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isInt16Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isInt32Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isFloat32Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isFloat64Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isMap: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isSet: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isWeakMap: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isWeakSet: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isRegExp: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isNativeError: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isPromise: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isProxy: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isAsyncFunction: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isGeneratorFunction: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isGeneratorObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isDate: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isSharedArrayBuffer: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isMapIterator: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isSetIterator: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isKeyObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isNumberObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isStringObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isBooleanObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isBigIntObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isSymbolObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isBoxedPrimitive: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isBigInt64Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isBigUint64Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isFloat16Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isCryptoKey: { fn: "util.typeIs", params: [DYN], result: BOOL }, - isExternal: { fn: "util.typeIs", params: [DYN], result: BOOL }, - }, - buffer: { - isAscii: { fn: "buffer.isAscii", params: [DYN], result: BOOL }, - isUtf8: { fn: "buffer.isUtf8", params: [DYN], result: BOOL }, - transcode: { fn: "buffer.transcode", params: [DYN, DYN, DYN], result: BYTES_U8 }, - }, - tty: { - isatty: { fn: "process.isTTY", params: [F64], result: BOOL }, - }, - process: { - loadEnvFile: { fn: "process.loadEnvFile", params: [DYN], result: VOID }, - }, - // node:string_decoder's surface is the StringDecoder CLASS — new/write/ - // end are special-cased (lowerNew + lowerStringDecoderMethodCall); no - // function members exist to table. - string_decoder: {}, - // node:querystring — the legacy query-string codec (NOT URLSearchParams; - // the escaping and '+' rules differ). escape/unescape ride the table - // path directly; parse and stringify are special-cased in - // lowerBuiltinModuleCall (parse's result is the call site's mapped - // ParsedUrlQuery dictionary — the networkInterfaces verification stance - // — and its sep/eq/options complete there; stringify's object argument - // crosses as a dyn value). decode/encode are Node's own aliases of - // parse/stringify (`const decode = parse` in lib/querystring.js) and - // route to the same special cases; the entries carry canonical shapes. - querystring: { - parse: { fn: "qs.parse", params: [STRING], result: VOID }, - decode: { fn: "qs.parse", params: [STRING], result: VOID }, - stringify: { fn: "qs.stringify", params: [DYN], result: STRING }, - encode: { fn: "qs.stringify", params: [DYN], result: STRING }, - escape: { - fn: "qs.escape", - params: [STRING], - result: STRING, - valueParams: [{ mode: "required", type: STRING }], - }, - unescape: { - fn: "qs.unescape", - params: [STRING], - result: STRING, - valueParams: [{ mode: "required", type: STRING }], - }, - }, - // node:punycode (deprecated): punycode.js 2.1.0's conversions over - // strings (scr_url.c). ucs2.decode/encode are members of the module's - // ucs2 object, dispatched as module members (builtinMemberOf). The - // DEP0040 report rides the module's load sites (punycodeLoadWarning). - punycode: { - decode: { fn: "punycode.decode", params: [STRING], result: STRING }, - encode: { fn: "punycode.encode", params: [STRING], result: STRING }, - toASCII: { fn: "punycode.toASCII", params: [STRING], result: STRING }, - toUnicode: { fn: "punycode.toUnicode", params: [STRING], result: STRING }, - "ucs2.decode": { fn: "punycode.ucs2Decode", params: [STRING], result: arrayOf(F64) }, - "ucs2.encode": { fn: "punycode.ucs2Encode", params: [arrayOf(F64)], result: STRING }, - }, - // node:constants (deprecated) is one frozen object (Object.assign of - // os.constants' groups, fs.constants and crypto.constants): bindings of - // the module read it as a checked-dynamic value (builtinConstantsObject). - constants: {}, - // node:readline: createInterface's options are entirely special-cased - // (exactly { input: process.stdin, output?: process.stdout } — see - // lowerBuiltinModuleCall); the entry carries the canonical shape. The - // interface's question/close/on lower through - // lowerReadlineMethodCall. - readline: { - createInterface: { fn: "rl.create", params: [], result: F64 }, - }, - // node:readline/promises: the same Interface with the promise question - // (lowerReadlineMethodCall tells the forms apart by the call's result). - "readline/promises": { - createInterface: { fn: "rl.create", params: [], result: F64 }, - }, - // node:net and node:http lower ENTIRELY through the server spoke - // (lower-server.ts — every call shape is special-cased: closures, the - // optional connect host, writeHead's literal headers); the member list - // is the lower-server dispatch there. The modules still need keys here so future - // table entries have a home, and so the "recognized module, unlowered - // member" fence wording applies. - net: { - // The process-wide happy-eyeballs attempt budget: one runtime double - // in the core unit (never links scr_net.c by itself), default 250ms - // like Node's. - getDefaultAutoSelectFamilyAttemptTimeout: { - fn: "net.getAutoSelTimeout", - params: [], - result: F64, - }, - setDefaultAutoSelectFamilyAttemptTimeout: { - fn: "net.setAutoSelTimeout", - params: [F64], - result: VOID, - }, - // Node's IPv4Reg / IPv6Reg tests over String(input). - isIP: { fn: "net.isIP", params: [DYN], result: F64 }, - isIPv4: { fn: "net.isIPv4", params: [DYN], result: BOOL }, - isIPv6: { fn: "net.isIPv6", params: [DYN], result: BOOL }, - }, - http: {}, - // node:tls and node:https ride the same spoke (tls.createServer's - // options + secureConnection handler, https.createServer, and the - // https client's rejectUnauthorized/ca options are all special-cased - // in lower-server.ts). - tls: {}, - https: {}, - // node:dgram and node:dns lower ENTIRELY through the dgram spoke - // (lower-dgram.ts — createSocket's option literal, dns.lookup's - // options + callback, and the Socket method surface are all - // special-cased); the keys exist for the same reason net/http's do. - dgram: {}, - dns: {}, - // node:dns/promises rides the same spoke (lookup's promise form). - "dns/promises": {}, - // node:v8: getHeapStatistics answers allocator-derived figures in - // Node's shape (builtins/v8.ts — the entry carries the canonical - // shape); every other member fences with the by-design reason - // (builtinFenceHintOf). - v8: { - getHeapStatistics: { fn: "process.memoryUsageTake", params: [], result: VOID }, - }, - // node:stream/web re-exports the web stream globals (the same objects); - // its members lower through the globals' own paths. - "stream/web": {}, - // node:assert lowers ENTIRELY through the assert spoke (lower-assert.ts - // — every call shape is special-cased: optional messages complete, the - // comparisons pick per-type libCalls, deep equality synthesizes - // helpers); the keys exist so unlowered members fence module-qualified - // with the hints below. - assert: {}, - "assert/strict": {}, - // node:test lowers ENTIRELY through the test spoke (lower-test.ts — - // registrations, suites, hooks, the TestContext surface); the key - // exists so unlowered members fence module-qualified. - test: {}, - // node:stream/promises lowers through the stream spoke too - // (lower-stream.ts — finished/pipeline's promise forms special-case - // their stream arguments exactly like the callback forms); the key - // exists so unlowered members fence module-qualified. - "stream/promises": {}, - // node:stream/consumers rides the stream spoke as well (text/json/ - // buffer/arrayBuffer special-case their stream argument); blob fences - // module-qualified with the pointed hint below. - "stream/consumers": {}, - // node:timers/promises — the delay-only setTimeout and bare setImmediate - // (void promises the shared timer heap settles), and scheduler.wait / - // scheduler.yield, which Node defines as setTimeout(delay, undefined, - // options) and setImmediate(). The arity completion (omitted delay = - // Node's 1ms) and the value/options fences are special-cased in - // lowerBuiltinModuleCall; these entries carry the canonical shapes and - // route the dispatch. - "timers/promises": { - setTimeout: { fn: "tp.setTimeout", params: [F64], result: { kind: "promise", inner: VOID } }, - setImmediate: { fn: "tp.setImmediate", params: [], result: { kind: "promise", inner: VOID } }, - "scheduler.wait": { - fn: "tp.setTimeout", - params: [F64], - result: { kind: "promise", inner: VOID }, - }, - "scheduler.yield": { - fn: "tp.setImmediate", - params: [], - result: { kind: "promise", inner: VOID }, - }, - }, - // node:diagnostics_channel — the pub/sub core. channel() answers an f64 - // handle (type-mapper.ts maps Channel to F64); the subscriber arguments box - // into the checked-dynamic tree (special-cased in lowerBuiltinModuleCall — the entries - // carry canonical shapes and route the dispatch). The Channel method - // surface lowers through lowerDiagnosticsChannelMethodCall/lowerDiagnosticsChannelProperty. - diagnostics_channel: { - channel: { fn: "dc.channel", params: [STRING], result: F64 }, - subscribe: { fn: "dc.subscribe", params: [STRING, DYN], result: VOID }, - unsubscribe: { fn: "dc.unsubscribe", params: [STRING, DYN], result: BOOL }, - hasSubscribers: { fn: "dc.hasSubscribers", params: [STRING], result: BOOL }, - // The string form's canonical shape; the collection form (an object - // literal of five Channels) is special-cased in lowerBuiltinModuleCall. - tracingChannel: { fn: "dc.tracingChannel", params: [STRING], result: F64 }, - }, -}; +type BuiltinModuleFnTable = Record | undefined>; + +let builtinModuleFnTable: BuiltinModuleFnTable | undefined; + +/** Every builtin module's lowered function members, keyed by module and + * member. The table is built on the first call, so runs that do not lower + * a module do not construct it. */ +export function builtinModuleFns(): BuiltinModuleFnTable { + builtinModuleFnTable ??= buildBuiltinModuleFns(); + return builtinModuleFnTable; +} + +function buildBuiltinModuleFns(): BuiltinModuleFnTable { + return { + fs: { + readFileSync: { fn: "fs.readFileSync", params: [STRING, STRING], result: STRING }, + // The exact-API subset is first-class. A fixed native ABI is not enough: + // rows whose Node function has behavior-bearing trailing options remain + // call-only, or function-width adaptation could discard those options + // without a diagnostic before entering the fixed runtime call. + writeFileSync: { fn: "fs.writeFileSync", params: [STRING, STRING], result: VOID }, + appendFileSync: { fn: "fs.appendFileSync", params: [STRING, STRING], result: VOID }, + existsSync: { + fn: "fs.existsSync", + params: [STRING], + result: BOOL, + valueParams: exactValueParams(STRING), + }, + mkdirSync: { fn: "fs.mkdirSync", params: [STRING], result: VOID }, + rmSync: { fn: "fs.rmSync", params: [STRING], result: VOID }, + rmdirSync: { fn: "fs.rmdirSync", params: [STRING], result: VOID }, + readdirSync: { fn: "fs.readdirSync", params: [STRING], result: arrayOf(STRING) }, + // Pattern and options are checked by the runtime (lowerFsGlobSyncCall). + globSync: { fn: "fs.globSync", params: [DYN, DYN], result: arrayOf(STRING) }, + statSync: { fn: "fs.statSync", params: [STRING], result: STATS_T }, + mkdtempSync: { fn: "fs.mkdtempSync", params: [STRING], result: STRING }, + // accessSync's omitted mode completes to 0 (F_OK) in the special case + // in lowerBuiltinModuleCall — string `defaults` can't spell a number. + accessSync: { fn: "fs.accessSync", params: [STRING, F64], result: VOID }, + unlinkSync: { + fn: "fs.unlinkSync", + params: [STRING], + result: VOID, + valueParams: exactValueParams(STRING), + }, + chmodSync: { + fn: "fs.chmodSync", + params: [STRING, F64], + result: VOID, + valueParams: exactValueParams(STRING, F64), + }, + chownSync: { + fn: "fs.chownSync", + params: [STRING, F64, F64], + result: VOID, + valueParams: exactValueParams(STRING, F64, F64), + }, + lchownSync: { + fn: "fs.lchownSync", + params: [STRING, F64, F64], + result: VOID, + valueParams: exactValueParams(STRING, F64, F64), + }, + fchownSync: { + fn: "fs.fchownSync", + params: [F64, F64, F64], + result: VOID, + valueParams: exactValueParams(F64, F64, F64), + }, + // truncateSync's omitted len completes to 0 in lowerBuiltinModuleCall, + // like accessSync's mode. + truncateSync: { fn: "fs.truncateSync", params: [STRING, F64], result: VOID }, + // The 2-argument form only: Node's mode flags (COPYFILE_EXCL, ...) + // land on the arity fence. + copyFileSync: { fn: "fs.copyFileSync", params: [STRING, STRING], result: VOID }, + // The callback form is special-cased in lowerBuiltinModuleCall: its + // Error | null parameter is program-shaped and needs an emitted + // adapter. The row still projects the static surface and routes the + // dispatch. + rename: { fn: "fs.renameCb", params: [], result: VOID }, + renameSync: { + fn: "fs.renameSync", + params: [STRING, STRING], + result: VOID, + valueParams: exactValueParams(STRING, STRING), + }, + // statSync's no-follow sibling; stats.isSymbolicLink answers what the + // follow-free snapshot saw. + // opendirSync's options (a literal { bufferSize, encoding: "utf8" }) + // lower in lowerBuiltinModuleCall; the row routes the dispatch. + opendirSync: { fn: "fs.opendirSync", params: [STRING, F64], result: DIR_T }, + // createReadStream/createWriteStream lower in lowerBuiltinModuleCall + // (path and options cross as dyn values); the rows route the dispatch. + createReadStream: { + fn: "fs.createReadStream", + params: [DYN, DYN], + result: { kind: "object", className: "%Readable" }, + }, + createWriteStream: { + fn: "fs.createWriteStream", + params: [DYN, DYN], + result: { kind: "object", className: "%Writable" }, + }, + lstatSync: { fn: "fs.lstatSync", params: [STRING], result: STATS_T }, + fstatSync: { + fn: "fs.fstatSync", + params: [F64], + result: STATS_T, + valueParams: exactValueParams(F64), + }, + fchmodSync: { + fn: "fs.fchmodSync", + params: [F64, F64], + result: VOID, + valueParams: exactValueParams(F64, F64), + }, + fsyncSync: { + fn: "fs.fsyncSync", + params: [F64], + result: VOID, + valueParams: exactValueParams(F64), + }, + fdatasyncSync: { + fn: "fs.fdatasyncSync", + params: [F64], + result: VOID, + valueParams: exactValueParams(F64), + }, + ftruncateSync: { fn: "fs.ftruncateSync", params: [F64, F64], result: VOID }, + utimesSync: { fn: "fs.utimesSync", params: [DYN, DYN, DYN], result: VOID }, + futimesSync: { fn: "fs.futimesSync", params: [DYN, DYN, DYN], result: VOID }, + lutimesSync: { fn: "fs.lutimesSync", params: [DYN, DYN, DYN], result: VOID }, + readvSync: { fn: "fs.readvSync", params: [F64, arrayOf(BYTES_U8), F64], result: F64 }, + writevSync: { fn: "fs.writevSync", params: [F64, arrayOf(BYTES_U8), F64], result: F64 }, + linkSync: { + fn: "fs.linkSync", + params: [DYN, DYN], + result: VOID, + valueParams: exactValueParams(DYN, DYN), + }, + // Options and paths are checked by the runtime (scr_fs_cp_begin). + cpSync: { + fn: "fs.cpSync", + params: [DYN, DYN, DYN], + result: VOID, + valueParams: [ + ...exactValueParams(DYN, DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + symlinkSync: { + fn: "fs.symlinkSync", + params: [DYN, DYN, DYN], + result: VOID, + valueParams: [ + ...exactValueParams(DYN, DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + readlinkSync: { + fn: "fs.readlinkSync", + params: [DYN, DYN], + result: DYN, + valueParams: [ + ...exactValueParams(DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + statfsSync: { + fn: "fs.statfsSync", + params: [DYN, DYN], + result: DYN, + valueParams: [ + ...exactValueParams(DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + // realpath(3) — Node's realpathSync (failures spell syscall "lstat", + // Node's own message shape). + realpathSync: { fn: "fs.realpathSync", params: [STRING], result: STRING }, + "realpathSync.native": { fn: "fs.realpathNativeSync", params: [STRING], result: STRING }, + // The fd pair behind spawn's fd-stdio form (the daemon-log idiom: + // openSync(logPath, "a") → spawn stdio ["ignore", fd, fd] → + // closeSync). String flags use the established two-argument path; + // inline numeric O_* expressions route through openNumericSync, with + // an optional creation mode as the third argument. + openSync: { fn: "fs.openSync", params: [STRING, STRING], result: F64 }, + // The buffer forms (fd, buffer, offset, length[, position]). The + // lowering completes an omitted or literal-null position to -1 (the + // runtime's current-offset sentinel); numeric positions read without + // advancing the fd. The options-object and bigint forms still fence. + readSync: { fn: "fs.readSync", params: [F64, BYTES_U8, F64, F64, F64], result: F64 }, + // Entirely special-cased: the classic Buffer window and utf8 string + // overloads lower to separate fixed-width runtime ABIs. As with + // readSync, -1 is the current-offset sentinel. + writeSync: { fn: "fs.writeSync", params: [F64, BYTES_U8, F64, F64, F64], result: F64 }, + closeSync: { + fn: "fs.closeSync", + params: [F64], + result: VOID, + valueParams: exactValueParams(F64), + }, + // Entirely special-cased (lowerFsWatchCall — the callback needs an + // adapter per listener shape); this entry only routes the dispatch. + watch: { fn: "fs.watch", params: [], result: VOID }, + }, + "fs/promises": { + // The sync operations behind already-settled promises: failures + // REJECT (catchable at the await) instead of throwing. readFile is + // utf8-fenced exactly like readFileSync (same special case below). + readFile: { + fn: "fsp.readFile", + params: [STRING, STRING], + result: { kind: "promise", inner: STRING }, + }, + writeFile: { + fn: "fsp.writeFile", + params: [STRING, STRING], + result: { kind: "promise", inner: VOID }, + }, + mkdir: { fn: "fsp.mkdir", params: [STRING], result: { kind: "promise", inner: VOID } }, + readdir: { + fn: "fsp.readdir", + params: [STRING], + result: { kind: "promise", inner: arrayOf(STRING) }, + }, + rm: { fn: "fsp.rm", params: [STRING], result: { kind: "promise", inner: VOID } }, + // The async iterator lowers only as a for await head (lowerFspGlobCall). + glob: { fn: "fsp.globStrings", params: [DYN, DYN], result: arrayOf(STRING) }, + stat: { fn: "fsp.stat", params: [STRING], result: { kind: "promise", inner: STATS_T } }, + realpath: { + fn: "fsp.realpath", + params: [STRING], + result: { kind: "promise", inner: STRING }, + }, + lstat: { fn: "fsp.lstat", params: [STRING], result: { kind: "promise", inner: STATS_T } }, + opendir: { + fn: "fsp.opendir", + params: [STRING, F64], + result: { kind: "promise", inner: DIR_T }, + }, + unlink: { + fn: "fsp.unlink", + params: [STRING], + result: { kind: "promise", inner: VOID }, + valueParams: exactValueParams(STRING), + }, + chmod: { + fn: "fsp.chmod", + params: [STRING, F64], + result: { kind: "promise", inner: VOID }, + valueParams: exactValueParams(STRING, F64), + }, + chown: { + fn: "fsp.chown", + params: [STRING, F64, F64], + result: { kind: "promise", inner: VOID }, + valueParams: exactValueParams(STRING, F64, F64), + }, + lchown: { + fn: "fsp.lchown", + params: [STRING, F64, F64], + result: { kind: "promise", inner: VOID }, + valueParams: exactValueParams(STRING, F64, F64), + }, + // access' omitted mode and truncate's omitted len complete to 0 in + // lowerBuiltinModuleCall, like their sync spellings. + access: { fn: "fsp.access", params: [STRING, F64], result: { kind: "promise", inner: VOID } }, + truncate: { + fn: "fsp.truncate", + params: [STRING, F64], + result: { kind: "promise", inner: VOID }, + }, + // String data; Uint8Array/Buffer data routes to fsp.appendFileBytes in + // lowerBuiltinModuleCall. + appendFile: { + fn: "fsp.appendFile", + params: [STRING, STRING], + result: { kind: "promise", inner: VOID }, + }, + // The 2-argument form only, like copyFileSync. + copyFile: { + fn: "fsp.copyFile", + params: [STRING, STRING], + result: { kind: "promise", inner: VOID }, + }, + mkdtemp: { fn: "fsp.mkdtemp", params: [STRING], result: { kind: "promise", inner: STRING } }, + rmdir: { fn: "fsp.rmdir", params: [STRING], result: { kind: "promise", inner: VOID } }, + link: { + fn: "fsp.link", + params: [DYN, DYN], + result: { kind: "promise", inner: VOID }, + valueParams: exactValueParams(DYN, DYN), + }, + cp: { + fn: "fsp.cp", + params: [DYN, DYN, DYN], + result: { kind: "promise", inner: VOID }, + valueParams: [ + ...exactValueParams(DYN, DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + symlink: { + fn: "fsp.symlink", + params: [DYN, DYN, DYN], + result: { kind: "promise", inner: VOID }, + valueParams: [ + ...exactValueParams(DYN, DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + readlink: { + fn: "fsp.readlink", + params: [DYN, DYN], + result: { kind: "promise", inner: DYN }, + valueParams: [ + ...exactValueParams(DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + statfs: { + fn: "fsp.statfs", + params: [DYN, DYN], + result: { kind: "promise", inner: DYN }, + valueParams: [ + ...exactValueParams(DYN), + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + utimes: { + fn: "fsp.utimes", + params: [DYN, DYN, DYN], + result: { kind: "promise", inner: VOID }, + }, + lutimes: { + fn: "fsp.lutimes", + params: [DYN, DYN, DYN], + result: { kind: "promise", inner: VOID }, + }, + rename: { + fn: "fsp.rename", + params: [STRING, STRING], + result: { kind: "promise", inner: VOID }, + valueParams: exactValueParams(STRING, STRING), + }, + // open's optional flags/mode completion is special-cased in + // lowerBuiltinModuleCall; this row routes all import spellings and + // gives coverage the static member. + open: { + fn: "fsp.open", + params: [STRING, STRING, F64], + result: { kind: "promise", inner: FILEHANDLE_T }, + valueParams: [ + { mode: "required", type: STRING }, + { mode: "optional", type: STRING, defaultValue: "r" }, + { mode: "optional", type: F64, defaultValue: 0o666 }, + ], + }, + }, + // The bare module's POSIX-target binding; a win32 target rebinds it to + // the win32 table (builtinModuleFnsOf — Node on Windows IS path.win32). + path: PATH_MODULE_FNS, + // Real Node modules ("node:path/posix" resolves), and the receivers of + // `path.posix.` / `path.win32.` through the namespace + // chain (builtinNamespaceModuleOf composes "path" + ".posix" to these + // keys). Both namespaces carry the FULL surface on every platform, + // Node's own rule: each answers its own platform's semantics anywhere. + "path/posix": PATH_MODULE_FNS, + "path/win32": PATH_WIN32_MODULE_FNS, + os: { + // One platform implementation: os.platform() === process.platform. + platform: { fn: "process.platform", params: [], result: STRING, valueParams: [] }, + homedir: { fn: "os.homedir", params: [], result: STRING, valueParams: [] }, + tmpdir: { fn: "os.tmpdir", params: [], result: STRING, valueParams: [] }, + // uname(2)'s release field — Node's own implementation. + release: { fn: "os.release", params: [], result: STRING, valueParams: [] }, + // uname(2)'s sysname field ("Darwin", "Linux", "Windows_NT") — the + // libFn/runtime pair predates this entry (the portless surface used + // them); this row makes the MODULE call reach them (test/common's + // isAIX/isIBMi getters are the canonical callers). + type: { fn: "os.type", params: [], result: STRING, valueParams: [] }, + // Total physical memory in bytes — same predating-pair story. + totalmem: { fn: "os.totalmem", params: [], result: F64, valueParams: [] }, + // uv_available_parallelism: the CPUs this process may use (affinity, + // then cgroup quota on Linux), at least 1. + availableParallelism: { + fn: "os.availableParallelism", + params: [], + result: F64, + valueParams: [], + }, + // os.arch() IS process.arch, like platform(). + arch: { fn: "process.arch", params: [], result: STRING, valueParams: [] }, + hostname: { fn: "os.hostname", params: [], result: STRING, valueParams: [] }, + // uv_os_uname's machine and version fields, read once at module load + // in Node (they never change while a process runs). + machine: { fn: "os.machine", params: [], result: STRING, valueParams: [] }, + version: { fn: "os.version", params: [], result: STRING, valueParams: [] }, + endianness: { fn: "os.endianness", params: [], result: STRING, valueParams: [] }, + freemem: { fn: "os.freemem", params: [], result: F64, valueParams: [] }, + uptime: { fn: "os.uptime", params: [], result: F64, valueParams: [] }, + loadavg: { fn: "os.loadavg", params: [], result: arrayOf(F64), valueParams: [] }, + // An omitted (or undefined) pid is 0, the current process + // (lowerOsGetPriorityCall completes it). + getPriority: { + fn: "os.getPriority", + params: [F64], + result: F64, + valueParams: [{ mode: "optional", type: F64, defaultValue: 0 }], + }, + // A one-argument call is setPriority(0, priority) + // (lowerOsSetPriorityCall completes it). + setPriority: { fn: "os.setPriority", params: [F64, F64], result: VOID }, + // Special-cased (lowerOsCpusCall): the call site's CpuInfo[] assembles + // from one uv_cpu_info snapshot. + cpus: { fn: "os.cpusTake", params: [], result: VOID }, + // Entirely special-cased (lowerOsNetworkInterfacesCall): the result is + // the call site's mapped Dict shape, verified + // structurally there — this entry only routes the dispatch. + networkInterfaces: { fn: "os.networkInterfaces", params: [], result: VOID }, + // Special-cased too (lowerOsUserInfoCall): the call site's mapped + // UserInfo record assembles field-by-field from scalar libCalls. + userInfo: { fn: "os.userName", params: [], result: VOID }, + }, + crypto: { + randomUUID: { + fn: "crypto.randomUUID", + params: [], + result: STRING, + // As a value (uuid's `export default { randomUUID }`), the optional + // options argument goes through the validating entry point. + valueFn: "crypto.randomUUIDOpts", + valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], + }, + // A real Buffer result. The composed randomBytes(n).toString(enc) + // form KEEPS its one-libCall lowering (lowerCryptoComposedCall runs + // first and the Buffer never materializes there); this entry covers + // the bare calls and non-composed uses. + randomBytes: { + fn: "crypto.randomBytes", + params: [F64], + result: BYTES_U8, + valueParams: exactValueParams(F64), + }, + // The crypto utility surface is special-cased for overloads and + // string/Buffer splits in lowerCryptoModuleCall. These rows carry the + // canonical signatures for manifest projection and fallback dispatch. + createHash: { fn: "crypto.hashNew", params: [STRING], result: CRYPTOHASH_T }, + createHmac: { fn: "crypto.hmacNewStr", params: [STRING, STRING], result: CRYPTOHMAC_T }, + hash: { fn: "crypto.hashDigestStr", params: [STRING, STRING, STRING], result: STRING }, + timingSafeEqual: { fn: "crypto.timingSafeEqual", params: [BYTES_U8, BYTES_U8], result: BOOL }, + randomFillSync: { fn: "crypto.randomFill", params: [BYTES_U8, F64, F64], result: BYTES_U8 }, + getRandomValues: { fn: "crypto.getRandomValues", params: [BYTES_U8], result: BYTES_U8 }, + randomInt: { fn: "crypto.randomInt", params: [F64, F64], result: F64 }, + randomFill: { fn: "crypto.randomFillCb", params: [], result: VOID }, + randomUUIDv7: { + fn: "crypto.randomUUIDv7", + params: [DYN], + result: STRING, + valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], + }, + 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 }, + diffieHellman: { fn: "crypto.diffieHellman", params: [], result: DYN }, + pbkdf2Sync: { + fn: "crypto.pbkdf2", + params: [BYTES_U8, BYTES_U8, F64, F64, STRING], + result: BYTES_U8, + }, + pbkdf2: { fn: "crypto.pbkdf2Cb", params: [], result: VOID }, + hkdfSync: { + fn: "crypto.hkdf", + params: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64], + result: DYN, + }, + hkdf: { fn: "crypto.hkdfCb", params: [], result: VOID }, + scryptSync: { fn: "crypto.scrypt", params: [BYTES_U8, BYTES_U8, F64, DYN], result: BYTES_U8 }, + scrypt: { fn: "crypto.scryptCb", params: [], result: VOID }, + // Checked-dynamic arguments; the result is a native Cipheriv or + // Decipheriv handle (lowerCryptoModuleCall). + createCipheriv: { fn: "crypto.cipherNew", params: [BOOL, DYN, DYN, DYN, DYN], result: DYN }, + createDecipheriv: { fn: "crypto.cipherNew", params: [BOOL, DYN, DYN, DYN, DYN], result: DYN }, + // 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 }, + // Prime checks and generation (lowerPrimeCall): checked-dynamic + // arguments, results converted to the call site's type. + checkPrimeSync: { fn: "crypto.checkPrimeSync", params: [], result: DYN }, + // Checked-dynamic arguments, the result converted to the call site's + // type (lowerPrimeCall). + getCipherInfo: { fn: "crypto.getCipherInfo", params: [], result: DYN }, + checkPrime: { fn: "crypto.checkPrimeCb", params: [], result: VOID }, + generatePrimeSync: { fn: "crypto.generatePrimeSync", params: [], result: DYN }, + generatePrime: { fn: "crypto.generatePrimeCb", params: [], result: VOID }, + }, + zlib: { + // The default-options convenience family is special-cased in + // lowerBuiltinModuleCall: string inputs UTF-8 encode, callback forms + // carry program-shaped closures, and explicit options fence per site. + // native-toolchain.ts links libz only when these appear on the IR. + deflate: { fn: "zlib.deflateCb", params: [], result: VOID }, + deflateSync: { fn: "zlib.deflateSync", params: [BYTES_U8], result: BYTES_U8 }, + inflate: { fn: "zlib.inflateCb", params: [], result: VOID }, + inflateSync: { fn: "zlib.inflateSync", params: [BYTES_U8], result: BYTES_U8 }, + deflateRaw: { fn: "zlib.deflateRawCb", params: [], result: VOID }, + deflateRawSync: { fn: "zlib.deflateRawSync", params: [BYTES_U8], result: BYTES_U8 }, + inflateRaw: { fn: "zlib.inflateRawCb", params: [], result: VOID }, + inflateRawSync: { fn: "zlib.inflateRawSync", params: [BYTES_U8], result: BYTES_U8 }, + gzip: { fn: "zlib.gzipCb", params: [], result: VOID }, + gzipSync: { fn: "zlib.gzipSync", params: [BYTES_U8], result: BYTES_U8 }, + gunzip: { fn: "zlib.gunzipCb", params: [], result: VOID }, + gunzipSync: { fn: "zlib.gunzipSync", params: [BYTES_U8], result: BYTES_U8 }, + unzip: { fn: "zlib.unzipCb", params: [], result: VOID }, + unzipSync: { fn: "zlib.unzipSync", params: [BYTES_U8], result: BYTES_U8 }, + crc32: { fn: "zlib.crc32", params: [], result: F64 }, + // Brotli (special-cased in lowerZlibModuleCall): every form takes the + // options entries with Node's BROTLI_ENCODE or BROTLI_DECODE mode. + brotliCompress: { fn: "zlib.cbOpts", params: [], result: VOID }, + brotliCompressSync: { fn: "zlib.syncOpts", params: [], result: BYTES_U8 }, + brotliDecompress: { fn: "zlib.cbOpts", params: [], result: VOID }, + brotliDecompressSync: { fn: "zlib.syncOpts", params: [], result: BYTES_U8 }, + // The stream factories (special-cased in lowerZlibModuleCall): each + // answers a runtime Transform stream over the zlib engine. + createDeflate: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createInflate: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createGzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createGunzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createDeflateRaw: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createInflateRaw: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createUnzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createBrotliCompress: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createBrotliDecompress: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + }, + url: { + fileURLToPath: { + fn: "url.fileURLToPathOptions", + params: [DYN, DYN], + result: STRING, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + fileURLToPathBuffer: { + fn: "url.fileURLToPathBuffer", + params: [DYN, DYN], + result: BYTES_U8, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + pathToFileURL: { + fn: "url.pathToFileURLChecked", + params: [DYN, DYN], + result: URL_T, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + // The legacy API and the domain pair (lowerLegacyUrlCall): arguments + // cross as dyn values for Node's own validators; url.parse() answers + // the checked-dynamic Url instance. + parse: { fn: "url.legacyParse", params: [DYN, DYN, DYN], result: DYN }, + format: { fn: "url.legacyFormat", params: [DYN, DYN], result: STRING }, + resolve: { fn: "url.legacyResolve", params: [DYN, DYN], result: STRING }, + domainToASCII: { fn: "url.domainToASCII", params: [DYN], result: STRING }, + domainToUnicode: { fn: "url.domainToUnicode", params: [DYN], result: STRING }, + urlToHttpOptions: { fn: "url.toHttpOptions", params: [DYN], result: DYN }, + }, + child_process: { + // spawnSync's and spawn's call completions are entirely special-cased + // (an omitted args list completes to an empty string[]; spawnSync + // accepts its documented literal options, while spawn supports the + // stdio/detached/env/cwd/windowsHide/shell subset — see + // lowerBuiltinModuleCall); the + // entries carry the canonical shapes. + spawnSync: { fn: "cp.spawnSync", params: [STRING, arrayOf(STRING)], result: SPAWNRES_T }, + spawn: { fn: "cp.spawn", params: [STRING, arrayOf(STRING)], result: CHILD_T }, + fork: { fn: "cp.fork", params: [STRING, arrayOf(STRING)], result: CHILD_T }, + // execFileSync(file, args?, options?) and execSync(command, options?) + // share the cp.execSync runtime entry (execSync sets the shell flag); + // both call completions are special-cased in lowerBuiltinModuleCall. + // The entries carry canonical shapes for the coverage/fence machinery. + execFileSync: { fn: "cp.execSync", params: [STRING, arrayOf(STRING)], result: STRING }, + execSync: { fn: "cp.execSync", params: [STRING], result: STRING }, + }, + // node:util lowers through dedicated paths: promisify + // only in the const-binding-over-execFile shape — recognized in + // lowerVarDecl / collectGlobals BEFORE any call lowering runs — and + // inspect/format/formatWithOptions plus parseArgs through the util spoke + // (lower-inspect.ts: per-type synthesized traversal helpers, + // compile-time format strings, checked-dynamic config/result for + // parseArgs), which both dispatch paths try before the generic table. + // parseArgs is tabled as well so the generated surface manifest records + // the dedicated static member; the spoke owns its optional arity and + // call-site-specific result type before this canonical signature is used. + util: { + styleText: { + fn: "util.styleText", + params: [DYN, DYN, DYN], + result: STRING, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + isDeepStrictEqual: { + fn: "util.isDeepStrictEqual", + params: [DYN, DYN, DYN], + result: BOOL, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + parseArgs: { fn: "util.parseArgs", params: [DYN], result: DYN }, + parseEnv: { fn: "util.parseEnv", params: [DYN], result: DYN }, + getSystemErrorName: { + fn: "util.getSystemErrorName", + params: [DYN], + result: STRING, + valueParams: exactValueParams(DYN), + }, + getSystemErrorMessage: { + fn: "util.getSystemErrorMessage", + params: [DYN], + result: STRING, + valueParams: exactValueParams(DYN), + }, + getSystemErrorMap: { fn: "util.systemErrorEntries", params: [], result: DYN }, + stripVTControlCharacters: { + fn: "util.stripVTControlCharacters", + params: [DYN], + result: STRING, + valueParams: exactValueParams(DYN), + }, + toUSVString: { + fn: "util.toUSVString", + params: [DYN], + result: STRING, + valueParams: exactValueParams(DYN), + }, + // The function helpers take checked-dynamic arguments and answer native + // dyn functions, which a static function slot checks when it receives + // one (lowerUtilCallableCall in the util spoke). + inherits: { + fn: "util.inherits", + params: [DYN, DYN], + result: VOID, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "required", type: DYN }, + ], + }, + deprecate: { + fn: "util.deprecate", + params: [DYN, DYN, DYN, DYN], + result: DYN, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + callbackify: { + fn: "util.callbackify", + params: [DYN], + result: DYN, + valueParams: [{ mode: "required", type: DYN }], + }, + debuglog: { + fn: "util.debuglog", + params: [DYN, DYN], + result: DYN, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + debug: { + fn: "util.debuglog", + params: [DYN, DYN], + result: DYN, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + }, + // The util/types spoke supplies the probe name to the shared brand check. + "util/types": { + isAnyArrayBuffer: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isArrayBuffer: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isArrayBufferView: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isDataView: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isTypedArray: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isUint8Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isUint8ClampedArray: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isUint16Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isUint32Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isInt8Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isInt16Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isInt32Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isFloat32Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isFloat64Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isMap: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isSet: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isWeakMap: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isWeakSet: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isRegExp: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isNativeError: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isPromise: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isProxy: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isAsyncFunction: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isGeneratorFunction: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isGeneratorObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isDate: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isSharedArrayBuffer: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isMapIterator: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isSetIterator: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isKeyObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isNumberObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isStringObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isBooleanObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isBigIntObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isSymbolObject: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isBoxedPrimitive: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isBigInt64Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isBigUint64Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isFloat16Array: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isCryptoKey: { fn: "util.typeIs", params: [DYN], result: BOOL }, + isExternal: { fn: "util.typeIs", params: [DYN], result: BOOL }, + }, + buffer: { + isAscii: { fn: "buffer.isAscii", params: [DYN], result: BOOL }, + isUtf8: { fn: "buffer.isUtf8", params: [DYN], result: BOOL }, + transcode: { fn: "buffer.transcode", params: [DYN, DYN, DYN], result: BYTES_U8 }, + }, + tty: { + isatty: { fn: "process.isTTY", params: [F64], result: BOOL }, + }, + process: { + loadEnvFile: { fn: "process.loadEnvFile", params: [DYN], result: VOID }, + }, + // node:string_decoder's surface is the StringDecoder CLASS — new/write/ + // end are special-cased (lowerNew + lowerStringDecoderMethodCall); no + // function members exist to table. + string_decoder: {}, + // node:querystring — the legacy query-string codec (NOT URLSearchParams; + // the escaping and '+' rules differ). escape/unescape ride the table + // path directly; parse and stringify are special-cased in + // lowerBuiltinModuleCall (parse's result is the call site's mapped + // ParsedUrlQuery dictionary — the networkInterfaces verification stance + // — and its sep/eq/options complete there; stringify's object argument + // crosses as a dyn value). decode/encode are Node's own aliases of + // parse/stringify (`const decode = parse` in lib/querystring.js) and + // route to the same special cases; the entries carry canonical shapes. + querystring: { + parse: { fn: "qs.parse", params: [STRING], result: VOID }, + decode: { fn: "qs.parse", params: [STRING], result: VOID }, + stringify: { fn: "qs.stringify", params: [DYN], result: STRING }, + encode: { fn: "qs.stringify", params: [DYN], result: STRING }, + escape: { + fn: "qs.escape", + params: [STRING], + result: STRING, + valueParams: [{ mode: "required", type: STRING }], + }, + unescape: { + fn: "qs.unescape", + params: [STRING], + result: STRING, + valueParams: [{ mode: "required", type: STRING }], + }, + }, + // node:punycode (deprecated): punycode.js 2.1.0's conversions over + // strings (scr_url.c). ucs2.decode/encode are members of the module's + // ucs2 object, dispatched as module members (builtinMemberOf). The + // DEP0040 report rides the module's load sites (punycodeLoadWarning). + punycode: { + decode: { fn: "punycode.decode", params: [STRING], result: STRING }, + encode: { fn: "punycode.encode", params: [STRING], result: STRING }, + toASCII: { fn: "punycode.toASCII", params: [STRING], result: STRING }, + toUnicode: { fn: "punycode.toUnicode", params: [STRING], result: STRING }, + "ucs2.decode": { fn: "punycode.ucs2Decode", params: [STRING], result: arrayOf(F64) }, + "ucs2.encode": { fn: "punycode.ucs2Encode", params: [arrayOf(F64)], result: STRING }, + }, + // node:constants (deprecated) is one frozen object (Object.assign of + // os.constants' groups, fs.constants and crypto.constants): bindings of + // the module read it as a checked-dynamic value (builtinConstantsObject). + constants: {}, + // node:readline: createInterface's options are entirely special-cased + // (exactly { input: process.stdin, output?: process.stdout } — see + // lowerBuiltinModuleCall); the entry carries the canonical shape. The + // interface's question/close/on lower through + // lowerReadlineMethodCall. + readline: { + createInterface: { fn: "rl.create", params: [], result: F64 }, + }, + // node:readline/promises: the same Interface with the promise question + // (lowerReadlineMethodCall tells the forms apart by the call's result). + "readline/promises": { + createInterface: { fn: "rl.create", params: [], result: F64 }, + }, + // node:net and node:http lower ENTIRELY through the server spoke + // (lower-server.ts — every call shape is special-cased: closures, the + // optional connect host, writeHead's literal headers); the member list + // is the lower-server dispatch there. The modules still need keys here so future + // table entries have a home, and so the "recognized module, unlowered + // member" fence wording applies. + net: { + // The process-wide happy-eyeballs attempt budget: one runtime double + // in the core unit (never links scr_net.c by itself), default 250ms + // like Node's. + getDefaultAutoSelectFamilyAttemptTimeout: { + fn: "net.getAutoSelTimeout", + params: [], + result: F64, + }, + setDefaultAutoSelectFamilyAttemptTimeout: { + fn: "net.setAutoSelTimeout", + params: [F64], + result: VOID, + }, + // Node's IPv4Reg / IPv6Reg tests over String(input). + isIP: { fn: "net.isIP", params: [DYN], result: F64 }, + isIPv4: { fn: "net.isIPv4", params: [DYN], result: BOOL }, + isIPv6: { fn: "net.isIPv6", params: [DYN], result: BOOL }, + }, + http: {}, + // node:tls and node:https ride the same spoke (tls.createServer's + // options + secureConnection handler, https.createServer, and the + // https client's rejectUnauthorized/ca options are all special-cased + // in lower-server.ts). + tls: {}, + https: {}, + // node:dgram and node:dns lower ENTIRELY through the dgram spoke + // (lower-dgram.ts — createSocket's option literal, dns.lookup's + // options + callback, and the Socket method surface are all + // special-cased); the keys exist for the same reason net/http's do. + dgram: {}, + dns: {}, + // node:dns/promises rides the same spoke (lookup's promise form). + "dns/promises": {}, + // node:v8: getHeapStatistics answers allocator-derived figures in + // Node's shape (builtins/v8.ts — the entry carries the canonical + // shape); every other member fences with the by-design reason + // (builtinFenceHintOf). + v8: { + getHeapStatistics: { fn: "process.memoryUsageTake", params: [], result: VOID }, + }, + // node:stream/web re-exports the web stream globals (the same objects); + // its members lower through the globals' own paths. + "stream/web": {}, + // node:assert lowers ENTIRELY through the assert spoke (lower-assert.ts + // — every call shape is special-cased: optional messages complete, the + // comparisons pick per-type libCalls, deep equality synthesizes + // helpers); the keys exist so unlowered members fence module-qualified + // with the hints below. + assert: {}, + "assert/strict": {}, + // node:test lowers ENTIRELY through the test spoke (lower-test.ts — + // registrations, suites, hooks, the TestContext surface); the key + // exists so unlowered members fence module-qualified. + test: {}, + // node:stream/promises lowers through the stream spoke too + // (lower-stream.ts — finished/pipeline's promise forms special-case + // their stream arguments exactly like the callback forms); the key + // exists so unlowered members fence module-qualified. + "stream/promises": {}, + // node:stream/consumers rides the stream spoke as well (text/json/ + // buffer/arrayBuffer special-case their stream argument); blob fences + // module-qualified with the pointed hint below. + "stream/consumers": {}, + // node:timers/promises — the delay-only setTimeout and bare setImmediate + // (void promises the shared timer heap settles), and scheduler.wait / + // scheduler.yield, which Node defines as setTimeout(delay, undefined, + // options) and setImmediate(). The arity completion (omitted delay = + // Node's 1ms) and the value/options fences are special-cased in + // lowerBuiltinModuleCall; these entries carry the canonical shapes and + // route the dispatch. + "timers/promises": { + setTimeout: { fn: "tp.setTimeout", params: [F64], result: { kind: "promise", inner: VOID } }, + setImmediate: { fn: "tp.setImmediate", params: [], result: { kind: "promise", inner: VOID } }, + "scheduler.wait": { + fn: "tp.setTimeout", + params: [F64], + result: { kind: "promise", inner: VOID }, + }, + "scheduler.yield": { + fn: "tp.setImmediate", + params: [], + result: { kind: "promise", inner: VOID }, + }, + }, + // node:diagnostics_channel — the pub/sub core. channel() answers an f64 + // handle (type-mapper.ts maps Channel to F64); the subscriber arguments box + // into the checked-dynamic tree (special-cased in lowerBuiltinModuleCall — the entries + // carry canonical shapes and route the dispatch). The Channel method + // surface lowers through lowerDiagnosticsChannelMethodCall/lowerDiagnosticsChannelProperty. + diagnostics_channel: { + channel: { fn: "dc.channel", params: [STRING], result: F64 }, + subscribe: { fn: "dc.subscribe", params: [STRING, DYN], result: VOID }, + unsubscribe: { fn: "dc.unsubscribe", params: [STRING, DYN], result: BOOL }, + hasSubscribers: { fn: "dc.hasSubscribers", params: [STRING], result: BOOL }, + // The string form's canonical shape; the collection form (an object + // literal of five Channels) is special-cased in lowerBuiltinModuleCall. + tracingChannel: { fn: "dc.tracingChannel", params: [STRING], result: F64 }, + }, + }; +} /** Builtin-module CONSTANTS (value reads of named imports): each lowers to * a literal of its value's kind. The bare-module values are the TARGET @@ -2080,7 +2099,7 @@ export const BUILTIN_MODULE_CONSTS: Record< * the sidecar's determinism attestation demotes on them by prefix, so the * member's fence detector must witness them too (fencing one spelling * fences the operation, the trimLeft/trimStart rule). Keyed like - * BUILTIN_MODULE_FNS; consumed by the fence taxonomy + * builtinModuleFns(); consumed by the fence taxonomy * (library/fence-eval.ts) and the attestation-parity test. */ export const BUILTIN_MODULE_FN_ALIASES: Record< string, @@ -2232,628 +2251,643 @@ export interface AmbientSurfaceRow { note?: string; } -export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ - { - id: "node-builtin.worker_threads.Worker", - kind: "node-builtin", - name: "worker_threads.Worker", - fns: ["worker.new"], - note: "native threads with statically compiled entry points, workerData, argv and message events", - }, - { - id: "node-builtin.worker_threads.metadata", - kind: "node-builtin", - name: "worker_threads metadata", - fns: [ - "worker.isMainThread", - "worker.threadId", - "worker.data", - "worker.parentPort", - "worker.resourceLimits", - "worker.threadName", - ], - note: "current-thread metadata and the worker's parent message port", - }, - { - id: "stdlib.sharedArrayBuffer", - kind: "stdlib", - name: "SharedArrayBuffer", - fns: ["sharedArrayBuffer.new"], - note: "fixed shared storage with context-local typed-array and DataView wrappers", - }, - { - id: "stdlib.blob", - kind: "stdlib", - name: "Blob", - fns: ["blob.new", "blob.slice", "blob.text", "blob.bytes", "blob.arrayBuffer", "blob.is"], - note: "immutable bytes with size, type, slice(), text(), bytes(), arrayBuffer() and structured cloning", - }, - { - id: "stdlib.file", - kind: "stdlib", - name: "File", - fns: ["blob.fileNew"], - note: "a Blob with name and lastModified (Date.now() when absent)", - }, - { - id: "stdlib.webcrypto.subtle", - kind: "stdlib", - name: "globalThis.crypto.subtle", - fns: [ - "webcrypto.generateKey", - "webcrypto.importKey", - "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, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, PBKDF2 and HKDF deriveBits and deriveKey, 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", - kind: "stdlib", - name: "CryptoKey", - fns: ["webcrypto.isCryptoKey"], - note: "secret HMAC, AES, PBKDF2 and HKDF keys with type, extractable, algorithm and usages, and instanceof CryptoKey", - }, - { - id: "stdlib.atomics", - kind: "stdlib", - name: "Atomics", - fns: ["atomics.op", "atomics.wait", "atomics.notify"], - note: "8-, 16- and 32-bit integer operations, plus Int32Array wait and notify", - }, - { - id: "node-builtin.fs.callbacks", - kind: "node-builtin", - name: "fs error-first callbacks", - fns: ["fs.callbackValue", "fs.callbackCall", "fs.callbackGlob"], - note: "native filesystem callbacks, including stored platform adapter functions", - }, - // ── the Date slice (lowerDateCall/lowerNew): statics, construction, - // stored read-only values, calendar getters, and ISO formatting. - { - id: "stdlib.date.now", - kind: "stdlib", - name: "Date.now", - fns: ["date.now"], - note: "the live clock", - }, - { - id: "stdlib.date.parse", - kind: "stdlib", - name: "Date.parse", - fns: ["date.parse"], - note: "one date string, using V8's date parser shared with new Date(dateString)", - }, - { - id: "stdlib.date.constructor", - kind: "stdlib", - name: "Date constructor", - fns: [ - "date.newNow", - "date.newMs", - "date.newString", - "date.nativeNew", - "date.newLocal", - "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", - }, - { - id: "stdlib.date.setters", - kind: "stdlib", - name: "Date.prototype setters", - fns: ["date.setFields", "date.timeClip", "date.store"], - note: "setTime and the local and UTC field setters, with their optional arguments", - }, - { - id: "stdlib.date.UTC", - kind: "stdlib", - name: "Date.UTC", - fns: ["date.utc"], - note: "the lowered call form takes 1 to 7 number arguments", - }, - { - id: "stdlib.date.getTime", - kind: "stdlib", - name: "Date.prototype.getTime", - fns: ["date.getTime", "date.parseGetTime"], - note: "the millisecond value of a stored Date", - }, - { - id: "stdlib.date.valueOf", - kind: "stdlib", - name: "Date.prototype.valueOf", - fns: ["date.valueOf"], - note: "the same millisecond read as getTime()", - }, - { - id: "stdlib.date.toISOString", - kind: "stdlib", - name: "Date.prototype.toISOString", - fns: ["date.toISOString", "date.toISOStringValue"], - note: "UTC ISO formatting over constructed and stored Date values", - }, - { - id: "stdlib.date.toUTCString", - kind: "stdlib", - name: "Date.prototype.toUTCString", - fns: ["date.toUTCString"], - note: "RFC 7231 IMF-fixdate formatting over stored Date values", - }, - { - id: "stdlib.date.getFullYear", - kind: "stdlib", - name: "Date.prototype.getFullYear", - fns: ["date.getFullYear"], - }, - { - id: "stdlib.date.getUTCFullYear", - kind: "stdlib", - name: "Date.prototype.getUTCFullYear", - fns: ["date.getUTCFullYear"], - }, - { - id: "stdlib.date.getMonth", - kind: "stdlib", - name: "Date.prototype.getMonth", - fns: ["date.getMonth"], - }, - { - id: "stdlib.date.getUTCMonth", - kind: "stdlib", - name: "Date.prototype.getUTCMonth", - fns: ["date.getUTCMonth"], - }, - { - id: "stdlib.date.getDate", - kind: "stdlib", - name: "Date.prototype.getDate", - fns: ["date.getDate"], - }, - { - id: "stdlib.date.getUTCDate", - kind: "stdlib", - name: "Date.prototype.getUTCDate", - fns: ["date.getUTCDate"], - }, - { id: "stdlib.date.getDay", kind: "stdlib", name: "Date.prototype.getDay", fns: ["date.getDay"] }, - { - id: "stdlib.date.getUTCDay", - kind: "stdlib", - name: "Date.prototype.getUTCDay", - fns: ["date.getUTCDay"], - }, - { - id: "stdlib.date.getHours", - kind: "stdlib", - name: "Date.prototype.getHours", - fns: ["date.getHours"], - }, - { - id: "stdlib.date.getUTCHours", - kind: "stdlib", - name: "Date.prototype.getUTCHours", - fns: ["date.getUTCHours"], - }, - { - id: "stdlib.date.getMinutes", - kind: "stdlib", - name: "Date.prototype.getMinutes", - fns: ["date.getMinutes"], - }, - { - id: "stdlib.date.getUTCMinutes", - kind: "stdlib", - name: "Date.prototype.getUTCMinutes", - fns: ["date.getUTCMinutes"], - }, - { - id: "stdlib.date.getSeconds", - kind: "stdlib", - name: "Date.prototype.getSeconds", - fns: ["date.getSeconds"], - }, - { - id: "stdlib.date.getUTCSeconds", - kind: "stdlib", - name: "Date.prototype.getUTCSeconds", - fns: ["date.getUTCSeconds"], - }, - { - id: "stdlib.date.getMilliseconds", - kind: "stdlib", - name: "Date.prototype.getMilliseconds", - fns: ["date.getMilliseconds"], - }, - { - id: "stdlib.date.getUTCMilliseconds", - kind: "stdlib", - name: "Date.prototype.getUTCMilliseconds", - fns: ["date.getUTCMilliseconds"], - }, - { - id: "stdlib.date.getTimezoneOffset", - kind: "stdlib", - name: "Date.prototype.getTimezoneOffset", - fns: ["date.getTimezoneOffset"], - }, - { - id: "stdlib.date.toString", - kind: "stdlib", - name: "Date.prototype.toString", - fns: ["date.toString"], - note: "local time with the host zone's long English name; String(date) and template literals use it", - }, - { - id: "stdlib.date.toDateString", - kind: "stdlib", - name: "Date.prototype.toDateString", - fns: ["date.toDateString"], - }, - { - id: "stdlib.date.toTimeString", - kind: "stdlib", - name: "Date.prototype.toTimeString", - fns: ["date.toTimeString"], - }, - // ── perf_hooks (lowerPerfHooksCall): the monotonic clock. - { - id: "node-builtin.perf_hooks.performance.now", - kind: "node-builtin", - name: "perf_hooks.performance.now", - fns: ["perf.now"], - note: "the global performance object and the performance.now.bind(performance) function value reach the same clock", - }, - { - id: "node-builtin.perf_hooks.performance.mark", - kind: "node-builtin", - name: "perf_hooks.performance.mark", - fns: [ - "perf.mark", - "perf.entryName", - "perf.entryType", - "perf.entryStart", - "perf.entryDuration", - "perf.entryDetail", - "perf.entryKeep", - "perf.entryRef", - ], - note: "records a mark in the thread's performance timeline at the live clock or the given startTime", - }, - { - id: "node-builtin.perf_hooks.performance.measure", - kind: "node-builtin", - name: "perf_hooks.performance.measure", - fns: ["perf.measure"], - note: "the mark-name, timestamp, and options forms are one surface", - }, - { - id: "node-builtin.perf_hooks.performance.getEntries", - kind: "node-builtin", - name: "perf_hooks.performance.getEntries", - fns: ["perf.entries"], - note: "getEntries, getEntriesByName, and getEntriesByType read the same timeline", - }, - { - id: "node-builtin.perf_hooks.performance.clearMarks", - kind: "node-builtin", - name: "perf_hooks.performance.clearMarks", - fns: ["perf.clearMarks"], - }, - { - id: "node-builtin.perf_hooks.performance.clearMeasures", - kind: "node-builtin", - name: "perf_hooks.performance.clearMeasures", - fns: ["perf.clearMeasures"], - }, - { - id: "node-builtin.perf_hooks.performance.eventLoopUtilization", - kind: "node-builtin", - name: "perf_hooks.performance.eventLoopUtilization", - fns: ["perf.loopStart", "perf.idleTime"], - note: "active time is the main thread's CPU time since the loop started; the one- and two-argument difference forms are supported", - }, - { - id: "node-builtin.perf_hooks.monitorEventLoopDelay", - kind: "node-builtin", - name: "perf_hooks.monitorEventLoopDelay", - fns: [ - "perf.eldNew", - "perf.eldEnable", - "perf.eldDisable", - "perf.eldReset", - "perf.eldPercentile", - "perf.eldStat", - ], - note: "an unref'd sampling interval records the delay histogram; enable, disable, reset, percentile and the min, max, mean, stddev, count and exceeds readings are supported", - }, - // ── the process global's ambient reads and authority calls - // (lowerProcessProperty/lowerProcessMethodCall — process is a - // provenance-checked global here, not an importable module). - { - id: "node-builtin.process.env", - kind: "node-builtin", - name: "process.env", - fns: ["process.envGet", "process.envSet", "process.envUnset", "process.envPairs"], - note: "reads, writes, deletes, and enumeration of the process environment (the process global)", - }, - { - id: "node-builtin.process.argv", - kind: "node-builtin", - name: "process.argv", - fns: ["process.argv"], - }, - { - id: "node-builtin.process.hrtime", - kind: "node-builtin", - name: "process.hrtime", - fns: ["process.hrtimeValue"], - note: "native monotonic tuple clock and bigint member; direct calls and stored JavaScript callable values", - }, - { - id: "node-builtin.process.getBuiltinModule", - kind: "node-builtin", - name: "process.getBuiltinModule", - fns: ["process.builtinId", "process.builtinModule", "process.builtinUnsupported"], - note: "native path and os export subsets plus current-thread worker_threads metadata; other modules and exports throw SC2020", - }, - { - id: "node-builtin.process.versions", - kind: "node-builtin", - name: "process.versions", - fns: ["process.versions"], - note: "shared version dictionary containing node and openssl; other components are absent unless defined by the program", - }, - { - id: "node-builtin.process.release", - kind: "node-builtin", - name: "process.release", - fns: ["process.release"], - note: "the compatibility target's release metadata as one stable read-only object", - }, - { - id: "node-builtin.process.cwd", - kind: "node-builtin", - name: "process.cwd", - fns: ["process.cwd"], - }, - { - id: "node-builtin.process.chdir", - kind: "node-builtin", - name: "process.chdir", - fns: ["process.chdir"], - }, - { - id: "node-builtin.process.pid", - kind: "node-builtin", - name: "process.pid", - fns: ["process.pid"], - }, - { - id: "node-builtin.process.getuid", - kind: "node-builtin", - name: "process.getuid", - fns: ["process.getuid"], - }, - { - id: "node-builtin.process.getgid", - kind: "node-builtin", - name: "process.getgid", - fns: ["process.getgid"], - }, - { - id: "node-builtin.process.geteuid", - kind: "node-builtin", - name: "process.geteuid", - fns: ["process.geteuid"], - }, - { - id: "node-builtin.process.getegid", - kind: "node-builtin", - name: "process.getegid", - fns: ["process.getegid"], - }, - { - id: "node-builtin.process.getgroups", - kind: "node-builtin", - name: "process.getgroups", - fns: ["process.getgroups"], - }, - { - id: "node-builtin.process.ppid", - kind: "node-builtin", - name: "process.ppid", - fns: ["process.ppid"], - }, - { - id: "node-builtin.process.title", - kind: "node-builtin", - name: "process.title", - fns: ["process.title", "process.setTitle"], - note: "reads and writes of the process title are one surface", - }, - { - id: "node-builtin.process.argv0", - kind: "node-builtin", - name: "process.argv0", - fns: ["process.argv0"], - }, - { - id: "node-builtin.process.execArgv", - kind: "node-builtin", - name: "process.execArgv", - fns: ["process.execArgv"], - }, - { - id: "node-builtin.process.execPath", - kind: "node-builtin", - name: "process.execPath", - fns: ["process.execPath"], - }, - { - id: "node-builtin.process.uptime", - kind: "node-builtin", - name: "process.uptime", - fns: ["process.uptime"], - }, - { - id: "node-builtin.process.availableMemory", - kind: "node-builtin", - name: "process.availableMemory", - fns: ["process.availableMemory"], - }, - { - id: "node-builtin.process.constrainedMemory", - kind: "node-builtin", - name: "process.constrainedMemory", - fns: ["process.constrainedMemory"], - }, - { - id: "node-builtin.process.memoryUsage", - kind: "node-builtin", - name: "process.memoryUsage", - fns: ["process.memoryUsageTake", "process.memoryUsageField", "process.rss"], - note: "the record and memoryUsage.rss() read live memory state; external and arrayBuffers are 0", - }, - { - id: "node-builtin.process.resourceUsage", - kind: "node-builtin", - name: "process.resourceUsage", - fns: ["process.rusage"], - note: "getrusage's 16 fields — every field read samples live machine state", - }, - { - id: "node-builtin.process.cpuUsage", - kind: "node-builtin", - name: "process.cpuUsage", - fns: [ - "process.cpuUser", - "process.cpuSystem", - "process.cpuUserDiff", - "process.cpuSystemDiff", - "process.cpuPrevValidate", - ], - note: "the plain-sample and previous-value diff forms are one surface", - }, - { - id: "node-builtin.process.threadCpuUsage", - kind: "node-builtin", - name: "process.threadCpuUsage", - fns: [ - "process.threadCpuUser", - "process.threadCpuSystem", - "process.threadCpuUserDiff", - "process.threadCpuSystemDiff", - ], - note: "the plain-sample and previous-value diff forms are one surface", - }, - { - id: "node-builtin.process.isTTY", - kind: "node-builtin", - name: "process.isTTY", - fns: ["process.isTTY"], - note: "the isTTY read on process.stdin/stdout/stderr — one surface across the three streams", - }, - { - id: "node-builtin.process.columns", - kind: "node-builtin", - name: "process.columns", - fns: ["process.columns"], - note: "the columns read on the process stdio streams (terminal geometry)", - }, - { - id: "node-builtin.process.rows", - kind: "node-builtin", - name: "process.rows", - fns: ["process.rows"], - note: "the rows read on process.stdout/stderr (terminal geometry)", - }, - { - id: "node-builtin.process.kill", - kind: "node-builtin", - name: "process.kill", - fns: ["process.kill", "process.killNum"], - note: "the signal-name and signal-number forms are one surface", - }, - { - id: "node-builtin.process.umask", - kind: "node-builtin", - name: "process.umask", - fns: ["process.umask"], - }, - { - id: "node-builtin.process.exit", - kind: "node-builtin", - name: "process.exit", - fns: ["process.exit", "process.exiting"], - note: "process.exit and the process._exiting flag read are one surface", - }, - { - id: "node-builtin.process.exitCode", - kind: "node-builtin", - name: "process.exitCode", - fns: ["process.setExitCode", "process.setExitCodeTracked", "process.hasExitCode"], - note: "numeric writes in statement position set the implicit exit status; reads answer the assigned code or undefined; process.exit() reads it", - }, - // ── the tls CA store (lowerTlsCaCall / lowerTlsRootCertificates): the - // host's trust anchors, read and replaced. Dedicated paths, and - // rootCertificates is a VALUE read with no call form at all, so none of - // the three can hang off a lowering-table row. Split three ways rather - // than folded into one "CA store" surface: a library author who denies - // REPLACING the trust anchors is making a different decision from one - // who denies reading them, and each Node spelling is the name they will - // reach for when writing the fence. - { - id: "node-builtin.tls.getCACertificates", - kind: "node-builtin", - name: "tls.getCACertificates", - fns: ["tlsca.get"], - note: "the per-type cached PEM bundle: 'default' and 'extra' additionally read NODE_EXTRA_CA_CERTS, 'system' the platform store", - }, - { - id: "node-builtin.tls.rootCertificates", - kind: "node-builtin", - name: "tls.rootCertificates", - fns: ["tlsca.root"], - note: "the value read; answers the same bundled array as getCACertificates('bundled'), but fenced under its own id — the spelling an author writes", - }, - { - id: "node-builtin.tls.setDefaultCACertificates", - kind: "node-builtin", - name: "tls.setDefaultCACertificates", - fns: ["tlsca.set"], - note: "replaces the default set and the client trust anchors for the rest of the process", - }, - // os.constants is a VALUE read (lowerNamespaceBuiltinProperty and the - // named-import path): the platform's null-prototype constants object. - { - id: "node-builtin.os.constants", - kind: "node-builtin", - name: "os.constants", - fns: ["os.constants"], - note: "the platform's dlopen, errno, signals and priority groups; members a platform lacks read as undefined", - }, - // fs.constants as a value, and its members beyond the baked access-mode - // bits: the target libc's null-prototype constants object. - { - id: "node-builtin.fs.constants", - kind: "node-builtin", - name: "fs.constants", - fns: ["fs.constants"], - note: "node_constants.cc's fs group under the target libc's macros; members a platform lacks read as undefined", - }, -]; +let ambientSurfaceRows: readonly AmbientSurfaceRow[] | undefined; + +/** The ambient surface rows, built on the first call. */ +export function ambientSurfaceFns(): readonly AmbientSurfaceRow[] { + ambientSurfaceRows ??= buildAmbientSurfaceFns(); + return ambientSurfaceRows; +} + +function buildAmbientSurfaceFns(): readonly AmbientSurfaceRow[] { + return [ + { + id: "node-builtin.worker_threads.Worker", + kind: "node-builtin", + name: "worker_threads.Worker", + fns: ["worker.new"], + note: "native threads with statically compiled entry points, workerData, argv and message events", + }, + { + id: "node-builtin.worker_threads.metadata", + kind: "node-builtin", + name: "worker_threads metadata", + fns: [ + "worker.isMainThread", + "worker.threadId", + "worker.data", + "worker.parentPort", + "worker.resourceLimits", + "worker.threadName", + ], + note: "current-thread metadata and the worker's parent message port", + }, + { + id: "stdlib.sharedArrayBuffer", + kind: "stdlib", + name: "SharedArrayBuffer", + fns: ["sharedArrayBuffer.new"], + note: "fixed shared storage with context-local typed-array and DataView wrappers", + }, + { + id: "stdlib.blob", + kind: "stdlib", + name: "Blob", + fns: ["blob.new", "blob.slice", "blob.text", "blob.bytes", "blob.arrayBuffer", "blob.is"], + note: "immutable bytes with size, type, slice(), text(), bytes(), arrayBuffer() and structured cloning", + }, + { + id: "stdlib.file", + kind: "stdlib", + name: "File", + fns: ["blob.fileNew"], + note: "a Blob with name and lastModified (Date.now() when absent)", + }, + { + id: "stdlib.webcrypto.subtle", + kind: "stdlib", + name: "globalThis.crypto.subtle", + fns: [ + "webcrypto.generateKey", + "webcrypto.importKey", + "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, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, PBKDF2 and HKDF deriveBits and deriveKey, 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", + kind: "stdlib", + name: "CryptoKey", + fns: ["webcrypto.isCryptoKey"], + note: "secret HMAC, AES, PBKDF2 and HKDF keys with type, extractable, algorithm and usages, and instanceof CryptoKey", + }, + { + id: "stdlib.atomics", + kind: "stdlib", + name: "Atomics", + fns: ["atomics.op", "atomics.wait", "atomics.notify"], + note: "8-, 16- and 32-bit integer operations, plus Int32Array wait and notify", + }, + { + id: "node-builtin.fs.callbacks", + kind: "node-builtin", + name: "fs error-first callbacks", + fns: ["fs.callbackValue", "fs.callbackCall", "fs.callbackGlob"], + note: "native filesystem callbacks, including stored platform adapter functions", + }, + // ── the Date slice (lowerDateCall/lowerNew): statics, construction, + // stored read-only values, calendar getters, and ISO formatting. + { + id: "stdlib.date.now", + kind: "stdlib", + name: "Date.now", + fns: ["date.now"], + note: "the live clock", + }, + { + id: "stdlib.date.parse", + kind: "stdlib", + name: "Date.parse", + fns: ["date.parse"], + note: "one date string, using V8's date parser shared with new Date(dateString)", + }, + { + id: "stdlib.date.constructor", + kind: "stdlib", + name: "Date constructor", + fns: [ + "date.newNow", + "date.newMs", + "date.newString", + "date.nativeNew", + "date.newLocal", + "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", + }, + { + id: "stdlib.date.setters", + kind: "stdlib", + name: "Date.prototype setters", + fns: ["date.setFields", "date.timeClip", "date.store"], + note: "setTime and the local and UTC field setters, with their optional arguments", + }, + { + id: "stdlib.date.UTC", + kind: "stdlib", + name: "Date.UTC", + fns: ["date.utc"], + note: "the lowered call form takes 1 to 7 number arguments", + }, + { + id: "stdlib.date.getTime", + kind: "stdlib", + name: "Date.prototype.getTime", + fns: ["date.getTime", "date.parseGetTime"], + note: "the millisecond value of a stored Date", + }, + { + id: "stdlib.date.valueOf", + kind: "stdlib", + name: "Date.prototype.valueOf", + fns: ["date.valueOf"], + note: "the same millisecond read as getTime()", + }, + { + id: "stdlib.date.toISOString", + kind: "stdlib", + name: "Date.prototype.toISOString", + fns: ["date.toISOString", "date.toISOStringValue"], + note: "UTC ISO formatting over constructed and stored Date values", + }, + { + id: "stdlib.date.toUTCString", + kind: "stdlib", + name: "Date.prototype.toUTCString", + fns: ["date.toUTCString"], + note: "RFC 7231 IMF-fixdate formatting over stored Date values", + }, + { + id: "stdlib.date.getFullYear", + kind: "stdlib", + name: "Date.prototype.getFullYear", + fns: ["date.getFullYear"], + }, + { + id: "stdlib.date.getUTCFullYear", + kind: "stdlib", + name: "Date.prototype.getUTCFullYear", + fns: ["date.getUTCFullYear"], + }, + { + id: "stdlib.date.getMonth", + kind: "stdlib", + name: "Date.prototype.getMonth", + fns: ["date.getMonth"], + }, + { + id: "stdlib.date.getUTCMonth", + kind: "stdlib", + name: "Date.prototype.getUTCMonth", + fns: ["date.getUTCMonth"], + }, + { + id: "stdlib.date.getDate", + kind: "stdlib", + name: "Date.prototype.getDate", + fns: ["date.getDate"], + }, + { + id: "stdlib.date.getUTCDate", + kind: "stdlib", + name: "Date.prototype.getUTCDate", + fns: ["date.getUTCDate"], + }, + { + id: "stdlib.date.getDay", + kind: "stdlib", + name: "Date.prototype.getDay", + fns: ["date.getDay"], + }, + { + id: "stdlib.date.getUTCDay", + kind: "stdlib", + name: "Date.prototype.getUTCDay", + fns: ["date.getUTCDay"], + }, + { + id: "stdlib.date.getHours", + kind: "stdlib", + name: "Date.prototype.getHours", + fns: ["date.getHours"], + }, + { + id: "stdlib.date.getUTCHours", + kind: "stdlib", + name: "Date.prototype.getUTCHours", + fns: ["date.getUTCHours"], + }, + { + id: "stdlib.date.getMinutes", + kind: "stdlib", + name: "Date.prototype.getMinutes", + fns: ["date.getMinutes"], + }, + { + id: "stdlib.date.getUTCMinutes", + kind: "stdlib", + name: "Date.prototype.getUTCMinutes", + fns: ["date.getUTCMinutes"], + }, + { + id: "stdlib.date.getSeconds", + kind: "stdlib", + name: "Date.prototype.getSeconds", + fns: ["date.getSeconds"], + }, + { + id: "stdlib.date.getUTCSeconds", + kind: "stdlib", + name: "Date.prototype.getUTCSeconds", + fns: ["date.getUTCSeconds"], + }, + { + id: "stdlib.date.getMilliseconds", + kind: "stdlib", + name: "Date.prototype.getMilliseconds", + fns: ["date.getMilliseconds"], + }, + { + id: "stdlib.date.getUTCMilliseconds", + kind: "stdlib", + name: "Date.prototype.getUTCMilliseconds", + fns: ["date.getUTCMilliseconds"], + }, + { + id: "stdlib.date.getTimezoneOffset", + kind: "stdlib", + name: "Date.prototype.getTimezoneOffset", + fns: ["date.getTimezoneOffset"], + }, + { + id: "stdlib.date.toString", + kind: "stdlib", + name: "Date.prototype.toString", + fns: ["date.toString"], + note: "local time with the host zone's long English name; String(date) and template literals use it", + }, + { + id: "stdlib.date.toDateString", + kind: "stdlib", + name: "Date.prototype.toDateString", + fns: ["date.toDateString"], + }, + { + id: "stdlib.date.toTimeString", + kind: "stdlib", + name: "Date.prototype.toTimeString", + fns: ["date.toTimeString"], + }, + // ── perf_hooks (lowerPerfHooksCall): the monotonic clock. + { + id: "node-builtin.perf_hooks.performance.now", + kind: "node-builtin", + name: "perf_hooks.performance.now", + fns: ["perf.now"], + note: "the global performance object and the performance.now.bind(performance) function value reach the same clock", + }, + { + id: "node-builtin.perf_hooks.performance.mark", + kind: "node-builtin", + name: "perf_hooks.performance.mark", + fns: [ + "perf.mark", + "perf.entryName", + "perf.entryType", + "perf.entryStart", + "perf.entryDuration", + "perf.entryDetail", + "perf.entryKeep", + "perf.entryRef", + ], + note: "records a mark in the thread's performance timeline at the live clock or the given startTime", + }, + { + id: "node-builtin.perf_hooks.performance.measure", + kind: "node-builtin", + name: "perf_hooks.performance.measure", + fns: ["perf.measure"], + note: "the mark-name, timestamp, and options forms are one surface", + }, + { + id: "node-builtin.perf_hooks.performance.getEntries", + kind: "node-builtin", + name: "perf_hooks.performance.getEntries", + fns: ["perf.entries"], + note: "getEntries, getEntriesByName, and getEntriesByType read the same timeline", + }, + { + id: "node-builtin.perf_hooks.performance.clearMarks", + kind: "node-builtin", + name: "perf_hooks.performance.clearMarks", + fns: ["perf.clearMarks"], + }, + { + id: "node-builtin.perf_hooks.performance.clearMeasures", + kind: "node-builtin", + name: "perf_hooks.performance.clearMeasures", + fns: ["perf.clearMeasures"], + }, + { + id: "node-builtin.perf_hooks.performance.eventLoopUtilization", + kind: "node-builtin", + name: "perf_hooks.performance.eventLoopUtilization", + fns: ["perf.loopStart", "perf.idleTime"], + note: "active time is the main thread's CPU time since the loop started; the one- and two-argument difference forms are supported", + }, + { + id: "node-builtin.perf_hooks.monitorEventLoopDelay", + kind: "node-builtin", + name: "perf_hooks.monitorEventLoopDelay", + fns: [ + "perf.eldNew", + "perf.eldEnable", + "perf.eldDisable", + "perf.eldReset", + "perf.eldPercentile", + "perf.eldStat", + ], + note: "an unref'd sampling interval records the delay histogram; enable, disable, reset, percentile and the min, max, mean, stddev, count and exceeds readings are supported", + }, + // ── the process global's ambient reads and authority calls + // (lowerProcessProperty/lowerProcessMethodCall — process is a + // provenance-checked global here, not an importable module). + { + id: "node-builtin.process.env", + kind: "node-builtin", + name: "process.env", + fns: ["process.envGet", "process.envSet", "process.envUnset", "process.envPairs"], + note: "reads, writes, deletes, and enumeration of the process environment (the process global)", + }, + { + id: "node-builtin.process.argv", + kind: "node-builtin", + name: "process.argv", + fns: ["process.argv"], + }, + { + id: "node-builtin.process.hrtime", + kind: "node-builtin", + name: "process.hrtime", + fns: ["process.hrtimeValue"], + note: "native monotonic tuple clock and bigint member; direct calls and stored JavaScript callable values", + }, + { + id: "node-builtin.process.getBuiltinModule", + kind: "node-builtin", + name: "process.getBuiltinModule", + fns: ["process.builtinId", "process.builtinModule", "process.builtinUnsupported"], + note: "native path and os export subsets plus current-thread worker_threads metadata; other modules and exports throw SC2020", + }, + { + id: "node-builtin.process.versions", + kind: "node-builtin", + name: "process.versions", + fns: ["process.versions"], + note: "shared version dictionary containing node and openssl; other components are absent unless defined by the program", + }, + { + id: "node-builtin.process.release", + kind: "node-builtin", + name: "process.release", + fns: ["process.release"], + note: "the compatibility target's release metadata as one stable read-only object", + }, + { + id: "node-builtin.process.cwd", + kind: "node-builtin", + name: "process.cwd", + fns: ["process.cwd"], + }, + { + id: "node-builtin.process.chdir", + kind: "node-builtin", + name: "process.chdir", + fns: ["process.chdir"], + }, + { + id: "node-builtin.process.pid", + kind: "node-builtin", + name: "process.pid", + fns: ["process.pid"], + }, + { + id: "node-builtin.process.getuid", + kind: "node-builtin", + name: "process.getuid", + fns: ["process.getuid"], + }, + { + id: "node-builtin.process.getgid", + kind: "node-builtin", + name: "process.getgid", + fns: ["process.getgid"], + }, + { + id: "node-builtin.process.geteuid", + kind: "node-builtin", + name: "process.geteuid", + fns: ["process.geteuid"], + }, + { + id: "node-builtin.process.getegid", + kind: "node-builtin", + name: "process.getegid", + fns: ["process.getegid"], + }, + { + id: "node-builtin.process.getgroups", + kind: "node-builtin", + name: "process.getgroups", + fns: ["process.getgroups"], + }, + { + id: "node-builtin.process.ppid", + kind: "node-builtin", + name: "process.ppid", + fns: ["process.ppid"], + }, + { + id: "node-builtin.process.title", + kind: "node-builtin", + name: "process.title", + fns: ["process.title", "process.setTitle"], + note: "reads and writes of the process title are one surface", + }, + { + id: "node-builtin.process.argv0", + kind: "node-builtin", + name: "process.argv0", + fns: ["process.argv0"], + }, + { + id: "node-builtin.process.execArgv", + kind: "node-builtin", + name: "process.execArgv", + fns: ["process.execArgv"], + }, + { + id: "node-builtin.process.execPath", + kind: "node-builtin", + name: "process.execPath", + fns: ["process.execPath"], + }, + { + id: "node-builtin.process.uptime", + kind: "node-builtin", + name: "process.uptime", + fns: ["process.uptime"], + }, + { + id: "node-builtin.process.availableMemory", + kind: "node-builtin", + name: "process.availableMemory", + fns: ["process.availableMemory"], + }, + { + id: "node-builtin.process.constrainedMemory", + kind: "node-builtin", + name: "process.constrainedMemory", + fns: ["process.constrainedMemory"], + }, + { + id: "node-builtin.process.memoryUsage", + kind: "node-builtin", + name: "process.memoryUsage", + fns: ["process.memoryUsageTake", "process.memoryUsageField", "process.rss"], + note: "the record and memoryUsage.rss() read live memory state; external and arrayBuffers are 0", + }, + { + id: "node-builtin.process.resourceUsage", + kind: "node-builtin", + name: "process.resourceUsage", + fns: ["process.rusage"], + note: "getrusage's 16 fields — every field read samples live machine state", + }, + { + id: "node-builtin.process.cpuUsage", + kind: "node-builtin", + name: "process.cpuUsage", + fns: [ + "process.cpuUser", + "process.cpuSystem", + "process.cpuUserDiff", + "process.cpuSystemDiff", + "process.cpuPrevValidate", + ], + note: "the plain-sample and previous-value diff forms are one surface", + }, + { + id: "node-builtin.process.threadCpuUsage", + kind: "node-builtin", + name: "process.threadCpuUsage", + fns: [ + "process.threadCpuUser", + "process.threadCpuSystem", + "process.threadCpuUserDiff", + "process.threadCpuSystemDiff", + ], + note: "the plain-sample and previous-value diff forms are one surface", + }, + { + id: "node-builtin.process.isTTY", + kind: "node-builtin", + name: "process.isTTY", + fns: ["process.isTTY"], + note: "the isTTY read on process.stdin/stdout/stderr — one surface across the three streams", + }, + { + id: "node-builtin.process.columns", + kind: "node-builtin", + name: "process.columns", + fns: ["process.columns"], + note: "the columns read on the process stdio streams (terminal geometry)", + }, + { + id: "node-builtin.process.rows", + kind: "node-builtin", + name: "process.rows", + fns: ["process.rows"], + note: "the rows read on process.stdout/stderr (terminal geometry)", + }, + { + id: "node-builtin.process.kill", + kind: "node-builtin", + name: "process.kill", + fns: ["process.kill", "process.killNum"], + note: "the signal-name and signal-number forms are one surface", + }, + { + id: "node-builtin.process.umask", + kind: "node-builtin", + name: "process.umask", + fns: ["process.umask"], + }, + { + id: "node-builtin.process.exit", + kind: "node-builtin", + name: "process.exit", + fns: ["process.exit", "process.exiting"], + note: "process.exit and the process._exiting flag read are one surface", + }, + { + id: "node-builtin.process.exitCode", + kind: "node-builtin", + name: "process.exitCode", + fns: ["process.setExitCode", "process.setExitCodeTracked", "process.hasExitCode"], + note: "numeric writes in statement position set the implicit exit status; reads answer the assigned code or undefined; process.exit() reads it", + }, + // ── the tls CA store (lowerTlsCaCall / lowerTlsRootCertificates): the + // host's trust anchors, read and replaced. Dedicated paths, and + // rootCertificates is a VALUE read with no call form at all, so none of + // the three can hang off a lowering-table row. Split three ways rather + // than folded into one "CA store" surface: a library author who denies + // REPLACING the trust anchors is making a different decision from one + // who denies reading them, and each Node spelling is the name they will + // reach for when writing the fence. + { + id: "node-builtin.tls.getCACertificates", + kind: "node-builtin", + name: "tls.getCACertificates", + fns: ["tlsca.get"], + note: "the per-type cached PEM bundle: 'default' and 'extra' additionally read NODE_EXTRA_CA_CERTS, 'system' the platform store", + }, + { + id: "node-builtin.tls.rootCertificates", + kind: "node-builtin", + name: "tls.rootCertificates", + fns: ["tlsca.root"], + note: "the value read; answers the same bundled array as getCACertificates('bundled'), but fenced under its own id — the spelling an author writes", + }, + { + id: "node-builtin.tls.setDefaultCACertificates", + kind: "node-builtin", + name: "tls.setDefaultCACertificates", + fns: ["tlsca.set"], + note: "replaces the default set and the client trust anchors for the rest of the process", + }, + // os.constants is a VALUE read (lowerNamespaceBuiltinProperty and the + // named-import path): the platform's null-prototype constants object. + { + id: "node-builtin.os.constants", + kind: "node-builtin", + name: "os.constants", + fns: ["os.constants"], + note: "the platform's dlopen, errno, signals and priority groups; members a platform lacks read as undefined", + }, + // fs.constants as a value, and its members beyond the baked access-mode + // bits: the target libc's null-prototype constants object. + { + id: "node-builtin.fs.constants", + kind: "node-builtin", + name: "fs.constants", + fns: ["fs.constants"], + note: "node_constants.cc's fs group under the target libc's macros; members a platform lacks read as undefined", + }, + ]; +} /** The win32-target overrides of the bare modules' constants: path's * constants ARE the path/win32 namespace's, and os.EOL is CRLF (Node's @@ -2866,7 +2900,7 @@ const WIN32_TARGET_CONSTS: Record< os: { EOL: "\r\n", devNull: "\\\\.\\nul" }, }; -/** BUILTIN_MODULE_FNS with the platform-conditional modules bound per +/** builtinModuleFns() with the platform-conditional modules bound per * TARGET: a win32 triple compiles Node-on-Windows semantics — `path` is * path.win32 and url's bridge selects the Windows runtime behavior. Fence wording is * unaffected — callers keep naming the module the source spelled. */ @@ -2875,9 +2909,9 @@ function builtinModuleFnsOf( module: string, ): Record | undefined { if (lowerer.targetPlatform === "win32") { - if (module === "path") return BUILTIN_MODULE_FNS["path/win32"]; + if (module === "path") return builtinModuleFns()["path/win32"]; } - return ownEntry(BUILTIN_MODULE_FNS, module); + return ownEntry(builtinModuleFns(), module); } /** Own-property lookup — lowerer.ts's own(), duplicated locally because diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 44b5f63ec..3e4871084 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -46,6 +46,8 @@ export type { AnalyzeResult, } from "./compile-types.js"; import { compilePackedLibrary } from "./backend/library-pack.js"; +import { node24FetchCompatProfile } from "./compat/fetch-profile.js"; +import { libFnSignatures } from "./ir/builtin-signatures.js"; import { InternalCompilerError } from "./errors.js"; import { frontendProcessExecArgv } from "./frontend-process-args.js"; import { type ChildProcess, spawn } from "node:child_process"; @@ -249,7 +251,7 @@ export { type SurfaceManifestEntry, } from "./coverage/surface-manifest.js"; export { - NODE24_FETCH_COMPAT_PROFILE, + node24FetchCompatProfile, type FetchCompatEvidence, type FetchCompatFacet, type FetchCompatInventory, @@ -261,7 +263,13 @@ export { type FetchCompatOption, type FetchCompatProfile, } from "./compat/fetch-profile.js"; -export { LIB_FN_SIGS } from "./ir/builtin-signatures.js"; +export { libFnSignatures } from "./ir/builtin-signatures.js"; +/** @deprecated Use `libFnSignatures()`. Built when this module loads; the + * native compiler does not load this module. */ +export const LIB_FN_SIGS = libFnSignatures(); +/** @deprecated Use `node24FetchCompatProfile()`. Built when this module + * loads; the native compiler does not load this module. */ +export const NODE24_FETCH_COMPAT_PROFILE = node24FetchCompatProfile(); export { validateModule } from "./ir/validate.js"; export { assignLegacyTiers, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index e768f257f..916c64583 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -58,5058 +58,5103 @@ function defineLibFnSignatures( * together here; frontend coercion and IR validation consume the same table. * All arguments are required. Lowering supplies optional-source defaults * before this boundary and preserves their evaluation order. */ -export const LIB_FN_SIGS = defineLibFnSignatures({ - /** Native static fetch and its Web-platform companions. fetch.start - * answers once the response head arrives; the response body readers - * consume the native body stream. AbortSignal and ReadableStream values - * are opaque checked-dynamic handles. */ - "fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } }, - "fetch.responseNew": { argTypes: [DYN, DYN], result: DYN }, - "fetch.responseJsonStatic": { argTypes: [DYN, DYN], result: DYN }, - "fetch.responseLight": { argTypes: [DYN], result: arrayOf(STRING) }, - "fetch.input": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "abort.controllerNew": { argTypes: [], result: DYN }, - /** EventTarget / Event / CustomEvent construction (scr_event_target.c): - * the Event constructors take the argument vector as one dyn array and - * throw Node's validation errors; `event.is` is `v instanceof Event` - * (kind 0), `CustomEvent` (1), `EventTarget` (2). addAbortListener - * answers the disposer (the Disposable's [Symbol.dispose]). */ - "event.targetNew": { argTypes: [], result: DYN }, - "event.new": { argTypes: [DYN], result: DYN }, - "event.customNew": { argTypes: [DYN], result: DYN }, - "event.is": { argTypes: [DYN, F64], result: BOOL }, - /** Compiled EventTarget and Event subclasses: the receiver is the - * instance (an object rooted at %EventTarget / %Event). targetInit and - * init are super() (init takes the argument vector as one dyn array - * and throws the constructor's validation errors; initType is the - * super(type) form with a string); targetCall runs an EventTarget method - * with the instance's own capsule as `self` (targetDispatch is its - * dispatchEvent(event) form, answering the bool); call, get and set - * reach the Event members. */ - "event.targetInit": { argTypes: [null], result: VOID }, - "event.init": { argTypes: [null, DYN], result: VOID }, - "event.initType": { argTypes: [null, STRING], result: VOID }, - /** %main hands the engine the compiled `listener.handleEvent` reader - * (lower-event-target.ts noteEventListeners). */ - "event.handleEventReader": { argTypes: [DYN], result: VOID }, - "event.targetDispatch": { argTypes: [null, DYN, DYN], result: BOOL }, - "event.targetCall": { argTypes: [null, DYN, STRING, DYN], result: DYN }, - "event.call": { argTypes: [null, STRING, DYN], result: DYN }, - "event.get": { argTypes: [null, STRING], result: DYN }, - "event.set": { argTypes: [null, STRING, DYN], result: VOID }, - "events.addAbortListener": { argTypes: [DYN, DYN], result: DYN }, - /** Blob / File (scr_blob.c): the constructors and slice() take their - * argument vector as one dyn array and throw Node's validation errors; - * the readers settle on Node's microtask turn for the Blob's parts. - * `blob.is` is `v instanceof Blob` (kind 0) or `File` (1). */ - "blob.new": { argTypes: [DYN], result: DYN }, - "blob.fileNew": { argTypes: [DYN], result: DYN }, - "blob.slice": { argTypes: [DYN, DYN], result: DYN }, - "blob.text": { argTypes: [DYN], result: { kind: "promise", inner: STRING } }, - "blob.bytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } }, - "blob.arrayBuffer": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "blob.is": { argTypes: [DYN, F64], result: BOOL }, - /** WebAssembly (scr_wasm.c): modules translated at build time - * (wasm/translate.ts), found at run time by the SHA-256 of their bytes. - * compile and instantiate over bytes settle on a later loop turn; - * instantiate over a Module settles now, as in Node. `wasm.is` answers - * instanceof for Module, Instance, Memory, Table and Global (kinds 0-4) - * and CompileError, LinkError and RuntimeError (5-7); `wasm.errorNew` - * constructs one of the error classes (kind 5-7, message, options). */ - "wasm.compile": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "wasm.validate": { argTypes: [DYN], result: BOOL }, - "wasm.instantiate": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "wasm.moduleNew": { argTypes: [DYN], result: DYN }, - "wasm.instanceNew": { argTypes: [DYN, DYN], result: DYN }, - "wasm.is": { argTypes: [DYN, F64], result: BOOL }, - "wasm.errorNew": { argTypes: [F64, DYN, DYN], result: DYN }, - /** new WebAssembly.Memory(descriptor), Table(descriptor, value) and - * Global(descriptor, value) (an undefined value is the type's default); - * `wasm.withoutNew` throws the TypeError for Module, Instance, Memory, - * Table or Global (kinds 0-4) called without new, after its arguments - * evaluate. */ - "wasm.memoryNew": { argTypes: [DYN], result: DYN }, - "wasm.tableNew": { argTypes: [DYN, DYN], result: DYN }, - "wasm.globalNew": { argTypes: [DYN, DYN], result: DYN }, - "wasm.withoutNew": { argTypes: [F64, DYN, DYN], result: DYN }, - /** WebAssembly.Module.imports(module), exports(module) and - * customSections(module, name): checked-dynamic arrays of descriptor - * objects and ArrayBuffers. */ - "wasm.moduleImports": { argTypes: [DYN], result: DYN }, - "wasm.moduleExports": { argTypes: [DYN], result: DYN }, - "wasm.moduleCustomSections": { argTypes: [DYN, DYN], result: DYN }, - /** `v.valueOf()` on a WebAssembly-typed receiver: a Global's value, - * dyn.valueOf's answer for anything else. */ - "wasm.valueOf": { argTypes: [DYN], result: DYN }, - /** WebAssembly.compile (0), instantiate (1) and validate (2) as function - * values, and the namespace object holding them. */ - "wasm.function": { argTypes: [F64], result: DYN }, - "wasm.namespace": { argTypes: [], result: DYN }, - /** The global object viewed through a cast to a type with a WebAssembly - * member: an object holding the namespace. */ - "wasm.globalView": { argTypes: [], result: DYN }, - "abort.timeout": { argTypes: [DYN], result: DYN }, - "abort.now": { argTypes: [DYN], result: DYN }, - "abort.any": { argTypes: [DYN], result: DYN }, - "fetch.function": { argTypes: [], result: DYN }, - "fetch.requestNew": { argTypes: [DYN, DYN], result: DYN }, - "fetch.headersNew": { argTypes: [DYN], result: DYN }, - "fetch.responseArrayBuffer": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "fetch.responseJson": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, - "fetch.responseText": { argTypes: [DYN], result: { kind: "promise", inner: STRING } }, - "fetch.responseBytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } }, - "fetch.abortControllerNew": { argTypes: [], result: DYN }, - "fetch.abortTimeout": { argTypes: [DYN], result: DYN }, - "fetch.abortNow": { argTypes: [DYN], result: DYN }, - "fetch.abortAny": { argTypes: [DYN], result: DYN }, - "fetch.streamNew": { argTypes: [DYN], result: DYN }, - "fetch.transformNew": { argTypes: [], result: DYN }, - // Program-dependent iterable: typed arrays/bytes/string stay intact so - // the native stream can pull lazily; checked-dynamic values are the - // fallback. The libCall validator in validate.ts checks the closed set. - "fetch.webIs": { argTypes: [DYN, STRING], result: BOOL }, - "fetch.streamIs": { argTypes: [DYN], result: BOOL }, - "fetch.streamFrom": { argTypes: [null], result: DYN }, - // The chunk/result record depends on ReadableStream; validated by validate.ts. - "fetch.readerRead": { argTypes: [DYN], result: VOID }, - /** Readable.fromWeb (scr_stream_from_web.c): the stream, objectMode, - * highWaterMark (< 0: the mode's default) and encoding ("": none). */ - "fetch.readableFromWeb": { argTypes: [DYN, BOOL, F64, STRING], result: VOID }, - "json.parse": { argTypes: [STRING], result: DYN }, - "json.parseReviver": { argTypes: [STRING, DYN], result: DYN }, - "json.stringifyReplacer": { argTypes: [DYN, DYN, STRING], result: DYN }, - "json.stringifyValue": { argTypes: [DYN, DYN, DYN], result: DYN }, - /** Keyed WRITE on a dyn value — `h.onDone = cb` / `h["k"] = v` on a - * checked-dynamic object (args: receiver, key string, value — all - * borrowed; the runtime copies the key and retains the value in). An - * OBJ receiver sets the member (later writes win, insertion order - * preserved — JS exactly); undefined/null throws Node's catchable - * "Cannot set properties of undefined (setting 'k')"; every other kind - * throws Node's STRICT-mode "Cannot create property 'k' on " — - * primitives quoting their rendering, V8's "on number '5'" - * (sloppy mode would ignore the write — suite tests are 'use strict'; - * SEMANTICS.md notes the sloppy divergence: the write throws). Void - * result; in the may-throw seed set. */ - "dyn.keySet": { argTypes: [DYN, STRING, DYN], result: VOID }, - "dyn.keySetComputed": { argTypes: [DYN, DYN, DYN], result: VOID }, - /** Delete an ordinary checked-native object's own key. Borrows both - * arguments; other receiver representations retain a runtime refusal. */ - "dyn.keyDelete": { argTypes: [DYN, STRING, BOOL], result: VOID }, - "dyn.keyDeleteComputed": { argTypes: [DYN, DYN, BOOL], result: VOID }, - "dyn.reflectDeleteProperty": { argTypes: [DYN, DYN], result: BOOL }, - "dyn.hasKeyComputed": { argTypes: [DYN, DYN], result: BOOL }, - "dyn.hasOwnComputed": { argTypes: [DYN, DYN], result: BOOL }, - "dyn.propertyIsEnumerableComputed": { argTypes: [DYN, DYN], result: BOOL }, - /** Native own properties on globalThis keyed by symbol identity. */ - "dyn.globalSymbolGet": { argTypes: [SYMBOL_T], result: DYN }, - "dyn.globalSymbolSet": { argTypes: [SYMBOL_T, DYN], result: VOID }, - "dyn.globalSymbolHas": { argTypes: [SYMBOL_T], result: BOOL }, - "dyn.globalSymbolDelete": { argTypes: [SYMBOL_T], result: VOID }, - /** Exact native class capsule identity, without materializing its fields. */ - "dyn.typedRefIs": { argTypes: [DYN, STRING], result: BOOL }, - /** A capsule boxed by this program from exactly this class: one key - * pointer test, false for equal keys boxed elsewhere. Guards fast paths - * whose fallback handles every other value. The key is a literal. */ - "dyn.typedRefKeyIs": { argTypes: [DYN, STRING], result: BOOL }, - "dyn.classIs": { argTypes: [DYN, STRING], result: BOOL }, - /** Destructuring pack over a dyn source — `const [a, b] = d`, a - * destructured dyn callback param (args: the source and the STATIC - * TypeError spelling, "" when the source has none — both borrowed; - * result: a fresh dyn array, +1). Iterable kinds collect like spread - * (arrays element-by-element, strings by code point, bytes by byte); - * every other kind throws V8's destructuring TypeError — the spelling - * verbatim when non-empty, else the runtime kind wording ("number 5 is - * not iterable (cannot read property Symbol(Symbol.iterator))"). In the - * may-throw seed set. */ - "dyn.iterPack": { argTypes: [DYN, STRING], result: DYN }, - "dyn.mathFold": { argTypes: [DYN, BOOL, STRING], result: F64 }, - "dyn.destructureRest": { argTypes: [DYN, STRING], result: DYN }, - "dyn.arrayFromIterator": { argTypes: [DYN], result: DYN }, - "dyn.iterator": { argTypes: [DYN, STRING], result: DYN }, - /** for await over a checked-dynamic value: the loop's iterator (a - * compiled async generator, Symbol.asyncIterator's result, or the sync - * iterator), and whether that iterator is synchronous. */ - "dyn.asyncIteratorOpen": { argTypes: [DYN, STRING], result: DYN }, - "dyn.asyncIteratorWasSync": { argTypes: [], result: BOOL }, - "dyn.iteratorResult": { argTypes: [DYN], result: DYN }, - "dyn.iteratorCanStep": { argTypes: [DYN, DYN], result: BOOL }, - "dyn.iteratorStep": { argTypes: [DYN], result: DYN }, - "dyn.iteratorStepDone": { argTypes: [DYN], result: BOOL }, - "dyn.setGeneratorMembers": { argTypes: [DYN, DYN], result: VOID }, - "dyn.iteratorHelperMethod": { argTypes: [STRING], result: DYN }, - "dyn.collectionIteratorIndex": { argTypes: [DYN], result: F64 }, - "dyn.mapSeedEntries": { argTypes: [DYN], result: DYN }, - "dyn.mapSeedEntry": { argTypes: [DYN], result: DYN }, - /** The for-of-over-dyn pack accessors — the emitted index loop drives - * them over a dyn.iterPack result (ARR by construction). arrLen: the - * ARR length as f64 (0 for non-ARR kinds; arg borrowed). arrAt: the - * element at index (+1; the undefined singleton past the end). Neither - * throws. */ - "dyn.arrLen": { argTypes: [DYN], result: F64 }, - "dyn.arrAt": { argTypes: [DYN, F64], result: DYN }, - /** `key in v` with a RUNTIME (string) key on a checked-dynamic - * receiver (args: value dyn, key string; result bool): OBJ answers - * own-member presence, ARR answers 'length'/a valid index — exactly - * the literal-key dynHasKey path. Proxy has traps may throw. */ - "dyn.hasKey": { argTypes: [DYN, STRING], result: BOOL }, - /** The `in` operator with a runtime string key: dyn.hasKey plus the - * members inherited from builtin and compiled class prototypes. */ - "dyn.in": { argTypes: [DYN, STRING], result: BOOL }, - "dyn.freeze": { argTypes: [DYN], result: DYN }, - "dyn.isFrozen": { argTypes: [DYN], result: BOOL }, - /** Object.defineProperty on a declared class field (scr_json.c): the - * field's current value (borrowed), its FIELD_* attribute word, the - * instance's CLASS_* integrity word, the field name and the descriptor. - * Validates as Node does (TypeError "Cannot redefine property: f") and - * answers a one-property holder of the defined value and attributes (+1), - * read back with class.definedBits and class.definedValue. */ - "class.defineField": { argTypes: [DYN, F64, F64, STRING, DYN], result: DYN }, - "class.definedBits": { argTypes: [DYN, STRING], result: F64 }, - "class.definedValue": { argTypes: [DYN, STRING], result: DYN }, - /** Whether a property descriptor has a `value` member. */ - "class.descriptorHasValue": { argTypes: [DYN], result: BOOL }, - /** Throws Node's TypeError for a write to a read-only class field: the - * field name and the instance's class name. */ - "class.throwReadOnly": { argTypes: [STRING, STRING], result: VOID }, - /** Throws Node's TypeError for a class field initializer on a - * non-extensible instance: the field name. */ - "class.throwNotExtensible": { argTypes: [STRING], result: VOID }, - /** Object.freeze of a record whose shape is freezable (frozen-records.ts): - * sets the frozen bit in its rc word (scr_record_freeze). The receiver is - * any record type. */ - "record.freeze": { argTypes: [null], result: VOID }, - /** Whether a record is frozen: its rc word's frozen bit, set by - * record.freeze and dyn.freeze of a capsule. Emitted inline. The receiver - * is any record type. */ - "record.isFrozen": { argTypes: [null], result: BOOL }, - /** Throws Node's TypeError for a write to a frozen record: the key, the - * object's text in the message ("#") and the operation (0 assign, - * 1 add, 2 delete). */ - "record.throwFrozen": { argTypes: [STRING, STRING, F64], result: VOID }, - /** Object.freeze of an array (scr_arr_freeze): sets its frozen bit, which - * the write guards of IrModule.freezableArrays test, and isFrozen reads. - * The receiver is any array type. */ - "array.freeze": { argTypes: [null], result: VOID }, - "array.isFrozen": { argTypes: [null], result: BOOL }, - /** Object.freeze, seal or preventExtensions of a Map or Set - * (scr_map_prevent_extensions), which only makes it not extensible: its - * entries stay writable. The receiver is any map or set type. */ - "collection.preventExtensions": { argTypes: [null], result: VOID }, - "collection.isExtensible": { argTypes: [null], result: BOOL }, - "dyn.nativeSetNew": { argTypes: [DYN], result: DYN }, - "dyn.nativeSetIs": { argTypes: [DYN], result: BOOL }, - "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 - * `.constructor`. The name is a compiler-chosen literal. */ - "dyn.globalConstructor": { argTypes: [STRING], result: DYN }, - /** A builtin global function (setTimeout, setInterval, setImmediate, - * the clear functions, queueMicrotask, process.nextTick) read as a - * JavaScript value: one interned function per name whose calls run the - * timer APIs (it pulls in the event loop). The name is a compiler-chosen - * literal. */ - "dyn.builtinFunction": { argTypes: [STRING], result: DYN }, - "dyn.namespaceNew": { argTypes: [STRING, DYN], result: DYN }, - "dyn.namespaceDefine": { argTypes: [DYN, STRING, DYN, F64, F64], result: VOID }, - "date.nativeNew": { argTypes: [DYN], result: DYN }, - /** `new Date(year, monthIndex, ...)` in JavaScript: the argument array's - * first seven values pass through ToNumber in order (later ones are - * evaluated, never converted), then the local-time field constructor. */ - "date.nativeNewFields": { argTypes: [DYN], result: DYN }, - /** `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 - * (bytes decode per the literal encoding — utf8 default; strings, - * numbers, booleans, arrays, objects answer JS-exactly; undefined and - * null throw the catchable TypeError). */ - "dyn.toString": { argTypes: [DYN, DYN, STRING], result: STRING }, - "dyn.valueOf": { argTypes: [DYN, STRING], result: DYN }, - /** Object.defineProperties over dyn values (args: target, descriptors — - * both borrowed dyn; result: the target, +1 — JS's return value). - * Value descriptors become own properties on OBJ and FUNC targets; - * OBJ targets preserve data attributes. get/set - * descriptors and non-object targets/descriptors throw catchably - * (Node's TypeError texts; accessors the "not supported yet" Error). In - * the may-throw seed set. */ - "dyn.defineProps": { argTypes: [DYN, DYN], result: DYN }, - "dyn.defineProperty": { argTypes: [DYN, DYN, DYN], result: DYN }, - "dyn.getOwnPropertyDescriptor": { argTypes: [DYN, DYN], result: DYN }, - "dyn.arrayProtoCall": { argTypes: [DYN, STRING, DYN], result: DYN }, - /** Promise.all / Promise.allSettled / Promise.any over a spread - * checked-dynamic array: the values, the settled-record array, or the - * first fulfillment (AggregateError once every entry rejects). - * Thenable elements adopt; the second argument reads a program class - * instance's `then` (the compiled property dispatch). */ - "dyn.promiseAll": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "dyn.promiseAllSettled": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "dyn.promiseAny": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - /** PromiseResolve over one checked-dynamic value: the promise it holds, - * a promise that adopts a thenable's settlement, or a promise fulfilled - * with the value. The second argument reads `then` as above. */ - "dyn.promiseResolve": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - /** Bare `typeof v` on a dyn value AS A STRING (arg: the dyn value, - * borrowed; result: an owned string) — the dyn kind's JS answer: - * undefined→"undefined", null/object/array/bytes→"object" (JS's oldest - * wart preserved), boolean/number/string by kind, function→"function". - * Never throws. */ - "dyn.typeof": { argTypes: [DYN], result: STRING }, - /** Object.prototype.toString.call on a checked-dynamic value. */ - "dyn.objectTag": { argTypes: [DYN], result: STRING }, - /** Engine-free CommonJS module graph. Module values are scalar f64 - * handles; generated startup defines the registry, init wrappers update - * loading/cache state, and the read surface answers Node's live metadata. */ - "module.registryInit": { argTypes: [F64], result: VOID }, - "module.define": { argTypes: [F64, STRING, STRING, STRING, arrayOf(STRING), BOOL], result: VOID }, - "module.enter": { argTypes: [F64], result: VOID }, - "module.link": { argTypes: [F64, F64], result: VOID }, - "module.finish": { argTypes: [F64], result: VOID }, - "module.fail": { argTypes: [F64], result: VOID }, - "module.filename": { argTypes: [F64], result: STRING }, - "module.id": { argTypes: [F64], result: STRING }, - "module.path": { argTypes: [F64], result: STRING }, - "module.paths": { argTypes: [F64], result: arrayOf(STRING) }, - "module.children": { argTypes: [F64], result: arrayOf(F64) }, - "module.parent": { argTypes: [F64], result: F64 }, - "module.loaded": { argTypes: [F64], result: BOOL }, - "module.cacheGet": { argTypes: [STRING], result: F64 }, - "module.cacheHas": { argTypes: [STRING], result: BOOL }, - "module.cacheKeys": { argTypes: [], result: arrayOf(STRING) }, - "timers.setTimeout": { argTypes: [{ kind: "func", params: [], ret: VOID }, F64], result: VOID }, - /** The repeating timer pair. setInterval takes (callback, ms) like - * setTimeout and RETURNS the f64 handle the fallback declarations - * promise (ids start at 1, so truthiness narrowing works); the loop - * owns the callback until clearInterval removes the entry (eagerly — a - * live interval keeps the loop alive, a cleared one releases it, like - * Node). Neither throws; a throw ESCAPING an interval callback ends the - * program like a setTimeout throw. */ - "timers.setInterval": { argTypes: [{ kind: "func", params: [], ret: VOID }, F64], result: F64 }, - "timers.clearInterval": { argTypes: [F64], result: VOID }, - /** setTimeout WITH a clear handle (the f64 id) — the clearable/unref-able - * one-shot; clearTimeout cancels it, .unref() drops it from loop - * liveness. The plain timers.setTimeout stays the handle-less - * fire-and-forget. */ - "timers.setTimeoutHandle": { - argTypes: [{ kind: "func", params: [], ret: VOID }, F64], - result: F64, - }, - "timers.clearTimeout": { argTypes: [F64], result: VOID }, - /** Timeout.unref()/ref()/hasRef() — loop-liveness bookkeeping over the - * handle id. unref/ref RETURN the handle (f64) for chaining; hasRef - * returns bool. */ - "timers.unref": { argTypes: [F64], result: F64 }, - "timers.ref": { argTypes: [F64], result: F64 }, - "timers.hasRef": { argTypes: [F64], result: BOOL }, - /** Timeout.refresh() — re-arm to now + the original delay (from the - * heap or from inside the firing callback; a one-shot that fired on an - * earlier turn is gone and no-ops — documented divergence). Returns - * the handle for chaining like unref/ref. */ - "timers.refresh": { argTypes: [F64], result: F64 }, - /** setImmediate/clearImmediate — Node's check phase: callbacks run - * once per loop turn AFTER due timers, FIFO, and immediates queued - * mid-phase wait for the next turn. setImmediate returns the f64 - * handle (its own id space — clearTimeout of an Immediate no-ops, - * like Node); the Immediate ref trio mirrors the Timeout one (an - * unref'd pending immediate neither keeps the loop alive nor fires - * once nothing reffed remains). */ - "timers.setImmediate": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: F64 }, - /** queueMicrotask (scr_async.c): the callback enters the SAME FIFO - * promise continuations ride — one microtask order, like V8's queue — - * and a throw is an UNCAUGHT exception (never a rejection). - * timers.queueMicrotask takes an owned zero-param closure and never - * throws; the Dyn form (checked-dynamic arguments — the mustCall - * wrapper, the suite's invalid-input probes) throws Node's - * ERR_INVALID_ARG_TYPE synchronously on a non-function value and calls - * the function with zero arguments at drain. */ - "timers.queueMicrotask": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: VOID }, - "timers.queueMicrotaskDyn": { argTypes: [DYN], result: VOID }, - "timers.clearImmediate": { argTypes: [F64], result: VOID }, - /** process.nextTick(cb, ...args) — the user tick queue. args: [cb - * (() => void; trailing call arguments ride the timer surface's - * interned dyn thunk)]. Ticks drain BEFORE promise jobs at every loop - * checkpoint, to joint exhaustion with them (Node's tick-then- - * microtask order); ticks enqueued by station listeners run at the - * NEXT checkpoint (the stream-tick station divergence, SEMANTICS.md). - * Pending ticks are always-ready work — the loop neither sleeps nor - * exits while any exist; ticks scheduled from 'exit' listeners never - * run (Node). The enqueue itself never throws. */ - "process.nextTick": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: VOID }, - /** The process introspection statics. uptime: seconds since the - * binary's own start (fractional — a load-time monotonic anchor). - * availableMemory/constrainedMemory: libuv's numbers (free-ish bytes; - * the cgroup cap or 0). cpuUser/cpuSystem (+ threadCpu twins): the - * process/thread CPU clocks in microseconds — the frontend composes - * the {user, system} records. The *Diff forms answer current − prev - * for one field; cpuPrevValidate throws Node's ERR_INVALID_ARG_VALUE - * RangeError for negative/non-finite prev fields (user first, then - * system — Node's order; MAY THROW). rusage(idx): one - * process.resourceUsage() field by canonical index (Node's units). - * activeResources: the loop's own bookkeeping — 'Timeout' per armed - * (or firing, uncleared) timer, 'Immediate' per queued unfired - * immediate; unmodeled resource kinds are absent (SEMANTICS.md). */ - "process.uptime": { argTypes: [], result: F64 }, - /** perf_hooks performance.now(): fractional ms since process start. */ - "perf.now": { argTypes: [], result: F64 }, - /** perf_hooks user timing over the thread's performance timeline. The - * producing calls take the call's whole argument vector as one dyn - * array (Node's validation and errors run in the runtime, MAY THROW): - * mark/measure answer 1, entries(args, which) the entry count of - * getEntries (0), getEntriesByName (1) or getEntriesByType (2). The - * entryName/Type/Start/Duration/Detail accessors read entry i of that - * answer; the frontend builds a new entry's record from them and hands - * it to entryKeep (a typed reference capsule), and the getters read the - * kept records back through entryRef, so entries keep their identity. */ - "perf.mark": { argTypes: [DYN], result: F64 }, - "perf.measure": { argTypes: [DYN], result: F64 }, - "perf.entries": { argTypes: [DYN, F64], result: F64 }, - "perf.clearMarks": { argTypes: [DYN], result: VOID }, - "perf.clearMeasures": { argTypes: [DYN], result: VOID }, - "perf.entryName": { argTypes: [F64], result: STRING }, - "perf.entryType": { argTypes: [F64], result: STRING }, - "perf.entryStart": { argTypes: [F64], result: F64 }, - "perf.entryDuration": { argTypes: [F64], result: F64 }, - "perf.entryDetail": { argTypes: [F64], result: DYN }, - "perf.entryKeep": { argTypes: [DYN], result: VOID }, - "perf.entryRef": { argTypes: [F64], result: DYN }, - /** perf_hooks event-loop metrics (scr_perf_loop.c): loopStart is the - * loop's start in performance.now() ms (0 before it), idleTime the idle - * ms since. eldNew(resolution ms) creates a monitorEventLoopDelay - * histogram handle (MAY THROW Node's resolution RangeError); enable/disable answer whether the state changed; - * eldPercentile expects Node's validated range; eldStat(which) reads - * 0 min, 1 max, 2 mean, 3 stddev, 4 count, 5 exceeds. */ - "perf.loopStart": { argTypes: [], result: F64 }, - "perf.idleTime": { argTypes: [], result: F64 }, - "perf.eldNew": { argTypes: [F64], result: F64 }, - "perf.eldEnable": { argTypes: [F64], result: BOOL }, - "perf.eldDisable": { argTypes: [F64], result: BOOL }, - "perf.eldReset": { argTypes: [F64], result: VOID }, - "perf.eldPercentile": { argTypes: [F64, F64], result: F64 }, - "perf.eldStat": { argTypes: [F64, F64], result: F64 }, - /** process.memoryUsage(): memoryUsageTake samples the resident set and - * the runtime heap once (MAY THROW Node's uv_resident_set_memory error) - * and memoryUsageField(k) reads that sample (0 rss, 1 heapTotal, 2 - * heapUsed, 3 external, 4 arrayBuffers). rss is memoryUsage.rss(). */ - "process.memoryUsageTake": { argTypes: [], result: VOID }, - "process.memoryUsageField": { argTypes: [F64], result: F64 }, - "process.rss": { argTypes: [], result: F64 }, - "process.availableMemory": { argTypes: [], result: F64 }, - "process.constrainedMemory": { argTypes: [], result: F64 }, - "process.cpuUser": { argTypes: [], result: F64 }, - "process.cpuSystem": { argTypes: [], result: F64 }, - "process.cpuUserDiff": { argTypes: [F64], result: F64 }, - "process.cpuSystemDiff": { argTypes: [F64], result: F64 }, - "process.threadCpuUser": { argTypes: [], result: F64 }, - "process.threadCpuSystem": { argTypes: [], result: F64 }, - "process.threadCpuUserDiff": { argTypes: [F64], result: F64 }, - "process.threadCpuSystemDiff": { argTypes: [F64], result: F64 }, - "process.cpuPrevValidate": { argTypes: [F64, F64], result: VOID }, - "process.rusage": { argTypes: [F64], result: F64 }, - "process.activeResources": { argTypes: [], result: arrayOf(STRING) }, - "timers.immediateUnref": { argTypes: [F64], result: F64 }, - "timers.immediateRef": { argTypes: [F64], result: F64 }, - "timers.immediateHasRef": { argTypes: [F64], result: BOOL }, - /** The tolerated non-handle clear (`clearTimeout(null)`, - * `clearInterval({})`, zero-argument forms): Node ignores anything - * that is not a live handle without an error — a VOID no-op the emitter drops - * (only syntactically side-effect-free arguments take this path). */ - "timers.clearNoop": { argTypes: [], result: VOID }, - // Signal listeners are zero-param (the ambient shape); exit/stdin - // callbacks carry program-dependent one-param shapes — null slots, the - // libCall case checks them (child.onExit precedent). - /** Named process signal events: [name string, callback dyn, once bool] - * or [name, callback] for removal. The runtime resolves platform signal - * numbers and delivers (name, number). Listeners do not keep the loop - * alive. Invalid callbacks and uncatchable signals throw. */ - "process.onSignal": { argTypes: [STRING, DYN, BOOL], result: VOID }, - "process.offSignal": { argTypes: [STRING, DYN], result: VOID }, - /** The process 'exit' event — process.on/once/off("exit"). args: [cb, - * once bool] / [cb]. Listeners run SYNCHRONOUSLY at termination - * (normal exit, process.exit(), the uncaught/unhandled exit-1 paths) - * with the exit code; anything they schedule never runs, like Node. - * Callback shapes: () => void or (code: number) => void — the emitter - * picks the runtime adapter. Never throw. */ - "process.onExit": { argTypes: [null, BOOL], result: VOID }, - "process.offExit": { argTypes: [null], result: VOID }, - /** process.stdin listener registration — stdin.on/once("data" | "end" | - * "error", cb). args: [cb, once bool]. data callbacks: () => void or - * (chunk: Uint8Array) => void; end: () => void; error: () => void or - * (err: Error) => void (the child error adapters are reused). While a - * data listener (or a parked for-await chunk promise) exists, stdin - * keeps the loop alive — Node's flowing-stdin keep-alive. Never - * throw. */ - "stdin.onData": { argTypes: [null, BOOL], result: VOID }, - "stdin.onEnd": { argTypes: [{ kind: "func", params: [], ret: VOID }, BOOL], result: VOID }, - "stdin.onError": { argTypes: [null, BOOL], result: VOID }, - /** process.stdin.on/once('keypress', cb) (scr_readline_tty.c): cb is the - * listener as a checked-dynamic function, called with (str, key). */ - "stdin.onKeypress": { argTypes: [DYN, BOOL], result: VOID }, - /** The for-await chunk source over process.stdin: no args, result - * Promise (+1). Fulfills with the next arrived chunk; the - * EMPTY bytes value is the done sentinel (POSIX reads never deliver - * empty data chunks), which the for-await desugar turns into loop - * exit. Never throws itself; awaiting it re-throws nothing (stdin - * errors surface through 'error' listeners, not the iterator). */ - "stdin.nextChunk": { argTypes: [], result: { kind: "promise", inner: BYTES_U8 } }, - "fs.readFileSync": { argTypes: [STRING, STRING], result: STRING }, - "fs.readFileEncoded": { argTypes: [STRING, STRING], result: STRING }, - "fs.readFdEncoded": { argTypes: [F64, STRING], result: STRING }, - /** readFileSync(path) — the Buffer read (+1 bytes); throws catchably - * like the utf8 form. */ - "fs.readFileSyncBuf": { argTypes: [STRING], result: BYTES_U8 }, - /** readFileSync(path, enc) with a RUNTIME encoding (an untyped JS - * parameter): undefined/null answer Buffers, utf8 a string, other real - * encodings throw the "not supported yet" Error, unknown names throw - * ERR_UNKNOWN_ENCODING. */ - "fs.readFileSyncDyn": { argTypes: [STRING, DYN], result: DYN }, - "fs.writeFileSync": { argTypes: [STRING, STRING], result: VOID }, - "fs.appendFileSync": { argTypes: [STRING, STRING], result: VOID }, - "fs.existsSync": { argTypes: [STRING], result: BOOL }, - "fs.mkdirSync": { argTypes: [STRING], result: VOID }, - "fs.rmSync": { argTypes: [STRING], result: VOID }, - "fs.rmdirSync": { argTypes: [STRING], result: VOID }, - "fs.readdirSync": { argTypes: [STRING], result: arrayOf(STRING) }, - // Result is the call site's Dirent record array (VOID is the - // structure-checked sentinel, the os.networkInterfaces pattern). - /** `fs.readdirSync(path, { withFileTypes: true })` — Dirent rows over - * one readdir pass (scr_lib.c's scandir snapshot; DT_UNKNOWN falls back - * to lstat, Node's getDirents rule). The result type is the call site's - * interned Dirent record array (name, parentPath, hidden %dtype in - * libuv's UV_DIRENT encoding) — verified by the frontend; the emitter - * assembles the rows from the snapshot. Byte order on POSIX (libuv - * sorts), the directory's order on Windows; no "."/"..". Throws - * Node's scandir errno error (may-throw seed set); fresh +1 array. */ - "fs.readdirTypesSync": { argTypes: [STRING], result: VOID }, - /** fs.globSync / fs.glob / fs/promises glob (scr_glob.c, Node's - * lib/internal/fs/glob.js walk): pattern and options are dyn values the - * runtime validates in Node's order. The string forms answer string[]; - * the Dirent forms answer the call site's Dirent record array (validated - * in validate.ts) assembled from the snapshot; fs.callbackGlob takes the - * argument vector and calls back with string[]. */ - "fs.globSync": { argTypes: [DYN, DYN], result: arrayOf(STRING) }, - "fs.globTypesSync": { argTypes: [DYN, DYN], result: VOID }, - "fs.callbackGlob": { argTypes: [DYN], result: VOID }, - "fsp.globStrings": { argTypes: [DYN, DYN], result: arrayOf(STRING) }, - "fsp.globTypes": { argTypes: [DYN, DYN], result: VOID }, - /** The error an fs/promises glob walk ended with (undefined when none), - * read once right after fsp.globStrings / fsp.globTypes. */ - "fsp.globError": { argTypes: [], result: DYN }, - /** fs.Dir (scr_lib.c): opendirSync(path, bufferSize) throws Node's - * opendir error (or the options.bufferSize RangeError); dir.readSync - * answers the call site's `Dirent | null` (VOID here — the emitter builds - * the row like readdirTypesSync) and dir.read is its promise twin; - * readSync and closeSync throw ERR_DIR_CLOSED on a closed Dir, read and - * close reject with it. */ - "fs.opendirSync": { argTypes: [STRING, F64], result: DIR_T }, - "fsp.opendir": { argTypes: [STRING, F64], result: { kind: "promise", inner: DIR_T } }, - "dir.path": { argTypes: [DIR_T], result: STRING }, - "dir.readSync": { argTypes: [DIR_T], result: VOID }, - "dir.read": { argTypes: [DIR_T], result: { kind: "promise", inner: VOID } }, - "dir.closeSync": { argTypes: [DIR_T], result: VOID }, - "dir.close": { argTypes: [DIR_T], result: { kind: "promise", inner: VOID } }, - /** node:path (scr_path.c ports BOTH of Node's implementations - * function-by-function): the `path.*` family is posix, the - * `path.win32*` family is Node v24's path.win32 ported exactly — the - * frontend binds the bare module to the TARGET platform's family - * (Node on Windows IS path.win32) and the path.posix / path.win32 - * namespaces to their own family everywhere. join and resolve take ONE - * string[] arg — the frontend packs the variadic call's arguments into - * an array literal. basename always receives its suffix (the frontend - * completes an omitted one with "", a Node no-op). toNamespacedPath is - * the posix identity and the win32 \\?\-prefixer. None of these throw; - * the resolves consult the process cwd like Node's. */ - "path.join": { argTypes: [arrayOf(STRING)], result: STRING }, - "path.resolve": { argTypes: [arrayOf(STRING)], result: STRING }, - "path.normalize": { argTypes: [STRING], result: STRING }, - "path.dirname": { argTypes: [STRING], result: STRING }, - "path.basename": { argTypes: [STRING, STRING], result: STRING }, - "path.extname": { argTypes: [STRING], result: STRING }, - "path.isAbsolute": { argTypes: [STRING], result: BOOL }, - "path.relative": { argTypes: [STRING, STRING], result: STRING }, - "path.toNamespacedPath": { argTypes: [STRING], result: STRING }, - "path.win32Join": { argTypes: [arrayOf(STRING)], result: STRING }, - "path.win32Resolve": { argTypes: [arrayOf(STRING)], result: STRING }, - "path.win32Normalize": { argTypes: [STRING], result: STRING }, - "path.win32Dirname": { argTypes: [STRING], result: STRING }, - "path.win32Basename": { argTypes: [STRING, STRING], result: STRING }, - "path.win32Extname": { argTypes: [STRING], result: STRING }, - "path.win32IsAbsolute": { argTypes: [STRING], result: BOOL }, - "path.win32Relative": { argTypes: [STRING, STRING], result: STRING }, - "path.win32ToNamespacedPath": { argTypes: [STRING], result: STRING }, - /** path.parse: [root, dir, base, ext, name] as a fresh string[] (the - * frontend builds the ParsedPath record); path.format: the pathObject's - * root, dir, base, name and ext with undefined spelled "". */ - "path.parse": { argTypes: [STRING], result: arrayOf(STRING) }, - "path.win32Parse": { argTypes: [STRING], result: arrayOf(STRING) }, - "path.format": { argTypes: [STRING, STRING, STRING, STRING, STRING], result: STRING }, - "path.win32Format": { argTypes: [STRING, STRING, STRING, STRING, STRING], result: STRING }, - /** path.matchesGlob(path, pattern): Node's matchGlobPattern over its - * bundled minimatch (scr_glob.c). Both families match case-insensitively - * on macOS and Windows hosts like Node's; win32 also accepts either - * separator. Throws TypeError for a pattern over 64 Ki code units, - * RangeError for a brace sequence too long to expand, and SyntaxError - * when a segment's regex is invalid under the u flag. */ - "path.matchesGlob": { argTypes: [STRING, STRING], result: BOOL }, - "path.win32MatchesGlob": { argTypes: [STRING, STRING], result: BOOL }, - /** node:os: homedir is $HOME else getpwuid(3); tmpdir is Node's env - * cascade ($TMPDIR/$TMP/$TEMP else /tmp, one trailing slash trimmed). - * os.platform() lowers to process.platform — one implementation. */ - "os.homedir": { argTypes: [], result: STRING }, - /** os.release(): uname(2)'s release field — Node's own implementation - * (the kernel version string, e.g. "24.6.0" on macOS 15). Interned; +1 - * per read. Never throws. */ - "os.release": { argTypes: [], result: STRING }, - /** os.type(): uname(2)'s sysname ("Darwin", "Linux"; "Windows_NT" on - * win32) — Node's uv_os_uname answer. Interned per call; never throws. */ - "os.type": { argTypes: [], result: STRING }, - /** os.totalmem(): total physical memory in bytes. Never throws. */ - "os.totalmem": { argTypes: [], result: F64 }, - /** os.availableParallelism(): CPUs the process may use (libuv's answer), >= 1. Never throws. */ - "os.availableParallelism": { argTypes: [], result: F64 }, - /** os.hostname/machine/version/endianness, freemem, uptime and loadavg: - * libuv's answers (uname's machine and version fields, gethostname, - * "LE"/"BE", free memory in bytes, uptime in seconds, the three load - * averages). Fresh results; never throw. */ - "os.hostname": { argTypes: [], result: STRING }, - "os.machine": { argTypes: [], result: STRING }, - "os.version": { argTypes: [], result: STRING }, - "os.endianness": { argTypes: [], result: STRING }, - "os.freemem": { argTypes: [], result: F64 }, - "os.uptime": { argTypes: [], result: F64 }, - "os.loadavg": { argTypes: [], result: arrayOf(F64) }, - /** os.getPriority(pid): validateInt32 (RangeError ERR_OUT_OF_RANGE), - * then the priority; a failure throws Node's SystemError. */ - "os.getPriority": { argTypes: [F64], result: F64 }, - /** os.setPriority(pid, priority): validateInt32 for both (priority in - * -20..19, RangeError ERR_OUT_OF_RANGE), then the priority; a failure - * throws Node's SystemError. */ - "os.setPriority": { argTypes: [F64, F64], result: VOID }, - /** os.cpus(): cpusTake snapshots every CPU and answers the count; - * cpusModel(i) and cpusField(i, k) (k: 0 speed, 1 user, 2 nice, 3 sys, - * 4 idle, 5 irq) read the snapshot. Never throw. */ - "os.cpusTake": { argTypes: [], result: F64 }, - "os.cpusModel": { argTypes: [F64], result: STRING }, - "os.cpusField": { argTypes: [F64, F64], result: F64 }, - /** os.constants: the per-thread null-prototype constants object (dlopen, - * errno, signals, priority, UV_UDP_REUSEADDR). Never throws. */ - "os.constants": { argTypes: [], result: DYN }, - /** fs.constants: the target's cached null-prototype constants object. */ - "fs.constants": { argTypes: [], result: DYN }, - /** The deprecated node:constants object (scr_os.c): the os groups and - * fs.constants merged with the borrowed baked crypto.constants table, - * frozen, one per thread. */ - "constants.object": { argTypes: [DYN], result: DYN }, - /** umask(2): arg < 0 reads without setting (umask has no read-only form - * — set 0, restore); otherwise sets and answers the PREVIOUS mask. - * Never throws. */ - "process.umask": { argTypes: [F64], result: F64 }, - /** chdir(2) — throws Node's fs-shaped error (ENOENT/EACCES/ENOTDIR, - * syscall "chdir") on failure. */ - "process.chdir": { argTypes: [STRING], result: VOID }, - "process.loadEnvFile": { argTypes: [DYN], result: VOID }, - /** process._exiting: true once the exit sequence began (exit listeners - * running) — the runtime flag scr_run_exit_listeners/process.exit set. - * Never throws. */ - "process.exiting": { argTypes: [], result: BOOL }, - /** net's process-wide happy-eyeballs attempt budget (Node's default - * 250ms): one runtime double in the core unit, so reading/writing it - * never forces the net unit into the link. Never throw. */ - "net.getAutoSelTimeout": { argTypes: [], result: F64 }, - "net.setAutoSelTimeout": { argTypes: [F64], result: VOID }, - /** realpath(3) with Node's error shape (syscall "lstat" in the message, - * Node's own spelling for realpathSync failures). +1 fresh string. */ - "fs.realpathSync": { argTypes: [STRING], result: STRING }, - /** fs.realpathSync (or its .native when the flag is true) as a function - * value: path and options arrive unchecked; the result is a string or a - * Buffer by the options' encoding. */ - "fs.realpathValue": { argTypes: [DYN, DYN, BOOL], result: DYN }, - /** Native realpath with syscall "realpath" on failure. +1 fresh string. */ - "fs.realpathNativeSync": { argTypes: [STRING], result: STRING }, - /** The os.userInfo() field trio (uv_os_get_passwd's slices): pw_name, - * pw_shell, pw_dir — the PASSWD homedir, not os.homedir's $HOME-first - * cascade (Node's own split). The frontend assembles the UserInfo - * record from these plus getuid/getgid. +1 fresh strings; a passwd - * lookup failure aborts (Node throws a system error there — no - * compiled program path reaches it for the running uid). */ - "os.userName": { argTypes: [], result: STRING }, - "os.userShell": { argTypes: [], result: STRING }, - "os.userHomedir": { argTypes: [], result: STRING }, - "os.tmpdir": { argTypes: [], result: STRING }, - // Result is the call site's Dict record (VOID is - // the dgram.address sentinel — the libCall case checks the structure). - /** os.networkInterfaces(): getifaddrs(3) → the Dict - * record, built inline by the emitter from a runtime snapshot (scr_lib.c). - * The result type is the CALL SITE's mapped @types/node shape — a pure - * index-signature record whose value is `Info[] | undefined`, Info a - * two-record union (IPv4: scopeid `number | undefined` holding undefined; - * IPv6: scopeid number) — verified structurally by the frontend. Rows - * match libuv's filter (IFF_UP && IFF_RUNNING, AF_INET/AF_INET6; loopback - * = internal; MACs from the interface's link-level sibling entry, zeros - * when absent; cidr from the netmask's contiguous prefix, the null arm - * when it is missing or non-contiguous). Key/row order follows the OS's - * getifaddrs enumeration — Node itself does not guarantee an order. - * Fresh +1 record; never throws (a getifaddrs failure yields {}). */ - "os.networkInterfaces": { argTypes: [], result: VOID }, - /** `Math.max(...xs)` / `Math.min(...xs)` over one spread number[] - * (scr_number.c): the JS fold exactly — any NaN element poisons the - * result, ±0 order by the JS comparison (max prefers +0, min prefers - * -0), and the empty array yields -Infinity / +Infinity like the - * zero-argument calls. Borrows the array; never throws. */ - "math.maxArr": { argTypes: [arrayOf(F64)], result: F64 }, - "math.minArr": { argTypes: [arrayOf(F64)], result: F64 }, - "math.hypotArr": { argTypes: [arrayOf(F64)], result: F64 }, - /** `Math.floor(x)` — C floor() IS the JS operation (NaN/±0/±Infinity - * pass through bit-exactly). Never throws. */ - "math.floor": { argTypes: [F64], result: F64 }, - /** Math.abs (C fabs — IS the JS operation) and Math.round (scr_lib.c: - * ECMA half-toward-+Infinity with the exact-fraction comparison — C - * round() is half-away-from-zero and floor(x+0.5) drifts at the - * epsilon boundary). Borrow nothing; never throw. */ - "math.abs": { argTypes: [F64], result: F64 }, - "math.round": { argTypes: [F64], result: F64 }, +function buildLibFnSignatures() { + return defineLibFnSignatures({ + /** Native static fetch and its Web-platform companions. fetch.start + * answers once the response head arrives; the response body readers + * consume the native body stream. AbortSignal and ReadableStream values + * are opaque checked-dynamic handles. */ + "fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } }, + "fetch.responseNew": { argTypes: [DYN, DYN], result: DYN }, + "fetch.responseJsonStatic": { argTypes: [DYN, DYN], result: DYN }, + "fetch.responseLight": { argTypes: [DYN], result: arrayOf(STRING) }, + "fetch.input": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "abort.controllerNew": { argTypes: [], result: DYN }, + /** EventTarget / Event / CustomEvent construction (scr_event_target.c): + * the Event constructors take the argument vector as one dyn array and + * throw Node's validation errors; `event.is` is `v instanceof Event` + * (kind 0), `CustomEvent` (1), `EventTarget` (2). addAbortListener + * answers the disposer (the Disposable's [Symbol.dispose]). */ + "event.targetNew": { argTypes: [], result: DYN }, + "event.new": { argTypes: [DYN], result: DYN }, + "event.customNew": { argTypes: [DYN], result: DYN }, + "event.is": { argTypes: [DYN, F64], result: BOOL }, + /** Compiled EventTarget and Event subclasses: the receiver is the + * instance (an object rooted at %EventTarget / %Event). targetInit and + * init are super() (init takes the argument vector as one dyn array + * and throws the constructor's validation errors; initType is the + * super(type) form with a string); targetCall runs an EventTarget method + * with the instance's own capsule as `self` (targetDispatch is its + * dispatchEvent(event) form, answering the bool); call, get and set + * reach the Event members. */ + "event.targetInit": { argTypes: [null], result: VOID }, + "event.init": { argTypes: [null, DYN], result: VOID }, + "event.initType": { argTypes: [null, STRING], result: VOID }, + /** %main hands the engine the compiled `listener.handleEvent` reader + * (lower-event-target.ts noteEventListeners). */ + "event.handleEventReader": { argTypes: [DYN], result: VOID }, + "event.targetDispatch": { argTypes: [null, DYN, DYN], result: BOOL }, + "event.targetCall": { argTypes: [null, DYN, STRING, DYN], result: DYN }, + "event.call": { argTypes: [null, STRING, DYN], result: DYN }, + "event.get": { argTypes: [null, STRING], result: DYN }, + "event.set": { argTypes: [null, STRING, DYN], result: VOID }, + "events.addAbortListener": { argTypes: [DYN, DYN], result: DYN }, + /** Blob / File (scr_blob.c): the constructors and slice() take their + * argument vector as one dyn array and throw Node's validation errors; + * the readers settle on Node's microtask turn for the Blob's parts. + * `blob.is` is `v instanceof Blob` (kind 0) or `File` (1). */ + "blob.new": { argTypes: [DYN], result: DYN }, + "blob.fileNew": { argTypes: [DYN], result: DYN }, + "blob.slice": { argTypes: [DYN, DYN], result: DYN }, + "blob.text": { argTypes: [DYN], result: { kind: "promise", inner: STRING } }, + "blob.bytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } }, + "blob.arrayBuffer": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "blob.is": { argTypes: [DYN, F64], result: BOOL }, + /** WebAssembly (scr_wasm.c): modules translated at build time + * (wasm/translate.ts), found at run time by the SHA-256 of their bytes. + * compile and instantiate over bytes settle on a later loop turn; + * instantiate over a Module settles now, as in Node. `wasm.is` answers + * instanceof for Module, Instance, Memory, Table and Global (kinds 0-4) + * and CompileError, LinkError and RuntimeError (5-7); `wasm.errorNew` + * constructs one of the error classes (kind 5-7, message, options). */ + "wasm.compile": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "wasm.validate": { argTypes: [DYN], result: BOOL }, + "wasm.instantiate": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "wasm.moduleNew": { argTypes: [DYN], result: DYN }, + "wasm.instanceNew": { argTypes: [DYN, DYN], result: DYN }, + "wasm.is": { argTypes: [DYN, F64], result: BOOL }, + "wasm.errorNew": { argTypes: [F64, DYN, DYN], result: DYN }, + /** new WebAssembly.Memory(descriptor), Table(descriptor, value) and + * Global(descriptor, value) (an undefined value is the type's default); + * `wasm.withoutNew` throws the TypeError for Module, Instance, Memory, + * Table or Global (kinds 0-4) called without new, after its arguments + * evaluate. */ + "wasm.memoryNew": { argTypes: [DYN], result: DYN }, + "wasm.tableNew": { argTypes: [DYN, DYN], result: DYN }, + "wasm.globalNew": { argTypes: [DYN, DYN], result: DYN }, + "wasm.withoutNew": { argTypes: [F64, DYN, DYN], result: DYN }, + /** WebAssembly.Module.imports(module), exports(module) and + * customSections(module, name): checked-dynamic arrays of descriptor + * objects and ArrayBuffers. */ + "wasm.moduleImports": { argTypes: [DYN], result: DYN }, + "wasm.moduleExports": { argTypes: [DYN], result: DYN }, + "wasm.moduleCustomSections": { argTypes: [DYN, DYN], result: DYN }, + /** `v.valueOf()` on a WebAssembly-typed receiver: a Global's value, + * dyn.valueOf's answer for anything else. */ + "wasm.valueOf": { argTypes: [DYN], result: DYN }, + /** WebAssembly.compile (0), instantiate (1) and validate (2) as function + * values, and the namespace object holding them. */ + "wasm.function": { argTypes: [F64], result: DYN }, + "wasm.namespace": { argTypes: [], result: DYN }, + /** The global object viewed through a cast to a type with a WebAssembly + * member: an object holding the namespace. */ + "wasm.globalView": { argTypes: [], result: DYN }, + "abort.timeout": { argTypes: [DYN], result: DYN }, + "abort.now": { argTypes: [DYN], result: DYN }, + "abort.any": { argTypes: [DYN], result: DYN }, + "fetch.function": { argTypes: [], result: DYN }, + "fetch.requestNew": { argTypes: [DYN, DYN], result: DYN }, + "fetch.headersNew": { argTypes: [DYN], result: DYN }, + "fetch.responseArrayBuffer": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "fetch.responseJson": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "fetch.responseText": { argTypes: [DYN], result: { kind: "promise", inner: STRING } }, + "fetch.responseBytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } }, + "fetch.abortControllerNew": { argTypes: [], result: DYN }, + "fetch.abortTimeout": { argTypes: [DYN], result: DYN }, + "fetch.abortNow": { argTypes: [DYN], result: DYN }, + "fetch.abortAny": { argTypes: [DYN], result: DYN }, + "fetch.streamNew": { argTypes: [DYN], result: DYN }, + "fetch.transformNew": { argTypes: [], result: DYN }, + // Program-dependent iterable: typed arrays/bytes/string stay intact so + // the native stream can pull lazily; checked-dynamic values are the + // fallback. The libCall validator in validate.ts checks the closed set. + "fetch.webIs": { argTypes: [DYN, STRING], result: BOOL }, + "fetch.streamIs": { argTypes: [DYN], result: BOOL }, + "fetch.streamFrom": { argTypes: [null], result: DYN }, + // The chunk/result record depends on ReadableStream; validated by validate.ts. + "fetch.readerRead": { argTypes: [DYN], result: VOID }, + /** Readable.fromWeb (scr_stream_from_web.c): the stream, objectMode, + * highWaterMark (< 0: the mode's default) and encoding ("": none). */ + "fetch.readableFromWeb": { argTypes: [DYN, BOOL, F64, STRING], result: VOID }, + "json.parse": { argTypes: [STRING], result: DYN }, + "json.parseReviver": { argTypes: [STRING, DYN], result: DYN }, + "json.stringifyReplacer": { argTypes: [DYN, DYN, STRING], result: DYN }, + "json.stringifyValue": { argTypes: [DYN, DYN, DYN], result: DYN }, + /** Keyed WRITE on a dyn value — `h.onDone = cb` / `h["k"] = v` on a + * checked-dynamic object (args: receiver, key string, value — all + * borrowed; the runtime copies the key and retains the value in). An + * OBJ receiver sets the member (later writes win, insertion order + * preserved — JS exactly); undefined/null throws Node's catchable + * "Cannot set properties of undefined (setting 'k')"; every other kind + * throws Node's STRICT-mode "Cannot create property 'k' on " — + * primitives quoting their rendering, V8's "on number '5'" + * (sloppy mode would ignore the write — suite tests are 'use strict'; + * SEMANTICS.md notes the sloppy divergence: the write throws). Void + * result; in the may-throw seed set. */ + "dyn.keySet": { argTypes: [DYN, STRING, DYN], result: VOID }, + "dyn.keySetComputed": { argTypes: [DYN, DYN, DYN], result: VOID }, + /** Delete an ordinary checked-native object's own key. Borrows both + * arguments; other receiver representations retain a runtime refusal. */ + "dyn.keyDelete": { argTypes: [DYN, STRING, BOOL], result: VOID }, + "dyn.keyDeleteComputed": { argTypes: [DYN, DYN, BOOL], result: VOID }, + "dyn.reflectDeleteProperty": { argTypes: [DYN, DYN], result: BOOL }, + "dyn.hasKeyComputed": { argTypes: [DYN, DYN], result: BOOL }, + "dyn.hasOwnComputed": { argTypes: [DYN, DYN], result: BOOL }, + "dyn.propertyIsEnumerableComputed": { argTypes: [DYN, DYN], result: BOOL }, + /** Native own properties on globalThis keyed by symbol identity. */ + "dyn.globalSymbolGet": { argTypes: [SYMBOL_T], result: DYN }, + "dyn.globalSymbolSet": { argTypes: [SYMBOL_T, DYN], result: VOID }, + "dyn.globalSymbolHas": { argTypes: [SYMBOL_T], result: BOOL }, + "dyn.globalSymbolDelete": { argTypes: [SYMBOL_T], result: VOID }, + /** Exact native class capsule identity, without materializing its fields. */ + "dyn.typedRefIs": { argTypes: [DYN, STRING], result: BOOL }, + /** A capsule boxed by this program from exactly this class: one key + * pointer test, false for equal keys boxed elsewhere. Guards fast paths + * whose fallback handles every other value. The key is a literal. */ + "dyn.typedRefKeyIs": { argTypes: [DYN, STRING], result: BOOL }, + "dyn.classIs": { argTypes: [DYN, STRING], result: BOOL }, + /** Destructuring pack over a dyn source — `const [a, b] = d`, a + * destructured dyn callback param (args: the source and the STATIC + * TypeError spelling, "" when the source has none — both borrowed; + * result: a fresh dyn array, +1). Iterable kinds collect like spread + * (arrays element-by-element, strings by code point, bytes by byte); + * every other kind throws V8's destructuring TypeError — the spelling + * verbatim when non-empty, else the runtime kind wording ("number 5 is + * not iterable (cannot read property Symbol(Symbol.iterator))"). In the + * may-throw seed set. */ + "dyn.iterPack": { argTypes: [DYN, STRING], result: DYN }, + "dyn.mathFold": { argTypes: [DYN, BOOL, STRING], result: F64 }, + "dyn.destructureRest": { argTypes: [DYN, STRING], result: DYN }, + "dyn.arrayFromIterator": { argTypes: [DYN], result: DYN }, + "dyn.iterator": { argTypes: [DYN, STRING], result: DYN }, + /** for await over a checked-dynamic value: the loop's iterator (a + * compiled async generator, Symbol.asyncIterator's result, or the sync + * iterator), and whether that iterator is synchronous. */ + "dyn.asyncIteratorOpen": { argTypes: [DYN, STRING], result: DYN }, + "dyn.asyncIteratorWasSync": { argTypes: [], result: BOOL }, + "dyn.iteratorResult": { argTypes: [DYN], result: DYN }, + "dyn.iteratorCanStep": { argTypes: [DYN, DYN], result: BOOL }, + "dyn.iteratorStep": { argTypes: [DYN], result: DYN }, + "dyn.iteratorStepDone": { argTypes: [DYN], result: BOOL }, + "dyn.setGeneratorMembers": { argTypes: [DYN, DYN], result: VOID }, + "dyn.iteratorHelperMethod": { argTypes: [STRING], result: DYN }, + "dyn.collectionIteratorIndex": { argTypes: [DYN], result: F64 }, + "dyn.mapSeedEntries": { argTypes: [DYN], result: DYN }, + "dyn.mapSeedEntry": { argTypes: [DYN], result: DYN }, + /** The for-of-over-dyn pack accessors — the emitted index loop drives + * them over a dyn.iterPack result (ARR by construction). arrLen: the + * ARR length as f64 (0 for non-ARR kinds; arg borrowed). arrAt: the + * element at index (+1; the undefined singleton past the end). Neither + * throws. */ + "dyn.arrLen": { argTypes: [DYN], result: F64 }, + "dyn.arrAt": { argTypes: [DYN, F64], result: DYN }, + /** `key in v` with a RUNTIME (string) key on a checked-dynamic + * receiver (args: value dyn, key string; result bool): OBJ answers + * own-member presence, ARR answers 'length'/a valid index — exactly + * the literal-key dynHasKey path. Proxy has traps may throw. */ + "dyn.hasKey": { argTypes: [DYN, STRING], result: BOOL }, + /** The `in` operator with a runtime string key: dyn.hasKey plus the + * members inherited from builtin and compiled class prototypes. */ + "dyn.in": { argTypes: [DYN, STRING], result: BOOL }, + "dyn.freeze": { argTypes: [DYN], result: DYN }, + "dyn.isFrozen": { argTypes: [DYN], result: BOOL }, + /** Object.defineProperty on a declared class field (scr_json.c): the + * field's current value (borrowed), its FIELD_* attribute word, the + * instance's CLASS_* integrity word, the field name and the descriptor. + * Validates as Node does (TypeError "Cannot redefine property: f") and + * answers a one-property holder of the defined value and attributes (+1), + * read back with class.definedBits and class.definedValue. */ + "class.defineField": { argTypes: [DYN, F64, F64, STRING, DYN], result: DYN }, + "class.definedBits": { argTypes: [DYN, STRING], result: F64 }, + "class.definedValue": { argTypes: [DYN, STRING], result: DYN }, + /** Whether a property descriptor has a `value` member. */ + "class.descriptorHasValue": { argTypes: [DYN], result: BOOL }, + /** Throws Node's TypeError for a write to a read-only class field: the + * field name and the instance's class name. */ + "class.throwReadOnly": { argTypes: [STRING, STRING], result: VOID }, + /** Throws Node's TypeError for a class field initializer on a + * non-extensible instance: the field name. */ + "class.throwNotExtensible": { argTypes: [STRING], result: VOID }, + /** Object.freeze of a record whose shape is freezable (frozen-records.ts): + * sets the frozen bit in its rc word (scr_record_freeze). The receiver is + * any record type. */ + "record.freeze": { argTypes: [null], result: VOID }, + /** Whether a record is frozen: its rc word's frozen bit, set by + * record.freeze and dyn.freeze of a capsule. Emitted inline. The receiver + * is any record type. */ + "record.isFrozen": { argTypes: [null], result: BOOL }, + /** Throws Node's TypeError for a write to a frozen record: the key, the + * object's text in the message ("#") and the operation (0 assign, + * 1 add, 2 delete). */ + "record.throwFrozen": { argTypes: [STRING, STRING, F64], result: VOID }, + /** Object.freeze of an array (scr_arr_freeze): sets its frozen bit, which + * the write guards of IrModule.freezableArrays test, and isFrozen reads. + * The receiver is any array type. */ + "array.freeze": { argTypes: [null], result: VOID }, + "array.isFrozen": { argTypes: [null], result: BOOL }, + /** Object.freeze, seal or preventExtensions of a Map or Set + * (scr_map_prevent_extensions), which only makes it not extensible: its + * entries stay writable. The receiver is any map or set type. */ + "collection.preventExtensions": { argTypes: [null], result: VOID }, + "collection.isExtensible": { argTypes: [null], result: BOOL }, + "dyn.nativeSetNew": { argTypes: [DYN], result: DYN }, + "dyn.nativeSetIs": { argTypes: [DYN], result: BOOL }, + "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 + * `.constructor`. The name is a compiler-chosen literal. */ + "dyn.globalConstructor": { argTypes: [STRING], result: DYN }, + /** A builtin global function (setTimeout, setInterval, setImmediate, + * the clear functions, queueMicrotask, process.nextTick) read as a + * JavaScript value: one interned function per name whose calls run the + * timer APIs (it pulls in the event loop). The name is a compiler-chosen + * literal. */ + "dyn.builtinFunction": { argTypes: [STRING], result: DYN }, + "dyn.namespaceNew": { argTypes: [STRING, DYN], result: DYN }, + "dyn.namespaceDefine": { argTypes: [DYN, STRING, DYN, F64, F64], result: VOID }, + "date.nativeNew": { argTypes: [DYN], result: DYN }, + /** `new Date(year, monthIndex, ...)` in JavaScript: the argument array's + * first seven values pass through ToNumber in order (later ones are + * evaluated, never converted), then the local-time field constructor. */ + "date.nativeNewFields": { argTypes: [DYN], result: DYN }, + /** `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 + * (bytes decode per the literal encoding — utf8 default; strings, + * numbers, booleans, arrays, objects answer JS-exactly; undefined and + * null throw the catchable TypeError). */ + "dyn.toString": { argTypes: [DYN, DYN, STRING], result: STRING }, + "dyn.valueOf": { argTypes: [DYN, STRING], result: DYN }, + /** Object.defineProperties over dyn values (args: target, descriptors — + * both borrowed dyn; result: the target, +1 — JS's return value). + * Value descriptors become own properties on OBJ and FUNC targets; + * OBJ targets preserve data attributes. get/set + * descriptors and non-object targets/descriptors throw catchably + * (Node's TypeError texts; accessors the "not supported yet" Error). In + * the may-throw seed set. */ + "dyn.defineProps": { argTypes: [DYN, DYN], result: DYN }, + "dyn.defineProperty": { argTypes: [DYN, DYN, DYN], result: DYN }, + "dyn.getOwnPropertyDescriptor": { argTypes: [DYN, DYN], result: DYN }, + "dyn.arrayProtoCall": { argTypes: [DYN, STRING, DYN], result: DYN }, + /** Promise.all / Promise.allSettled / Promise.any over a spread + * checked-dynamic array: the values, the settled-record array, or the + * first fulfillment (AggregateError once every entry rejects). + * Thenable elements adopt; the second argument reads a program class + * instance's `then` (the compiled property dispatch). */ + "dyn.promiseAll": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "dyn.promiseAllSettled": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "dyn.promiseAny": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + /** PromiseResolve over one checked-dynamic value: the promise it holds, + * a promise that adopts a thenable's settlement, or a promise fulfilled + * with the value. The second argument reads `then` as above. */ + "dyn.promiseResolve": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + /** Bare `typeof v` on a dyn value AS A STRING (arg: the dyn value, + * borrowed; result: an owned string) — the dyn kind's JS answer: + * undefined→"undefined", null/object/array/bytes→"object" (JS's oldest + * wart preserved), boolean/number/string by kind, function→"function". + * Never throws. */ + "dyn.typeof": { argTypes: [DYN], result: STRING }, + /** Object.prototype.toString.call on a checked-dynamic value. */ + "dyn.objectTag": { argTypes: [DYN], result: STRING }, + /** Engine-free CommonJS module graph. Module values are scalar f64 + * handles; generated startup defines the registry, init wrappers update + * loading/cache state, and the read surface answers Node's live metadata. */ + "module.registryInit": { argTypes: [F64], result: VOID }, + "module.define": { + argTypes: [F64, STRING, STRING, STRING, arrayOf(STRING), BOOL], + result: VOID, + }, + "module.enter": { argTypes: [F64], result: VOID }, + "module.link": { argTypes: [F64, F64], result: VOID }, + "module.finish": { argTypes: [F64], result: VOID }, + "module.fail": { argTypes: [F64], result: VOID }, + "module.filename": { argTypes: [F64], result: STRING }, + "module.id": { argTypes: [F64], result: STRING }, + "module.path": { argTypes: [F64], result: STRING }, + "module.paths": { argTypes: [F64], result: arrayOf(STRING) }, + "module.children": { argTypes: [F64], result: arrayOf(F64) }, + "module.parent": { argTypes: [F64], result: F64 }, + "module.loaded": { argTypes: [F64], result: BOOL }, + "module.cacheGet": { argTypes: [STRING], result: F64 }, + "module.cacheHas": { argTypes: [STRING], result: BOOL }, + "module.cacheKeys": { argTypes: [], result: arrayOf(STRING) }, + "timers.setTimeout": { argTypes: [{ kind: "func", params: [], ret: VOID }, F64], result: VOID }, + /** The repeating timer pair. setInterval takes (callback, ms) like + * setTimeout and RETURNS the f64 handle the fallback declarations + * promise (ids start at 1, so truthiness narrowing works); the loop + * owns the callback until clearInterval removes the entry (eagerly — a + * live interval keeps the loop alive, a cleared one releases it, like + * Node). Neither throws; a throw ESCAPING an interval callback ends the + * program like a setTimeout throw. */ + "timers.setInterval": { argTypes: [{ kind: "func", params: [], ret: VOID }, F64], result: F64 }, + "timers.clearInterval": { argTypes: [F64], result: VOID }, + /** setTimeout WITH a clear handle (the f64 id) — the clearable/unref-able + * one-shot; clearTimeout cancels it, .unref() drops it from loop + * liveness. The plain timers.setTimeout stays the handle-less + * fire-and-forget. */ + "timers.setTimeoutHandle": { + argTypes: [{ kind: "func", params: [], ret: VOID }, F64], + result: F64, + }, + "timers.clearTimeout": { argTypes: [F64], result: VOID }, + /** Timeout.unref()/ref()/hasRef() — loop-liveness bookkeeping over the + * handle id. unref/ref RETURN the handle (f64) for chaining; hasRef + * returns bool. */ + "timers.unref": { argTypes: [F64], result: F64 }, + "timers.ref": { argTypes: [F64], result: F64 }, + "timers.hasRef": { argTypes: [F64], result: BOOL }, + /** Timeout.refresh() — re-arm to now + the original delay (from the + * heap or from inside the firing callback; a one-shot that fired on an + * earlier turn is gone and no-ops — documented divergence). Returns + * the handle for chaining like unref/ref. */ + "timers.refresh": { argTypes: [F64], result: F64 }, + /** setImmediate/clearImmediate — Node's check phase: callbacks run + * once per loop turn AFTER due timers, FIFO, and immediates queued + * mid-phase wait for the next turn. setImmediate returns the f64 + * handle (its own id space — clearTimeout of an Immediate no-ops, + * like Node); the Immediate ref trio mirrors the Timeout one (an + * unref'd pending immediate neither keeps the loop alive nor fires + * once nothing reffed remains). */ + "timers.setImmediate": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: F64 }, + /** queueMicrotask (scr_async.c): the callback enters the SAME FIFO + * promise continuations ride — one microtask order, like V8's queue — + * and a throw is an UNCAUGHT exception (never a rejection). + * timers.queueMicrotask takes an owned zero-param closure and never + * throws; the Dyn form (checked-dynamic arguments — the mustCall + * wrapper, the suite's invalid-input probes) throws Node's + * ERR_INVALID_ARG_TYPE synchronously on a non-function value and calls + * the function with zero arguments at drain. */ + "timers.queueMicrotask": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: VOID }, + "timers.queueMicrotaskDyn": { argTypes: [DYN], result: VOID }, + "timers.clearImmediate": { argTypes: [F64], result: VOID }, + /** process.nextTick(cb, ...args) — the user tick queue. args: [cb + * (() => void; trailing call arguments ride the timer surface's + * interned dyn thunk)]. Ticks drain BEFORE promise jobs at every loop + * checkpoint, to joint exhaustion with them (Node's tick-then- + * microtask order); ticks enqueued by station listeners run at the + * NEXT checkpoint (the stream-tick station divergence, SEMANTICS.md). + * Pending ticks are always-ready work — the loop neither sleeps nor + * exits while any exist; ticks scheduled from 'exit' listeners never + * run (Node). The enqueue itself never throws. */ + "process.nextTick": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: VOID }, + /** The process introspection statics. uptime: seconds since the + * binary's own start (fractional — a load-time monotonic anchor). + * availableMemory/constrainedMemory: libuv's numbers (free-ish bytes; + * the cgroup cap or 0). cpuUser/cpuSystem (+ threadCpu twins): the + * process/thread CPU clocks in microseconds — the frontend composes + * the {user, system} records. The *Diff forms answer current − prev + * for one field; cpuPrevValidate throws Node's ERR_INVALID_ARG_VALUE + * RangeError for negative/non-finite prev fields (user first, then + * system — Node's order; MAY THROW). rusage(idx): one + * process.resourceUsage() field by canonical index (Node's units). + * activeResources: the loop's own bookkeeping — 'Timeout' per armed + * (or firing, uncleared) timer, 'Immediate' per queued unfired + * immediate; unmodeled resource kinds are absent (SEMANTICS.md). */ + "process.uptime": { argTypes: [], result: F64 }, + /** perf_hooks performance.now(): fractional ms since process start. */ + "perf.now": { argTypes: [], result: F64 }, + /** perf_hooks user timing over the thread's performance timeline. The + * producing calls take the call's whole argument vector as one dyn + * array (Node's validation and errors run in the runtime, MAY THROW): + * mark/measure answer 1, entries(args, which) the entry count of + * getEntries (0), getEntriesByName (1) or getEntriesByType (2). The + * entryName/Type/Start/Duration/Detail accessors read entry i of that + * answer; the frontend builds a new entry's record from them and hands + * it to entryKeep (a typed reference capsule), and the getters read the + * kept records back through entryRef, so entries keep their identity. */ + "perf.mark": { argTypes: [DYN], result: F64 }, + "perf.measure": { argTypes: [DYN], result: F64 }, + "perf.entries": { argTypes: [DYN, F64], result: F64 }, + "perf.clearMarks": { argTypes: [DYN], result: VOID }, + "perf.clearMeasures": { argTypes: [DYN], result: VOID }, + "perf.entryName": { argTypes: [F64], result: STRING }, + "perf.entryType": { argTypes: [F64], result: STRING }, + "perf.entryStart": { argTypes: [F64], result: F64 }, + "perf.entryDuration": { argTypes: [F64], result: F64 }, + "perf.entryDetail": { argTypes: [F64], result: DYN }, + "perf.entryKeep": { argTypes: [DYN], result: VOID }, + "perf.entryRef": { argTypes: [F64], result: DYN }, + /** perf_hooks event-loop metrics (scr_perf_loop.c): loopStart is the + * loop's start in performance.now() ms (0 before it), idleTime the idle + * ms since. eldNew(resolution ms) creates a monitorEventLoopDelay + * histogram handle (MAY THROW Node's resolution RangeError); enable/disable answer whether the state changed; + * eldPercentile expects Node's validated range; eldStat(which) reads + * 0 min, 1 max, 2 mean, 3 stddev, 4 count, 5 exceeds. */ + "perf.loopStart": { argTypes: [], result: F64 }, + "perf.idleTime": { argTypes: [], result: F64 }, + "perf.eldNew": { argTypes: [F64], result: F64 }, + "perf.eldEnable": { argTypes: [F64], result: BOOL }, + "perf.eldDisable": { argTypes: [F64], result: BOOL }, + "perf.eldReset": { argTypes: [F64], result: VOID }, + "perf.eldPercentile": { argTypes: [F64, F64], result: F64 }, + "perf.eldStat": { argTypes: [F64, F64], result: F64 }, + /** process.memoryUsage(): memoryUsageTake samples the resident set and + * the runtime heap once (MAY THROW Node's uv_resident_set_memory error) + * and memoryUsageField(k) reads that sample (0 rss, 1 heapTotal, 2 + * heapUsed, 3 external, 4 arrayBuffers). rss is memoryUsage.rss(). */ + "process.memoryUsageTake": { argTypes: [], result: VOID }, + "process.memoryUsageField": { argTypes: [F64], result: F64 }, + "process.rss": { argTypes: [], result: F64 }, + "process.availableMemory": { argTypes: [], result: F64 }, + "process.constrainedMemory": { argTypes: [], result: F64 }, + "process.cpuUser": { argTypes: [], result: F64 }, + "process.cpuSystem": { argTypes: [], result: F64 }, + "process.cpuUserDiff": { argTypes: [F64], result: F64 }, + "process.cpuSystemDiff": { argTypes: [F64], result: F64 }, + "process.threadCpuUser": { argTypes: [], result: F64 }, + "process.threadCpuSystem": { argTypes: [], result: F64 }, + "process.threadCpuUserDiff": { argTypes: [F64], result: F64 }, + "process.threadCpuSystemDiff": { argTypes: [F64], result: F64 }, + "process.cpuPrevValidate": { argTypes: [F64, F64], result: VOID }, + "process.rusage": { argTypes: [F64], result: F64 }, + "process.activeResources": { argTypes: [], result: arrayOf(STRING) }, + "timers.immediateUnref": { argTypes: [F64], result: F64 }, + "timers.immediateRef": { argTypes: [F64], result: F64 }, + "timers.immediateHasRef": { argTypes: [F64], result: BOOL }, + /** The tolerated non-handle clear (`clearTimeout(null)`, + * `clearInterval({})`, zero-argument forms): Node ignores anything + * that is not a live handle without an error — a VOID no-op the emitter drops + * (only syntactically side-effect-free arguments take this path). */ + "timers.clearNoop": { argTypes: [], result: VOID }, + // Signal listeners are zero-param (the ambient shape); exit/stdin + // callbacks carry program-dependent one-param shapes — null slots, the + // libCall case checks them (child.onExit precedent). + /** Named process signal events: [name string, callback dyn, once bool] + * or [name, callback] for removal. The runtime resolves platform signal + * numbers and delivers (name, number). Listeners do not keep the loop + * alive. Invalid callbacks and uncatchable signals throw. */ + "process.onSignal": { argTypes: [STRING, DYN, BOOL], result: VOID }, + "process.offSignal": { argTypes: [STRING, DYN], result: VOID }, + /** The process 'exit' event — process.on/once/off("exit"). args: [cb, + * once bool] / [cb]. Listeners run SYNCHRONOUSLY at termination + * (normal exit, process.exit(), the uncaught/unhandled exit-1 paths) + * with the exit code; anything they schedule never runs, like Node. + * Callback shapes: () => void or (code: number) => void — the emitter + * picks the runtime adapter. Never throw. */ + "process.onExit": { argTypes: [null, BOOL], result: VOID }, + "process.offExit": { argTypes: [null], result: VOID }, + /** process.stdin listener registration — stdin.on/once("data" | "end" | + * "error", cb). args: [cb, once bool]. data callbacks: () => void or + * (chunk: Uint8Array) => void; end: () => void; error: () => void or + * (err: Error) => void (the child error adapters are reused). While a + * data listener (or a parked for-await chunk promise) exists, stdin + * keeps the loop alive — Node's flowing-stdin keep-alive. Never + * throw. */ + "stdin.onData": { argTypes: [null, BOOL], result: VOID }, + "stdin.onEnd": { argTypes: [{ kind: "func", params: [], ret: VOID }, BOOL], result: VOID }, + "stdin.onError": { argTypes: [null, BOOL], result: VOID }, + /** process.stdin.on/once('keypress', cb) (scr_readline_tty.c): cb is the + * listener as a checked-dynamic function, called with (str, key). */ + "stdin.onKeypress": { argTypes: [DYN, BOOL], result: VOID }, + /** The for-await chunk source over process.stdin: no args, result + * Promise (+1). Fulfills with the next arrived chunk; the + * EMPTY bytes value is the done sentinel (POSIX reads never deliver + * empty data chunks), which the for-await desugar turns into loop + * exit. Never throws itself; awaiting it re-throws nothing (stdin + * errors surface through 'error' listeners, not the iterator). */ + "stdin.nextChunk": { argTypes: [], result: { kind: "promise", inner: BYTES_U8 } }, + "fs.readFileSync": { argTypes: [STRING, STRING], result: STRING }, + "fs.readFileEncoded": { argTypes: [STRING, STRING], result: STRING }, + "fs.readFdEncoded": { argTypes: [F64, STRING], result: STRING }, + /** readFileSync(path) — the Buffer read (+1 bytes); throws catchably + * like the utf8 form. */ + "fs.readFileSyncBuf": { argTypes: [STRING], result: BYTES_U8 }, + /** readFileSync(path, enc) with a RUNTIME encoding (an untyped JS + * parameter): undefined/null answer Buffers, utf8 a string, other real + * encodings throw the "not supported yet" Error, unknown names throw + * ERR_UNKNOWN_ENCODING. */ + "fs.readFileSyncDyn": { argTypes: [STRING, DYN], result: DYN }, + "fs.writeFileSync": { argTypes: [STRING, STRING], result: VOID }, + "fs.appendFileSync": { argTypes: [STRING, STRING], result: VOID }, + "fs.existsSync": { argTypes: [STRING], result: BOOL }, + "fs.mkdirSync": { argTypes: [STRING], result: VOID }, + "fs.rmSync": { argTypes: [STRING], result: VOID }, + "fs.rmdirSync": { argTypes: [STRING], result: VOID }, + "fs.readdirSync": { argTypes: [STRING], result: arrayOf(STRING) }, + // Result is the call site's Dirent record array (VOID is the + // structure-checked sentinel, the os.networkInterfaces pattern). + /** `fs.readdirSync(path, { withFileTypes: true })` — Dirent rows over + * one readdir pass (scr_lib.c's scandir snapshot; DT_UNKNOWN falls back + * to lstat, Node's getDirents rule). The result type is the call site's + * interned Dirent record array (name, parentPath, hidden %dtype in + * libuv's UV_DIRENT encoding) — verified by the frontend; the emitter + * assembles the rows from the snapshot. Byte order on POSIX (libuv + * sorts), the directory's order on Windows; no "."/"..". Throws + * Node's scandir errno error (may-throw seed set); fresh +1 array. */ + "fs.readdirTypesSync": { argTypes: [STRING], result: VOID }, + /** fs.globSync / fs.glob / fs/promises glob (scr_glob.c, Node's + * lib/internal/fs/glob.js walk): pattern and options are dyn values the + * runtime validates in Node's order. The string forms answer string[]; + * the Dirent forms answer the call site's Dirent record array (validated + * in validate.ts) assembled from the snapshot; fs.callbackGlob takes the + * argument vector and calls back with string[]. */ + "fs.globSync": { argTypes: [DYN, DYN], result: arrayOf(STRING) }, + "fs.globTypesSync": { argTypes: [DYN, DYN], result: VOID }, + "fs.callbackGlob": { argTypes: [DYN], result: VOID }, + "fsp.globStrings": { argTypes: [DYN, DYN], result: arrayOf(STRING) }, + "fsp.globTypes": { argTypes: [DYN, DYN], result: VOID }, + /** The error an fs/promises glob walk ended with (undefined when none), + * read once right after fsp.globStrings / fsp.globTypes. */ + "fsp.globError": { argTypes: [], result: DYN }, + /** fs.Dir (scr_lib.c): opendirSync(path, bufferSize) throws Node's + * opendir error (or the options.bufferSize RangeError); dir.readSync + * answers the call site's `Dirent | null` (VOID here — the emitter builds + * the row like readdirTypesSync) and dir.read is its promise twin; + * readSync and closeSync throw ERR_DIR_CLOSED on a closed Dir, read and + * close reject with it. */ + "fs.opendirSync": { argTypes: [STRING, F64], result: DIR_T }, + "fsp.opendir": { argTypes: [STRING, F64], result: { kind: "promise", inner: DIR_T } }, + "dir.path": { argTypes: [DIR_T], result: STRING }, + "dir.readSync": { argTypes: [DIR_T], result: VOID }, + "dir.read": { argTypes: [DIR_T], result: { kind: "promise", inner: VOID } }, + "dir.closeSync": { argTypes: [DIR_T], result: VOID }, + "dir.close": { argTypes: [DIR_T], result: { kind: "promise", inner: VOID } }, + /** node:path (scr_path.c ports BOTH of Node's implementations + * function-by-function): the `path.*` family is posix, the + * `path.win32*` family is Node v24's path.win32 ported exactly — the + * frontend binds the bare module to the TARGET platform's family + * (Node on Windows IS path.win32) and the path.posix / path.win32 + * namespaces to their own family everywhere. join and resolve take ONE + * string[] arg — the frontend packs the variadic call's arguments into + * an array literal. basename always receives its suffix (the frontend + * completes an omitted one with "", a Node no-op). toNamespacedPath is + * the posix identity and the win32 \\?\-prefixer. None of these throw; + * the resolves consult the process cwd like Node's. */ + "path.join": { argTypes: [arrayOf(STRING)], result: STRING }, + "path.resolve": { argTypes: [arrayOf(STRING)], result: STRING }, + "path.normalize": { argTypes: [STRING], result: STRING }, + "path.dirname": { argTypes: [STRING], result: STRING }, + "path.basename": { argTypes: [STRING, STRING], result: STRING }, + "path.extname": { argTypes: [STRING], result: STRING }, + "path.isAbsolute": { argTypes: [STRING], result: BOOL }, + "path.relative": { argTypes: [STRING, STRING], result: STRING }, + "path.toNamespacedPath": { argTypes: [STRING], result: STRING }, + "path.win32Join": { argTypes: [arrayOf(STRING)], result: STRING }, + "path.win32Resolve": { argTypes: [arrayOf(STRING)], result: STRING }, + "path.win32Normalize": { argTypes: [STRING], result: STRING }, + "path.win32Dirname": { argTypes: [STRING], result: STRING }, + "path.win32Basename": { argTypes: [STRING, STRING], result: STRING }, + "path.win32Extname": { argTypes: [STRING], result: STRING }, + "path.win32IsAbsolute": { argTypes: [STRING], result: BOOL }, + "path.win32Relative": { argTypes: [STRING, STRING], result: STRING }, + "path.win32ToNamespacedPath": { argTypes: [STRING], result: STRING }, + /** path.parse: [root, dir, base, ext, name] as a fresh string[] (the + * frontend builds the ParsedPath record); path.format: the pathObject's + * root, dir, base, name and ext with undefined spelled "". */ + "path.parse": { argTypes: [STRING], result: arrayOf(STRING) }, + "path.win32Parse": { argTypes: [STRING], result: arrayOf(STRING) }, + "path.format": { argTypes: [STRING, STRING, STRING, STRING, STRING], result: STRING }, + "path.win32Format": { argTypes: [STRING, STRING, STRING, STRING, STRING], result: STRING }, + /** path.matchesGlob(path, pattern): Node's matchGlobPattern over its + * bundled minimatch (scr_glob.c). Both families match case-insensitively + * on macOS and Windows hosts like Node's; win32 also accepts either + * separator. Throws TypeError for a pattern over 64 Ki code units, + * RangeError for a brace sequence too long to expand, and SyntaxError + * when a segment's regex is invalid under the u flag. */ + "path.matchesGlob": { argTypes: [STRING, STRING], result: BOOL }, + "path.win32MatchesGlob": { argTypes: [STRING, STRING], result: BOOL }, + /** node:os: homedir is $HOME else getpwuid(3); tmpdir is Node's env + * cascade ($TMPDIR/$TMP/$TEMP else /tmp, one trailing slash trimmed). + * os.platform() lowers to process.platform — one implementation. */ + "os.homedir": { argTypes: [], result: STRING }, + /** os.release(): uname(2)'s release field — Node's own implementation + * (the kernel version string, e.g. "24.6.0" on macOS 15). Interned; +1 + * per read. Never throws. */ + "os.release": { argTypes: [], result: STRING }, + /** os.type(): uname(2)'s sysname ("Darwin", "Linux"; "Windows_NT" on + * win32) — Node's uv_os_uname answer. Interned per call; never throws. */ + "os.type": { argTypes: [], result: STRING }, + /** os.totalmem(): total physical memory in bytes. Never throws. */ + "os.totalmem": { argTypes: [], result: F64 }, + /** os.availableParallelism(): CPUs the process may use (libuv's answer), >= 1. Never throws. */ + "os.availableParallelism": { argTypes: [], result: F64 }, + /** os.hostname/machine/version/endianness, freemem, uptime and loadavg: + * libuv's answers (uname's machine and version fields, gethostname, + * "LE"/"BE", free memory in bytes, uptime in seconds, the three load + * averages). Fresh results; never throw. */ + "os.hostname": { argTypes: [], result: STRING }, + "os.machine": { argTypes: [], result: STRING }, + "os.version": { argTypes: [], result: STRING }, + "os.endianness": { argTypes: [], result: STRING }, + "os.freemem": { argTypes: [], result: F64 }, + "os.uptime": { argTypes: [], result: F64 }, + "os.loadavg": { argTypes: [], result: arrayOf(F64) }, + /** os.getPriority(pid): validateInt32 (RangeError ERR_OUT_OF_RANGE), + * then the priority; a failure throws Node's SystemError. */ + "os.getPriority": { argTypes: [F64], result: F64 }, + /** os.setPriority(pid, priority): validateInt32 for both (priority in + * -20..19, RangeError ERR_OUT_OF_RANGE), then the priority; a failure + * throws Node's SystemError. */ + "os.setPriority": { argTypes: [F64, F64], result: VOID }, + /** os.cpus(): cpusTake snapshots every CPU and answers the count; + * cpusModel(i) and cpusField(i, k) (k: 0 speed, 1 user, 2 nice, 3 sys, + * 4 idle, 5 irq) read the snapshot. Never throw. */ + "os.cpusTake": { argTypes: [], result: F64 }, + "os.cpusModel": { argTypes: [F64], result: STRING }, + "os.cpusField": { argTypes: [F64, F64], result: F64 }, + /** os.constants: the per-thread null-prototype constants object (dlopen, + * errno, signals, priority, UV_UDP_REUSEADDR). Never throws. */ + "os.constants": { argTypes: [], result: DYN }, + /** fs.constants: the target's cached null-prototype constants object. */ + "fs.constants": { argTypes: [], result: DYN }, + /** The deprecated node:constants object (scr_os.c): the os groups and + * fs.constants merged with the borrowed baked crypto.constants table, + * frozen, one per thread. */ + "constants.object": { argTypes: [DYN], result: DYN }, + /** umask(2): arg < 0 reads without setting (umask has no read-only form + * — set 0, restore); otherwise sets and answers the PREVIOUS mask. + * Never throws. */ + "process.umask": { argTypes: [F64], result: F64 }, + /** chdir(2) — throws Node's fs-shaped error (ENOENT/EACCES/ENOTDIR, + * syscall "chdir") on failure. */ + "process.chdir": { argTypes: [STRING], result: VOID }, + "process.loadEnvFile": { argTypes: [DYN], result: VOID }, + /** process._exiting: true once the exit sequence began (exit listeners + * running) — the runtime flag scr_run_exit_listeners/process.exit set. + * Never throws. */ + "process.exiting": { argTypes: [], result: BOOL }, + /** net's process-wide happy-eyeballs attempt budget (Node's default + * 250ms): one runtime double in the core unit, so reading/writing it + * never forces the net unit into the link. Never throw. */ + "net.getAutoSelTimeout": { argTypes: [], result: F64 }, + "net.setAutoSelTimeout": { argTypes: [F64], result: VOID }, + /** realpath(3) with Node's error shape (syscall "lstat" in the message, + * Node's own spelling for realpathSync failures). +1 fresh string. */ + "fs.realpathSync": { argTypes: [STRING], result: STRING }, + /** fs.realpathSync (or its .native when the flag is true) as a function + * value: path and options arrive unchecked; the result is a string or a + * Buffer by the options' encoding. */ + "fs.realpathValue": { argTypes: [DYN, DYN, BOOL], result: DYN }, + /** Native realpath with syscall "realpath" on failure. +1 fresh string. */ + "fs.realpathNativeSync": { argTypes: [STRING], result: STRING }, + /** The os.userInfo() field trio (uv_os_get_passwd's slices): pw_name, + * pw_shell, pw_dir — the PASSWD homedir, not os.homedir's $HOME-first + * cascade (Node's own split). The frontend assembles the UserInfo + * record from these plus getuid/getgid. +1 fresh strings; a passwd + * lookup failure aborts (Node throws a system error there — no + * compiled program path reaches it for the running uid). */ + "os.userName": { argTypes: [], result: STRING }, + "os.userShell": { argTypes: [], result: STRING }, + "os.userHomedir": { argTypes: [], result: STRING }, + "os.tmpdir": { argTypes: [], result: STRING }, + // Result is the call site's Dict record (VOID is + // the dgram.address sentinel — the libCall case checks the structure). + /** os.networkInterfaces(): getifaddrs(3) → the Dict + * record, built inline by the emitter from a runtime snapshot (scr_lib.c). + * The result type is the CALL SITE's mapped @types/node shape — a pure + * index-signature record whose value is `Info[] | undefined`, Info a + * two-record union (IPv4: scopeid `number | undefined` holding undefined; + * IPv6: scopeid number) — verified structurally by the frontend. Rows + * match libuv's filter (IFF_UP && IFF_RUNNING, AF_INET/AF_INET6; loopback + * = internal; MACs from the interface's link-level sibling entry, zeros + * when absent; cidr from the netmask's contiguous prefix, the null arm + * when it is missing or non-contiguous). Key/row order follows the OS's + * getifaddrs enumeration — Node itself does not guarantee an order. + * Fresh +1 record; never throws (a getifaddrs failure yields {}). */ + "os.networkInterfaces": { argTypes: [], result: VOID }, + /** `Math.max(...xs)` / `Math.min(...xs)` over one spread number[] + * (scr_number.c): the JS fold exactly — any NaN element poisons the + * result, ±0 order by the JS comparison (max prefers +0, min prefers + * -0), and the empty array yields -Infinity / +Infinity like the + * zero-argument calls. Borrows the array; never throws. */ + "math.maxArr": { argTypes: [arrayOf(F64)], result: F64 }, + "math.minArr": { argTypes: [arrayOf(F64)], result: F64 }, + "math.hypotArr": { argTypes: [arrayOf(F64)], result: F64 }, + /** `Math.floor(x)` — C floor() IS the JS operation (NaN/±0/±Infinity + * pass through bit-exactly). Never throws. */ + "math.floor": { argTypes: [F64], result: F64 }, + /** Math.abs (C fabs — IS the JS operation) and Math.round (scr_lib.c: + * ECMA half-toward-+Infinity with the exact-fraction comparison — C + * round() is half-away-from-zero and floor(x+0.5) drifts at the + * epsilon boundary). Borrow nothing; never throw. */ + "math.abs": { argTypes: [F64], result: F64 }, + "math.round": { argTypes: [F64], result: F64 }, - "math.trunc": { argTypes: [F64], result: F64 }, - "math.ceil": { argTypes: [F64], result: F64 }, - "math.sin": { argTypes: [F64], result: F64 }, - "math.sinh": { argTypes: [F64], result: F64 }, - "math.cos": { argTypes: [F64], result: F64 }, - "math.cosh": { argTypes: [F64], result: F64 }, - "math.tan": { argTypes: [F64], result: F64 }, - "math.tanh": { argTypes: [F64], result: F64 }, - "math.asin": { argTypes: [F64], result: F64 }, - "math.asinh": { argTypes: [F64], result: F64 }, - "math.acos": { argTypes: [F64], result: F64 }, - "math.acosh": { argTypes: [F64], result: F64 }, - "math.atan": { argTypes: [F64], result: F64 }, - "math.atanh": { argTypes: [F64], result: F64 }, - "math.cbrt": { argTypes: [F64], result: F64 }, - "math.clz32": { argTypes: [F64], result: F64 }, - /** Return -1, +1, or the original NaN/zero. In particular, -0 remains -0. */ - "math.sign": { argTypes: [F64], result: F64 }, - "math.exp": { argTypes: [F64], result: F64 }, - "math.expm1": { argTypes: [F64], result: F64 }, - "math.f16round": { argTypes: [F64], result: F64 }, - "math.fround": { argTypes: [F64], result: F64 }, - "math.sqrt": { argTypes: [F64], result: F64 }, - "math.log": { argTypes: [F64], result: F64 }, - "math.log1p": { argTypes: [F64], result: F64 }, - "math.log2": { argTypes: [F64], result: F64 }, - "math.log10": { argTypes: [F64], result: F64 }, - "math.atan2": { argTypes: [F64, F64], result: F64 }, - "math.pow": { argTypes: [F64, F64], result: F64 }, - "math.imul": { argTypes: [F64, F64], result: F64 }, - /** `Math.min(a, b)` / `Math.max(a, b)` — the two-argument scalar forms - * (scr_lib.c), JS-exact like the Arr folds: NaN poisons, max prefers +0 - * over -0 (min the reverse). C fmin/fmax are NOT these (they drop NaN). - * Never throw. */ - "math.min": { argTypes: [F64, F64], result: F64 }, - "math.max": { argTypes: [F64, F64], result: F64 }, - /** `Math.random()` — a uniform double in [0,1) with the spec's 53-bit - * granularity, drawn from arc4random_buf (the CSPRNG behind the crypto - * lowerings). Same distribution as Node, NECESSARILY different sequence - * (SEMANTICS.md 62 — no seeded sequence exists to match). Never throws. */ - "math.random": { argTypes: [], result: F64 }, - /** The static global parsers/tests (scr_string.c). num.parseInt is - * ECMA-262 19.2.5 exactly — JS whitespace, sign, ToInt32 radix (the - * frontend completes an omitted radix to 0 = the spec's "undefined": - * base 10 with the 0x hex escape), longest digit prefix, and the exact - * mathematical value correctly rounded (u64 fast path, bignum beyond — - * overflow is ±Infinity). num.isNaN is the NaN self-test on an - * already-number argument (tsc pins the argument to number, so no - * ToNumber coercion exists to model). Borrow; never throw. */ - "num.parseInt": { argTypes: [STRING, F64], result: F64 }, - /** ES parseFloat (scr_string.c): the longest StrDecimalLiteral prefix - * of the trimmed input (no hex, "Infinity" exact-case), NaN when none — - * ECMA-262 19.2.4 over a string argument (non-string arguments keep the - * fence: Node would ToNumber-coerce). Borrows; never throws. */ - "num.parseFloat": { argTypes: [STRING], result: F64 }, - /** `a.localeCompare(b)`: -1/0/1 in Node's root-locale collation for - * Latin-script text (scr_str_locale_compare). Never throws. */ - "str.localeCompare": { argTypes: [STRING, STRING], result: F64 }, - /** Annex B substr(start, length) with start ToNumber'd and length's - * presence; never throws. */ - "str.substr": { argTypes: [STRING, F64, F64, BOOL], result: STRING }, - /** toLocaleLowerCase/toLocaleUpperCase (scr_regex.c): the locales value - * as written, upper=true for toLocaleUpperCase. Throws for null and for - * locale forms with no native rules. */ - "str.localeCase": { argTypes: [STRING, DYN, BOOL], result: STRING }, - "str.replaceLiteral": { argTypes: [STRING, STRING, STRING, BOOL], result: STRING }, - // (subject, characters, NUL-separated replacement texts): the fused - // chain of global one-character replacements. - "str.replaceChars": { argTypes: [STRING, STRING, STRING], result: STRING }, - /* ToNumber(string) — ECMA-262 7.1.4.1 StringToNumber (scr_string.c): - * trim the JS StrWhiteSpace set, empty/whitespace-only → +0, then the - * whole span must be one StrNumericLiteral — signed decimal (Infinity - * included, strtod-over-validated-span correct rounding) or unsigned - * 0x/0o/0b (exact value, nearest-even; signed forms are NaN) — with - * any trailing garbage answering NaN. Number(aString), unary + on - * strings, and util.format %d over strings lower here. Borrows; never - * throws. */ - "num.fromString": { argTypes: [STRING], result: F64 }, - "num.isNaN": { argTypes: [F64], result: BOOL }, - /** The static URI component codecs (scr_string.c), ECMA-262 Encode/ - * Decode with the component sets over the runtime's UTF-8 strings. - * str.encodeUriComponent percent-encodes every byte outside the - * unreserved component set (ALPHA/DIGIT/- _ . ! ~ * ' ( )) as uppercase - * %XX — the spec's per-code-point UTF-8 encoding IS a byte scan here — - * and THROWS the spec's URIError ("URI malformed") on a lone surrogate. - * str.decodeUriComponent - * decodes %XX escapes bytewise (raw non-escape bytes copy through) and - * requires the escaped bytes to form strictly valid UTF-8 (overlong - * forms, surrogate code points, and >U+10FFFF refused, per UTF8-decode - * without replacement); bad hex or an invalid sequence THROWS the - * spec's URIError ("URI malformed"), catchable. Borrow; results +1. */ - "str.encodeUriComponent": { argTypes: [STRING], result: STRING }, - // The base64 globals: the argument is a dyn value (WebIDL ToString - // runs in the runtime); the zero-argument form always throws. - /** The WHATWG base64 globals (scr_string.c), Node-global since v16. - * Arguments are borrowed dyn values — WebIDL ToString runs in the - * runtime over the dyn kind (String(null) is "null": the html spec's - * coercion, which Node's atob(null) exercises). str.atob decodes - * forgiving-base64 (ASCII whitespace stripped, %4==0 strips up to two - * '=', %4==1 refuses, leftover bits discarded) into the latin1 code - * points as a string; a malformed input THROWS the catchable - * DOMException InvalidCharacterError ("The string to be decoded is not - * correctly encoded."). str.btoa encodes the string's code points as - * base64; any code point over U+00FF THROWS InvalidCharacterError - * ("Invalid character"). str.b64Missing is the zero-argument call of - * either: always throws Node's TypeError [ERR_MISSING_ARGS] "The - * \"input\" argument must be specified". Results +1. */ - "str.atob": { argTypes: [DYN], result: STRING }, - "str.btoa": { argTypes: [DYN], result: STRING }, - "str.b64Missing": { argTypes: [], result: STRING }, - "str.decodeUriComponent": { argTypes: [STRING], result: STRING }, - "str.decodeUri": { argTypes: [STRING], result: STRING }, - /** encodeURI: the same Encode() keeping the reserved set and '#' - * unescaped — throwing like the component encoder. Borrow; result +1. */ - "str.encodeUri": { argTypes: [STRING], result: STRING }, - /** RegExp.escape (ES2025): per-code-point EncodeForRegExpEscape — - * leading ASCII alphanumeric hex-escapes, syntax characters and '/' - * take a backslash, other punctuators/whitespace/line terminators - * hex-escape, the rest passes through. Total; borrows; result +1. */ - "regexp.escape": { argTypes: [STRING], result: STRING }, - /** Number.prototype formatters (scr_lib.c), JS-exact: - * num.toExponential is toExponential() with the spec's "as many digits - * as necessary"; num.toFixed0 is the non-throwing omitted-argument - * toFixed() fast path; num.toFixed implements an explicit fractionDigits - * with exact binary-value rounding and THROWS RangeError outside 0..100. - * Successful results +1. */ - "num.toExponential": { argTypes: [F64], result: STRING }, - "num.toFixed0": { argTypes: [F64], result: STRING }, - "num.toFixed": { argTypes: [F64, F64], result: STRING }, - /** toPrecision(precision) with the argument ToNumber'd and its presence - * (undefined answers ToString); toExponential with a DEFINED - * fractionDigits. Both throw V8's RangeError for out-of-range digits. */ - "num.toPrecision": { argTypes: [F64, F64, BOOL], result: STRING }, - "num.toExponentialDigits": { argTypes: [F64, F64], result: STRING }, - "num.toStringRadix": { argTypes: [F64, DYN], result: STRING }, - /** toString(radix) with a number radix (scr_lib.c): truncates it as - * ToIntegerOrInfinity does, THROWS RangeError outside 2..36. */ - "num.toStringRadixF64": { argTypes: [F64, F64], result: STRING }, - /** Object.is over two numbers — the spec's SameValue on doubles: NaN - * equals NaN, +0 differs from -0, everything else is `===`. Plain bool - * result; never throws. (Union-armed operands take unionEq's sameValue - * flag instead — this is the both-f64 fast path.) */ - "num.sameValue": { argTypes: [F64, F64], result: BOOL }, - /** `new Intl.NumberFormat("en-US").format(x)` and - * `x.toLocaleString("en-US")` with DEFAULT options — the one locale - * whose data the runtime embeds (Node's default-build locale): decimal - * notation, 0–3 fraction digits rounded half-up on the SHORTEST - * round-tripping decimal (ICU's rounding input, probed vs Node — - * format(1.0005) is "1.001" though toFixed(3) answers "1.000"), "," - * grouping every three integer digits, "∞"/"NaN" texts, and "-0" for - * negative inputs rounding to zero. Result +1; never throws. */ - "intl.numFormatEnUs": { argTypes: [F64], result: STRING }, - /** ES Symbol values (scr_symbol.c — link-gated by moduleUsesSymbol). - * sym.new: `Symbol(desc)` — a fresh runtime-unique identity (+1) whose - * one arg is the description string (borrowed); sym.newAnon is the - * description-less `Symbol()` form. sym.for: the Symbol.for global - * registry — one interned symbol per key (borrowed), +1 on every call. - * sym.toString: "Symbol(desc)" (+1 string; "Symbol()" when absent). - * sym.desc / sym.keyFor answer the interned `string | undefined` union - * (the runtime returns +1-or-NULL; the backend builds the union arms — - * the child.stdout pattern). None of these throw. */ - "sym.new": { argTypes: [STRING], result: SYMBOL_T }, - "sym.newAnon": { argTypes: [], result: SYMBOL_T }, - "sym.for": { argTypes: [STRING], result: SYMBOL_T }, - "sym.wellKnown": { argTypes: [STRING], result: SYMBOL_T }, - // Result is the interned `string | undefined` union — the libCall case - // checks the arms (the spawnRes.error pattern). - "sym.keyFor": { argTypes: [SYMBOL_T], result: VOID }, - "sym.desc": { argTypes: [SYMBOL_T], result: VOID }, - "sym.toString": { argTypes: [SYMBOL_T], result: STRING }, - /** Engine-free arbitrary-precision bigint operations (scr_bigint.c). - * parse accepts the ECMAScript BigInt string grammar; fromF64 accepts - * finite integral Numbers. Arithmetic returns fresh immutable values. - * cmp returns -1/0/1 as f64; toString accepts a numeric radix. */ - "bigint.parse": { argTypes: [STRING], result: BIGINT_T }, - "bigint.fromF64": { argTypes: [F64], result: BIGINT_T }, - "bigint.neg": { argTypes: [BIGINT_T], result: BIGINT_T }, - "bigint.not": { argTypes: [BIGINT_T], result: BIGINT_T }, - "bigint.add": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.sub": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.mul": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.div": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.mod": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.pow": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.and": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.or": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.xor": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.shl": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.shr": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, - "bigint.eq": { argTypes: [BIGINT_T, BIGINT_T], result: BOOL }, - "bigint.eqString": { argTypes: [BIGINT_T, STRING], result: BOOL }, - "bigint.cmp": { argTypes: [BIGINT_T, BIGINT_T], result: F64 }, - "bigint.cmpNumber": { argTypes: [BIGINT_T, F64], result: F64 }, - "bigint.truthy": { argTypes: [BIGINT_T], result: BOOL }, - "bigint.toString": { argTypes: [BIGINT_T, F64], result: STRING }, - "bigint.inspect": { argTypes: [BIGINT_T], result: STRING }, - "bigint.toF64": { argTypes: [BIGINT_T], result: F64 }, - "bigint.asUintN": { argTypes: [F64, BIGINT_T], result: BIGINT_T }, - "bigint.asIntN": { argTypes: [F64, BIGINT_T], result: BIGINT_T }, - "bigint.bufferRead": { argTypes: [BYTES_U8, F64, BOOL, BOOL], result: BIGINT_T }, - "bigint.bufferWrite": { argTypes: [BYTES_U8, BIGINT_T, F64, BOOL, BOOL], result: F64 }, - "bigint.dataViewGet": { argTypes: [BYTES_U8, F64, BOOL, BOOL], result: BIGINT_T }, - "bigint.dataViewSet": { argTypes: [BYTES_U8, F64, BIGINT_T, BOOL], result: VOID }, - /** node:url + the URL class (scr_url.c). url.new parses one absolute - * URL string into a mutable URL value (+1) — invalid input THROWS a - * catchable TypeError ("Invalid URL"), like Node's constructor. The - * getters (borrowed receiver, +1 string) never throw; url.href doubles - * as toString(). fileURLToPath has one libFn per receiver form (URL - * value / string) — both THROW Node's TypeErrors on non-file schemes, - * encoded slashes, and non-empty hosts. url.pathToFileURL resolves the - * path (getcwd) and never throws. */ - "url.new": { argTypes: [STRING], result: URL_T }, - "url.newBase": { argTypes: [STRING, STRING], result: URL_T }, - /** Factories consume already-converted input/base strings. canParse - * answers a boolean; parse returns a checked native URL or null (+1). - * Parser failures are suppressed; conversion occurs before these calls. */ - "url.canParse": { argTypes: [STRING], result: BOOL }, - "url.canParseBase": { argTypes: [STRING, STRING], result: BOOL }, - "url.parse": { argTypes: [STRING], result: DYN }, - "url.parseBase": { argTypes: [STRING, STRING], result: DYN }, - "url.protocol": { argTypes: [URL_T], result: STRING }, - "url.origin": { argTypes: [URL_T], result: STRING }, - "url.username": { argTypes: [URL_T], result: STRING }, - "url.password": { argTypes: [URL_T], result: STRING }, - "url.host": { argTypes: [URL_T], result: STRING }, - "url.hostname": { argTypes: [URL_T], result: STRING }, - "url.port": { argTypes: [URL_T], result: STRING }, - "url.pathname": { argTypes: [URL_T], result: STRING }, - "url.href": { argTypes: [URL_T], result: STRING }, - /** Selected component writes borrow the receiver and converted string. - * href replaces its parsed fields while retaining cached params identity. */ - "url.set": { argTypes: [URL_T, STRING, STRING], result: VOID }, - "url.setChecked": { argTypes: [URL_T, STRING, DYN], result: VOID }, - "url.fileURLToPathUrl": { argTypes: [URL_T], result: STRING }, - /** fileURLToPathUrl as a value: the path string, or the error it would - * throw (fs.existsSync and fs.promises URL paths). Never throws. */ - "url.fileURLToPathValue": { argTypes: [URL_T], result: DYN }, - /** getValidatedPath's null-byte check on a string path, and - * readFileSync's encoding check: what Node validates before it converts - * a later URL path. (path, argument name) and (encoding). */ - "fs.checkPath": { argTypes: [STRING, STRING], result: VOID }, - "fs.checkReadEncoding": { argTypes: [STRING], result: VOID }, - "url.fileURLToPathChecked": { argTypes: [DYN], result: STRING }, - "url.fileURLToPathOptions": { argTypes: [DYN, DYN], result: STRING }, - /** node:url's legacy API (scr_url_legacy.c): parse(url, parseQueryString, - * slashesDenoteHost) answers the checked-dynamic Url instance; format - * takes the urlObject (a Url-shaped object, a string, or a URL) and the - * URL form's options; resolve takes (from, to). The trailing bool says - * whether the call site emits DEP0169. domainToASCII/domainToUnicode - * take the domain and whether it was omitted. Node's validation errors - * throw catchably. */ - "url.legacyParse": { argTypes: [DYN, DYN, DYN, BOOL], result: DYN }, - "url.legacyFormat": { argTypes: [DYN, DYN, BOOL], result: STRING }, - "url.legacyResolve": { argTypes: [DYN, DYN, BOOL], result: STRING }, - "url.domainToASCII": { argTypes: [DYN, BOOL], result: STRING }, - "url.domainToUnicode": { argTypes: [DYN, BOOL], result: STRING }, - /** url.urlToHttpOptions over a checked-dynamic argument: a URL answers - * Node's null-prototype options object; a primitive or null throws - * ERR_INVALID_ARG_TYPE. */ - "url.toHttpOptions": { argTypes: [DYN], result: DYN }, - /** node:punycode (scr_url.c): punycode.js 2.1.0's string conversions; - * decode/toUnicode (and, past 32-bit deltas, encode/toASCII) throw the - * module's RangeErrors, ucs2Encode String.fromCodePoint's. warn marks the - * module's first load (scr_async_dyn.c) and reports DEP0040 then unless - * the second argument is false: at once for a static ES import, on the - * next tick for a require() call. */ - "punycode.decode": { argTypes: [STRING], result: STRING }, - "punycode.encode": { argTypes: [STRING], result: STRING }, - "punycode.toASCII": { argTypes: [STRING], result: STRING }, - "punycode.toUnicode": { argTypes: [STRING], result: STRING }, - "punycode.ucs2Decode": { argTypes: [STRING], result: arrayOf(F64) }, - "punycode.ucs2Encode": { argTypes: [arrayOf(F64)], result: STRING }, - "punycode.warn": { argTypes: [BOOL, BOOL], result: VOID }, - "url.fileURLToPathBuffer": { argTypes: [DYN, DYN], result: BYTES_U8 }, - "url.pathToFileURLChecked": { argTypes: [DYN, DYN], result: URL_T }, - "url.fileURLToPathStr": { argTypes: [STRING], result: STRING }, - "url.pathToFileURL": { argTypes: [STRING], result: URL_T }, - /** Explicit path syntax from the options.windows argument, independent - * of the executable's host platform. Windows UNC input can throw. */ - "url.pathToFileURLPlatform": { argTypes: [STRING, BOOL], result: URL_T }, - /** pathToFileURL under a win32 TARGET: the same scr_url_from_path call - * (the runtime selects the win32 arm by _WIN32), but a distinct IR name - * because that arm THROWS for malformed UNC inputs — may-throw seeds on - * it while posix pathToFileURL emission stays byte-identical. */ - "url.pathToFileURLWin32": { argTypes: [STRING], result: URL_T }, - /** URLSearchParams (scr_url.c — always linked with the url unit). - * Construction: sp.new (empty), sp.parse (one borrowed init string — - * a single leading '?' strips, Node's constructor), sp.copy (snapshot - * of another list), sp.fromPairs (a string[][] value — THROWS Node's - * ERR_INVALID_TUPLE TypeError on a non-[name, value] row; the one - * throwing entry in the family), sp.with (the record-literal init - * desugar: append one pair, answer the same list +1 — chains fold - * `new URLSearchParams({...})` into nested calls). url.searchParams - * answers the URL's LIVE cached view (+1, one identity per URL); - * url.search is the WHATWG search getter ("" for no/empty query). - * Methods mirror the WHATWG surface: sp.get answers +1-or-NULL (the - * sym.desc union pattern, null arm), sp.getAll a fresh string[]; - * sp.append/sp.set/sp.delete/sp.deleteValue/sp.sort mutate and - * re-serialize a live view's URL query; sp.has/sp.hasValue answer - * bools; sp.size/sp.toString are pure reads. sp.keyAt/sp.valAt are - * the for-of/forEach desugar's index reads (live — the loop re-reads - * sp.size each pass). */ - "sp.new": { argTypes: [], result: SEARCH_PARAMS_T }, - "sp.newChecked": { argTypes: [DYN], result: SEARCH_PARAMS_T }, - "sp.parse": { argTypes: [STRING], result: SEARCH_PARAMS_T }, - "sp.copy": { argTypes: [SEARCH_PARAMS_T], result: SEARCH_PARAMS_T }, - // The pairs argument is string[][] — checked structurally in validate.ts (the - // generic array-of slot has no named constant here). - "sp.fromPairs": { argTypes: [null], result: SEARCH_PARAMS_T }, - "sp.with": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: SEARCH_PARAMS_T }, - "url.searchParams": { argTypes: [URL_T], result: SEARCH_PARAMS_T }, - "url.search": { argTypes: [URL_T], result: STRING }, - "url.hash": { argTypes: [URL_T], result: STRING }, - // Result is the interned `string | null` union — the libCall case - // checks the arms (the spawnRes.signal pattern). - "sp.get": { argTypes: [SEARCH_PARAMS_T, STRING], result: VOID }, - "sp.getAll": { argTypes: [SEARCH_PARAMS_T, STRING], result: arrayOf(STRING) }, - "sp.append": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID }, - "sp.set": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID }, - "sp.delete": { argTypes: [SEARCH_PARAMS_T, STRING], result: VOID }, - "sp.deleteValue": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID }, - "sp.has": { argTypes: [SEARCH_PARAMS_T, STRING], result: BOOL }, - "sp.hasValue": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: BOOL }, - "sp.sort": { argTypes: [SEARCH_PARAMS_T], result: VOID }, - "sp.size": { argTypes: [SEARCH_PARAMS_T], result: F64 }, - "sp.toString": { argTypes: [SEARCH_PARAMS_T], result: STRING }, - "sp.keyAt": { argTypes: [SEARCH_PARAMS_T, F64], result: STRING }, - "sp.valAt": { argTypes: [SEARCH_PARAMS_T, F64], result: STRING }, - // node:querystring. qs.parse's result is the call site's ParsedUrlQuery - // dictionary record (VOID is the networkInterfaces sentinel — the - // libCall case checks the structure); qs.stringify's object argument - // is a dyn value (the frontend dynFroms typed records). - /** node:querystring (scr_qs.c — link-gated by moduleUsesQs; NOT - * URLSearchParams: the legacy codec's escaping and '+' rules differ). - * qs.escape is Node's qsEscape, which encodes exactly the component - * unreserved set — it emits the always-linked scr_qs_escape, so - * escape-only programs never pull the unit. qs.unescape is Node's qsUnescape: strict decodeURIComponent - * first, the lenient legacy unescapeBuffer fallback on failure (never - * throws). qs.parse takes (str, sep, eq, maxKeys) — the frontend - * completes omitted/null sep/eq to "&"/"=" and the omitted maxKeys - * option to Node's 1000 (0 and negatives mean unlimited, Node's rule) — - * and its result type is the CALL SITE's mapped ParsedUrlQuery shape (a - * pure index-signature record over `string | string[]`, an undefined - * arm tolerated — @types/node's Dict), verified structurally by the - * frontend (lowerQuerystringParseCall); the emitters construct the - * record and hand its overflow map to scr_qs_parse_into with the two - * union tags. qs.stringify takes (obj, sep, eq) with obj a dyn value - * (the frontend dynFroms the typed record; JS-world dyn values pass - * through) — Node's encodeStringified rules run in the runtime, so - * arrays expand to repeated keys and null/undefined/nested objects are - * empty values. Custom encoder/decoder options fence at compile time. - * All borrow; string results +1; escape and stringify throw URIError - * [ERR_INVALID_URI] on a lone surrogate. */ - "qs.parse": { argTypes: [STRING, STRING, STRING, F64], result: VOID }, - "qs.stringify": { argTypes: [DYN, STRING, STRING], result: STRING }, - "qs.escape": { argTypes: [STRING], result: STRING }, - "qs.unescape": { argTypes: [STRING], result: STRING }, - /** node:util.parseArgs: one checked-dynamic config object in, one - * checked-dynamic ParsedResults tree out. Statically typed member reads - * validate their values when leaving that tree. The native parser may - * throw Node's coded validation/grammar TypeErrors. */ - "util.parseArgs": { argTypes: [DYN], result: DYN }, - "util.parseEnv": { argTypes: [DYN], result: DYN }, - "util.getSystemErrorName": { argTypes: [DYN], result: STRING }, - "util.getSystemErrorMessage": { argTypes: [DYN], result: STRING }, - "util.systemErrorEntries": { argTypes: [], result: DYN }, - "util.stripVTControlCharacters": { argTypes: [DYN], result: STRING }, - "util.toUSVString": { argTypes: [DYN], result: STRING }, - "util.isDeepStrictEqual": { argTypes: [DYN, DYN, DYN], result: BOOL }, - "util.styleText": { argTypes: [DYN, DYN, DYN], result: STRING }, - /** util.inherits/deprecate/callbackify/debuglog over checked-dynamic - * arguments (scr_util_callable.c). The function results are native dyn - * wrappers; call sites with a static function type check them back. */ - "util.inherits": { argTypes: [DYN, DYN], result: VOID }, - "util.inheritsPrototype": { argTypes: [DYN, DYN, DYN], result: VOID }, - "util.deprecate": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, - "util.callbackify": { argTypes: [DYN], result: DYN }, - /** util.promisify over an untyped function value: a native dyn function - * whose calls answer a dyn promise the appended callback settles. */ - "util.promisifyDyn": { argTypes: [DYN], result: DYN }, - "util.debuglog": { argTypes: [DYN, DYN], result: DYN }, - "util.typeIs": { argTypes: [DYN, STRING], result: BOOL }, - "util.typeValue": { argTypes: [DYN, STRING], result: DYN }, - /** fs.statSync → a Stats value (may throw, like the other sync fs - * calls); the stats.* getters are pure reads on it. */ - "fs.statSync": { argTypes: [STRING], result: STATS_T }, - "fs.lstatSync": { argTypes: [STRING], result: STATS_T }, - // statSync / lstatSync(path, { throwIfNoEntry }): arg 1 is the flag. - // Result is the interned `Stats | undefined` union (the runtime answers - // +1-or-NULL; the backend builds the arms). - "fs.statProbeSync": { argTypes: [STRING, BOOL], result: VOID }, - "fs.lstatProbeSync": { argTypes: [STRING, BOOL], result: VOID }, - "fs.fstatSync": { argTypes: [F64], result: STATS_T }, - "fs.fchmodSync": { argTypes: [F64, F64], result: VOID }, - "fs.fsyncSync": { argTypes: [F64], result: VOID }, - "fs.fdatasyncSync": { argTypes: [F64], result: VOID }, - "fs.ftruncateSync": { argTypes: [F64, F64], result: VOID }, - "fs.utimesSync": { argTypes: [DYN, DYN, DYN], result: VOID }, - "fs.futimesSync": { argTypes: [DYN, DYN, DYN], result: VOID }, - "fs.lutimesSync": { argTypes: [DYN, DYN, DYN], result: VOID }, - "fs.readvSync": { argTypes: [F64, arrayOf(BYTES_U8), F64], result: F64 }, - "fs.writevSync": { argTypes: [F64, arrayOf(BYTES_U8), F64], result: F64 }, - "fs.linkSync": { argTypes: [DYN, DYN], result: VOID }, - "fs.symlinkSync": { argTypes: [DYN, DYN, DYN], result: VOID }, - /** fs.cpSync / fs/promises cp (scr_fs_cp.c): src, dest and options are - * dyn values the runtime validates in Node's order. */ - "fs.cpSync": { argTypes: [DYN, DYN, DYN], result: VOID }, - "fs.readlinkSync": { argTypes: [DYN, DYN], result: DYN }, - "fs.statfsSync": { argTypes: [DYN, DYN], result: DYN }, - "fs.readlinkSyncStr": { argTypes: [DYN, DYN], result: STRING }, - "fs.readlinkSyncBuffer": { argTypes: [DYN, DYN], result: BYTES_U8 }, - /** fs.openSync(path, flags) → the raw fd as f64; fs.readSync/fs.writeSync - * over Buffer windows perform sequential I/O when position is -1 and - * offset-preserving positioned I/O otherwise; fs.writeStrSync is the - * utf8 string twin; and fs.closeSync(fd) closes the descriptor. This is - * the fd slice behind spawn/log-processing forms. - * flags is Node's string - * grammar ("r", "w", "a" and the +/x variants; unknown flags throw - * Node's ERR_INVALID_ARG_VALUE TypeError text). All throw Node-shaped - * fs errors (openSync ENOENT/EACCES..., readSync EBADF/range errors, - * closeSync EBADF). */ - "fs.openSync": { argTypes: [STRING, STRING], result: F64 }, - "fs.openNumericSync": { argTypes: [STRING, F64, F64], result: F64 }, - "fs.readSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 }, - "fs.readSyncDyn": { argTypes: [DYN, DYN, DYN, DYN, DYN], result: F64 }, - "fs.writeSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 }, - "fs.writeStrSync": { argTypes: [F64, STRING, F64, STRING], result: F64 }, - // fs.watch's callback func type is program-dependent (zero params, or - // the eventType string) — the slot pins arity and the path/receiver. - /** fs.watch(path, listener?) → an FSWatcher handle (scr_watch.c — - * linked, and scr_watch_install() emitted, only when these appear on - * the IR; moduleUsesFsWatch is the switch). The path opens NOW — - * failure throws Node's fs error synchronously ("ENOENT: ..., watch - * 'x'", the polling-fallback catch shape) — and the unit's kqueue - * (EVFILT_VNODE) delivers "rename"/"change" through the loop's watch - * hook. The callback (nullable) MOVES in with an adapter per listener - * shape (zero-param, or the (eventType: string) form — runtime- - * provided); an open watcher keeps the loop alive until watcher.close() - * (idempotent, receiver borrowed, statement position only). */ - "fs.watch": { argTypes: [STRING], result: FSWATCHER_T }, - "fs.watchCb": { argTypes: [STRING, null], result: FSWATCHER_T }, - "watcher.close": { argTypes: [FSWATCHER_T], result: VOID }, - /** The composed `new crypto.X509Certificate(data).fingerprint` read - * (scr_lib.c — no certificate handle exists): the SHA-1 of the DER, - * uppercase colon-separated, over PEM or raw-DER Buffer input; other - * inputs throw Node's ERR_OSSL_PEM_NO_START_LINE Error (may-throw). */ - "crypto.x509Fingerprint": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509FingerprintStr": { argTypes: [STRING], result: STRING }, - /** The certificate's Validity window (validFrom / validTo reads — - * scr_lib.c's minimal ASN.1 walk to the TBSCertificate validity - * SEQUENCE): UTCTime and GeneralizedTime render in Node's exact - * ASN1_TIME_print shape ("Jul 1 00:00:00 2026 GMT" — %2d space-padded - * day). Same input contract and PEM error as the fingerprint pair - * (may-throw). */ - "crypto.x509ValidFrom": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509ValidFromStr": { argTypes: [STRING], result: STRING }, - "crypto.x509ValidTo": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509ValidToStr": { argTypes: [STRING], result: STRING }, - /** The certificate's DER as a fresh Buffer (the raw field), decoded - * once from the constructor's input; the same PEM error (may-throw). */ - "crypto.x509Raw": { argTypes: [BYTES_U8], result: BYTES_U8 }, - "crypto.x509RawDer": { argTypes: [BYTES_U8], result: BYTES_U8 }, - "crypto.x509RawStr": { argTypes: [STRING], result: BYTES_U8 }, - /** The remaining X509Certificate fields over that DER, in Node's - * renderings (scr_lib.c). subjectAltName, infoAccess and - * signatureAlgorithm answer "" where Node answers undefined. Borrowed; - * never throw. */ - "crypto.x509Subject": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509Issuer": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509SubjectAltName": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509InfoAccess": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509SerialNumber": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509Fingerprint256": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509Fingerprint512": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509Ca": { argTypes: [BYTES_U8], result: BOOL }, - "crypto.x509SignatureAlgorithm": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509SignatureAlgorithmOid": { argTypes: [BYTES_U8], result: STRING }, - /** X509Certificate methods over its raw DER. checkHost/checkEmail/checkIP - * answer the matching name or "" (undefined), checkIssued compares two - * certificates, x509Pem is toString's PEM (scr_x509.c); publicKey, - * verify and checkPrivateKey read keys (scr_pki.c). */ - "crypto.x509CheckHost": { argTypes: [BYTES_U8, DYN, DYN], result: STRING }, - "crypto.x509CheckEmail": { argTypes: [BYTES_U8, DYN, DYN], result: STRING }, - "crypto.x509CheckIP": { argTypes: [BYTES_U8, DYN, DYN], result: STRING }, - "crypto.x509CheckIssued": { argTypes: [BYTES_U8, BYTES_U8], result: BOOL }, - "crypto.x509LegacyObject": { argTypes: [BYTES_U8], result: DYN }, - "crypto.x509Pem": { argTypes: [BYTES_U8], result: STRING }, - "crypto.x509PublicKey": { argTypes: [BYTES_U8], result: DYN }, - "crypto.x509Verify": { argTypes: [BYTES_U8, DYN], result: BOOL }, - "crypto.x509CheckPrivateKey": { argTypes: [BYTES_U8, DYN], result: BOOL }, - "fs.closeSync": { argTypes: [F64], result: VOID }, - "stats.isFile": { argTypes: [STATS_T], result: BOOL }, - "stats.isDirectory": { argTypes: [STATS_T], result: BOOL }, - "stats.isSymbolicLink": { argTypes: [STATS_T], result: BOOL }, - "stats.size": { argTypes: [STATS_T], result: F64 }, - "stats.dev": { argTypes: [STATS_T], result: F64 }, - "stats.ino": { argTypes: [STATS_T], result: F64 }, - "stats.blocks": { argTypes: [STATS_T], result: F64 }, - "stats.nlink": { argTypes: [STATS_T], result: F64 }, - "stats.atimeMs": { argTypes: [STATS_T], result: F64 }, - "stats.mtimeMs": { argTypes: [STATS_T], result: F64 }, - "stats.ctimeMs": { argTypes: [STATS_T], result: F64 }, - "stats.birthtimeMs": { argTypes: [STATS_T], result: F64 }, - "stats.mode": { argTypes: [STATS_T], result: F64 }, - "stats.uid": { argTypes: [STATS_T], result: F64 }, - "stats.gid": { argTypes: [STATS_T], result: F64 }, - "stats.rdev": { argTypes: [STATS_T], result: F64 }, - "stats.blksize": { argTypes: [STATS_T], result: F64 }, - "stats.isBlockDevice": { argTypes: [STATS_T], result: BOOL }, - "stats.isCharacterDevice": { argTypes: [STATS_T], result: BOOL }, - "stats.isFIFO": { argTypes: [STATS_T], result: BOOL }, - "stats.isSocket": { argTypes: [STATS_T], result: BOOL }, - /** The Date fields: the *Ms value rounded (Node's dateFromMs). */ - "stats.atime": { argTypes: [STATS_T], result: DATE_T }, - "stats.mtime": { argTypes: [STATS_T], result: DATE_T }, - "stats.ctime": { argTypes: [STATS_T], result: DATE_T }, - "stats.birthtime": { argTypes: [STATS_T], result: DATE_T }, - // The wider sync fs slice (unlink/chmod/chown/copyfile and the - // mode-carrying write/mkdir forms). - /** The wider sync fs slice (scr_lib.c), all throwing catchably with - * Node's errno message shapes and `.code` stamped like the rest of - * sync fs. unlink/chmod/chown wrap the syscalls 1:1 (Node reports the - * syscall's own name). copyFileSync copies contents into a fresh (or - * truncated) destination carrying the SOURCE's mode (libuv's - * uv_fs_copyfile behavior); its errors carry BOTH paths — Node's - * "copyfile 'src' -> 'dest'". lstatSync is statSync without following - * a trailing symlink (Node reports lstat); stats.isSymbolicLink / - * stats.dev / ino / blocks / nlink / atimeMs / mtimeMs / ctimeMs are pure reads on the widened - * snapshot (blocks is allocated 512-byte units; the times are milliseconds - * with their sub-second fractions, Node's arithmetic). - * writeFileModeSync is writeFileSync(path, data, { mode }): the mode - * applies at CREATION only (open(2) with O_CREAT, umask applying), - * exactly Node — an existing file keeps its permissions. mkdirModeSync - * / mkdirRecursiveModeSync are the mkdirSync option forms with an - * explicit mode (the recursive walk passes it to every directory it - * creates, like Node's). */ - "fs.unlinkSync": { argTypes: [STRING], result: VOID }, - "fs.chmodSync": { argTypes: [STRING, F64], result: VOID }, - "fs.chownSync": { argTypes: [STRING, F64, F64], result: VOID }, - "fs.lchownSync": { argTypes: [STRING, F64, F64], result: VOID }, - "fs.fchownSync": { argTypes: [F64, F64, F64], result: VOID }, - /** truncateSync opens the path with 'r+', then truncates (len checked and - * clamped like ftruncateSync's) and closes it. */ - "fs.truncateSync": { argTypes: [STRING, F64], result: VOID }, - "fs.copyFileSync": { argTypes: [STRING, STRING], result: VOID }, - /** renameSync is the direct two-path syscall wrapper. renameCb defers - * the operation to the loop and fires a program-shaped Error | null - * callback through a backend-emitted adapter. */ - "fs.renameSync": { argTypes: [STRING, STRING], result: VOID }, - // Callback type is program-dependent (zero params or Error | null). - "fs.renameCb": { argTypes: [STRING, STRING, null], result: VOID }, - "fs.writeFileModeSync": { argTypes: [STRING, STRING, F64], result: VOID }, - "fs.mkdirModeSync": { argTypes: [STRING, F64], result: VOID }, - "fs.mkdirRecursiveModeSync": { argTypes: [STRING, F64], result: VOID }, - "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 }, - "worker.root": { argTypes: [], result: F64 }, - "worker.data": { argTypes: [], result: DYN }, - "worker.parentPort": { argTypes: [], result: DYN }, - /** worker_threads.resourceLimits: {} in the main thread; a refusal in a - * worker (scr_worker_events.c). */ - "worker.resourceLimits": { argTypes: [], result: DYN }, - /** worker_threads.threadName: the Worker's name in a worker, "" in the - * main thread (scr_worker.c). */ - "worker.threadName": { argTypes: [], result: DYN }, - /** new MessageChannel() ({ port1, port2 }) and receiveMessageOnPort(port) - * (scr_worker_events.c; the latter may throw). */ - "worker.channel": { argTypes: [], result: DYN }, - "worker.receiveMessageOnPort": { argTypes: [DYN], result: DYN }, - /** setEnvironmentData(key, value) and getEnvironmentData(key) - * (scr_worker_events.c; arguments borrowed). */ - "worker.setEnvironmentData": { argTypes: [DYN, DYN], result: VOID }, - "worker.getEnvironmentData": { argTypes: [DYN], result: DYN }, - /** markAsUntransferable(value) and isMarkedAsUntransferable(value) - * (scr_worker_events.c; the value borrowed). */ - "worker.markUntransferable": { argTypes: [DYN], result: VOID }, - "worker.isMarkedUntransferable": { argTypes: [DYN], result: BOOL }, - /** markAsUncloneable(value) (scr_json.c; the value borrowed). */ - "dyn.markUncloneable": { argTypes: [DYN], result: VOID }, - /** before new Worker() with the stdout or stderr option: links the stream - * unit that worker.stdout and worker.stderr read through (scr_stream.c). */ - "stream.workerStdio": { argTypes: [], result: VOID }, - /** new BroadcastChannel(...args) (scr_worker_events.c; may throw), after - * event.broadcastInstall, which installs its EventTarget side. */ - "worker.broadcastChannel": { argTypes: [DYN], result: DYN }, - "event.broadcastInstall": { argTypes: [], result: VOID }, - /** child_process.spawnSync (scr_child.c): posix_spawn + waitpid + piped - * utf8 capture — cmd borrowed, args one borrowed string[] (the frontend - * completes an omitted list to an empty literal), result an owned (+1) - * spawnRes. NEVER throws: spawn failure (nonexistent binary, EACCES) is - * data, like Node's error property — status null and empty outputs - * (SEMANTICS.md documents the divergence from Node's null stdout). - * The getters are pure reads: status is the interned `number | null` - * union (null = spawn failure or signal death, type-directed - * construction in the backend like process.envGet); stdout/stderr are - * +1 strings. */ - "cp.spawnSync": { argTypes: [STRING, arrayOf(STRING)], result: SPAWNRES_T }, - // spawnSync's options form: timeout, killSignal name ("" = SIGTERM), - // and the three stdio modes. - /** spawnSync with options (scr_child.c): the cp.spawnSync core plus the - * option slice portless-class CLIs pass — timeout (killSignal fires at - * the deadline and the result carries error: ETIMEDOUT + the signal, - * Node's shape), killSignal (a signal NAME; "" = the SIGTERM default), - * and per-fd stdio modes (in: 0 /dev/null, 1 ignore, 2 inherit; out/ - * err: 0 capture, 1 ignore → "", 2 inherit → ""). NEVER throws — spawn - * failure and timeout are data on the result, like Node's error - * property. spawnRes.signal is the result's termination signal as the - * call site's `Signals | null` union (null = exited normally or spawn - * failure), constructed type-directedly like spawnRes.status. */ - "cp.spawnSyncOpts": { - 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 - * maps the value to the three modes. Args (cmd, argv, timeout, - * killSignal, stdio). Never throws, like the other spawnSync forms. */ - "cp.spawnSyncStdioStr": { - argTypes: [STRING, arrayOf(STRING), F64, STRING, STRING], - result: SPAWNRES_T, - }, - // node:net (scr_net.c). Listener slots are program-dependent closures — - // null slots, checked in the libCall case (the child.onExit precedent); - // the zero-param callbacks pin the exact func type here. The trailing - // BOOL on registrations is the once flag. - /** node:net (scr_net.c — linked, and scr_net_install() emitted, only - * when one of these appears on the IR; moduleUsesNet is the switch). - * Receivers are borrowed; CALLBACKS MOVE into the handle's listener - * registry and are released at settlement (the child.onExit story). - * All listener registrations carry a trailing BOOL once-flag (`on` vs - * `once`) and are void — chaining is fenced. None of these throw: - * listen/connect failures are the async 'error' event, like Node. - * - * net.createServer's optional connection handler and the - * serverOnConnection listeners take the runtime-provided adapters - * (zero-param, or the one-param socket shape); sockOnData's adapters - * are the stdin pair's shapes (zero-param / bytes chunk); the error - * events reuse the child %Error adapters. net.listen/net.listenCb bind - * NOW and defer 'listening' to the next loop turn (Node's next-tick - * emit); net.serverPort is the composed `server.address().port` read. - * net.connect's host argument is a string ("localhost" pins to - * 127.0.0.1 — SEMANTICS.md); the connect callback is once('connect'). */ - "net.createServer": { argTypes: [], result: NETSERVER_T }, - "net.createServerCb": { argTypes: [null], result: NETSERVER_T }, - "net.listen": { argTypes: [NETSERVER_T, F64], result: VOID }, - /** listen(path[, cb]): a Unix-socket server; the socket file is removed - * when the server closes. Failures are the async 'error'. */ - "net.listenPath": { argTypes: [NETSERVER_T, STRING], result: VOID }, - "net.listenPathCb": { - argTypes: [NETSERVER_T, STRING, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "net.listenCb": { - argTypes: [NETSERVER_T, F64, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - /** listen({ port, host, ipv6Only }[, cb]) — the explicit-interface bind - * (portless's listenOnProxyInterface): args [server, port, host, - * ipv6Only]. host is an IP literal string ("" = the host-less - * dual-stack any default); ipv6Only sets IPV6_V6ONLY before the bind. - * These are the old-shaped calls, retained for omitted reusePort so - * serialized IR remains compatible. Failures are the async 'error', - * message in Node's listen shape with the requested host. Never throws. */ - "net.listenOpts": { argTypes: [NETSERVER_T, F64, STRING, BOOL], result: VOID }, - // The callback slot is a zero-param void closure OR its - // `(() => void) | undefined` optional-binding union (checked specially). - "net.listenOptsCb": { argTypes: [NETSERVER_T, F64, STRING, BOOL, null], result: VOID }, - /** Additive ABI for listen({ ..., reusePort }) — args - * [server, port, host, ipv6Only, reusePort], with the callback following - * those values in the callback form. The final BOOL is the requested - * kernel connection-distribution option; old spellings remain - * four/five-argument calls so serialized IR stays compatible. */ - "net.listenOptsReusePort": { argTypes: [NETSERVER_T, F64, STRING, BOOL, BOOL], result: VOID }, - // The callback is a zero-param void closure OR its optional-binding union; - // the callback follows the reusePort BOOL in this additive ABI. - "net.listenOptsReusePortCb": { - argTypes: [NETSERVER_T, F64, STRING, BOOL, BOOL, null], - result: VOID, - }, - "net.serverPort": { argTypes: [NETSERVER_T], result: F64 }, - "net.serverListening": { argTypes: [NETSERVER_T], result: BOOL }, - /** socket.ref()/unref() and server.ref()/unref(): whether the handle - * keeps the event loop alive. */ - "net.sockSetRef": { argTypes: [NETSOCKET_T, BOOL], result: VOID }, - "net.serverSetRef": { argTypes: [NETSERVER_T, BOOL], result: VOID }, - // net.serverAddress's record result is shape-checked in the libCall case - // (the dgram.address sentinel pattern). - /** server.address() as the full AddressInfo record (the dgram.address - * materialization pattern: the emitter builds the record from the three - * runtime reads; the frontend pinned the shape). Never throws — before - * listen it answers the any-form defaults with port 0 where Node - * answers null (the serverPort stance). */ - "net.serverAddress": { argTypes: [NETSERVER_T], result: VOID }, - "net.serverClose": { argTypes: [NETSERVER_T], result: VOID }, - /** server.close((err) => ...): the callback (a dyn function) gets - * undefined once the server closes, or Node's ERR_SERVER_NOT_RUNNING - * when it was not listening. */ - "net.serverCloseDyn": { argTypes: [NETSERVER_T, DYN], result: VOID }, - /** server.address() in programs that may listen on a Unix socket: the - * path a listening server answers ("" for a TCP server), and the check - * a record-typed address() makes first (throws for a Unix server). */ - "net.serverUnixPath": { argTypes: [NETSERVER_T], result: STRING }, - "net.serverAddressCheck": { argTypes: [NETSERVER_T], result: VOID }, - "net.serverCloseCb": { - argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "net.serverOnError": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - "net.serverOnClose": { - argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "net.serverOnConnection": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - /** 'secureConnection' — the TLS server's deferred 'connection' list - * (handshake-completion timing); on a server without deferred - * connections (a plain net server) the registration never fires, - * exactly Node's split. */ - "net.serverOnSecureConnection": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - "net.connect": { argTypes: [F64, STRING], result: NETSOCKET_T }, - /** connect with a validated autoSelectFamilyAttemptTimeout option (the - * budget runs Node's validateInt32-from-1 ladder and is then inert — - * the single dial has nothing to time). May-throw. */ - "net.connectAttempt": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, - /** net.connect/createConnection over a RUNTIME option bag (computed - * keys — the invalid-input probes): Node-order validation (the - * objectMode trio's ERR_INVALID_ARG_VALUE, validatePort, host string, - * autoSelectFamily boolean, the attempt budget), then the trailing - * compiler-rendered fence — ALWAYS THROWS (the error.nodeThrow - * polymorphic-result carve-out). May-throw seed. */ - "net.connectOptsChk": { argTypes: [DYN, STRING], result: VOID }, - "net.connectCb": { - argTypes: [F64, STRING, { kind: "func", params: [], ret: VOID }], - result: NETSOCKET_T, - }, - // The lookup's exact func shape is program data (its answer callback's - // union/record types) — checked specially in the libCall case. - /** connect({ port, host, autoSelectFamily: true, lookup }) — the - * caller-resolver dial (portless's createLoopbackConnection): args - * [port, host, lookup]. The runtime creates the (connecting) socket - * handle, invokes the lookup as Node does — lookup(hostname, options, - * callback), options crossing as the dyn undefined — and the answer - * closure (an emitter-synthesized per-shape thunk over a boxed socket, - * the SNI-answer pattern) dials the answered addresses IN ORDER: each - * connect failure tries the next, the LAST failure's message is the - * socket's 'error' (Node's autoSelectFamily aggregate is a documented - * divergence), and a lookup error surfaces as the deferred socket - * 'error'. A synchronous throw INSIDE the lookup propagates out of the - * connect call (may-throw seed). */ - "net.connectLookup": { argTypes: [F64, STRING, null], result: NETSOCKET_T }, - /** connect({ host, port, lookup }) with a lookup of Node's own type, as - * a checked-dynamic function: the runtime calls it with { hints, all: - * true } and a callback taking an address string or list, then dials - * the answers in order. MAY THROW (host validation, a lookup throw). */ - "net.connectLookupDyn": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, - "net.sockWrite": { argTypes: [NETSOCKET_T, STRING], result: VOID }, - "net.sockWriteBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID }, - "net.sockEnd": { argTypes: [NETSOCKET_T], result: VOID }, - "net.sockEndStr": { argTypes: [NETSOCKET_T, STRING], result: VOID }, - "net.sockEndBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID }, - /** write/end with a CHECKED-DYNAMIC chunk (an untyped JS payload into a - * typed socket): the runtime dispatches STR/BYTES and throws Node's - * ERR_INVALID_ARG_TYPE chunk TypeError on any other kind. */ - "net.sockWriteDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, - "net.sockEndDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, - "net.sockDestroy": { argTypes: [NETSOCKET_T], result: VOID }, - "net.sockPipe": { argTypes: [NETSOCKET_T, NETSOCKET_T], result: VOID }, - /** socket.pipe(res) — raw socket chunks into a ServerResponse body - * (the extended-CONNECT bridge leg): each chunk is a body write (the - * response's own framing applies); source EOF end()s the response, - * pipe's default. Borrows both. Never throws. */ - "net.sockPipeRes": { argTypes: [NETSOCKET_T, HTTPRES_T], result: VOID }, - "net.sockOnData": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, - "net.sockOnEnd": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "net.sockOnClose": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "net.sockOnError": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, - "net.sockOnConnect": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - // node:dgram + node:dns (scr_dgram.c). The message/error listeners and - // dns.lookup's callback are program-dependent closures (null slots, - // checked in the libCall case); dgram.address's record result is - // program-dependent too (VOID here is the envGet sentinel — the libCall - // case checks the {address, family, port} shape). - /** node:dgram + node:dns (scr_dgram.c — linked, and - * scr_dgram_install() emitted, only when one of these appears on the - * IR; moduleUsesDgram is the switch — dns.lookup rides the same unit). - * The net listener discipline verbatim: receivers borrowed, CALLBACKS - * MOVE into the handle's registry and release at settlement, `on` vs - * `once` is the trailing bool, registrations are void. bind/connect - * bind NOW and defer 'listening'/'connect' to the next loop turn (the - * net.listen story); their optional-host completions are "" - * (bind → 0.0.0.0) and "127.0.0.1" (connect — udp4's Node default). - * bind/connect/send/close/address THROW Node's state errors ("Socket - * is already bound", "Already connected", "Not running") — may-throw - * seeded. dgram.address returns the AddressInfo RECORD (the frontend - * pins the {address, family, port} shape; the emitter builds it from - * runtime parts). onMessage adapters are emitted per rinfo record - * shape (the child.onExit precedent); onError reuses the child %Error - * adapters. dns.lookup resolves via getaddrinfo AT CALL TIME and - * defers the callback to the next turn (SEMANTICS.md documents the - * blocking divergence); its per-union adapter builds the - * `Error | null` first argument. */ - "dgram.createSocket": { argTypes: [BOOL], result: DGRAMSOCK_T }, - "dgram.bind": { argTypes: [DGRAMSOCK_T, F64, STRING], result: VOID }, - "dgram.bindCb": { - argTypes: [DGRAMSOCK_T, F64, STRING, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "dgram.connect": { argTypes: [DGRAMSOCK_T, F64, STRING], result: VOID }, - "dgram.connectCb": { - argTypes: [DGRAMSOCK_T, F64, STRING, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "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 - * sends and completion callbacks included. Up to six arguments, the - * missing ones undefined. May-throw. */ - "dgram.sendChk": { argTypes: [DGRAMSOCK_T, DYN, DYN, DYN, DYN, DYN, DYN], result: VOID }, - "dgram.address": { argTypes: [DGRAMSOCK_T], result: VOID }, - "dgram.close": { argTypes: [DGRAMSOCK_T], result: VOID }, - "dgram.closeCb": { - argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "dgram.unref": { argTypes: [DGRAMSOCK_T], result: VOID }, - "dgram.ref": { argTypes: [DGRAMSOCK_T], result: VOID }, - /** The socket options (scr_dgram.c): the membership calls take the - * interface and whether it was given. May-throw. */ - "dgram.setBroadcast": { argTypes: [DGRAMSOCK_T, BOOL], result: VOID }, - "dgram.setTTL": { argTypes: [DGRAMSOCK_T, F64], result: F64 }, - "dgram.setMulticastTTL": { argTypes: [DGRAMSOCK_T, F64], result: F64 }, - "dgram.setMulticastLoopback": { argTypes: [DGRAMSOCK_T, BOOL], result: BOOL }, - "dgram.setMulticastInterface": { argTypes: [DGRAMSOCK_T, STRING], result: VOID }, - "dgram.addMembership": { argTypes: [DGRAMSOCK_T, STRING, STRING, BOOL], result: VOID }, - "dgram.dropMembership": { argTypes: [DGRAMSOCK_T, STRING, STRING, BOOL], result: VOID }, - "dgram.getRecvBufferSize": { argTypes: [DGRAMSOCK_T], result: F64 }, - "dgram.getSendBufferSize": { argTypes: [DGRAMSOCK_T], result: F64 }, - "dgram.setRecvBufferSize": { argTypes: [DGRAMSOCK_T, F64], result: VOID }, - "dgram.setSendBufferSize": { argTypes: [DGRAMSOCK_T, F64], result: VOID }, - "dgram.getSendQueueSize": { argTypes: [DGRAMSOCK_T], result: F64 }, - "dgram.getSendQueueCount": { argTypes: [DGRAMSOCK_T], result: F64 }, - "dgram.disconnect": { argTypes: [DGRAMSOCK_T], result: VOID }, - "dgram.onMessage": { argTypes: [DGRAMSOCK_T, null, BOOL], result: VOID }, - "dgram.onError": { argTypes: [DGRAMSOCK_T, null, BOOL], result: VOID }, - "dgram.onListening": { - argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "dgram.onClose": { - argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "dgram.onConnect": { - argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "dns.lookup": { argTypes: [STRING, F64, null], result: VOID }, - /** Node's argument forms with dyn callbacks: lookup(hostname, options, - * callback) and lookupService(address, port, callback). */ - "dns.lookupDyn": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, - "dns.lookupService": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, - /** node:dns/promises: lookup(hostname, options) and lookupService(address, - * port) over the callback forms; the promise fulfills with the dyn - * { address, family } (array for all: true) / { hostname, service }. - * Argument errors throw synchronously (may-throw), unlike the - * util.promisify forms below. */ - "dns.promisesLookup": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "dns.promisesLookupService": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "dns.getServers": { argTypes: [], result: DYN }, - "dns.getDefaultResultOrder": { argTypes: [], result: STRING }, - "dns.setDefaultResultOrder": { argTypes: [DYN], result: VOID }, - // node:test (scr_test.c). Bodies are program-dependent closures (0 or - // 1 testCtx param, void or Promise result — the spoke pinned the - // shape): null slots. sub's result is the settled Promise the - // await consumes. - /** node:test (scr_test.c — linked only when one of these appears on - * the IR; moduleUsesNodeTest is the switch, and the main epilogue asks - * scr_test_exit_code() for the process's exit status). Strings are - * BORROWED, callbacks MOVE. register/suite/hook attach to the runner - * tree (register: name, mode 0|1|2 run/skip/todo, directive message "" - * = none, cb or absent via registerEmpty, flags 1 async | 2 takes-ctx - * | 4 only, "file:line:col"); suite runs its body AT registration - * (Node's collection phase). sub is t.test — runs the subtest INLINE - * on the runner fiber and returns the settled promise the await - * consumes. ctxSkip/ctxTodo mark the running test; ctxDiagnostic - * queues an ℹ line; ctxName reads t.name. Every registration keeps - * the loop-run emitted (usesTimers) so the runner fiber drains. */ - // The trailing three are the { plan, timeout, concurrency } options (plan - // -1 without one; timeout read under flag 8; concurrency read under flag - // 16, -1 for true). An out-of-range timeout or concurrency throws. - "test.register": { - argTypes: [STRING, F64, STRING, null, F64, STRING, F64, F64, F64], - result: VOID, - }, - "test.registerEmpty": { - argTypes: [STRING, F64, STRING, F64, STRING, F64, F64, F64], - result: VOID, - }, - // The suite body takes no parameters or its SuiteContext (flag 2). - "test.suite": { argTypes: [STRING, F64, STRING, null, STRING, F64, F64], result: VOID }, - // The trailing string is the hook call's "file:line:col". - "test.hook": { argTypes: [F64, null, F64, STRING], result: VOID }, - "test.sub": { - argTypes: [TESTCTX_T, STRING, F64, STRING, null, F64, STRING, F64, F64, F64], - result: { kind: "promise", inner: VOID }, - }, - "test.subEmpty": { - argTypes: [TESTCTX_T, STRING, F64, STRING, F64, STRING, F64, F64, F64], - result: VOID, - }, - "test.ctxSkip": { argTypes: [TESTCTX_T, STRING], result: VOID }, - "test.ctxTodo": { argTypes: [TESTCTX_T, STRING], result: VOID }, - "test.ctxDiagnostic": { argTypes: [TESTCTX_T, STRING], result: VOID }, - "test.ctxName": { argTypes: [TESTCTX_T], result: STRING }, - "test.ctxFullName": { argTypes: [TESTCTX_T], result: STRING }, - // t.passed, and t.workerId's Number(NODE_TEST_WORKER_ID) with 0 for - // undefined. - "test.ctxPassed": { argTypes: [TESTCTX_T], result: BOOL }, - "test.workerId": { argTypes: [], result: F64 }, - // getTestContext(): whether a test, suite or hook is running in the - // current async context, and that context. - "test.hasContext": { argTypes: [], result: BOOL }, - "test.context": { argTypes: [], result: TESTCTX_T }, - // t.before/t.after/t.beforeEach/t.afterEach (which: 0 before, 1 after, - // 2 beforeEach, 3 afterEach); a throwing t.before is captured on the test. - "test.ctxHook": { argTypes: [TESTCTX_T, F64, null, F64], result: VOID }, - // t.plan(count), and the count of one t.assert call. - "test.ctxPlan": { argTypes: [TESTCTX_T, F64], result: VOID }, - "test.ctxCount": { argTypes: [TESTCTX_T], result: VOID }, - // The mock trackers: the module's `mock` and t.mock keep each mock's - // restore closure (moved) for restoreAll and reset. - "test.mockTrack": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: VOID }, - "test.mockRestoreAll": { argTypes: [], result: VOID }, - "test.mockReset": { argTypes: [], result: VOID }, - "test.ctxMockTrack": { - argTypes: [TESTCTX_T, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "test.ctxMockRestoreAll": { argTypes: [TESTCTX_T], result: VOID }, - "test.ctxMockReset": { argTypes: [TESTCTX_T], result: VOID }, - // mock.timers on the module's `mock` and on t.mock. enable takes one - // code per listed API ('T' setTimeout, 'I' setInterval), the `now` - // value and its flags (1 given, 2 from a Date). enable, tick, runAll and - // setTime throw Node's errors; tick and runAll run timer callbacks. - // t.assert.snapshot and fileSnapshot: the escaped, wrapped serialized - // value, and the stored value it is compared with (snapshot: the test, - // the value, the snapshot file; fileSnapshot: the value, the path). - // Both read or, in update mode, record; missing and unreadable - // snapshots throw Node's ERR_INVALID_STATE. - "test.snapshotValue": { argTypes: [STRING], result: STRING }, - "test.ctxSnapshot": { argTypes: [TESTCTX_T, STRING, STRING], result: STRING }, - "test.fileSnapshot": { argTypes: [STRING, STRING], result: STRING }, - // Node's serializeValue over a dyn array of serializers (undefined for - // the default JSON.stringify(value, null, 2)); a throw becomes Node's - // "The provided serializers did not generate a string." ERR_INVALID_STATE. - "test.snapshotSerialize": { argTypes: [DYN, DYN], result: DYN }, - "test.mockTimersEnable": { argTypes: [STRING, F64, F64], result: VOID }, - "test.mockTimersTick": { argTypes: [F64], result: VOID }, - "test.mockTimersRunAll": { argTypes: [], result: VOID }, - "test.mockTimersReset": { argTypes: [], result: VOID }, - "test.mockTimersSetTime": { argTypes: [F64], result: VOID }, - "test.ctxMockTimersEnable": { argTypes: [TESTCTX_T, STRING, F64, F64], result: VOID }, - "test.ctxMockTimersTick": { argTypes: [TESTCTX_T, F64], result: VOID }, - "test.ctxMockTimersRunAll": { argTypes: [TESTCTX_T], result: VOID }, - "test.ctxMockTimersReset": { argTypes: [TESTCTX_T], result: VOID }, - "test.ctxMockTimersSetTime": { argTypes: [TESTCTX_T, F64], result: VOID }, - // node:http (scr_http.c over scr_net.c). The handler and the data - // listeners are program-dependent closures (null slots, checked in the - // libCall case); reqHeader's result is the interned string|undefined - // union, checked like process.envGet. - /** node:http, the server slice (scr_http.c over scr_net.c — linked - * only when these appear on the IR; moduleUsesHttpServer is the - * switch, and any http.* libCall also counts as net use so scr_net.c - * links and installs). http.createServer's handler MOVES in and takes - * the runtime adapters for its (req, res) / (req) / () shapes; req - * body listeners follow the net.sockOnData story (bytes chunks, once - * flags); reqHeader answers the interned `string | undefined` union - * exactly like process.envGet; the res writers are borrowed-argument - * voids with Node's framing decided in the runtime (Content-Length - * for end-before-head, chunked after an explicit writeHead/write). */ - "http.createServer": { argTypes: [null], result: NETSERVER_T }, - /** http.createServer() / http.Server() with no handler — the - * on("request") route; option helpers set parser behavior on a fresh - * server before the constructor/factory result is returned. */ - "http.createServerEmpty": { argTypes: [], result: NETSERVER_T }, - "http.validateHeaderName": { argTypes: [STRING, STRING], result: VOID }, - "http.validateHeaderValue": { argTypes: [STRING, DYN], result: VOID }, - "http.serverJoinDupHeaders": { argTypes: [NETSERVER_T], result: VOID }, - "http.serverAllowMissingHostHeader": { argTypes: [NETSERVER_T], result: VOID }, - /** The five writable numeric http.Server timeout fields use one - * selector ABI: 0 timeout, 1 keepAliveTimeout, 2 headersTimeout, - * 3 requestTimeout, 4 keepAliveTimeoutBuffer. These calls store/read - * the property values; timer enforcement remains outside this surface. - * The constructor-option setter takes dyn so explicit undefined remains - * distinguishable from a numeric value until its Node validation ladder. */ - "http.serverTimeoutGet": { argTypes: [NETSERVER_T, F64], result: F64 }, - "http.serverSetTimeout": { argTypes: [NETSERVER_T, F64], result: VOID }, - "http.serverSetTimeoutCb": { argTypes: [NETSERVER_T, F64, null], result: VOID }, - "http.serverOnTimeout": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - "http.serverCloseAllConnections": { argTypes: [NETSERVER_T], result: VOID }, - "http.serverCloseIdleConnections": { argTypes: [NETSERVER_T], result: VOID }, - "http.serverTimeoutSet": { argTypes: [NETSERVER_T, F64, F64], result: VOID }, - "http.serverTimeoutOptionSet": { argTypes: [NETSERVER_T, F64, DYN], result: VOID }, - /** server.on("listening", cb) — the deferred listen-callback list. */ - "net.serverOnListening": { - argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - /** The ServerResponse member surface: statusCode/statusMessage reads - * and assignments (Node's writable properties), the header CRUD trio - * (getHeader answers `string | undefined` like reqHeader), and - * end(cb)'s callback slot (resOnEndCallback registers, the end call - * follows — the callback fires deferred, just before Node's 'finish' - * emit; resOnFinish is the 'finish' listener slot). writeHead's - * statusMessage forms compose in interned helpers: resStatusMsgSet - * then the ordinary writeHead entry. */ - "http.resStatusGet": { argTypes: [HTTPRES_T], result: F64 }, - "http.resStatusSet": { argTypes: [HTTPRES_T, F64], result: VOID }, - "http.resStatusMsgGet": { argTypes: [HTTPRES_T], result: VOID }, - "http.resStatusMsgSet": { argTypes: [HTTPRES_T, STRING], result: VOID }, - "http.resRequest": { argTypes: [HTTPRES_T], result: HTTPREQ_T }, - "http.resSocket": { argTypes: [HTTPRES_T], result: VOID }, - "http.resWritableFinished": { argTypes: [HTTPRES_T], result: BOOL }, - "http.resSendDateGet": { argTypes: [HTTPRES_T], result: BOOL }, - "http.resSendDateSet": { argTypes: [HTTPRES_T, BOOL], result: VOID }, - "http.resStrictContentLengthGet": { argTypes: [HTTPRES_T], result: BOOL }, - "http.resStrictContentLengthSet": { argTypes: [HTTPRES_T, BOOL], result: VOID }, - "http.resSetTimeout": { argTypes: [HTTPRES_T, F64], result: VOID }, - "http.resSetTimeoutCb": { - argTypes: [HTTPRES_T, F64, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - // resGetHeader answers the interned `string | undefined` union — the - // reqHeader/envGet sentinel pattern (VOID here, checked specially). - "http.resGetHeader": { argTypes: [HTTPRES_T, STRING], result: VOID }, - /** getHeader as Node answers it: the number, string or string[] that was - * set, or undefined, as a checked-dynamic value. */ - "http.resGetHeaderDyn": { argTypes: [HTTPRES_T, STRING], result: DYN }, - "http.resGetHeaderNames": { argTypes: [HTTPRES_T], result: arrayOf(STRING) }, - "http.resGetRawHeaderNames": { argTypes: [HTTPRES_T], result: arrayOf(STRING) }, - "http.resGetHeaders": { argTypes: [HTTPRES_T], result: DYN }, - "http.resHasHeader": { argTypes: [HTTPRES_T, STRING], result: BOOL }, - "http.resRemoveHeader": { argTypes: [HTTPRES_T, STRING], result: VOID }, - "http.resOnFinish": { - argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "http.resOnEndCallback": { - argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "http.reqUrl": { argTypes: [HTTPREQ_T], result: STRING }, - "http.reqMethod": { argTypes: [HTTPREQ_T], result: STRING }, - "http.reqHeader": { argTypes: [HTTPREQ_T, STRING], result: VOID }, - "http.reqTrailer": { argTypes: [HTTPREQ_T, STRING], result: VOID }, - "http.reqHeaderValues": { argTypes: [HTTPREQ_T, STRING], result: VOID }, - "http.reqTrailerValues": { argTypes: [HTTPREQ_T, STRING], result: VOID }, - "http.reqOnData": { argTypes: [HTTPREQ_T, null, BOOL], result: VOID }, - "http.reqOnEnd": { - argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.resSetHeader": { argTypes: [HTTPRES_T, STRING, STRING], result: VOID }, - /** setHeader / appendHeader with a number, string or array value; the - * stored value keeps its type for getHeader/getHeaders. MAY THROW. */ - "http.resSetHeaderDyn": { argTypes: [HTTPRES_T, STRING, DYN], result: VOID }, - "http.resAppendHeader": { argTypes: [HTTPRES_T, STRING, DYN], result: VOID }, - "http.resWriteHead": { argTypes: [HTTPRES_T, F64], result: VOID }, - "http.resWriteHeadN": { - argTypes: [HTTPRES_T, F64, arrayOf(STRING), arrayOf(STRING)], - result: VOID, - }, - /** HTTP/1.1 informational heads; early hints takes flat string pairs, - * with a lowercase `link` key required to send anything. */ - "http.resWriteContinue": { argTypes: [HTTPRES_T], result: VOID }, - "http.resWriteProcessing": { argTypes: [HTTPRES_T], result: VOID }, - "http.resWriteEarlyHints": { argTypes: [HTTPRES_T, arrayOf(STRING)], result: VOID }, - "http.resWrite": { argTypes: [HTTPRES_T, STRING], result: VOID }, - "http.resWriteBytes": { argTypes: [HTTPRES_T, BYTES_U8], result: VOID }, - "http.resEnd": { argTypes: [HTTPRES_T], result: VOID }, - "http.resEndStr": { argTypes: [HTTPRES_T, STRING], result: VOID }, - "http.resEndBytes": { argTypes: [HTTPRES_T, BYTES_U8], result: VOID }, - /** The checked-dynamic chunk twins (the net.sockWriteDyn story). */ - "http.resWriteDyn": { argTypes: [HTTPRES_T, DYN], result: VOID }, - "http.resEndDyn": { argTypes: [HTTPRES_T, DYN], result: VOID }, - "http.resHeadersSent": { argTypes: [HTTPRES_T], result: BOOL }, - "http.resWritableEnded": { argTypes: [HTTPRES_T], result: BOOL }, - "http.resFlushHeaders": { argTypes: [HTTPRES_T], result: VOID }, - "http.resAddTrailers": { argTypes: [HTTPRES_T, arrayOf(STRING)], result: VOID }, - "http.resCork": { argTypes: [HTTPRES_T], result: VOID }, - "http.resUncork": { argTypes: [HTTPRES_T], result: VOID }, - "http.resWritableCorked": { argTypes: [HTTPRES_T], result: F64 }, - // The member follow-ups: reqStatusCode's `number | undefined` and - // sockRemoteAddress's `string | undefined` results are shape-checked in - // the special cases in validate.ts (like reqHeader/columns). - /** The server-surface member follow-ups: reqStatusCode answers the - * interned `number | undefined` union (negative = the undefined arm — - * a SERVER request, where Node's statusCode is undefined; every client - * response carries a real status); reqSocket is the underlying - * connection (+1, the same handle net.connect would give); - * sockRemoteAddress answers `string | undefined` (NULL after the fd - * closed, Node's destroyed-socket undefined; a dual-stack accept of an - * IPv4 peer reads "::ffff:a.b.c.d" like Node). reqResume/reqPause/reqDestroy and - * the req error/close listener slots complete the IncomingMessage - * surface portless's client responses use; resDestroy/resOnClose and - * resWriteHeadPairs ([k0,v0,k1,v1,...] — the env.pairs helper's flat - * shape) complete ServerResponse. sockSetTimeout arms the idle - * EVFILT_TIMER ('timeout' fires after ms of inactivity, once per idle - * period, never destroying the socket — Node's semantics). */ - "http.reqStatusCode": { argTypes: [HTTPREQ_T], result: VOID }, - // reqStatusMessage's `string | undefined` is program-interned like - // reqHeader's (the VOID here is the envGet sentinel). - "http.reqStatusMessage": { argTypes: [HTTPREQ_T], result: VOID }, - /** req.rawHeaders — [name, value, name, value, ...] in arrival order, - * names in their ORIGINAL case (Node's shape); a fresh string[] per - * read. reqStatusMessage answers the interned `string | undefined` - * union — the reason phrase on client responses ("" when the status - * line carried none), the undefined arm on server requests (the - * statusCode split). sockDestroyed is socket.destroyed — true once the - * fd is gone (destroy() or full close). */ - "http.reqRawHeaders": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, - /** The `{ ...req.headers }` snapshot feed: [lowercased name, value, - * ...] pairs in arrival order — the interned %headers.snapshot helper - * builds the record over it, exactly the process.envPairs pattern. */ - "http.reqHeaderPairs": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, - /** Every value of one header in arrival order (empty when absent): the - * set-cookie array of a `{ ...req.headers }` snapshot. */ - "http.reqHeaderList": { argTypes: [HTTPREQ_T, STRING], result: arrayOf(STRING) }, - "http.reqRawTrailers": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, - "http.reqTrailerPairs": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, - "net.sockDestroyed": { argTypes: [NETSOCKET_T], result: BOOL }, - /** socket.writable — the write half is open: no end() yet, no FIN sent, - * fd alive (connecting sockets answer true; writes queue). Node's - * stream flag. Borrows; never throws. */ - "net.sockWritable": { argTypes: [NETSOCKET_T], result: BOOL }, - // The 'upgrade' registrations: the callback shapes are program-typed - // ((req, socket, head) and shorter prefixes — the libCall case checks). - /** The 'upgrade' events, both sides (SEMANTICS.md — the WebSocket - * proxying surface): serverOnUpgrade registers (req, socket, head) - * listeners fired INSTEAD of 'request' for Connection: upgrade - * requests (the parser steps aside; the socket is raw; `head` carries - * bytes past the request head; no listener = the socket destroys, - * Node's default). clientOnUpgrade is the client twin: a 101 response - * fires (res, socket, head) INSTEAD of 'response'. */ - "http.serverOnUpgrade": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - /** server.on("connect", ...) — HTTP CONNECT tunneling, the 'upgrade' - * machinery's twin: (req, socket, head) fired INSTEAD of 'request' for - * CONNECT-method requests (no listener = the socket destroys, Node's - * default). The h2 compat server's 'connect' (portless's RFC 8441 - * handler) only ever sees the HTTP/1.1 arm under the allowHTTP1 - * lowering, so a listener whose second parameter is a UNION with a - * netSocket arm takes the socket wrapped at that arm (an emitted - * per-shape adapter — the tags are program data). */ - "http.serverOnConnect": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - "http.clientOnUpgrade": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, - "http.reqSocket": { argTypes: [HTTPREQ_T], result: NETSOCKET_T }, - // reqH2Stream's program-interned Http2Stream | undefined result is - // checked in the special libCall cases in validate.ts. - "http.reqH2Stream": { argTypes: [HTTPREQ_T], result: VOID }, - "http.reqH2StreamOrThrow": { argTypes: [HTTPREQ_T, STRING], result: HTTP2STREAM_T }, - /** req.pipe(dest) — the IncomingMessage body streaming into a - * ServerResponse (the proxy's response leg), a ClientRequest (the - * request-body forward), or a raw socket (the upgrade-rejection leg); - * chunk-for-chunk, natural end ends the destination (Node's pipe - * default; no backpressure — divergence 54's stream model). */ - "http.reqPipeRes": { argTypes: [HTTPREQ_T, HTTPRES_T], result: VOID }, - "http.reqPipeClient": { argTypes: [HTTPREQ_T, HTTPCLIENTREQ_T], result: VOID }, - "http.reqPipeSock": { argTypes: [HTTPREQ_T, NETSOCKET_T], result: VOID }, - "http.reqResume": { argTypes: [HTTPREQ_T], result: VOID }, - "http.reqPause": { argTypes: [HTTPREQ_T], result: VOID }, - "http.reqDestroy": { argTypes: [HTTPREQ_T], result: VOID }, - "http.reqOnError": { argTypes: [HTTPREQ_T, null, BOOL], result: VOID }, - "http.reqOnClose": { - argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.reqOnAborted": { - argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.reqHttpVersion": { argTypes: [HTTPREQ_T], result: STRING }, - "http.reqHttpVersionMajor": { argTypes: [HTTPREQ_T], result: F64 }, - "http.reqHttpVersionMinor": { argTypes: [HTTPREQ_T], result: F64 }, - "http.reqAborted": { argTypes: [HTTPREQ_T], result: BOOL }, - "http.reqComplete": { argTypes: [HTTPREQ_T], result: BOOL }, - "http.resDestroy": { argTypes: [HTTPRES_T], result: VOID }, - "http.resOnClose": { - argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.resWriteHeadPairs": { argTypes: [HTTPRES_T, F64, arrayOf(STRING)], result: VOID }, - /** writeHead(status, headers) with a checked-dynamic headers value — - * the runtime OBJ walk (string/number values; other values throw - * Node's invalid-argument TypeError; may throw). */ - "http.resWriteHeadDyn": { argTypes: [HTTPRES_T, F64, DYN], result: VOID }, - "net.sockSetTimeout": { argTypes: [NETSOCKET_T, F64], result: VOID }, - /** setEncoding('utf8') — 'data' delivers strings inside the chunk-encoding window; other real encodings throw the "not supported yet" Error, unknown names throw ERR_UNKNOWN_ENCODING (may throw). */ - "net.sockSetEncoding": { argTypes: [NETSOCKET_T, STRING], result: VOID }, - "http.reqSetEncoding": { argTypes: [HTTPREQ_T, STRING], result: VOID }, - "net.sockOnTimeout": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "net.sockRemoteAddress": { argTypes: [NETSOCKET_T], result: VOID }, - /** socket.encrypted — `boolean | undefined`: the true arm iff the - * socket carries a TLS transport (Node types `encrypted: true` on - * TLSSocket; plain sockets answer undefined — the proxy.ts isEncrypted - * idiom reads it through a cast). */ - "net.sockEncrypted": { argTypes: [NETSOCKET_T], result: VOID }, - // node:http, the client slice. The response callback shapes are - // program-dependent (checked by validate.ts); header pairs arrive flat. - /** node:http, the CLIENT slice (http.request/http.get over the net - * client machinery): request/requestCb take (host, port, path, method, - * timeoutMs, headerPairs, autoEnd[, responseCb]) — headerPairs is the - * flat [k0,v0,...] array (empty for none), autoEnd true is http.get's - * eager end(). The handle owns one dialed connection (NO pooling — the - * wire still carries Node's exact head: user headers, then Host, - * Connection: keep-alive, and the framing header; the socket closes - * when the response completes). The response delivered to responseCb / - * 'response' listeners IS an httpReq (IncomingMessage), status and - * headers parsed from the wire, body via reqOnData/reqOnEnd. Errors are - * Node-shaped ('connect ECONNREFUSED ip:port', 'socket hang up') and - * fire 'error' then 'close'; an unhandled 'error' exits 1 like every - * net handle. clientWrite before end commits to chunked framing unless - * the caller set content-length; clientEnd(data) before any write sends - * Content-Length exactly like Node. */ - "http.request": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL], - result: HTTPCLIENTREQ_T, - }, - "http.requestCb": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, null], - result: HTTPCLIENTREQ_T, - }, - /** new http.Agent(opts) / new https.Agent(opts): (secure, keepAlive, - * keepAliveMsecs, maxSockets, maxFreeSockets, timeoutMs, port) — the - * numeric options arrive < 0 for "unset" (Infinity/256/none; port - * seeds the settable defaultPort, Node's option merge). Returns the - * Agent as a checked-dynamic HANDLE (getName/destroy and the - * sockets/requests/freeSockets counters dispatch through the dyn - * handle ops). keepAlive: true THROWS the named construction fence — - * socket POOLING is not modeled (one dial per request); maxSockets - * accounting is real: over-limit requests defer their dial and queue. - * MAY THROW. */ - "http.agentNew": { argTypes: [BOOL, BOOL, F64, F64, F64, F64, F64, DYN], result: DYN }, - /** http.globalAgent / https.globalAgent (true): Node's keepAlive global - * agent, the one requests without an agent option use. */ - "http.globalAgent": { argTypes: [BOOL], result: DYN }, - /** http.request's `signal` option over the fresh request: an aborted - * signal destroys it with Node's AbortError now, a later abort event - * does the same. A falsy value is ignored; a non-signal throws - * ERR_INVALID_ARG_TYPE. */ - "http.clientSignal": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID }, - /** options.signal checked before the request is built (ERR_INVALID_ARG_TYPE). */ - "http.clientSignalCheck": { argTypes: [DYN], result: VOID }, - /** https.request's servername option, stored for the construction call - * that follows: the TLS name sent and verified, and part of the pooled - * socket key. The "https." prefix keeps the TLS unit linked. */ - "https.nextServername": { argTypes: [STRING], result: VOID }, - /** The maxHeaderSize option of createServer / http.request: Node's - * validateInteger (ERR_OUT_OF_RANGE) then the header size bound. - * MAY THROW. */ - "http.serverMaxHeaderSize": { argTypes: [NETSERVER_T, F64], result: VOID }, - "http.clientMaxHeaderSize": { argTypes: [HTTPCLIENTREQ_T, F64], result: VOID }, - /** req.destroy(err): err becomes the request's 'error'. */ - "http.clientDestroyError": { - argTypes: [HTTPCLIENTREQ_T, { kind: "object", className: "%Error" }], - result: VOID, - }, - /** The agent-threaded request rows: the http.request/https.request - * shape with a trailing `agent` dyn argument (an Agent handle, false — - * the one-shot Connection: close dial — or null/undefined for the - * default path). port < 0 means "no port option": the agent's settable - * defaultPort, then the scheme's. MAY THROW (a non-Agent value is - * Node's ERR_INVALID_ARG_TYPE). */ - "http.requestAgent": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, DYN], - result: HTTPCLIENTREQ_T, - }, - "http.requestAgentCb": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, DYN, null], - result: HTTPCLIENTREQ_T, - }, - /** request/get with a URL-STRING first argument: the runtime parses it - * (WHATWG) and dials — throws catchably on an unparsable input or a - * non-http scheme. */ - "http.requestUrl": { argTypes: [STRING, STRING, BOOL], result: HTTPCLIENTREQ_T }, - "http.requestUrlCb": { argTypes: [STRING, STRING, BOOL, null], result: HTTPCLIENTREQ_T }, - /** request/get with a runtime options record: a RequestOptions - * variable, or a literal that spreads one or sets socketPath. Args - * (options, autoEnd[, cb]). The runtime reads the record in the - * ClientRequest constructor's order (host/port or socketPath, path, - * method, headers, timeout, agent, signal, maxHeaderSize) with Node's - * errors, and documented options it does not implement throw the - * runtime fence. The "https." pair adds the TLS options. MAY THROW. */ - "http.requestDyn": { argTypes: [DYN, BOOL], result: HTTPCLIENTREQ_T }, - "http.requestDynCb": { argTypes: [DYN, BOOL, null], result: HTTPCLIENTREQ_T }, - "https.requestDyn": { argTypes: [DYN, BOOL], result: HTTPCLIENTREQ_T }, - "https.requestDynCb": { argTypes: [DYN, BOOL, null], result: HTTPCLIENTREQ_T }, - /** The paused-mode demux surface (portless's first-byte TLS peek: - * once('readable') + read(1) + unshift + emit('connection')). - * sockOnReadable registers a zero-param listener (a consumer: arrived - * bytes buffer instead of flowing, and EOF announces too); sockRead - * answers `Buffer | null` (exactly n buffered bytes, or null — Node's - * less-than-n answer; n <= 0 drains everything); sockUnshift returns - * bytes to the front of the stream; serverEmitConnection routes a - * socket into another server's protocol layer (a TLS target's - * 'connection' waits for its handshake). */ - "net.sockOnReadable": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, - // sockRead's result is the interned `Buffer | null` union — checked - // specially in validate.ts (the reqHeader/envGet pattern; VOID here is a - // placeholder the special case overrides). - "net.sockRead": { argTypes: [NETSOCKET_T, F64], result: VOID }, - "net.sockUnshift": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID }, - /** Socket flow control and the compat surface: pause/resume (reads - * gate off/on — kernel backpressure holds paused bytes; resume flows - * and discards sans listeners) and setNoDelay answer the SOCKET (+1, - * Node's chaining); destroySoon ends now and destroys once the FIN - * flushed; bytesWritten counts accepted bytes; readable is true until - * the read half ends. */ - "net.sockPause": { argTypes: [NETSOCKET_T], result: NETSOCKET_T }, - "net.sockResume": { argTypes: [NETSOCKET_T], result: NETSOCKET_T }, - "net.sockSetNoDelay": { argTypes: [NETSOCKET_T, BOOL], result: NETSOCKET_T }, - "net.sockDestroySoon": { argTypes: [NETSOCKET_T], result: VOID }, - /** RST close, answers the socket; throws ERR_INVALID_HANDLE_TYPE on TLS. */ - "net.sockResetAndDestroy": { argTypes: [NETSOCKET_T], result: NETSOCKET_T }, - /** socket.address(): { address, family, port } or {} as a dyn. */ - "net.sockAddress": { argTypes: [NETSOCKET_T], result: DYN }, - "net.sockBytesWritten": { argTypes: [NETSOCKET_T], result: F64 }, - /** remotePort (0), remoteFamily (1), localAddress (2), localPort (3), - * localFamily (4): dyn values, undefined when unknown. */ - "net.sockAddrField": { argTypes: [NETSOCKET_T, F64], result: DYN }, - "net.sockBytesRead": { argTypes: [NETSOCKET_T], result: F64 }, - "net.sockConnecting": { argTypes: [NETSOCKET_T], result: BOOL }, - "net.sockPending": { argTypes: [NETSOCKET_T], result: BOOL }, - "net.sockReadyState": { argTypes: [NETSOCKET_T], result: STRING }, - "net.sockSetKeepAlive": { argTypes: [NETSOCKET_T, BOOL, F64], result: NETSOCKET_T }, - "net.sockOnReady": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - /** Listeners with a parameter, as dyn functions: close(hadError), - * write's cb(err) and end's cb. */ - "net.sockOnCloseDyn": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID }, - /** socket.on/once('lookup', listener): the listener (a dyn function) - * gets (err, address, family, host) once per resolved address, or once - * with the lookup error, before the connect. */ - "net.sockOnLookup": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID }, - "net.sockOnWriteFlush": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "net.sockOnWriteFlushDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, - "net.sockOnFinishDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, - "net.sockSetTimeoutCb": { - argTypes: [NETSOCKET_T, F64, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "net.isIP": { argTypes: [DYN], result: F64 }, - "net.isIPv4": { argTypes: [DYN], result: BOOL }, - "net.isIPv6": { argTypes: [DYN], result: BOOL }, - "net.serverMaxConnectionsGet": { argTypes: [NETSERVER_T], result: DYN }, - "net.serverMaxConnectionsSet": { argTypes: [NETSERVER_T, DYN], result: VOID }, - "net.serverDropMaxConnectionGet": { argTypes: [NETSERVER_T], result: DYN }, - "net.serverDropMaxConnectionSet": { argTypes: [NETSERVER_T, DYN], result: VOID }, - /** net.SocketAddress and net.BlockList (scr_net.c): the constructors - * take the argument array; the handles are dyn values. */ - "net.socketAddressNew": { argTypes: [DYN], result: DYN }, - "net.socketAddressParse": { argTypes: [DYN], result: DYN }, - "net.isSocketAddress": { argTypes: [DYN], result: BOOL }, - "net.blockListNew": { argTypes: [DYN], result: DYN }, - "net.isBlockList": { argTypes: [DYN], result: BOOL }, - "net.blockListPrivateRanges": { argTypes: [], result: DYN }, - "net.serverOnDrop": { argTypes: [NETSERVER_T, DYN, BOOL], result: VOID }, - "net.serverGetConnections": { argTypes: [NETSERVER_T, DYN], result: VOID }, - "net.sockReadable": { argTypes: [NETSOCKET_T], result: BOOL }, - /** socket.on('finish', cb) / end(cb): fires once when the FIN goes out - * (sweep-deferred, never the registering stack). */ - "net.sockOnFinish": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "net.serverEmitConnection": { argTypes: [NETSERVER_T, NETSOCKET_T], result: VOID }, - // tls/https: cert/key/ca PEM arguments are strings OR Buffers (null = - // both accepted; the emitter passes data+len either way). - /** node:tls + node:https (scr_tls.c over scr_net.c/scr_http.c, with - * the vendored mbedTLS archive — linked only when one of these appears - * on the IR; moduleUsesTls is the switch, and every tls/https libCall - * also counts as net AND http use so both units link and install). - * tls.createServer takes (cert, key[, handler]) — cert/key are PEM - * strings or Buffers (the emitter passes data+len for either); the - * handler is Node's 'secureConnection' (fires post-handshake with a - * socket that behaves exactly like a net socket, the same adapters as - * net.createServer). https.createServer is (cert, key, handler) with - * the http request-handler adapters. https.request/requestCb extend - * the http client row with (…, rejectUnauthorized: bool, ca: PEM - * string/Buffer — "" for none) and default port 443; everything else - * (write/end/destroy/events, the response surface) IS the http client - * 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). - /** RUNTIME options records (the divergence-66 stance): the *Dyn - * creators take a checked-dynamic (dyn) options value whose members - * read at runtime — cert/key extract like the literal path, members - * whose literal forms fence THROW the catchable fence at runtime, and - * undocumented keys drop like Node drops them. tls.pemDyn is the - * literal walk's runtime-valued cert/key extraction: (value, whatLit) - * → PEM bytes (strings/Buffers/one-element arrays of those) or the - * thrown fence. All of them may throw. */ - "tls.pemDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, - "tls.createServerDyn": { argTypes: [DYN], result: NETSERVER_T }, - "tls.createServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T }, - "https.createServerDyn": { argTypes: [DYN], result: NETSERVER_T }, - "https.createServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T }, - /** createSecureServer(options, handler) — the eager COMPAT handler as - * the first 'request' listener (Node's exact route), on both literal - * flavors: Req is the dual ALPN allowHTTP1 server, H2Req the ALPN=h2-only - * server (both use compat handles over h2 streams). Args are - * (cert, key, cb, enableConnectProtocol). */ - "http2.createSecureServerReq": { argTypes: [null, null, null, BOOL], result: NETSERVER_T }, - "http2.createSecureServerH2Req": { argTypes: [null, null, null, BOOL], result: NETSERVER_T }, - /** createSecureServer with a RUNTIME options record (the divergence-66 - * stance): allowHTTP1/cert/key read at runtime — allowHTTP1 picks the - * flavor, the TLS server walk fences its out-of-bounds members with - * the catchable runtime fence, and h2 session-tuning keys drop exactly - * like the literal walk ignores them. May throw. The Cb form carries - * the eager compat handler. */ - "http2.createSecureServerDyn": { argTypes: [DYN], result: NETSERVER_T }, - "http2.createSecureServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T }, - // tls.connect(port, host, opts[, cb]) — port -1 / host "" read the - // options record; the callback fires post-handshake (secureConnect). - /** tls.connect — the TLS client socket: (port, host, opts[, cb]) where - * port -1 reads options.port, host "" reads options.host, and opts is - * the runtime (dyn) options record (rejectUnauthorized/ca/servername - * implemented; other documented members throw the runtime fence). The - * callback fires post-handshake — Node's secureConnect timing. */ - "tls.connect": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, - "tls.connectCb": { argTypes: [F64, STRING, DYN, null], result: NETSOCKET_T }, - // The TLSSocket member surface (authError's `string | null` union is - // shape-checked in the special cases in validate.ts — the reqHeader pattern). - /** The TLSSocket member surface on the socket kind: authorized (bool — - * Node's verify verdict), authorizationError (the verify-failure CODE - * STRING or null), the 'secureConnect' registration (a TLS socket's - * conn list fires at establishment; plain sockets never fire it), and - * the 'session' registration (fires once with the serialized session — - * a Buffer; the received-ticket event). */ - "tls.sockAuthorized": { argTypes: [NETSOCKET_T], result: BOOL }, - /** getProtocol() (string | null), getCipher() (CipherNameAndProtocol, - * undefined before the handshake, null after close) and alpnProtocol - * (string | false | null), as dyn values. */ - "tls.sockGetProtocol": { argTypes: [NETSOCKET_T], result: DYN }, - "tls.sockGetCipher": { argTypes: [NETSOCKET_T], result: DYN }, - "tls.sockAlpnProtocol": { argTypes: [NETSOCKET_T], result: DYN }, - /** getPeerCertificate(detailed) and getCertificate() (the legacy object, - * {} or null; scr_tls_x509.c), and the DER behind getPeerX509Certificate() - * and getX509Certificate() (a Buffer, empty without a certificate). */ - "tls.sockGetPeerCertificate": { argTypes: [NETSOCKET_T, DYN], result: DYN }, - "tls.sockGetCertificate": { argTypes: [NETSOCKET_T], result: DYN }, - "tls.sockPeerCertDer": { argTypes: [NETSOCKET_T], result: BYTES_U8 }, - "tls.sockOwnCertDer": { argTypes: [NETSOCKET_T], result: BYTES_U8 }, - "tls.sockAuthError": { argTypes: [NETSOCKET_T], result: VOID }, - "tls.sockOnSecureConnect": { - argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], - 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 - * answer with; cert/key are PEM strings or Buffers like createServer's. */ - "tls.createSecureContext": { argTypes: [null, null], result: SECURECTX_T }, - /** createSecureContext over a RUNTIME options record (the checked- - * dynamic lane): Node's typed option validations first (the ciphers/ - * passphrase/engine/version/timeout/ticketKeys ladders), then the pem - * walk — a validated { cert, key } bag builds the real context. - * May-throw. */ - "tls.createSecureContextDyn": { argTypes: [DYN], result: SECURECTX_T }, - /** server.addContext(servername, options | SecureContext), - * server.setSecureContext(options) and the 'tlsClientError' listener of - * a tls or https server (scr_tls.c). */ - "tls.serverAddContext": { argTypes: [NETSERVER_T, DYN, DYN], result: VOID }, - "tls.serverAddContextCtx": { argTypes: [NETSERVER_T, DYN, SECURECTX_T], result: VOID }, - "tls.serverSetSecureContext": { argTypes: [NETSERVER_T, DYN], result: VOID }, - "tls.serverOnClientError": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - /** tls.getCACertificates(type): validateString + the documented name - * set, then the trailing compiler-rendered fence — ALWAYS THROWS (the - * error.nodeThrow polymorphic-result carve-out). May-throw seed. */ - "tls.caCertsChk": { argTypes: [DYN, STRING], result: VOID }, - // The CA-store introspection unit (scr_tls_ca.c): per-type cached PEM - // string arrays and the default-set replacement. - /** The CA-store introspection surface (scr_tls_ca.c — its own unit and - * link gate, "tlsca." NOT "tls.", so a getCACertificates-only binary - * never pulls mbedTLS): tlsca.get is tls.getCACertificates(type) — the - * cached per-type string[] of PEM blocks (identity-stable across calls, - * Node's caching; an unknown type throws Node's ERR_INVALID_ARG_VALUE - * TypeError); tlsca.root is the tls.rootCertificates value read; - * tlsca.set is tls.setDefaultCACertificates(certs) — replaces the - * default set (deduped) and the anchors the TLS client verifies - * against, throwing Node's ERR_CRYPTO_OPERATION_FAILED when no entry - * carries a certificate block. */ - "tlsca.get": { argTypes: [STRING], result: arrayOf(STRING) }, - "tlsca.root": { argTypes: [], result: arrayOf(STRING) }, - "tlsca.set": { argTypes: [arrayOf(STRING)], result: VOID }, - "https.createServer": { argTypes: [null, null, null], result: NETSERVER_T }, - // http2's allowHTTP1 compatibility server (divergence 57): cert/key - // like tls.createServer; the 'request' handler arrives separately via - // http.serverOnRequest (shape checked in the libCall case, like - // http.createServer's). sessionError callback shape is checked by validate.ts. - /** node:http2 allowHTTP1: one TLS server advertises h2 + http/1.1 and - * dispatches to the matching parser after ALPN. Request listeners mirror - * into both compatibility lists; the remaining session/req.stream rows - * below retain their explicitly limited behavior. */ - "http2.createSecureServer": { argTypes: [null, null, BOOL], result: NETSERVER_T }, - // The SNI-callback form: arg 2 is the JS SNICallback closure — a - // `(servername, cb) => void` func, or its `| undefined` union from the - // conditional-spread spelling (the libCall case checks the shape). - /** createSecureServer with an SNI callback: args are (cert, key, sniCb) - * where sniCb is the JS SNICallback — a `(servername, cb) => void` - * closure, or the `SNICallback | undefined` union from the conditional- - * spread spelling (`...(x ? { SNICallback: x } : {})`; the emitter - * unwraps the union — the undefined arm means "no callback", exactly - * the no-SNI server). The runtime parses each connection's ClientHello - * for the server_name extension BEFORE the TLS handshake begins, calls - * the callback with (servername, answer-closure), and resumes the - * handshake when the answer arrives — cb(err) tears the socket down - * without an error event (Node's 'tlsClientError' default), cb(null, ctx) serves - * ctx's cert/key, cb(null, undefined) serves the default pair. */ - "http2.createSecureServerSni": { argTypes: [null, null, null, BOOL], result: NETSERVER_T }, - // The ALPN=h2 server (createSecureServer without allowHTTP1): the real - // h2 session machinery behind the TLS handshake. - /** The REAL h2-over-TLS server (createSecureServer WITHOUT allowHTTP1): - * scr_http2_create_secure_server — the h2c session machinery behind an - * mbedTLS handshake whose ALPN advertises h2 alone (an http/1.1-only - * client fails the handshake with no_application_protocol, Node's - * h2-only split). args are (cert, key) — PEM strings or Buffers. */ - "http2.createSecureServerH2": { argTypes: [null, null, BOOL], result: NETSERVER_T }, - "http.serverOnRequest": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - /** server.on('checkContinue' | 'checkExpectation', (req, res)): the - * 'request' listener shape; the last argument picks checkExpectation. */ - "http.serverOnCheck": { argTypes: [NETSERVER_T, null, BOOL, BOOL], result: VOID }, - "http2.serverOnSessionError": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - /** The guarded absent-session compatibility call remains a no-op. */ - "http2.streamNoop": { argTypes: [], result: VOID }, - /** The UNGUARDED h2-only stream call (`req.stream.on(...)`): stream is - * undefined on every connection the allowHTTP1 lowering accepts — and - * on every HTTP/1.1 connection of Node's own allowHTTP1 server — so the - * call IS Node's member read on undefined: throws the exact catchable - * TypeError ("Cannot read properties of undefined (reading 'on')"). - * One arg: the read member's name (a string literal). Never returns. */ - "http2.streamUndefCall": { argTypes: [STRING], result: VOID }, - // The REAL h2c surface (scr_http2.c). Callback slots are null (the - // emitter picks the thunk); pairs slots are string arrays; the - // endStream tri-state and event flags ride as f64. - /** node:http2, the REAL h2c surface (scr_http2.c — frame codec + HPACK - * over the net loop; the design note atop that file has the story). - * Sessions and streams are first-class handle kinds; 'stream'/'response' - * payloads cross as flat [name, value, ...] pairs arrays and an EMITTED - * adapter closure builds the program-side headers record (the response - * :status rides separately as a number). */ - "http2.createServer": { argTypes: [], result: NETSERVER_T }, - "http2.createServerReq": { argTypes: [null], result: NETSERVER_T }, - "http2.serverOnStream": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - "http2.serverOnSession": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, - // connect(authority, reject, ca[, cb]): the TLS client knobs an https - // authority reads (inert on http ones, exactly Node); ca is a PEM - // string/Buffer ("" = the system anchors). - /** connect(authority[, listener]) — h2c prior knowledge; the listener - * closure (if any) is the 'connect' once-listener. */ - "http2.connect": { argTypes: [STRING, BOOL, null], result: HTTP2SESSION_T }, - "http2.connectCb": { argTypes: [STRING, BOOL, null, null], result: HTTP2SESSION_T }, - /** session.request(pairs?, endStream) — endStream is a tri-state f64: - * -1 the method's payload-meaningless default, 0/1 explicit. */ - "http2.sessionRequest": { - argTypes: [HTTP2SESSION_T, arrayOf(STRING), F64], - result: HTTP2STREAM_T, - }, - "http2.sessionClose": { argTypes: [HTTP2SESSION_T], result: VOID }, - "http2.sessionCloseCb": { argTypes: [HTTP2SESSION_T, null], result: VOID }, - "http2.sessionDestroy": { argTypes: [HTTP2SESSION_T], result: VOID }, - "http2.sessionOnClose": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, - "http2.sessionOnError": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, - "http2.sessionOnConnect": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, - "http2.sessionOnStream": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, - "http2.sessionOnGoaway": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, - "http2.sessionClosed": { argTypes: [HTTP2SESSION_T], result: BOOL }, - "http2.sessionDestroyed": { argTypes: [HTTP2SESSION_T], result: BOOL }, - "http2.sessionEncrypted": { argTypes: [HTTP2SESSION_T], result: BOOL }, - "http2.sessionType": { argTypes: [HTTP2SESSION_T], result: F64 }, - "http2.sessionAlpn": { argTypes: [HTTP2SESSION_T], result: STRING }, - "http2.sessionSocket": { argTypes: [HTTP2SESSION_T], result: NETSOCKET_T }, - /** stream.respond(pairs?, endStream) — the server answer. */ - "http2.streamRespond": { argTypes: [HTTP2STREAM_T, arrayOf(STRING), BOOL], result: VOID }, - "http2.streamWrite": { argTypes: [HTTP2STREAM_T, STRING], result: VOID }, - "http2.streamWriteBytes": { argTypes: [HTTP2STREAM_T, BYTES_U8], result: VOID }, - "http2.streamEnd": { argTypes: [HTTP2STREAM_T], result: VOID }, - "http2.streamEndStr": { argTypes: [HTTP2STREAM_T, STRING], result: VOID }, - "http2.streamEndBytes": { argTypes: [HTTP2STREAM_T, BYTES_U8], result: VOID }, - "http2.streamClose": { argTypes: [HTTP2STREAM_T, F64], result: VOID }, - "http2.streamCloseCb": { argTypes: [HTTP2STREAM_T, F64, null], result: VOID }, - "http2.streamDestroy": { argTypes: [HTTP2STREAM_T], result: VOID }, - "http2.sessionSettings0": { argTypes: [HTTP2SESSION_T], result: VOID }, - "http2.sessionSettings": { argTypes: [HTTP2SESSION_T, DYN], result: VOID }, - "http2.sessionSettingsDynCb": { argTypes: [HTTP2SESSION_T, DYN, DYN], result: VOID }, - "http2.sessionSettingsCb0": { - argTypes: [HTTP2SESSION_T, DYN, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "http2.sessionOnSettingsDyn": { argTypes: [HTTP2SESSION_T, DYN, BOOL, BOOL], result: VOID }, - "http2.sessionOnSettings0": { - argTypes: [HTTP2SESSION_T, { kind: "func", params: [], ret: VOID }, BOOL, BOOL], - result: VOID, - }, - "http2.sessionSettingsGet": { argTypes: [HTTP2SESSION_T, BOOL], result: DYN }, - "http2.sessionPendingSettingsAck": { argTypes: [HTTP2SESSION_T], result: BOOL }, - "http2.getDefaultSettings": { argTypes: [], result: DYN }, - "http2.streamSetEncoding": { argTypes: [HTTP2STREAM_T, STRING], result: VOID }, - "http2.streamSetEncodingRet": { argTypes: [HTTP2STREAM_T, STRING], result: HTTP2STREAM_T }, - "http2.streamResume": { argTypes: [HTTP2STREAM_T], result: VOID }, - "http2.streamPause": { argTypes: [HTTP2STREAM_T], result: VOID }, - "http2.streamOnData": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, - "http2.streamOnEnd": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, - "http2.streamOnClose": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, - "http2.streamOnAborted": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, - "http2.streamOnError": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, - "http2.streamOnResponse": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, - "http2.streamId": { argTypes: [HTTP2STREAM_T], result: F64 }, - "http2.streamRstCode": { argTypes: [HTTP2STREAM_T], result: F64 }, - "http2.streamDestroyed": { argTypes: [HTTP2STREAM_T], result: BOOL }, - "http2.streamClosed": { argTypes: [HTTP2STREAM_T], result: BOOL }, - "http2.streamAborted": { argTypes: [HTTP2STREAM_T], result: BOOL }, - "http2.streamPending": { argTypes: [HTTP2STREAM_T], result: BOOL }, - "http2.streamSession": { argTypes: [HTTP2STREAM_T], result: HTTP2SESSION_T }, - // The createConnection forms: arg 0 is the dialer closure (() => - // Socket — the libCall case checks), then path/method/timeout/headers/ - // autoEnd like http.request. - /** The createConnection form (the proxy's own dialer): args are - * (connCb, path, method, timeout, headers, autoEnd[, cb]) — connCb is - * a `() => net.Socket` closure the runtime invokes ONCE, synchronously - * (Node's onSocket timing); everything else matches http.request. The - * Host header defaults to "localhost" — a headers.host entry wins - * verbatim, the proxy shape. */ - "http.requestConn": { - argTypes: [null, STRING, STRING, F64, arrayOf(STRING), BOOL], - result: HTTPCLIENTREQ_T, - }, - "http.requestConnCb": { - argTypes: [null, STRING, STRING, F64, arrayOf(STRING), BOOL, null], - result: HTTPCLIENTREQ_T, - }, - "https.request": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null], - result: HTTPCLIENTREQ_T, - }, - "https.requestCb": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, null], - result: HTTPCLIENTREQ_T, - }, - /** The URL-string first argument, the http.requestUrl row over TLS: - * no options means Node's defaults (verification on, default trust - * anchors), and a non-https scheme is ERR_INVALID_PROTOCOL. */ - "https.requestUrl": { argTypes: [STRING, STRING, BOOL], result: HTTPCLIENTREQ_T }, - "https.requestUrlCb": { argTypes: [STRING, STRING, BOOL, null], result: HTTPCLIENTREQ_T }, - "https.requestAgent": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, DYN], - result: HTTPCLIENTREQ_T, - }, - "https.requestAgentCb": { - argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, DYN, null], - result: HTTPCLIENTREQ_T, - }, - // The requestFn binding's runtime-secure rows: https.request's shape - // with the leading `secure` bool. - /** A call through a `const requestFn = tls ? https.request : - * http.request` binding — the module-function-as-value ternary between - * the two known clients: the https.request row with a leading `secure` - * bool that picks the dial at RUNTIME (true = the TLS client, exactly - * https.request; false = the plain client, exactly http.request — - * rejectUnauthorized/ca ignored there like Node ignores TLS options on - * http.request). The "https." prefix keeps the TLS unit linked. */ - "https.requestFn": { - argTypes: [BOOL, STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null], - result: HTTPCLIENTREQ_T, - }, - "https.requestFnCb": { - argTypes: [BOOL, STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, null], - result: HTTPCLIENTREQ_T, - }, - "http.clientWrite": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, - "http.clientSetHeader": { argTypes: [HTTPCLIENTREQ_T, STRING, STRING], result: VOID }, - "http.clientGetHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, - "http.clientHasHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: BOOL }, - "http.clientRemoveHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, - "http.clientGetHeaderNames": { argTypes: [HTTPCLIENTREQ_T], result: arrayOf(STRING) }, - "http.clientGetRawHeaderNames": { argTypes: [HTTPCLIENTREQ_T], result: arrayOf(STRING) }, - "http.clientGetHeaders": { argTypes: [HTTPCLIENTREQ_T], result: DYN }, - "http.clientWriteBytes": { argTypes: [HTTPCLIENTREQ_T, BYTES_U8], result: VOID }, - "http.clientEnd": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, - "http.clientEndStr": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, - "http.clientEndBytes": { argTypes: [HTTPCLIENTREQ_T, BYTES_U8], result: VOID }, - /** The checked-dynamic chunk twins (the net.sockWriteDyn story). */ - "http.clientWriteDyn": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID }, - "http.clientEndDyn": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID }, - "http.clientFlushHeaders": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, - "http.clientAddTrailers": { argTypes: [HTTPCLIENTREQ_T, arrayOf(STRING)], result: VOID }, - "http.clientCork": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, - "http.clientUncork": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, - "http.clientWritableCorked": { argTypes: [HTTPCLIENTREQ_T], result: F64 }, - "http.clientMethod": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, - "http.clientPath": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, - "http.clientHost": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, - "http.clientProtocol": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, - "http.clientHeadersSent": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, - "http.clientWritableEnded": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, - "http.clientWritableFinished": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, - "http.clientSocket": { argTypes: [HTTPCLIENTREQ_T], result: NETSOCKET_T }, - "http.clientReusedSocket": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, - "http.clientSetNoDelay": { argTypes: [HTTPCLIENTREQ_T, BOOL], result: VOID }, - "http.clientSetSocketKeepAlive": { argTypes: [HTTPCLIENTREQ_T, BOOL, F64], result: VOID }, - "http.clientSetTimeout": { argTypes: [HTTPCLIENTREQ_T, F64], result: VOID }, - "http.clientSetTimeoutCb": { - argTypes: [HTTPCLIENTREQ_T, F64, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "http.statusCodes": { argTypes: [], result: DYN }, - "http.methods": { argTypes: [], result: arrayOf(STRING) }, - "http.reqSetTimeout": { argTypes: [HTTPREQ_T, F64], result: VOID }, - "http.reqSetTimeoutCb": { - argTypes: [HTTPREQ_T, F64, { kind: "func", params: [], ret: VOID }], - result: VOID, - }, - "http.clientDestroy": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, - "http.clientAbort": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, - "http.clientAborted": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, - "http.clientDestroyed": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, - "http.clientOnResponse": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, - "http.clientOnSocket": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, - "http.clientOnFinish": { - argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.clientOnError": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, - "http.clientOnTimeout": { - argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.clientOnClose": { - argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.clientOnAbort": { - argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - "http.clientOnContinue": { - argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], - result: VOID, - }, - /** execFileSync/execSync as ONE entry (args: cmd, argv, shell, input, - * cwd, hasEnv, envPairs, timeoutMs, stdoutMode, stderrMode, buffers — - * see scr_child.h). Throws Node's exact errors: "Command failed: " - * (+ captured stderr) on non-zero exit or signal death, "spawnSync - * ENOENT" on spawn failure, "spawnSync ETIMEDOUT" after - * the SIGTERM timeout, with Node's status, signal, output, pid, stdout - * and stderr properties (Buffer outputs when buffers is set: no - * encoding option). Result is the captured utf8 stdout (+1). */ - "cp.execSync": { - argTypes: [ - STRING, - arrayOf(STRING), - BOOL, - STRING, - BOOL, - STRING, - BOOL, - arrayOf(STRING), - F64, - F64, - F64, - BOOL, - ], - result: STRING, - }, - /** The promisified-execFile/exec capture (args: cmd, argv, shell, cwd, - * hasEnv, envPairs, timeoutMs; shell marks exec's /bin/sh -c argv, as - * for execSync, so failures display the command string): the same exec - * core in the async shape — both streams captured, no echo, Node's ASYNC messages on the throw paths - * ("Command failed: \n" with the unconditional newline, - * "spawn ENOENT" with .code, timeouts reporting as ordinary - * SIGTERM command failures — never ETIMEDOUT). Result reuses the - * ScrSpawnRes container (+1; stdout/stderr strings, status unused). - * Called only from the frontend's interned %execFileAsync and - * %execAsync ASYNC helpers, whose fiber turns the throw into the - * rejection. */ - "cp.execCapture": { - argTypes: [STRING, arrayOf(STRING), BOOL, STRING, BOOL, arrayOf(STRING), F64], - result: SPAWNRES_T, - }, - /** child_process.spawn (scr_child.c + the scr_async.c loop): posix_spawnp - * with per-slot ignore/inherit/pipe modes (and number output fds), the - * all-piped Node default, and the child registered with the event loop, - * which polls waitpid(WNOHANG) at quiescence like timers. - * NEVER throws: spawn failure defers to the "error" event, Node-exact - * (the error message is Node's "spawn "; an "error" - * event with no listener prints it and exits 1 like an EventEmitter). - * cmd/args borrowed; result an owned (+1) child handle. The loop will - * not exhaust while any spawned child is unreaped — Node's keep-alive. - * - * child.onExit / child.onClose / child.onError — terminal listener - * registration through child.on/once. Close uses the exit adapter shape - * but fires only after every piped stdio handle reaches EOF. The - * receiver is borrowed, the CALLBACK MOVES into the child's listener - * registry (released after the terminal event fires, or at reap for - * the event that never fires). Both are void (chaining is fenced). - * onExit's third emitted ingredient is an ADAPTER the backend interns - * per callback shape: the runtime invokes adapter(cb, has_code, code) - * and the adapter builds the `number | null` union (tags are program- - * dependent) or ignores the code for a zero-param callback. onError's - * adapters are runtime-provided (zero-param, or the %Error one-param - * shape — scr_error_new needs no program types). "exit" fires once - * with the code (f64 arm) or null (signal death); "error" fires only - * for spawn failure, exactly Node's split. */ - "cp.spawn": { argTypes: [STRING, arrayOf(STRING)], result: CHILD_T }, - /** Static-native fork startup and IPC. process.forkTarget initializes a - * re-executed child's inherited channel and returns its embedded target id - * (-1 in the parent). The remaining entries mirror Node's JSON channel. */ - "process.forkTarget": { argTypes: [F64], result: F64 }, - "cp.fork": { - argTypes: [F64, arrayOf(STRING), F64, F64, F64, BOOL, arrayOf(STRING), STRING], - result: CHILD_T, - }, - "child.connected": { argTypes: [CHILD_T], result: BOOL }, - "child.hasChannel": { argTypes: [CHILD_T], result: BOOL }, - "child.send": { argTypes: [CHILD_T, STRING], result: BOOL }, - "child.sendCb": { argTypes: [CHILD_T, STRING, null], result: BOOL }, - "child.disconnect": { argTypes: [CHILD_T], result: VOID }, - "child.onMessage": { argTypes: [CHILD_T, null, BOOL], result: VOID }, - "child.onDisconnect": { argTypes: [CHILD_T, null, BOOL], result: VOID }, - "child.onSpawn": { argTypes: [CHILD_T, null], result: VOID }, - /** Rest listeners use the checked function bridge to pack event arguments. */ - "child.onDyn": { argTypes: [CHILD_T, STRING, DYN], result: VOID }, - // Own properties JavaScript adds to children (handle-properties.ts). - "child.propsInstall": { argTypes: [], result: VOID }, - "child.propsDefine": { argTypes: [DYN, DYN, DYN], result: DYN }, - "child.propsDescriptor": { argTypes: [DYN, DYN], result: DYN }, - "child.propsResolve": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "child.propsOnDyn": { argTypes: [CHILD_T, STRING, DYN], result: VOID }, - "process.connected": { argTypes: [], result: BOOL }, - /** Whether this fork child opened its channel; part of process.send's - * presence in programs with fork targets. Never throws. */ - "process.ipcOpen": { argTypes: [], result: BOOL }, - "process.send": { argTypes: [STRING], result: BOOL }, - "process.sendCb": { argTypes: [STRING, null], result: BOOL }, - "process.disconnect": { argTypes: [], result: VOID }, - "process.onMessage": { argTypes: [null, BOOL], result: VOID }, - "process.onDisconnect": { argTypes: [null, BOOL], result: VOID }, - /** child_process.execFile's callback slice: starts an all-piped child, - * captures stdout/stderr, and moves the error-first callback into the - * child registry. The callback shape is program-dependent and checked - * by the validator/backends. The last argument is exec's shell flag: - * the argv is ["-c", command] and failure messages show the command. */ - "cp.execFile": { argTypes: [STRING, arrayOf(STRING), null, BOOL], result: CHILD_T }, - // spawn's options form: per-slot stdio modes (0 ignore / 1 inherit / - // 2 fd) with the out/err fds for mode 2, detached, env replacement - // pairs, cwd ("" = inherit). - /** spawn with options (scr_child.c): the cp.spawn core plus PER-SLOT - * stdio — args are (cmd, argv, inMode, outMode, errMode, outFd, errFd, - * detached, hasEnv, envPairs, cwd); modes 0 ignore (/dev/null), 1 - * inherit, 2 fd (out/err only — the fd dup2s into the child's slot, - * Node's stdio fd form; the daemon-log idiom ["ignore", logFd, logFd]). - * detached is POSIX_SPAWN_SETSID (the child gets its own session and - * process group, Node's semantics), env is a REPLACEMENT ([k,v,...] - * pairs like cp.execSync's), cwd ""=inherit. Same event/loop story as - * cp.spawn. */ - "cp.spawnOpts": { - argTypes: [ - STRING, - arrayOf(STRING), - F64, - F64, - F64, - F64, - F64, - BOOL, - BOOL, - BOOL, - arrayOf(STRING), - STRING, - ], - result: CHILD_T, - }, - /** Runtime options normalized into the native spawn core; unsupported - * process features retain explicit refusals. (command, args, options). */ - "cp.spawnDynamic": { argTypes: [STRING, arrayOf(STRING), DYN], result: CHILD_T }, - // The callback's func type is program-dependent (zero params, or the - // `number | null` union / the %Error class) — the libCall case checks - // the shape; the slot here only pins arity and the child receiver. - "child.onExit": { argTypes: [CHILD_T, null], result: VOID }, - "child.onClose": { argTypes: [CHILD_T, null], result: VOID }, - "child.onError": { argTypes: [CHILD_T, null], result: VOID }, - // Like process.envGet, spawnRes.status's result type is program-dependent - // (the interned `number | null` union) — the libCall case checks the arms. - "spawnRes.status": { argTypes: [SPAWNRES_T], result: VOID }, - // spawnRes.error's result is the interned `Error | undefined` union — - // the libCall case checks the arms, the spawnRes.status pattern. - /** Node's spawn-failure carrier `error?: Error`: a fresh +1 %Error - * ("spawnSync ENOENT", `code` stamped) when the spawn itself - * failed, the interned undefined arm otherwise — the result type is the - * call site's `Error | undefined` union, constructed type-directedly in - * the backend (the envGet convention). Never throws. */ - "spawnRes.error": { argTypes: [SPAWNRES_T], result: VOID }, - // spawnRes.signal's result is the interned `string | null` union — - // same pattern. - "spawnRes.signal": { argTypes: [SPAWNRES_T], result: VOID }, - // The ChildProcess lifecycle members: pid/exitCode are program-dependent - // unions (`number | undefined` / `number | null` — the libCall case - // checks the arms, the spawnRes.status pattern). - /** The ChildProcess lifecycle members (scr_child.c), Node's shapes - * exactly (SEMANTICS.md has the pinned matrix). child.pid is the - * checker's `number | undefined` (undefined = spawn failure) and - * child.exitCode its `number | null` (null while running and after a - * signal death; -errno once a spawn failure settled) — both unions are - * type-directed constructions in the backend over a has/get runtime - * pair, the spawnRes.status pattern. child.killed is Node's - * sent-a-signal flag. child.kill sends while the child is un-reaped - * (false after — Node's null-handle answer) and THROWS the - * Unknown-signal TypeError on bad names (may-throw); killNum passes - * numbers through (0 probes; never throws). child.unref drops the - * child from the loop's keep-alive set — it is still reaped while the - * loop runs for other reasons, and one the loop never reaps is left to - * the OS at exit. All receivers borrowed. */ - "child.pid": { argTypes: [CHILD_T], result: VOID }, - "child.exitCode": { argTypes: [CHILD_T], result: VOID }, - // The close-override pair: the bound close's result func type and the - // override's func argument are program-dependent (the callback union's - // tags) — the slots pin the server receiver; the libCall case checks - // the bound value's shape. - /** The close-override pair (the portless close-proxy idiom). - * serverCloseBind is `wrapper.close.bind(wrapper)` as a VALUE: a - * compiler-emitted closure over the server whose invocation runs the - * REAL close (scr_net_server_close_direct — never the override), so - * the override body's `origClose(cb)` cannot recurse; its callback - * argument (the `((err?: Error) => void) | undefined` union) registers - * as a once-'close' listener, a one-param callback wrapped by an - * emitted zero-arg trampoline firing the undefined arm (a clean close - * carries no error). serverSetCloseOverride is `wrapper.close = fn`: - * the override MOVES in behind an emitted zero-arg wrapper (it invokes - * the user function with the undefined-arm callback — tags are program - * data), and server.close() consults it before closing. */ - "net.serverCloseBind": { argTypes: [NETSERVER_T], result: VOID }, - "net.serverSetCloseOverride": { argTypes: [NETSERVER_T, null], result: VOID }, - // The piped-output stream reads: `Readable | null` unions (the libCall - // case checks the arms — the child.pid pattern with a ref arm). - /** The piped-output streams (stdio mode 3 — scr_child.c's stream - * slice). child.stdout/child.stderr answer the checker's - * `Readable | null` union (type-directed construction in the backend - * over the +1-or-NULL runtime pair — the child.pid pattern with a ref - * arm). stream.onData/onEnd register 'data'/'end' listeners (receiver - * borrowed, CALLBACK MOVES, trailing once-flag, void — chaining - * fenced): 'data' fires one Buffer chunk per read (zero-param and - * Buffer/string adapters are runtime-provided; setEncoding threads - * split multibyte sequences through the shared StringDecoder core. A - * union-param listener — - * ngrok's `Buffer | string` — gets a compiler-emitted adapter wrapping - * the chunk at its Buffer arm), 'end' fires once at EOF, always BEFORE - * the child's 'exit' (the pinned ordering). A flowing stream keeps the - * loop alive: usesTimers. */ - "child.stdout": { argTypes: [CHILD_T], result: VOID }, - "child.stderr": { argTypes: [CHILD_T], result: VOID }, - /** The piped-input writer (stdio mode 3 on fd 0). child.stdin answers - * `Writable | null`; writes copy borrowed data into the nonblocking - * queue, end/destroy settle it, writable is a pure state read, and - * listener callbacks move into the writer registry. */ - "child.stdin": { argTypes: [CHILD_T], result: VOID }, - /** Under the general stream representation (general-streams.ts), a - * stdio read (slot 0, 1 or 2) answers the child's bridged runtime stream - * — `Writable | null` for stdin, `Readable | null` otherwise; the bridge - * throws when the program lacks the stream unit's boxing helpers. */ - "child.stdioStream": { argTypes: [CHILD_T, F64], result: VOID }, - // Listener registrations: callback func shapes are program-dependent - // (zero-param / Buffer / string / unknown / Buffer-armed union) — the slots pin arity, - // the stream receiver, and the once-flag. - "stream.onData": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID }, - "stream.onDataStr": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID }, - "stream.onEnd": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID }, - "stream.childSetEncoding": { argTypes: [CHILDSTREAM_T, STRING], result: CHILDSTREAM_T }, - "writer.writeString": { argTypes: [CHILDWRITER_T, STRING], result: BOOL }, - "writer.writeBytes": { argTypes: [CHILDWRITER_T, BYTES_U8], result: BOOL }, - "writer.end": { argTypes: [CHILDWRITER_T], result: VOID }, - "writer.destroy": { argTypes: [CHILDWRITER_T], result: VOID }, - "writer.writable": { argTypes: [CHILDWRITER_T], result: BOOL }, - "writer.onDrain": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID }, - "writer.onFinish": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID }, - "writer.onError": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID }, - /** The first-class WritableStream write (`output.write(line)` — the - * prefixStream idiom): the receiver IS the fd scalar (process.stdout/ - * stderr reads mint 1/2), dispatched onto the exact stdoutWrite/ - * stderrWrite paths so buffering and ordering stay identical. Data - * borrowed; the bool is Node's always-true backpressure signal. */ - "procStream.write": { argTypes: [PROCSTREAM_T, STRING], result: BOOL }, - /** process.stdout/stderr flowing into a runtime Writable slot under the - * general stream representation: an fd-backed Writable (+1) whose - * writes take process.stdout.write's synchronous path. */ - /** A pipeline() stage that is a checked-dynamic value: a boxed native - * stream unwraps by identity (+1); anything else throws the given - * not-supported message. Typed as a Duplex so every stage position - * accepts it; the runtime pipeline checks the stream's real sides. */ - "stream.unboxDyn": { - argTypes: [DYN, STRING], - result: { kind: "object", className: "%Duplex" }, - }, - /** Whether pipeline() pumps a checked-dynamic first stage as an - * iterable (a string, or a value with an iterator method that is not a - * stream with pipe() and on()) instead of piping it as a stream. */ - "stream.dynIterableSource": { argTypes: [DYN], result: BOOL }, - /** 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; - * the result union is `%Readable | null` or `%Writable | null`. */ - "stream.dynNative": { argTypes: [DYN, F64], result: VOID }, - /** stream.dynNative for a pipe or unpipe destination: also a - * constructor-function stream's runtime stream, in programs that link - * that unit, unless it overrides write() or end(). */ - "stream.dynNativePipe": { argTypes: [DYN, F64], result: VOID }, - /** readable.pipe(destination, end) into a checked-dynamic destination - * that is not a native stream: [source, the source boxed, destination, - * end]. Node's pipe listener protocol over the destination's members - * (scr_stream_dyn.c); throws what those members throw. */ - "stream.pipeDyn": { argTypes: [null, DYN, DYN, BOOL], result: VOID }, - /** readable.unpipe(destination) for a checked-dynamic destination that - * is not a native stream: [source, destination]. Unpipes the pipe - * stream.pipeDyn registered into it, if any (scr_stream_dyn.c). */ - "stream.unpipeDyn": { argTypes: [null, DYN], result: VOID }, - "stream.stdioWritable": { - argTypes: [PROCSTREAM_T], - result: { kind: "object", className: "%Writable" }, - }, - "child.killed": { argTypes: [CHILD_T], result: BOOL }, - "child.kill": { argTypes: [CHILD_T, STRING], result: BOOL }, - "child.killNum": { argTypes: [CHILD_T, F64], result: BOOL }, - "child.unref": { argTypes: [CHILD_T], result: VOID }, - "child.ref": { argTypes: [CHILD_T], result: VOID }, - "spawnRes.stdout": { argTypes: [SPAWNRES_T], result: STRING }, - "spawnRes.stderr": { argTypes: [SPAWNRES_T], result: STRING }, - /** fs/promises (scr_lib.c over scr_async.c's settled minting): the SAME - * sync syscalls, wrapped in an ALREADY-SETTLED promise — failure - * REJECTS (catchable at the await) instead of throwing, so none of - * these are in the may-throw seed. readFile is utf8-fenced like - * readFileSync. The non-interleaving divergence is documented in - * SEMANTICS.md. */ - /** node:crypto randomness: randomUUID plus randomBytes as either a real - * Buffer or the fused randomBytes(n).toString("hex"|"base64") path. - * The size-taking forms throw Node's RangeError on invalid values. */ - "crypto.randomUUID": { argTypes: [], result: STRING }, - "crypto.randomBytesToString": { argTypes: [F64, STRING], result: STRING }, - /** crypto.randomBytes(n) → a real u8 Buffer (+1). THROWS Node's - * RangeError on out-of-range sizes, exactly like the composed - * randomBytesToString (which keeps its one-libCall lowering — the two - * coexist: the composed form never materializes the Buffer). */ - "crypto.randomBytes": { argTypes: [F64], result: BYTES_U8 }, - /** The fused createHash(alg).update(data).digest(enc) fast path and the - * one-shot crypto.hash implementation. First-class Hash/Hmac handles use - * the entries below; all paths share the incremental runtime core. */ - "crypto.hashDigestStr": { argTypes: [STRING, STRING, STRING], result: STRING }, - "crypto.hashDigestBytes": { argTypes: [STRING, BYTES_U8, STRING], result: STRING }, - /** First-class static Hash/Hmac handles. Constructors validate the - * runtime algorithm string (every digest Node 24 lists); update returns the same - * handle by retained identity, copy snapshots Hash state, and digest - * finalizes the handle and returns either a Buffer or encoded string. */ - "crypto.native": { argTypes: [], result: DYN }, - "crypto.hashNew": { argTypes: [STRING], result: CRYPTOHASH_T }, - /** The Transform a Hash or Hmac reads and writes as a stream (+1, - * created on first use and owned by the value; scr_stream.c). The Piped - * form, for pipe destinations and pipeline stages, also keeps the value - * alive until the stream flushes or is destroyed. */ - "crypto.hashStream": { argTypes: [null], result: { kind: "object", className: "%Transform" } }, - "crypto.hashStreamPiped": { - argTypes: [null], - result: { kind: "object", className: "%Transform" }, - }, - /** createCipheriv (false) / createDecipheriv (true) over checked-dynamic - * cipher, key, iv and options arguments (scr_cipher.c): a native - * Cipheriv/Decipheriv handle whose methods dispatch through dynInvoke. */ - "crypto.cipherNew": { argTypes: [BOOL, DYN, DYN, DYN, DYN], result: DYN }, - /** createECDH(curveName) over a checked-dynamic curve (scr_ecdh.c): a - * native ECDH handle whose methods dispatch through dynInvoke. */ - "crypto.ecdhNew": { argTypes: [DYN], result: DYN }, - /** 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 }, - /** 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 }, - /** getCipherInfo(nameOrNid, options) over checked-dynamic arguments - * (scr_cipher_info.c): the info object or undefined. */ - "crypto.getCipherInfo": { argTypes: [DYN, DYN], result: DYN }, - "crypto.checkPrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, - "crypto.generatePrimeSync": { argTypes: [DYN, DYN], result: DYN }, - "crypto.generatePrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, - "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 }, - "crypto.hashUpdateBytes": { argTypes: [CRYPTOHASH_T, BYTES_U8], result: CRYPTOHASH_T }, - "crypto.hmacUpdateStr": { argTypes: [CRYPTOHMAC_T, STRING], result: CRYPTOHMAC_T }, - "crypto.hmacUpdateBytes": { argTypes: [CRYPTOHMAC_T, BYTES_U8], result: CRYPTOHMAC_T }, - "crypto.hashCopy": { argTypes: [CRYPTOHASH_T], result: CRYPTOHASH_T }, - "crypto.hashDigestString": { argTypes: [CRYPTOHASH_T, STRING], result: STRING }, - "crypto.hashDigestBuffer": { argTypes: [CRYPTOHASH_T], result: BYTES_U8 }, - "crypto.hmacDigestString": { argTypes: [CRYPTOHMAC_T, STRING], result: STRING }, - "crypto.hmacDigestBuffer": { argTypes: [CRYPTOHMAC_T], result: BYTES_U8 }, - /** The option and any-value forms, with Node's checks at run time. The - * Dyn digests return a string or a Buffer, and crypto.hash's Dyn form - * either one by its outputEncoding. */ - "crypto.hashNewOpts": { argTypes: [STRING, DYN], result: CRYPTOHASH_T }, - "crypto.hmacNewDyn": { argTypes: [STRING, DYN, DYN], result: CRYPTOHMAC_T }, - "crypto.hashCopyOpts": { argTypes: [CRYPTOHASH_T, DYN], result: CRYPTOHASH_T }, - "crypto.hashUpdateEnc": { argTypes: [CRYPTOHASH_T, STRING, STRING], result: CRYPTOHASH_T }, - "crypto.hmacUpdateEnc": { argTypes: [CRYPTOHMAC_T, STRING, STRING], result: CRYPTOHMAC_T }, - "crypto.hashUpdateDyn": { argTypes: [CRYPTOHASH_T, DYN, DYN], result: CRYPTOHASH_T }, - "crypto.hmacUpdateDyn": { argTypes: [CRYPTOHMAC_T, DYN, DYN], result: CRYPTOHMAC_T }, - "crypto.hashDigestDyn": { argTypes: [CRYPTOHASH_T, DYN], result: DYN }, - "crypto.hmacDigestDyn": { argTypes: [CRYPTOHMAC_T, DYN], result: DYN }, - "crypto.hashOneshot": { argTypes: [STRING, DYN, DYN], result: DYN }, - /** Views of any element kind (the validator checks both are views). */ - "crypto.timingSafeEqual": { argTypes: [null, null], result: BOOL }, - "crypto.timingSafeEqualDyn": { argTypes: [DYN, DYN], result: BOOL }, - "crypto.randomFill": { argTypes: [BYTES_U8, F64, F64], result: BYTES_U8 }, - "crypto.randomFillRest": { argTypes: [BYTES_U8, F64], result: BYTES_U8 }, - "crypto.randomInt": { argTypes: [F64, F64], result: F64 }, - "crypto.randomIntMax": { argTypes: [F64], result: F64 }, - /** randomFillSync over any view returns that view at its own type (the - * validator checks the result against the first argument). The Dyn form - * takes ArrayBuffers and other values. The callback forms carry - * randomFill's argument count before the callback; randomIntCb carries - * whether min was given. */ - "crypto.randomFillView": { argTypes: [null, F64, F64], result: BYTES_U8 }, - "crypto.randomFillViewRest": { argTypes: [null, F64], result: BYTES_U8 }, - /** crypto.getRandomValues: a view returns itself at its own type (the - * float/size DOMExceptions are thrown at run time); any other value - * takes the checked form. */ - "crypto.getRandomValues": { argTypes: [null], result: BYTES_U8 }, - "crypto.getRandomValuesDyn": { argTypes: [DYN], result: DYN }, - "crypto.randomFillSyncDyn": { argTypes: [DYN, DYN, DYN], result: DYN }, - "crypto.randomFillCb": { argTypes: [null, F64, F64, F64, null], result: VOID }, - "crypto.randomFillCbDyn": { argTypes: [DYN, DYN, DYN, F64, null], result: VOID }, - "crypto.randomIntCb": { argTypes: [F64, F64, F64, null], result: VOID }, - "crypto.randomUUIDOpts": { argTypes: [DYN], result: STRING }, - /** Secret KeyObjects: native handles whose getters and methods dispatch - * through the handle. keyObjectInput reads a key's bytes for hkdf. */ - "crypto.createSecretKey": { argTypes: [DYN, DYN], result: DYN }, - "crypto.generateKeySync": { argTypes: [DYN, DYN], result: DYN }, - "crypto.generateKeyCb": { argTypes: [DYN, DYN, null], result: VOID }, - "crypto.keyObjectInput": { argTypes: [DYN], result: BYTES_U8 }, - "crypto.isKeyObject": { argTypes: [DYN], result: BOOL }, - /** 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.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 } }, - "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 }, - "crypto.pbkdf2Cb": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING, null], result: VOID }, - "crypto.hkdf": { argTypes: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64], result: DYN }, - "crypto.hkdfCb": { argTypes: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64, null], result: VOID }, - "crypto.scrypt": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN], result: BYTES_U8 }, - "crypto.scryptCb": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN, null], result: VOID }, - /** util.promisify over the callback forms: the callback form's - * arguments without the callback → a promise the callback form settles, - * or rejected with an argument error; never throws. The result's value - * type is the declared one (a Buffer, the filled view, a number, or a - * checked-dynamic value); the validator checks it. */ - "crypto.randomBytesPromise": { argTypes: [F64], result: VOID }, - "crypto.pbkdf2Promise": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: VOID }, - "crypto.scryptPromise": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN], result: VOID }, - "crypto.hkdfPromise": { argTypes: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64], result: VOID }, - "crypto.randomFillPromise": { argTypes: [null, F64, F64, F64], result: VOID }, - "crypto.randomFillPromiseDyn": { argTypes: [DYN, DYN, DYN, F64], result: VOID }, - "crypto.randomIntPromise": { argTypes: [F64, F64, F64], result: VOID }, - "crypto.generateKeyPromise": { argTypes: [DYN, DYN], result: VOID }, - /** util.promisify(dns.lookup) and util.promisify(dns.lookupService): - * [hostname, options] and [address, port] → a promise of the - * checked-dynamic result object (the address array with options.all), - * or rejected with the error; never throws. */ - "dns.lookupPromise": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "dns.lookupServicePromise": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - // The Buffer statics and the fs/zlib Buffer forms: fixed always-u8 - // signatures (Buffer IS a Uint8Array — one bytes kind). - /** The Buffer statics with fixed (always-u8) signatures. fromStr is - * `Buffer.from(string, enc)` — the frontend completes an omitted - * encoding to "utf8" and fences non-literal/unsupported ones; hex and - * base64 decode Node-leniently, so it never throws. concat takes ONE - * bytes[] arg (the list) and returns a fresh copy. `Buffer.from(u8)` - * and `Buffer.alloc(n)` need no libFn — they lower to bytesNew. */ - "buffer.fromStr": { argTypes: [STRING, STRING], result: BYTES_U8 }, - "buffer.fromStrChecked": { argTypes: [STRING, STRING], result: BYTES_U8 }, - "buffer.brand": { argTypes: [BYTES_U8], result: BYTES_U8 }, - /** Buffer.isBuffer over a typed view: whether the view carries the - * Buffer brand (any element kind; only Buffers do). Never throws. */ - "buffer.isBranded": { argTypes: [null], result: BOOL }, - /** Buffer.from on checked-native strings, bytes, arrays and data-only - * array-like/Buffer-JSON objects. The encoding is a normalized literal; - * non-string inputs ignore it. Copies the input and may throw during - * element coercion or argument validation. Custom object valueOf and - * opaque reference inputs retain explicit runtime refusals. */ - "buffer.fromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, - "buffer.concat": { argTypes: [arrayOf(BYTES_U8)], result: BYTES_U8 }, - "buffer.concatDyn": { argTypes: [DYN], result: BYTES_U8 }, - /** The pooled Buffer factories (Node's allocate/fromStringFast/ - * fromArrayLike): allocUnsafe, Buffer.from of strings, Uint8Arrays, - * number[] and checked values, and concat return views into the - * per-thread pool slab below Buffer.poolSize >>> 1 bytes. alloc and - * allocUnsafeSlow never pool. The size forms validate like Node - * (ERR_OUT_OF_RANGE). poolSize reads and writes Buffer.poolSize. */ - "buffer.alloc": { argTypes: [F64], result: BYTES_U8 }, - /** Buffer.alloc as a function value: the size, fill and encoding arrive - * unchecked, and the runtime fills as Node's alloc does. */ - "buffer.allocValue": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, - "buffer.allocUnsafe": { argTypes: [F64], result: BYTES_U8 }, - "buffer.allocUnsafeSlow": { argTypes: [F64], result: BYTES_U8 }, - "buffer.poolFromStr": { argTypes: [STRING, STRING], result: BYTES_U8 }, - "buffer.poolFromStrChecked": { argTypes: [STRING, STRING], result: BYTES_U8 }, - "buffer.poolFromBytes": { argTypes: [BYTES_U8], result: BYTES_U8 }, - "buffer.poolFromArray": { argTypes: [arrayOf(F64)], result: BYTES_U8 }, - "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.from(value, byteOffset, length): an ArrayBuffer's range with - * Node's fromArrayBuffer checks; other values as poolFromDynOffset. */ - "buffer.poolFromDynRange": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, - "buffer.poolSize": { argTypes: [], result: F64 }, - "buffer.setPoolSize": { argTypes: [F64], result: VOID }, - /** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like - * fromStr's — and Buffer.isEncoding(name) over a runtime string - * (case-insensitive against Node's alias set). Pure; never throw. */ - "buffer.byteLenDyn": { argTypes: [DYN, STRING], result: F64 }, - /** Buffer.byteLength as a function value: the encoding arrives at run time. */ - "buffer.byteLenValue": { argTypes: [DYN, DYN], result: F64 }, - "buffer.byteLenStr": { argTypes: [STRING, STRING], result: F64 }, - "buffer.isEncoding": { argTypes: [STRING], result: BOOL }, - /** Buffer.concat(list, totalLength): the concatenation truncated or - * zero-padded to the total. THROWS Node's 'length' RangeError on a - * negative/non-integer total (may-throw seed). */ - "buffer.concatLen": { argTypes: [arrayOf(BYTES_U8), F64], result: BYTES_U8 }, - "buffer.concatLenDyn": { argTypes: [DYN, F64], result: BYTES_U8 }, - // The checked-dynamic compare/equals validators (Node's argument - // ladders over dyn-boxed invalid-input probes). - /** The checked-dynamic compare/equals validators (scr_bytes_io.c) — - * the lowered form when an argument is NOT statically bytes (the - * invalid-input probes; a dyn from an untyped JS helper): Node's own - * argument ladders run at runtime — ERR_INVALID_ARG_TYPE with the - * API's argument name ("buf1"/"buf2", "otherBuffer", "target"; offsets - * "of type number"), validateOffset's ERR_OUT_OF_RANGE for bad - * numbers, undefined offsets taking their Node defaults — and a - * well-typed value still computes the real answer. All args borrowed - * dyn; compareChk's four offset slots pass the undefined dyn when - * syntactically absent. May-throw seeds. */ - "buffer.compareChk": { argTypes: [DYN, DYN], result: F64 }, - "bytes.equalsChk": { argTypes: [BYTES_U8, DYN], result: BOOL }, - "bytes.compareChk": { argTypes: [BYTES_U8, DYN, DYN, DYN, DYN, DYN], result: F64 }, - /** The deprecated `new Buffer(number, 'enc')` string-arm rejection: - * always throws Node's ERR_INVALID_ARG_TYPE ("The \"string\" argument - * must be of type string. Received ..."). Borrowed dyn; may-throw. */ - "buffer.newStringFail": { argTypes: [DYN], result: BYTES_U8 }, - "buffer.deprecation": { argTypes: [], result: VOID }, - "buffer.deprecationCount": { argTypes: [], result: VOID }, - "buffer.constructor": { argTypes: [], result: DYN }, - "buffer.installValue": { argTypes: [], result: VOID }, - "buffer.staticValue": { argTypes: [STRING], result: DYN }, - "buffer.prototypeMember": { argTypes: [DYN], result: DYN }, - /** fs._toUnixTimestamp(time) over a dyn value: numeric strings and - * finite numbers coerce (negatives answer now/1000, Node's shape); - * everything else throws Node's ERR_INVALID_ARG_TYPE. May-throw. */ - "fs.toUnixTimestamp": { argTypes: [DYN], result: F64 }, - /** The fs argument-validation ladders (checked-dynamic lane): each Chk - * replicates its API's Node-order validation over dyn values (Node's - * exact typed errors — ERR_INVALID_ARG_TYPE/VALUE, ERR_OUT_OF_RANGE), - * and a full pass meets the trailing compiler-rendered SC2020 fence - * string — so the ALWAYS-THROW forms take the error.nodeThrow - * polymorphic-result carve-out. mkdtempSyncChk and the lchmod sync/ - * promise pair run the REAL operation on a validated pass instead - * (macOS lchmod(2); non-APPLE answers Node's not-a-function / - * ERR_METHOD_NOT_IMPLEMENTED shapes). May-throw seeds, all of them. */ - "fs.existsChk": { argTypes: [DYN, DYN], result: DYN }, - "fs.mkdtempChk": { argTypes: [DYN, DYN, STRING], result: VOID }, - "fs.mkdtempSyncChk": { argTypes: [DYN, DYN, STRING], result: STRING }, - "fs.readFileChk": { argTypes: [DYN, DYN, DYN, STRING], result: VOID }, - "fs.opendirChk": { argTypes: [DYN, DYN, STRING], result: VOID }, - "fs.watchFileChk": { argTypes: [DYN, DYN, STRING], result: VOID }, - "fs.lchmodChk": { argTypes: [DYN, DYN, DYN, STRING], result: VOID }, - "fs.lchmodSyncChk": { argTypes: [DYN, DYN], result: DYN }, - "fsp.lchmodChk": { argTypes: [DYN, DYN], result: { kind: "promise", inner: VOID } }, - "fs.readChk": { argTypes: [DYN, DYN, DYN, DYN, DYN, STRING], result: VOID }, - "fs.streamOptsChk": { argTypes: [DYN, DYN, STRING], result: VOID }, - /** fs.ReadStream / fs.WriteStream (scr_fs_stream.c): the constructors - * take the path and options as dyn values and throw Node's option - * errors; the members read bytesRead/bytesWritten, pending and the path - * value, and close() / close(cb) (a `() => void` callback). */ - "fs.createReadStream": { - argTypes: [DYN, DYN], - result: { kind: "object", className: "%Readable" }, - }, - "fs.createWriteStream": { - argTypes: [DYN, DYN], - result: { kind: "object", className: "%Writable" }, - }, - "fs.fsStreamBytes": { argTypes: [null], result: F64 }, - "fs.fsStreamPending": { argTypes: [null], result: BOOL }, - "fs.fsStreamPath": { argTypes: [null], result: DYN }, - "fs.fsStreamPropDyn": { argTypes: [null, STRING], result: DYN }, - "fileHandle.createReadStream": { - argTypes: [FILEHANDLE_T, DYN], - result: { kind: "object", className: "%Readable" }, - }, - "fileHandle.createWriteStream": { - argTypes: [FILEHANDLE_T, DYN], - result: { kind: "object", className: "%Writable" }, - }, - "fs.fsStreamClose": { argTypes: [null], result: VOID }, - "fs.fsStreamCloseCb": { argTypes: [null, funcOf([], VOID)], result: VOID }, - /** The compiler-resolved ERR_INVALID_ARG_TYPE throw with a RUNTIME- - * rendered Received tail: args [argname, "of type ..." clause, the - * offending dyn value]. ALWAYS THROWS; polymorphic result (the - * error.nodeThrow pattern). May-throw seed. */ - "error.argTypeThrow": { argTypes: [STRING, STRING, DYN], result: VOID }, - /** The property flavor of argTypeThrow ("The \"options.x\" property - * must be ..."): the option-bag ladders' provably-invalid arms. ALWAYS - * THROWS; polymorphic result. May-throw seed. */ - "error.propTypeThrow": { argTypes: [STRING, STRING, DYN], result: VOID }, - /** The checked-dynamic max-listeners ladders: setMaxChk is the - * instance form over a dyn n (non-numbers ERR_INVALID_ARG_TYPE, - * negatives/NaN ERR_OUT_OF_RANGE; +1 receiver back — chaining); - * setDefaultMaxChk is the static/property form, its second argument - * naming the message slot ("setMaxListeners" for the static call, - * "defaultMaxListeners" for the module-property assignment). */ - "emitter.setMaxChk": { argTypes: [null, DYN], result: VOID }, - "emitter.setDefaultMaxChk": { argTypes: [DYN, STRING], result: VOID }, - /** The Buffer forms of fs: readFileSync(path) with NO - * encoding → bytes (+1), writeFileSync/appendFileSync(path, bytes), and the - * fs/promises readFile(path) no-encoding form (an already-settled - * promise, rejecting on failure like the other fsp members). The sync - * forms THROW catchably on failure exactly like the utf8 forms. */ - "fs.readFileSyncBytes": { argTypes: [STRING], result: BYTES_U8 }, - "fs.writeFileSyncBytes": { argTypes: [STRING, BYTES_U8], result: VOID }, - "fs.appendFileSyncBytes": { argTypes: [STRING, BYTES_U8], result: VOID }, - "fsp.readFileBytes": { argTypes: [STRING], result: { kind: "promise", inner: BYTES_U8 } }, - /** node:zlib (scr_zlib.c — native-toolchain.ts compiles/links it ONLY when these - * appear on the IR, the regex/libcurl gating precedent): one-shot zlib, - * raw-DEFLATE, gzip, and auto-detect codecs over u8 bytes with Node's - * default options, or a validated literal compression level from -1 to 9. - * Compression never throws (OOM aborts); decompression - * of corrupt input THROWS Node's error catchably. */ - "zlib.deflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, - "zlib.inflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, - "zlib.deflateRawSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, - /** bytes, mode (0 zlib / 1 raw / 2 gzip), validated compression level. */ - "zlib.deflateLevelSync": { argTypes: [BYTES_U8, F64, F64], result: BYTES_U8 }, - "zlib.inflateRawSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, - "zlib.gzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, - "zlib.gunzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, - "zlib.unzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, - /** The default-options callback twins run on the executable's shared - * native work pool and deliver (Error | null, Buffer) on a later loop - * turn. crc32 validates its optional uint32 seed synchronously. */ - "zlib.deflateCb": { argTypes: [BYTES_U8, null], result: VOID }, - "zlib.inflateCb": { argTypes: [BYTES_U8, null], result: VOID }, - "zlib.deflateRawCb": { argTypes: [BYTES_U8, null], result: VOID }, - "zlib.inflateRawCb": { argTypes: [BYTES_U8, null], result: VOID }, - "zlib.gzipCb": { argTypes: [BYTES_U8, null], result: VOID }, - "zlib.gunzipCb": { argTypes: [BYTES_U8, null], result: VOID }, - "zlib.unzipCb": { argTypes: [BYTES_U8, null], result: VOID }, - "zlib.crc32": { argTypes: [BYTES_U8, F64], result: F64 }, - /** The one-shot forms with an options value: node_zlib_mode, the input, - * the options (validated at runtime with Node's ladder). The Cb form - * takes the program-shaped (Error | null, Buffer) callback, which moves. */ - "zlib.syncOpts": { argTypes: [F64, BYTES_U8, DYN], result: BYTES_U8 }, - "zlib.cbOpts": { argTypes: [F64, BYTES_U8, DYN, null], result: VOID }, - /** The checked-dynamic input twins (the JS lane's untyped data). */ - "zlib.syncOptsDyn": { argTypes: [F64, DYN, DYN], result: BYTES_U8 }, - "zlib.cbOptsDyn": { argTypes: [F64, DYN, DYN, null], result: VOID }, - /** util.promisify over the callback one-shots: [mode, data, options] - * like zlib.cbOpts → a promise the worker-pool run settles with the - * Buffer or the callback form's Error. An options or input error rejects - * it; never throws. The Dyn twin takes the JS lane's untyped data. */ - "zlib.promise": { argTypes: [F64, BYTES_U8, DYN], result: { kind: "promise", inner: BYTES_U8 } }, - "zlib.promiseDyn": { argTypes: [F64, DYN, DYN], result: { kind: "promise", inner: BYTES_U8 } }, - /** The stream classes (scr_zlib_stream.c, linked with the stream unit): - * streamNew takes Node's node_zlib_mode number and the options value - * (undefined when absent), validates it with Node's ladder, and answers - * the +1 %Transform stream. flush takes the kind value, params the level - * and strategy values; the Cb forms move their () => void callback. The - * receivers are borrowed. */ - /** Connects scr_brotli.c to the zlib engine (%main, before any module). */ - "zlib.brotliInstall": { argTypes: [], result: VOID }, - "zlib.streamNew": { argTypes: [F64, DYN], result: VOID }, - "zlib.streamBytesWritten": { argTypes: [null], result: F64 }, - /** `stream instanceof zlib.Gzip`: the borrowed stream and the class's - * node_zlib_mode; true when the stream is a zlib stream of that mode. */ - "zlib.streamIs": { argTypes: [null, F64], result: BOOL }, - "zlib.streamFlush": { argTypes: [null, DYN], result: VOID }, - "zlib.streamFlushCb": { argTypes: [null, DYN, null], result: VOID }, - "zlib.streamParams": { argTypes: [null, DYN, DYN], result: VOID }, - "zlib.streamParamsCb": { argTypes: [null, DYN, DYN, null], result: VOID }, - "zlib.streamClose": { argTypes: [null], result: VOID }, - "zlib.streamCloseCb": { argTypes: [null, null], result: VOID }, - "zlib.streamReset": { argTypes: [null], result: VOID }, - /** The Buffer overloads of the raw stream writes — same promptly - * submitted streams as process.stdoutWrite/stderrWrite, constantly true. - * The encoding arg is evaluated but ignored for bytes, like Node. The Cb - * forms move their program-shaped completion callback to the tick queue. */ - "process.stdoutWriteBytes": { argTypes: [BYTES_U8, STRING], result: BOOL }, - "process.stderrWriteBytes": { argTypes: [BYTES_U8, STRING], result: BOOL }, - // Completion callback is program-dependent: zero params, checked-dynamic, - // or an optional Error | null slot (same success shape as fs.rename). - "process.stdoutWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL }, - "process.stderrWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL }, - /** The value of a FULFILLED promise, read without a microtask turn (+1 - * for references): the tuple form of Promise.all reads its entries this - * way once the combinator fulfills, each entry widened to Promise - * and read at the type it was built with (program-dependent). */ - "promise.fulfilledValue": { argTypes: [null], result: VOID }, - /** Promise.all's tuple form as a value: the entries (each widened to - * Promise) join the runtime combinator, and when it fulfills the - * function reads the fulfilled entries into the tuple inside that - * settlement, so the result settles on the combinator's turn; a - * rejection settles it the same way. The result is the tuple's promise - * (program-dependent). */ - "promise.allTuple": { argTypes: [null, null], result: VOID }, - "fsp.readFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: STRING } }, - "fsp.writeFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, - /** fs.promises.writeFile(path, data, { mode }): the settled-promise - * twin of fs.writeFileModeSync. */ - "fsp.writeFileMode": { - argTypes: [STRING, STRING, F64], - result: { kind: "promise", inner: VOID }, - }, - "fsp.mkdir": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, - /** The fs/promises option/member tail the certs pipeline uses: mkdir's - * literal { recursive?, mode? } options (the mkdirSync matrix behind - * settled promises), unlink, chmod. Failures REJECT (catchable at the - * await), like the rest of the fsp family. */ - "fsp.mkdirMode": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, - "fsp.mkdirRecursive": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, - "fsp.mkdirRecursiveMode": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, - "fsp.unlink": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, - "fsp.chmod": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, - "fsp.chown": { argTypes: [STRING, F64, F64], result: { kind: "promise", inner: VOID } }, - "fsp.lchown": { argTypes: [STRING, F64, F64], result: { kind: "promise", inner: VOID } }, - "fsp.access": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, - "fsp.truncate": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, - "fsp.appendFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, - "fsp.appendFileBytes": { argTypes: [STRING, BYTES_U8], result: { kind: "promise", inner: VOID } }, - "fsp.copyFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, - "fsp.mkdtemp": { argTypes: [STRING], result: { kind: "promise", inner: STRING } }, - /** readFile(path, encoding | { encoding }) for any runtime encoding name. */ - "fsp.readFileEncoded": { argTypes: [STRING, STRING], result: { kind: "promise", inner: STRING } }, - "fsp.rmdir": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, - "fsp.link": { argTypes: [DYN, DYN], result: { kind: "promise", inner: VOID } }, - "fsp.symlink": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, - "fsp.cp": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, - "fsp.readlink": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "fsp.statfs": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, - "fsp.readlinkStr": { argTypes: [DYN, DYN], result: { kind: "promise", inner: STRING } }, - "fsp.readlinkBuffer": { argTypes: [DYN, DYN], result: { kind: "promise", inner: BYTES_U8 } }, - "fsp.utimes": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, - "fsp.lutimes": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, - "fsp.rename": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, - "fsp.readdir": { argTypes: [STRING], result: { kind: "promise", inner: arrayOf(STRING) } }, - // Result is promise; the libCall case - // validates the program-dependent record shape in validate.ts. - /** `fs.promises.readdir(path, { withFileTypes: true })` — the settled- - * promise twin of fs.readdirTypesSync. The backends assemble the same - * call-site-shaped Dirent rows, then move the array into a promise; - * scandir failure becomes its rejection. */ - "fsp.readdirTypes": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, - "fsp.rm": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, - /** `fs.promises.rm(path, options)`: fs.rmOptsSync and fs.rmRetrySync - * behind a settled promise; their errors become its rejection. */ - "fsp.rmOpts": { argTypes: [STRING, BOOL, BOOL], result: { kind: "promise", inner: VOID } }, - "fsp.rmRetry": { - argTypes: [STRING, BOOL, BOOL, F64, F64], - result: { kind: "promise", inner: VOID }, - }, - "fsp.stat": { argTypes: [STRING], result: { kind: "promise", inner: STATS_T } }, - "fsp.realpath": { argTypes: [STRING], result: { kind: "promise", inner: STRING } }, - "fsp.lstat": { argTypes: [STRING], result: { kind: "promise", inner: STATS_T } }, - /** fs/promises.open and the statically represented FileHandle surface. - * Every operation returns an already-settled promise; syscall failures - * become rejections rather than escaping synchronously. read/write - * results are call-site record shapes ({ bytesRead/bytesWritten, - * buffer }) assembled by the backends around the fixed runtime ABI. */ - "fsp.open": { argTypes: [STRING, STRING, F64], result: { kind: "promise", inner: FILEHANDLE_T } }, - "fileHandle.fd": { argTypes: [FILEHANDLE_T], result: F64 }, - "fileHandle.close": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: VOID } }, - "fileHandle.sync": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: VOID } }, - "fileHandle.datasync": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: VOID } }, - "fileHandle.truncate": { - argTypes: [FILEHANDLE_T, F64], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.chmod": { argTypes: [FILEHANDLE_T, F64], result: { kind: "promise", inner: VOID } }, - "fileHandle.utimes": { - argTypes: [FILEHANDLE_T, DYN, DYN], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.readv": { - argTypes: [FILEHANDLE_T, arrayOf(BYTES_U8), F64], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.writev": { - argTypes: [FILEHANDLE_T, arrayOf(BYTES_U8), F64], - result: { kind: "promise", inner: VOID }, - }, - // read/write carry call-site result record shapes; the validator checks - // those in validate.ts, so promise is only a table sentinel. - "fileHandle.read": { - argTypes: [FILEHANDLE_T, BYTES_U8, F64, F64, F64, BOOL], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.writeBytes": { - argTypes: [FILEHANDLE_T, BYTES_U8, F64, F64, F64, BOOL], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.writeStr": { - argTypes: [FILEHANDLE_T, STRING, F64, STRING], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.readFile": { - argTypes: [FILEHANDLE_T, STRING], - result: { kind: "promise", inner: STRING }, - }, - "fileHandle.readFileBytes": { - argTypes: [FILEHANDLE_T, STRING], - result: { kind: "promise", inner: BYTES_U8 }, - }, - "fileHandle.writeFile": { - argTypes: [FILEHANDLE_T, STRING, STRING], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.writeFileBytes": { - argTypes: [FILEHANDLE_T, BYTES_U8, STRING], - result: { kind: "promise", inner: VOID }, - }, - "fileHandle.stat": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: STATS_T } }, - "process.argv": { argTypes: [], result: arrayOf(STRING) }, - // The Node runtime flag a node:test binary took out of process.argv - // (--test-update-snapshots), or [] — a fresh array per read. - "process.execArgv": { argTypes: [], result: arrayOf(STRING) }, - "process.platform": { argTypes: [], result: STRING }, - // The one libCall whose result type is program-dependent (union ids are - // per-module): the `result` here is a placeholder — the libCall case - // checks the union's ARMS ([string, undefinedT] in canonical order) - // against the module's registry instead. - /** getenv(3): one string key arg → the interned `string | undefined` - * union (present: +1 string wrapped into the string arm; absent: the - * interned undefined-arm instance). BOTH source forms — `process.env.FOO` - * and `process.env[expr]` — lower here. Purely static; never throws. */ - "process.envGet": { argTypes: [STRING], result: VOID }, - /** setenv(3): (name, value) string args → void. Later envGet reads and - * spawned children observe the write, like Node (values are strings — - * the frontend fences non-string RHS). Never throws. */ - "process.envSet": { argTypes: [STRING, STRING], result: VOID }, - /** `delete process.env[NAME]` — unsetenv(3): the mutation is visible to - * every later read (process.envGet asks getenv fresh) and inherited by - * spawned children, exactly Node. Statement position only (JS's boolean - * result is constant true there). Borrows the name; never throws. */ - "process.envUnset": { argTypes: [STRING], result: VOID }, - /** The whole environment as alternating [k0, v0, k1, v1, ...] strings in - * environ order — the raw material of the process.env SNAPSHOT record - * (the frontend's interned %env.snapshot helper keyed-writes the pairs - * into a fresh `{ [k: string]: string | undefined }` record). Fresh +1 - * array; never throws. */ - "process.envPairs": { argTypes: [], result: arrayOf(STRING) }, - "process.exit": { argTypes: [F64], result: VOID }, - /** Numeric process.exitCode write (integer validation, implicit exit status). */ - "process.setExitCode": { argTypes: [F64], result: VOID }, - /** The process.exitCode write of a program that also reads the - * property: the same validation, and the code counts as assigned. */ - "process.setExitCodeTracked": { argTypes: [F64], result: VOID }, - /** The code for process.exit() with no argument, or zero when unset. */ - "process.currentExitCode": { argTypes: [], result: F64 }, - /** Whether process.exitCode reads a code rather than undefined. Never throws. */ - "process.hasExitCode": { argTypes: [], result: BOOL }, - "process.cwd": { argTypes: [], result: STRING }, - /** getpid(2) / getuid(2): zero args → f64. POSIX-only target, so both - * always answer (the checker's `getuid?` optionality covers Windows — - * `process.getuid?.()` lowers as the plain call). Never throw. */ - "process.pid": { argTypes: [], result: F64 }, - "dyn.this": { argTypes: [], result: DYN }, - "dyn.generatorThis": { argTypes: [], result: DYN }, - "process.getuid": { argTypes: [], result: F64 }, - "process.getgid": { argTypes: [], result: F64 }, - /** geteuid(2) / getegid(2) / getgroups(2) (the effective gid appended - * when the list lacks it, like Node). Windows has no such members: the - * calls throw the TypeError a missing member does (MAY THROW). */ - "process.geteuid": { argTypes: [], result: F64 }, - "process.getegid": { argTypes: [], result: F64 }, - "process.getgroups": { argTypes: [], result: arrayOf(F64) }, - /** process.ppid: the parent's pid at the read. Never throws. */ - "process.ppid": { argTypes: [], result: F64 }, - /** process.title: libuv's process title (argv[0] until set); setTitle - * writes it (truncated to the original argument area on POSIX, the - * console title on Windows). Never throw. */ - "process.title": { argTypes: [], result: STRING }, - "process.setTitle": { argTypes: [STRING], result: VOID }, - /** process.argv0: the original argv[0]. Never throws. */ - "process.argv0": { argTypes: [], result: STRING }, - /** process.release: the compatibility target's stable release object - * (name, lts, sourceUrl, headersUrl, and libUrl on Windows). +1. */ - "process.release": { argTypes: [], result: DYN }, - /** process.execPath: the compiled binary's own resolved absolute path - * (one interned string, +1 per read) — the honest answer where Node's - * is the node executable's (SEMANTICS.md divergence 12). Never throws. */ - "process.execPath": { argTypes: [], result: STRING }, - /** process.arch: the compiled binary's OWN architecture ("arm64", - * "x64") — Node's answer for its own build on the same machine. - * Interned; +1 per read. Never throws. */ - "process.arch": { argTypes: [], result: STRING }, - /** Stable native version dictionary. node/openssl identify compatibility - * targets; components absent from the native runtime are omitted. */ - "process.versions": { argTypes: [], result: DYN }, - "process.builtinId": { argTypes: [DYN, arrayOf(STRING)], result: STRING }, - "process.builtinModule": { argTypes: [STRING, DYN], result: DYN }, - "process.builtinUnsupported": { argTypes: [STRING, STRING], result: DYN }, - /** A module namespace object as a checked-dynamic value - * (scr_module_namespace_value): the module id, its sorted export names - * and the compiled getter. One object per module. Never throws. */ - "module.namespaceValue": { argTypes: [STRING, arrayOf(STRING), DYN], result: DYN }, - "fs.callbackValue": { argTypes: [STRING], result: DYN }, - "fs.callbackCall": { argTypes: [STRING, DYN], result: DYN }, - "process.hrtimeValue": { argTypes: [], result: DYN }, - /** process.versions.node: the runtime's Node COMPATIBILITY TARGET — - * there is no Node under the binary, so this reports the version whose - * semantics the runtime implements (SEMANTICS.md divergence 60, the - * execPath stance). Interned; +1 per read. Never throws. */ - "process.versionsNode": { argTypes: [], result: STRING }, - "process.versionsOpenssl": { argTypes: [], result: STRING }, - /** kill(2) with Node's exact semantics and error shapes: the pid must be - * an int32 (else the ERR_INVALID_ARG_TYPE TypeError text), the named - * form resolves Node's signal-name table (unknown names throw the - * ERR_UNKNOWN_SIGNAL TypeError), an omitted signal completes to - * "SIGTERM" in the frontend, signal 0 probes, and a kill(2) failure - * throws Node's `kill ESRCH`/`kill EPERM` Error. Result is Node's - * constant true. */ - "process.kill": { argTypes: [F64, STRING], result: BOOL }, - "process.killNum": { argTypes: [F64, F64], result: BOOL }, - /** The raw byte writes: one borrowed string arg → bool (constantly true - * — this synchronous runtime never queues backpressure). stdoutWrite - * shares console.log's stream, each call is submitted promptly, and - * interleaved output keeps source order. No newline, no formatting, - * never throws. */ - "process.stdoutWrite": { argTypes: [STRING], result: BOOL }, - "process.stdio": { argTypes: [F64], result: DYN }, - "process.stderrWrite": { argTypes: [STRING], result: BOOL }, - // error.new's result and the receiver slots are builtin-error classes — - // program-dependent object types, checked in the libCall case. - /** The runtime-provided Error hierarchy's entry points (scr_error.c). - * error.new: one borrowed string arg (the message), result an owned (+1) - * builtin error instance — the result TYPE names which builtin class - * (backends derive the runtime kind from it). error.ctor: the - * super(message) call of an `extends Error` constructor — borrowed - * receiver (already allocated by the derived class's new) + borrowed - * message, void; the receiver's type names the builtin class whose name - * field to stamp. error.toString: borrowed `%Error`-typed receiver, +1 - * string in Node's "name: message" shape. Formatting can invoke a user - * message accessor, which can throw. */ - "error.new": { argTypes: [STRING], result: VOID }, - /** ECMAScript constructors with raw checked-dynamic message/options. - * Options retain cause presence; constructor calls borrow their args. - * Message coercion may throw before the cause is installed. */ - "error.newOptions": { argTypes: [DYN, DYN], result: VOID }, - "error.ctorOptions": { argTypes: [null, DYN, DYN], result: VOID }, - /** Borrowed Error receiver. cause returns an owned dyn value (undefined - * when absent); hasCause distinguishes absence from present undefined. */ - "error.cause": { argTypes: [null], result: DYN }, - "error.hasCause": { argTypes: [null], result: BOOL }, - /** Assignment borrows both operands and retains the new cause. */ - "error.setCause": { argTypes: [null, DYN], result: VOID }, - /** `err.code = s` on an error-hierarchy receiver: the code slot that - * error.code reads and coded throws stamp. Borrows both; never throws. */ - "error.setCode": { argTypes: [null, STRING], result: VOID }, - /** `err.code = v` with a value that is not statically a string: strings - * take the code slot, anything else clears it and sets the dynamic own - * property. Borrows both; never throws. */ - "error.setCodeDyn": { argTypes: [null, DYN], result: VOID }, - /** `Object.assign(err, ...sources)` onto an error-hierarchy receiver - * (args: the error, a dyn array of the sources): the own-member copy - * onto its checked-dynamic node, then the typed slots (name, message, - * stack, cause, code) re-read from it. Throws what the copy throws. */ - "error.assign": { argTypes: [null, DYN], result: VOID }, - /** `key in err` for a key no compiled class of the receiver declares - * (args: the error, the key): a runtime-set own property (Object.assign, - * throw-site fields, dynamic writes) or an Object.prototype member. */ - "error.hasKey": { argTypes: [null, STRING], result: BOOL }, - "error.hasCode": { argTypes: [null], result: BOOL }, - /** Whether the error's own `name` or `message` (the key) is an accessor - * a reflective definition installed. Borrows both; never throws. */ - "error.ownAccessor": { argTypes: [null, STRING], result: BOOL }, - /** That accessor's getter called on the error (owned result), and its - * setter called with the value (V8's TypeError without one). Borrow - * their arguments; throw what the accessor throws. */ - "error.accessorGet": { argTypes: [null, STRING], result: DYN }, - "error.accessorSet": { argTypes: [null, STRING, DYN], result: VOID }, - "error.defineCause": { argTypes: [null, DYN], result: VOID }, - "error.deleteCause": { argTypes: [null], result: VOID }, - /** The compiler-resolved Node-parity throw for always-throwing lowered - * arms (ERR_INVALID_THIS receivers, ERR_MISSING_ARGS arity ladders, - * the symbol-to-string TypeError): args are [error-kind f64 (the - * SCR_ERR_* index: 0 Error, 1 TypeError, 2 RangeError), code (empty = - * no code slot), message]. ALWAYS THROWS catchably; the result type is - * the replaced expression's own (never materialized — the - * global.undefRead pattern). May-throw seed. */ - "error.nodeThrow": { argTypes: [F64, STRING, STRING], result: VOID }, - /** JS ToString over a dyn value WITH the object protocol (a user - * toString/valueOf member is CALLED and its throw propagates; - * exhaustion throws "Cannot convert object to primitive value"; units - * render "null"/"undefined") — the WHATWG USVString conversions - * (URLSearchParams names/values). Borrowed dyn; +1 string. May-throw. */ - "dyn.toStringCoerce": { argTypes: [DYN], result: STRING }, - "dyn.stringConstructor": { argTypes: [DYN], result: STRING }, - "dyn.propertyKey": { argTypes: [DYN], result: DYN }, - "dyn.toNumeric": { argTypes: [DYN], result: DYN }, - "dyn.increment": { argTypes: [DYN, BOOL], result: DYN }, - /** JS ToNumber over a dyn value WITH the object protocol (number-hint - * valueOf/toString ordering; user throws propagate). Borrowed dyn; - * f64 result, or a throw. Used by statically lowered numeric coercions - * whose checker type remained any. */ - "dyn.numberConstructor": { argTypes: [DYN], result: F64 }, - "dyn.bigintConstructor": { argTypes: [DYN], result: BIGINT_T }, - "dyn.toNumberCoerce": { argTypes: [DYN], result: F64 }, - "dyn.add": { argTypes: [DYN, DYN], result: DYN }, - "dyn.arithmetic": { argTypes: [DYN, DYN, STRING], result: DYN }, - "dyn.compare": { argTypes: [DYN, DYN, STRING], result: BOOL }, - "dyn.bitwise": { argTypes: [DYN, DYN, STRING], result: DYN }, - "dyn.proxyNew": { argTypes: [DYN, DYN], result: DYN }, - /* Dynamic-tier values (jsval; two-tier design stage S3). The jsval pass - * (ir/jsval.ts) introduces these in dynamic-tier functions only; the - * runtime is scr_js_value.c. Arguments are borrowed and results owned, - * like every libCall. Boundary conversions: js.fromDyn (a cell keeps its - * identity), js.toDyn (an immediate is boxed), js.fromF64/fromBool/ - * fromStr/fromBigInt (the design's toJs: immediates, and strings and - * BigInts under a tag without a copy) and js.toF64 (fromJs to number: dynCheck's - * number check and TypeError). The operators are the dyn.* operators - * over jsvals, with identical JS semantics. */ - "js.fromDyn": { argTypes: [DYN], result: JSVAL }, - "js.toDyn": { argTypes: [JSVAL], result: DYN }, - "js.fromF64": { argTypes: [F64], result: JSVAL }, - "js.fromBool": { argTypes: [BOOL], result: JSVAL }, - "js.fromStr": { argTypes: [STRING], result: JSVAL }, - "js.fromBigInt": { argTypes: [BIGINT_T], result: JSVAL }, - "js.toF64": { argTypes: [JSVAL], result: F64 }, - "js.add": { argTypes: [JSVAL, JSVAL], result: JSVAL }, - "js.arithmetic": { argTypes: [JSVAL, JSVAL, STRING], result: JSVAL }, - "js.bitwise": { argTypes: [JSVAL, JSVAL, STRING], result: JSVAL }, - "js.compare": { argTypes: [JSVAL, JSVAL, STRING], result: BOOL }, - "js.toNumeric": { argTypes: [JSVAL], result: JSVAL }, - "js.increment": { argTypes: [JSVAL, BOOL], result: JSVAL }, - "js.toNumberCoerce": { argTypes: [JSVAL], result: F64 }, - // Always throws; the result is the READ's declared type (a typed dummy - // the unwind abandons) — the libCall case skips the result check. - /** A read of a `declare`d const NOTHING defines (the bundler-define - * pattern — __VERSION__): always throws the catchable ReferenceError - * Node raises at the access (" is not defined"). args[0] is the - * name; the result type is the read's declared type (a typed dummy the - * unwind abandons — the value never exists). */ - "global.undefRead": { argTypes: [STRING], result: VOID }, - "console.native": { argTypes: [], result: DYN }, - /** node:console Console instances (scr_console_native.c): new mints the - * handle over the stdout/stderr fds (1/2; 0 = a stream binds next) and - * the ignoreErrors flag; bind attaches a borrowed writable stream to - * target 0 (stdout) or 1 (stderr); write prints `text\n` to the - * selected target of an instance or the global console handle. */ - "console.new": { argTypes: [F64, F64, BOOL], result: DYN }, - "console.bind": { argTypes: [DYN, F64, null], result: VOID }, - "console.write": { argTypes: [DYN, STRING, BOOL], result: VOID }, - /** The global console's remaining members (scr_console_native.c; the - * group indentation itself lives with the writers in scr_console.c). - * Arguments are borrowed; string results are owned. noop is the - * emitter-dropped tail of a lowering whose effects sit in statements. - * label is `${label}` over a defaulted label (undefined -> "default"; - * a Symbol throws Node's TypeError). count prints "label: n". - * countReset answers the warning text for an unknown (or non-string) - * label, "" otherwise. time answers the duplicate-label warning text - * or ""; timeEnd/timeLog answer Node's formatted elapsed time, or "" - * for an unknown label (timeEnd drops the timer). group adds (+1) or - * removes (-1) one two-space indentation step. clear writes the - * cursor-home and erase-down sequences when stdout is a terminal. - * table renders and prints Node's table for a dyn value (undefined - * properties = all). dirDyn is console.dir's inspect of a dyn value - * (custom inspection off: a Buffer anywhere inside throws the fence). */ - "console.noop": { argTypes: [], result: VOID }, - "console.label": { argTypes: [DYN], result: STRING }, - "console.count": { argTypes: [STRING], result: VOID }, - "console.countReset": { argTypes: [DYN], result: STRING }, - "console.time": { argTypes: [STRING], result: STRING }, - "console.timeEnd": { argTypes: [STRING], result: STRING }, - "console.timeLog": { argTypes: [STRING], result: STRING }, - "console.group": { argTypes: [F64], result: VOID }, - "console.clear": { argTypes: [], result: VOID }, - "console.table": { argTypes: [DYN, DYN], result: VOID }, - "console.dirDyn": { argTypes: [DYN, F64], result: STRING }, - /** A native reference to the global object; known names retain value fences. */ - "global.native": { argTypes: [arrayOf(STRING)], result: DYN }, - // `X.name` through a class value: the arg is a program-dependent - // classval (a null slot; the libCall case checks the kind). - /** `X.name` through a class VALUE (scr_object.c): args[0] is a borrowed - * classval; the result is the class object's stored .name string, - * retained (+1 — the string is an interned immortal, so the retain is a - * no-op, kept for ownership uniformity). Never throws. A direct - * `C.name` on the class name itself folds to a strLit instead. */ - "class.name": { argTypes: [null], result: STRING }, - "error.ctor": { argTypes: [null, STRING], result: VOID }, - "error.toString": { argTypes: [null], result: STRING }, - /** SameValue over checked values, including NaN, signed zero and reference identity. */ - "dyn.sameValue": { argTypes: [DYN, DYN], result: BOOL }, - "error.stack": { argTypes: [null], result: STRING }, - "error.setStack": { argTypes: [null, STRING], result: VOID }, - "error.stackLimitGet": { argTypes: [], result: F64 }, - "error.stackLimitSet": { argTypes: [F64], result: VOID }, - // Receiver (any error-hierarchy object) and the program-dependent - // `string | undefined` result are checked in the libCall case. - /** NodeJS.ErrnoException's `.code` read: borrowed error-hierarchy - * receiver → the interned `string | undefined` union (type-directed - * construction in the backend, the process.envGet pattern) — the errno - * name where a throw site stamped one (fs, exec spawn/timeout, - * process.kill, the spawn 'error' event), the undefined arm everywhere - * else. Never throws. */ - "error.code": { argTypes: [null], result: VOID }, - // DOMException: construction takes the two dyn args (WebIDL's - // resolution runs in the runtime); the %DOMException result is a - // program-dependent object type, checked in the libCall case. The - // read surface takes the %DOMException receiver (a null slot — the - // libCall case checks the class name). - /** `new DOMException(message?, nameOrOptions?)` (scr_error.c): both args - * are borrowed dyn values (the lowering passes the dyn undefined for an - * absent argument, so WebIDL's optionality lives in one place). The - * runtime ToStrings the message ("" for absent/undefined), resolves the - * name — absent/undefined → "Error", a non-null object → ToString of its - * `name` member plus the `cause` own-property record, anything else → - * ToString — and stamps the legacy code from the WebIDL name table (0 - * off-table). Result is an owned (+1) %DOMException. Never throws (dyn - * ToString is total). */ - "error.newDom": { argTypes: [DYN, DYN], result: VOID }, - /** DOMException's own read surface (scr_error.c; %DOMException receivers - * only — borrowed). domCode: the legacy numeric code. domHasCause: the - * options form's own-property record (`'cause' in e`). domCause: the - * cause value, +1 (the dyn undefined when absent — matching Node's - * undefined read). None throws. */ - "error.domCode": { argTypes: [null], result: F64 }, - "error.domHasCause": { argTypes: [null], result: BOOL }, - "error.domCause": { argTypes: [null], result: DYN }, - /** structuredClone of a %DOMException receiver (scr_error.c): WebIDL - * serialization — name/message copy, the legacy code re-derives, cause - * does not serialize. args are the borrowed receiver and the borrowed - * options dyn value (the dyn undefined when absent — the shared - * validation throws Node's exact option errors; any non-empty transfer - * list throws DataCloneError, nothing static is transferable). Result - * +1 %DOMException. */ - "error.domClone": { argTypes: [null, DYN], result: VOID }, - /** `new AggregateError(errors, message?, options?)` (scr_json.c), in - * spec order: newAggregate installs the message and cause (both dyn, - * borrowed — the message conversion can throw) and returns an owned - * %AggregateError; aggregateSetErrors then stores the iterated list - * (borrowed receiver, the dyn array borrowed and retained). - * aggregateErrors reads the list (+1, the same array every read). */ - "error.newAggregate": { - argTypes: [DYN, DYN], - result: { kind: "object", className: "%AggregateError" }, - }, - "error.aggregateSetErrors": { argTypes: [null, DYN], result: VOID }, - "error.aggregateErrors": { argTypes: [null], result: DYN }, - /** `new SuppressedError(error, suppressed, message?)` (scr_json.c): all - * three dyn and borrowed; the message conversion can throw. Returns an - * owned %SuppressedError. */ - "error.newSuppressed": { - argTypes: [DYN, DYN, DYN], - result: { kind: "object", className: "%SuppressedError" }, - }, - /** `d instanceof TypeError` (and the other BUILTIN error classes) on a - * checked-dynamic value (scr_json.c): the from_error cache holds the - * dyn↔error identity edge, so the test resolves the runtime error and - * asks its vtable's stamped preorder interval — exact for every error - * that crossed the boundary. A dyn object that never came from an - * error (a hand-built {%error} literal) answers false: subclass - * identity is unknowable there (the root keeps dynTest's marker - * answer). args are the borrowed dyn and the SCR_ERR_* kind literal. - * Never throws. */ - "dyn.errInstanceof": { argTypes: [DYN, F64], result: BOOL }, - "weakMap.is": { argTypes: [DYN], result: BOOL }, - "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, 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 - * pushed (Node's listener receiver, a dyn OBJ method's object, an - * apply/call thisArg), or the undefined dyn singleton with none bound - * (the strict-mode plain-call answer, the old constant). Zero args → - * dyn (+1). Never throws. */ - "dyn.dataViewIs": { argTypes: [DYN], result: BOOL }, - "dyn.construct": { argTypes: [DYN, DYN, STRING], result: DYN }, - "arrayBuffer.constructor": { argTypes: [], result: DYN }, - "sharedArrayBuffer.new": { argTypes: [DYN], result: DYN }, - "sharedArrayBuffer.is": { argTypes: [DYN], result: BOOL }, - "arrayBuffer.new": { argTypes: [DYN], result: DYN }, - "ffi.argument": { argTypes: [DYN, STRING], result: DYN }, - "ffi.memoryModule": { argTypes: [DYN], result: DYN }, - "intl.segmenterNew": { argTypes: [], result: DYN }, - "intl.segmenterRefusing": { argTypes: [STRING], result: DYN }, - "arrayBuffer.is": { argTypes: [DYN], result: BOOL }, - "arrayBuffer.isView": { argTypes: [DYN], result: BOOL }, - "arrayBuffer.byteLengthGetter": { argTypes: [], result: F64 }, - "arrayBuffer.byteLengthDescriptor": { argTypes: [DYN], result: DYN }, - "arrayBuffer.viewU8C": { argTypes: [DYN, DYN, DYN], result: bytesOf("u8c") }, - "arrayBuffer.viewI8": { argTypes: [DYN, DYN, DYN], result: bytesOf("i8") }, - "arrayBuffer.viewU16": { argTypes: [DYN, DYN, DYN], result: bytesOf("u16") }, - "arrayBuffer.viewI16": { argTypes: [DYN, DYN, DYN], result: bytesOf("i16") }, - "arrayBuffer.viewU8": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, - "arrayBuffer.viewU32": { argTypes: [DYN, DYN, DYN], result: bytesOf("u32") }, - "arrayBuffer.viewI32": { argTypes: [DYN, DYN, DYN], result: bytesOf("i32") }, - "arrayBuffer.viewF16": { argTypes: [DYN, DYN, DYN], result: bytesOf("f16") }, - "arrayBuffer.viewF32": { argTypes: [DYN, DYN, DYN], result: bytesOf("f32") }, - "arrayBuffer.viewF64": { argTypes: [DYN, DYN, DYN], result: bytesOf("f64") }, - "arrayBuffer.viewDV": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, - /** Object.keys/values/entries over a CHECKED-DYNAMIC receiver - * (scr_json.c): the runtime walks the dyn node's own members in JS - * own-key order (array-index keys ascending first, then insertion - * order) and answers a dyn array (entries: an array of [key, value] - * pairs; values RETAIN the member nodes — reference semantics, like - * JS). Strings/arrays/bytes answer their index keys; other scalars an - * empty array; null/undefined throw Node's catchable TypeError - * ("Cannot convert undefined or null to object"). */ - "dyn.objKeys": { argTypes: [DYN], result: DYN }, - "dyn.forInKeys": { argTypes: [DYN], result: DYN }, - "dyn.forInKeyStrings": { argTypes: [DYN], result: arrayOf(STRING) }, - "dyn.forInStable": { argTypes: [DYN], result: BOOL }, - "dyn.objKeyStrings": { argTypes: [DYN], result: arrayOf(STRING) }, - /** Object.entries as a native [string, any][] (VOID: the tuple row's - * shape is program-dependent; the validator checks it). */ - "dyn.objEntryPairs": { argTypes: [DYN], result: VOID }, - "dyn.hasOwn": { argTypes: [DYN, STRING], result: BOOL }, - "dyn.propertyIsEnumerable": { argTypes: [DYN, STRING], result: BOOL }, - "dyn.assign": { argTypes: [DYN, DYN], result: DYN }, - "dyn.copyDataProperties": { argTypes: [DYN, DYN], result: DYN }, - "dyn.objectRest": { argTypes: [DYN, DYN], result: DYN }, - /** Variadic Object.assign over CHECKED-DYNAMIC targets (`Object.assign( - * {}, ...arr.map(f), tail)` — the option-table merge): the lowering - * builds one fresh dyn pack of sources (packPush retains a plain source - * in; packPushSpread flattens a spread source through the spread-call - * walk — V8's exact TypeError texts, the string arg spelling the spread - * expression for the nullish form), so every source evaluates and - * flattens BEFORE any copying (JS's ArgumentListEvaluation), then - * assignAll copies each pack element's own enumerable keys onto the - * target left to right and answers the TARGET (+1) — identity, like JS. - * assignAll throws Node's ToObject TypeError on a nullish target. */ - "dyn.packPush": { argTypes: [DYN, DYN], result: VOID }, - "dyn.packPushSpread": { argTypes: [DYN, DYN, STRING], result: VOID }, - /** The iterated-path spread twin: V8 spells the optimized apply-path - * texts (packPushSpread's — the expression named for nullish sources) - * only for the SINGLE LAST argument's spread; any other spread position - * drives the real iterator protocol, whose failure describes the VALUE - * ("object null is not iterable (cannot read property - * Symbol(Symbol.iterator))"). The frontend picks by position. */ - "dyn.packPushSpreadIter": { argTypes: [DYN, DYN], result: VOID }, - "dyn.reflectApply": { argTypes: [DYN, DYN, DYN], result: DYN }, - "dyn.reflectGet": { argTypes: [DYN, DYN, DYN], result: DYN }, - /** `using` / `await using` with a dyn value (scr_json.c): usingMethod - * runs the declaration's checks and answers what usingDispose calls at - * scope exit (undefined for a null or undefined value). The bool selects - * `await using`. Both throw Node's TypeErrors or the method's error. */ - "dyn.usingMethod": { argTypes: [DYN, BOOL], result: DYN }, - "dyn.usingDispose": { argTypes: [DYN, DYN], result: DYN }, - /** DisposableStack (scr_symbol.c): a new stack handle; whether a value is - * one; and `stack.use(value)` for a class instance, registering the - * compiler's disposal function for it. The last throws Node's - * ReferenceError on a disposed stack. */ - "disposable.stackNew": { argTypes: [], result: DYN }, - "disposable.stackIs": { argTypes: [DYN], result: BOOL }, - "disposable.stackUseCallback": { argTypes: [DYN, DYN, DYN], result: BOOL }, - "dyn.reflectSet": { argTypes: [DYN, DYN, DYN, DYN], result: BOOL }, - "dyn.reflectDefine": { argTypes: [DYN, DYN, DYN], result: BOOL }, - "dyn.abstractEq": { argTypes: [DYN, DYN], result: BOOL }, - "dyn.assignAll": { argTypes: [DYN, DYN], result: DYN }, + "math.trunc": { argTypes: [F64], result: F64 }, + "math.ceil": { argTypes: [F64], result: F64 }, + "math.sin": { argTypes: [F64], result: F64 }, + "math.sinh": { argTypes: [F64], result: F64 }, + "math.cos": { argTypes: [F64], result: F64 }, + "math.cosh": { argTypes: [F64], result: F64 }, + "math.tan": { argTypes: [F64], result: F64 }, + "math.tanh": { argTypes: [F64], result: F64 }, + "math.asin": { argTypes: [F64], result: F64 }, + "math.asinh": { argTypes: [F64], result: F64 }, + "math.acos": { argTypes: [F64], result: F64 }, + "math.acosh": { argTypes: [F64], result: F64 }, + "math.atan": { argTypes: [F64], result: F64 }, + "math.atanh": { argTypes: [F64], result: F64 }, + "math.cbrt": { argTypes: [F64], result: F64 }, + "math.clz32": { argTypes: [F64], result: F64 }, + /** Return -1, +1, or the original NaN/zero. In particular, -0 remains -0. */ + "math.sign": { argTypes: [F64], result: F64 }, + "math.exp": { argTypes: [F64], result: F64 }, + "math.expm1": { argTypes: [F64], result: F64 }, + "math.f16round": { argTypes: [F64], result: F64 }, + "math.fround": { argTypes: [F64], result: F64 }, + "math.sqrt": { argTypes: [F64], result: F64 }, + "math.log": { argTypes: [F64], result: F64 }, + "math.log1p": { argTypes: [F64], result: F64 }, + "math.log2": { argTypes: [F64], result: F64 }, + "math.log10": { argTypes: [F64], result: F64 }, + "math.atan2": { argTypes: [F64, F64], result: F64 }, + "math.pow": { argTypes: [F64, F64], result: F64 }, + "math.imul": { argTypes: [F64, F64], result: F64 }, + /** `Math.min(a, b)` / `Math.max(a, b)` — the two-argument scalar forms + * (scr_lib.c), JS-exact like the Arr folds: NaN poisons, max prefers +0 + * over -0 (min the reverse). C fmin/fmax are NOT these (they drop NaN). + * Never throw. */ + "math.min": { argTypes: [F64, F64], result: F64 }, + "math.max": { argTypes: [F64, F64], result: F64 }, + /** `Math.random()` — a uniform double in [0,1) with the spec's 53-bit + * granularity, drawn from arc4random_buf (the CSPRNG behind the crypto + * lowerings). Same distribution as Node, NECESSARILY different sequence + * (SEMANTICS.md 62 — no seeded sequence exists to match). Never throws. */ + "math.random": { argTypes: [], result: F64 }, + /** The static global parsers/tests (scr_string.c). num.parseInt is + * ECMA-262 19.2.5 exactly — JS whitespace, sign, ToInt32 radix (the + * frontend completes an omitted radix to 0 = the spec's "undefined": + * base 10 with the 0x hex escape), longest digit prefix, and the exact + * mathematical value correctly rounded (u64 fast path, bignum beyond — + * overflow is ±Infinity). num.isNaN is the NaN self-test on an + * already-number argument (tsc pins the argument to number, so no + * ToNumber coercion exists to model). Borrow; never throw. */ + "num.parseInt": { argTypes: [STRING, F64], result: F64 }, + /** ES parseFloat (scr_string.c): the longest StrDecimalLiteral prefix + * of the trimmed input (no hex, "Infinity" exact-case), NaN when none — + * ECMA-262 19.2.4 over a string argument (non-string arguments keep the + * fence: Node would ToNumber-coerce). Borrows; never throws. */ + "num.parseFloat": { argTypes: [STRING], result: F64 }, + /** `a.localeCompare(b)`: -1/0/1 in Node's root-locale collation for + * Latin-script text (scr_str_locale_compare). Never throws. */ + "str.localeCompare": { argTypes: [STRING, STRING], result: F64 }, + /** Annex B substr(start, length) with start ToNumber'd and length's + * presence; never throws. */ + "str.substr": { argTypes: [STRING, F64, F64, BOOL], result: STRING }, + /** toLocaleLowerCase/toLocaleUpperCase (scr_regex.c): the locales value + * as written, upper=true for toLocaleUpperCase. Throws for null and for + * locale forms with no native rules. */ + "str.localeCase": { argTypes: [STRING, DYN, BOOL], result: STRING }, + "str.replaceLiteral": { argTypes: [STRING, STRING, STRING, BOOL], result: STRING }, + // (subject, characters, NUL-separated replacement texts): the fused + // chain of global one-character replacements. + "str.replaceChars": { argTypes: [STRING, STRING, STRING], result: STRING }, + /* ToNumber(string) — ECMA-262 7.1.4.1 StringToNumber (scr_string.c): + * trim the JS StrWhiteSpace set, empty/whitespace-only → +0, then the + * whole span must be one StrNumericLiteral — signed decimal (Infinity + * included, strtod-over-validated-span correct rounding) or unsigned + * 0x/0o/0b (exact value, nearest-even; signed forms are NaN) — with + * any trailing garbage answering NaN. Number(aString), unary + on + * strings, and util.format %d over strings lower here. Borrows; never + * throws. */ + "num.fromString": { argTypes: [STRING], result: F64 }, + "num.isNaN": { argTypes: [F64], result: BOOL }, + /** The static URI component codecs (scr_string.c), ECMA-262 Encode/ + * Decode with the component sets over the runtime's UTF-8 strings. + * str.encodeUriComponent percent-encodes every byte outside the + * unreserved component set (ALPHA/DIGIT/- _ . ! ~ * ' ( )) as uppercase + * %XX — the spec's per-code-point UTF-8 encoding IS a byte scan here — + * and THROWS the spec's URIError ("URI malformed") on a lone surrogate. + * str.decodeUriComponent + * decodes %XX escapes bytewise (raw non-escape bytes copy through) and + * requires the escaped bytes to form strictly valid UTF-8 (overlong + * forms, surrogate code points, and >U+10FFFF refused, per UTF8-decode + * without replacement); bad hex or an invalid sequence THROWS the + * spec's URIError ("URI malformed"), catchable. Borrow; results +1. */ + "str.encodeUriComponent": { argTypes: [STRING], result: STRING }, + // The base64 globals: the argument is a dyn value (WebIDL ToString + // runs in the runtime); the zero-argument form always throws. + /** The WHATWG base64 globals (scr_string.c), Node-global since v16. + * Arguments are borrowed dyn values — WebIDL ToString runs in the + * runtime over the dyn kind (String(null) is "null": the html spec's + * coercion, which Node's atob(null) exercises). str.atob decodes + * forgiving-base64 (ASCII whitespace stripped, %4==0 strips up to two + * '=', %4==1 refuses, leftover bits discarded) into the latin1 code + * points as a string; a malformed input THROWS the catchable + * DOMException InvalidCharacterError ("The string to be decoded is not + * correctly encoded."). str.btoa encodes the string's code points as + * base64; any code point over U+00FF THROWS InvalidCharacterError + * ("Invalid character"). str.b64Missing is the zero-argument call of + * either: always throws Node's TypeError [ERR_MISSING_ARGS] "The + * \"input\" argument must be specified". Results +1. */ + "str.atob": { argTypes: [DYN], result: STRING }, + "str.btoa": { argTypes: [DYN], result: STRING }, + "str.b64Missing": { argTypes: [], result: STRING }, + "str.decodeUriComponent": { argTypes: [STRING], result: STRING }, + "str.decodeUri": { argTypes: [STRING], result: STRING }, + /** encodeURI: the same Encode() keeping the reserved set and '#' + * unescaped — throwing like the component encoder. Borrow; result +1. */ + "str.encodeUri": { argTypes: [STRING], result: STRING }, + /** RegExp.escape (ES2025): per-code-point EncodeForRegExpEscape — + * leading ASCII alphanumeric hex-escapes, syntax characters and '/' + * take a backslash, other punctuators/whitespace/line terminators + * hex-escape, the rest passes through. Total; borrows; result +1. */ + "regexp.escape": { argTypes: [STRING], result: STRING }, + /** Number.prototype formatters (scr_lib.c), JS-exact: + * num.toExponential is toExponential() with the spec's "as many digits + * as necessary"; num.toFixed0 is the non-throwing omitted-argument + * toFixed() fast path; num.toFixed implements an explicit fractionDigits + * with exact binary-value rounding and THROWS RangeError outside 0..100. + * Successful results +1. */ + "num.toExponential": { argTypes: [F64], result: STRING }, + "num.toFixed0": { argTypes: [F64], result: STRING }, + "num.toFixed": { argTypes: [F64, F64], result: STRING }, + /** toPrecision(precision) with the argument ToNumber'd and its presence + * (undefined answers ToString); toExponential with a DEFINED + * fractionDigits. Both throw V8's RangeError for out-of-range digits. */ + "num.toPrecision": { argTypes: [F64, F64, BOOL], result: STRING }, + "num.toExponentialDigits": { argTypes: [F64, F64], result: STRING }, + "num.toStringRadix": { argTypes: [F64, DYN], result: STRING }, + /** toString(radix) with a number radix (scr_lib.c): truncates it as + * ToIntegerOrInfinity does, THROWS RangeError outside 2..36. */ + "num.toStringRadixF64": { argTypes: [F64, F64], result: STRING }, + /** Object.is over two numbers — the spec's SameValue on doubles: NaN + * equals NaN, +0 differs from -0, everything else is `===`. Plain bool + * result; never throws. (Union-armed operands take unionEq's sameValue + * flag instead — this is the both-f64 fast path.) */ + "num.sameValue": { argTypes: [F64, F64], result: BOOL }, + /** `new Intl.NumberFormat("en-US").format(x)` and + * `x.toLocaleString("en-US")` with DEFAULT options — the one locale + * whose data the runtime embeds (Node's default-build locale): decimal + * notation, 0–3 fraction digits rounded half-up on the SHORTEST + * round-tripping decimal (ICU's rounding input, probed vs Node — + * format(1.0005) is "1.001" though toFixed(3) answers "1.000"), "," + * grouping every three integer digits, "∞"/"NaN" texts, and "-0" for + * negative inputs rounding to zero. Result +1; never throws. */ + "intl.numFormatEnUs": { argTypes: [F64], result: STRING }, + /** ES Symbol values (scr_symbol.c — link-gated by moduleUsesSymbol). + * sym.new: `Symbol(desc)` — a fresh runtime-unique identity (+1) whose + * one arg is the description string (borrowed); sym.newAnon is the + * description-less `Symbol()` form. sym.for: the Symbol.for global + * registry — one interned symbol per key (borrowed), +1 on every call. + * sym.toString: "Symbol(desc)" (+1 string; "Symbol()" when absent). + * sym.desc / sym.keyFor answer the interned `string | undefined` union + * (the runtime returns +1-or-NULL; the backend builds the union arms — + * the child.stdout pattern). None of these throw. */ + "sym.new": { argTypes: [STRING], result: SYMBOL_T }, + "sym.newAnon": { argTypes: [], result: SYMBOL_T }, + "sym.for": { argTypes: [STRING], result: SYMBOL_T }, + "sym.wellKnown": { argTypes: [STRING], result: SYMBOL_T }, + // Result is the interned `string | undefined` union — the libCall case + // checks the arms (the spawnRes.error pattern). + "sym.keyFor": { argTypes: [SYMBOL_T], result: VOID }, + "sym.desc": { argTypes: [SYMBOL_T], result: VOID }, + "sym.toString": { argTypes: [SYMBOL_T], result: STRING }, + /** Engine-free arbitrary-precision bigint operations (scr_bigint.c). + * parse accepts the ECMAScript BigInt string grammar; fromF64 accepts + * finite integral Numbers. Arithmetic returns fresh immutable values. + * cmp returns -1/0/1 as f64; toString accepts a numeric radix. */ + "bigint.parse": { argTypes: [STRING], result: BIGINT_T }, + "bigint.fromF64": { argTypes: [F64], result: BIGINT_T }, + "bigint.neg": { argTypes: [BIGINT_T], result: BIGINT_T }, + "bigint.not": { argTypes: [BIGINT_T], result: BIGINT_T }, + "bigint.add": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.sub": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.mul": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.div": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.mod": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.pow": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.and": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.or": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.xor": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.shl": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.shr": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T }, + "bigint.eq": { argTypes: [BIGINT_T, BIGINT_T], result: BOOL }, + "bigint.eqString": { argTypes: [BIGINT_T, STRING], result: BOOL }, + "bigint.cmp": { argTypes: [BIGINT_T, BIGINT_T], result: F64 }, + "bigint.cmpNumber": { argTypes: [BIGINT_T, F64], result: F64 }, + "bigint.truthy": { argTypes: [BIGINT_T], result: BOOL }, + "bigint.toString": { argTypes: [BIGINT_T, F64], result: STRING }, + "bigint.inspect": { argTypes: [BIGINT_T], result: STRING }, + "bigint.toF64": { argTypes: [BIGINT_T], result: F64 }, + "bigint.asUintN": { argTypes: [F64, BIGINT_T], result: BIGINT_T }, + "bigint.asIntN": { argTypes: [F64, BIGINT_T], result: BIGINT_T }, + "bigint.bufferRead": { argTypes: [BYTES_U8, F64, BOOL, BOOL], result: BIGINT_T }, + "bigint.bufferWrite": { argTypes: [BYTES_U8, BIGINT_T, F64, BOOL, BOOL], result: F64 }, + "bigint.dataViewGet": { argTypes: [BYTES_U8, F64, BOOL, BOOL], result: BIGINT_T }, + "bigint.dataViewSet": { argTypes: [BYTES_U8, F64, BIGINT_T, BOOL], result: VOID }, + /** node:url + the URL class (scr_url.c). url.new parses one absolute + * URL string into a mutable URL value (+1) — invalid input THROWS a + * catchable TypeError ("Invalid URL"), like Node's constructor. The + * getters (borrowed receiver, +1 string) never throw; url.href doubles + * as toString(). fileURLToPath has one libFn per receiver form (URL + * value / string) — both THROW Node's TypeErrors on non-file schemes, + * encoded slashes, and non-empty hosts. url.pathToFileURL resolves the + * path (getcwd) and never throws. */ + "url.new": { argTypes: [STRING], result: URL_T }, + "url.newBase": { argTypes: [STRING, STRING], result: URL_T }, + /** Factories consume already-converted input/base strings. canParse + * answers a boolean; parse returns a checked native URL or null (+1). + * Parser failures are suppressed; conversion occurs before these calls. */ + "url.canParse": { argTypes: [STRING], result: BOOL }, + "url.canParseBase": { argTypes: [STRING, STRING], result: BOOL }, + "url.parse": { argTypes: [STRING], result: DYN }, + "url.parseBase": { argTypes: [STRING, STRING], result: DYN }, + "url.protocol": { argTypes: [URL_T], result: STRING }, + "url.origin": { argTypes: [URL_T], result: STRING }, + "url.username": { argTypes: [URL_T], result: STRING }, + "url.password": { argTypes: [URL_T], result: STRING }, + "url.host": { argTypes: [URL_T], result: STRING }, + "url.hostname": { argTypes: [URL_T], result: STRING }, + "url.port": { argTypes: [URL_T], result: STRING }, + "url.pathname": { argTypes: [URL_T], result: STRING }, + "url.href": { argTypes: [URL_T], result: STRING }, + /** Selected component writes borrow the receiver and converted string. + * href replaces its parsed fields while retaining cached params identity. */ + "url.set": { argTypes: [URL_T, STRING, STRING], result: VOID }, + "url.setChecked": { argTypes: [URL_T, STRING, DYN], result: VOID }, + "url.fileURLToPathUrl": { argTypes: [URL_T], result: STRING }, + /** fileURLToPathUrl as a value: the path string, or the error it would + * throw (fs.existsSync and fs.promises URL paths). Never throws. */ + "url.fileURLToPathValue": { argTypes: [URL_T], result: DYN }, + /** getValidatedPath's null-byte check on a string path, and + * readFileSync's encoding check: what Node validates before it converts + * a later URL path. (path, argument name) and (encoding). */ + "fs.checkPath": { argTypes: [STRING, STRING], result: VOID }, + "fs.checkReadEncoding": { argTypes: [STRING], result: VOID }, + "url.fileURLToPathChecked": { argTypes: [DYN], result: STRING }, + "url.fileURLToPathOptions": { argTypes: [DYN, DYN], result: STRING }, + /** node:url's legacy API (scr_url_legacy.c): parse(url, parseQueryString, + * slashesDenoteHost) answers the checked-dynamic Url instance; format + * takes the urlObject (a Url-shaped object, a string, or a URL) and the + * URL form's options; resolve takes (from, to). The trailing bool says + * whether the call site emits DEP0169. domainToASCII/domainToUnicode + * take the domain and whether it was omitted. Node's validation errors + * throw catchably. */ + "url.legacyParse": { argTypes: [DYN, DYN, DYN, BOOL], result: DYN }, + "url.legacyFormat": { argTypes: [DYN, DYN, BOOL], result: STRING }, + "url.legacyResolve": { argTypes: [DYN, DYN, BOOL], result: STRING }, + "url.domainToASCII": { argTypes: [DYN, BOOL], result: STRING }, + "url.domainToUnicode": { argTypes: [DYN, BOOL], result: STRING }, + /** url.urlToHttpOptions over a checked-dynamic argument: a URL answers + * Node's null-prototype options object; a primitive or null throws + * ERR_INVALID_ARG_TYPE. */ + "url.toHttpOptions": { argTypes: [DYN], result: DYN }, + /** node:punycode (scr_url.c): punycode.js 2.1.0's string conversions; + * decode/toUnicode (and, past 32-bit deltas, encode/toASCII) throw the + * module's RangeErrors, ucs2Encode String.fromCodePoint's. warn marks the + * module's first load (scr_async_dyn.c) and reports DEP0040 then unless + * the second argument is false: at once for a static ES import, on the + * next tick for a require() call. */ + "punycode.decode": { argTypes: [STRING], result: STRING }, + "punycode.encode": { argTypes: [STRING], result: STRING }, + "punycode.toASCII": { argTypes: [STRING], result: STRING }, + "punycode.toUnicode": { argTypes: [STRING], result: STRING }, + "punycode.ucs2Decode": { argTypes: [STRING], result: arrayOf(F64) }, + "punycode.ucs2Encode": { argTypes: [arrayOf(F64)], result: STRING }, + "punycode.warn": { argTypes: [BOOL, BOOL], result: VOID }, + "url.fileURLToPathBuffer": { argTypes: [DYN, DYN], result: BYTES_U8 }, + "url.pathToFileURLChecked": { argTypes: [DYN, DYN], result: URL_T }, + "url.fileURLToPathStr": { argTypes: [STRING], result: STRING }, + "url.pathToFileURL": { argTypes: [STRING], result: URL_T }, + /** Explicit path syntax from the options.windows argument, independent + * of the executable's host platform. Windows UNC input can throw. */ + "url.pathToFileURLPlatform": { argTypes: [STRING, BOOL], result: URL_T }, + /** pathToFileURL under a win32 TARGET: the same scr_url_from_path call + * (the runtime selects the win32 arm by _WIN32), but a distinct IR name + * because that arm THROWS for malformed UNC inputs — may-throw seeds on + * it while posix pathToFileURL emission stays byte-identical. */ + "url.pathToFileURLWin32": { argTypes: [STRING], result: URL_T }, + /** URLSearchParams (scr_url.c — always linked with the url unit). + * Construction: sp.new (empty), sp.parse (one borrowed init string — + * a single leading '?' strips, Node's constructor), sp.copy (snapshot + * of another list), sp.fromPairs (a string[][] value — THROWS Node's + * ERR_INVALID_TUPLE TypeError on a non-[name, value] row; the one + * throwing entry in the family), sp.with (the record-literal init + * desugar: append one pair, answer the same list +1 — chains fold + * `new URLSearchParams({...})` into nested calls). url.searchParams + * answers the URL's LIVE cached view (+1, one identity per URL); + * url.search is the WHATWG search getter ("" for no/empty query). + * Methods mirror the WHATWG surface: sp.get answers +1-or-NULL (the + * sym.desc union pattern, null arm), sp.getAll a fresh string[]; + * sp.append/sp.set/sp.delete/sp.deleteValue/sp.sort mutate and + * re-serialize a live view's URL query; sp.has/sp.hasValue answer + * bools; sp.size/sp.toString are pure reads. sp.keyAt/sp.valAt are + * the for-of/forEach desugar's index reads (live — the loop re-reads + * sp.size each pass). */ + "sp.new": { argTypes: [], result: SEARCH_PARAMS_T }, + "sp.newChecked": { argTypes: [DYN], result: SEARCH_PARAMS_T }, + "sp.parse": { argTypes: [STRING], result: SEARCH_PARAMS_T }, + "sp.copy": { argTypes: [SEARCH_PARAMS_T], result: SEARCH_PARAMS_T }, + // The pairs argument is string[][] — checked structurally in validate.ts (the + // generic array-of slot has no named constant here). + "sp.fromPairs": { argTypes: [null], result: SEARCH_PARAMS_T }, + "sp.with": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: SEARCH_PARAMS_T }, + "url.searchParams": { argTypes: [URL_T], result: SEARCH_PARAMS_T }, + "url.search": { argTypes: [URL_T], result: STRING }, + "url.hash": { argTypes: [URL_T], result: STRING }, + // Result is the interned `string | null` union — the libCall case + // checks the arms (the spawnRes.signal pattern). + "sp.get": { argTypes: [SEARCH_PARAMS_T, STRING], result: VOID }, + "sp.getAll": { argTypes: [SEARCH_PARAMS_T, STRING], result: arrayOf(STRING) }, + "sp.append": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID }, + "sp.set": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID }, + "sp.delete": { argTypes: [SEARCH_PARAMS_T, STRING], result: VOID }, + "sp.deleteValue": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID }, + "sp.has": { argTypes: [SEARCH_PARAMS_T, STRING], result: BOOL }, + "sp.hasValue": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: BOOL }, + "sp.sort": { argTypes: [SEARCH_PARAMS_T], result: VOID }, + "sp.size": { argTypes: [SEARCH_PARAMS_T], result: F64 }, + "sp.toString": { argTypes: [SEARCH_PARAMS_T], result: STRING }, + "sp.keyAt": { argTypes: [SEARCH_PARAMS_T, F64], result: STRING }, + "sp.valAt": { argTypes: [SEARCH_PARAMS_T, F64], result: STRING }, + // node:querystring. qs.parse's result is the call site's ParsedUrlQuery + // dictionary record (VOID is the networkInterfaces sentinel — the + // libCall case checks the structure); qs.stringify's object argument + // is a dyn value (the frontend dynFroms typed records). + /** node:querystring (scr_qs.c — link-gated by moduleUsesQs; NOT + * URLSearchParams: the legacy codec's escaping and '+' rules differ). + * qs.escape is Node's qsEscape, which encodes exactly the component + * unreserved set — it emits the always-linked scr_qs_escape, so + * escape-only programs never pull the unit. qs.unescape is Node's qsUnescape: strict decodeURIComponent + * first, the lenient legacy unescapeBuffer fallback on failure (never + * throws). qs.parse takes (str, sep, eq, maxKeys) — the frontend + * completes omitted/null sep/eq to "&"/"=" and the omitted maxKeys + * option to Node's 1000 (0 and negatives mean unlimited, Node's rule) — + * and its result type is the CALL SITE's mapped ParsedUrlQuery shape (a + * pure index-signature record over `string | string[]`, an undefined + * arm tolerated — @types/node's Dict), verified structurally by the + * frontend (lowerQuerystringParseCall); the emitters construct the + * record and hand its overflow map to scr_qs_parse_into with the two + * union tags. qs.stringify takes (obj, sep, eq) with obj a dyn value + * (the frontend dynFroms the typed record; JS-world dyn values pass + * through) — Node's encodeStringified rules run in the runtime, so + * arrays expand to repeated keys and null/undefined/nested objects are + * empty values. Custom encoder/decoder options fence at compile time. + * All borrow; string results +1; escape and stringify throw URIError + * [ERR_INVALID_URI] on a lone surrogate. */ + "qs.parse": { argTypes: [STRING, STRING, STRING, F64], result: VOID }, + "qs.stringify": { argTypes: [DYN, STRING, STRING], result: STRING }, + "qs.escape": { argTypes: [STRING], result: STRING }, + "qs.unescape": { argTypes: [STRING], result: STRING }, + /** node:util.parseArgs: one checked-dynamic config object in, one + * checked-dynamic ParsedResults tree out. Statically typed member reads + * validate their values when leaving that tree. The native parser may + * throw Node's coded validation/grammar TypeErrors. */ + "util.parseArgs": { argTypes: [DYN], result: DYN }, + "util.parseEnv": { argTypes: [DYN], result: DYN }, + "util.getSystemErrorName": { argTypes: [DYN], result: STRING }, + "util.getSystemErrorMessage": { argTypes: [DYN], result: STRING }, + "util.systemErrorEntries": { argTypes: [], result: DYN }, + "util.stripVTControlCharacters": { argTypes: [DYN], result: STRING }, + "util.toUSVString": { argTypes: [DYN], result: STRING }, + "util.isDeepStrictEqual": { argTypes: [DYN, DYN, DYN], result: BOOL }, + "util.styleText": { argTypes: [DYN, DYN, DYN], result: STRING }, + /** util.inherits/deprecate/callbackify/debuglog over checked-dynamic + * arguments (scr_util_callable.c). The function results are native dyn + * wrappers; call sites with a static function type check them back. */ + "util.inherits": { argTypes: [DYN, DYN], result: VOID }, + "util.inheritsPrototype": { argTypes: [DYN, DYN, DYN], result: VOID }, + "util.deprecate": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, + "util.callbackify": { argTypes: [DYN], result: DYN }, + /** util.promisify over an untyped function value: a native dyn function + * whose calls answer a dyn promise the appended callback settles. */ + "util.promisifyDyn": { argTypes: [DYN], result: DYN }, + "util.debuglog": { argTypes: [DYN, DYN], result: DYN }, + "util.typeIs": { argTypes: [DYN, STRING], result: BOOL }, + "util.typeValue": { argTypes: [DYN, STRING], result: DYN }, + /** fs.statSync → a Stats value (may throw, like the other sync fs + * calls); the stats.* getters are pure reads on it. */ + "fs.statSync": { argTypes: [STRING], result: STATS_T }, + "fs.lstatSync": { argTypes: [STRING], result: STATS_T }, + // statSync / lstatSync(path, { throwIfNoEntry }): arg 1 is the flag. + // Result is the interned `Stats | undefined` union (the runtime answers + // +1-or-NULL; the backend builds the arms). + "fs.statProbeSync": { argTypes: [STRING, BOOL], result: VOID }, + "fs.lstatProbeSync": { argTypes: [STRING, BOOL], result: VOID }, + "fs.fstatSync": { argTypes: [F64], result: STATS_T }, + "fs.fchmodSync": { argTypes: [F64, F64], result: VOID }, + "fs.fsyncSync": { argTypes: [F64], result: VOID }, + "fs.fdatasyncSync": { argTypes: [F64], result: VOID }, + "fs.ftruncateSync": { argTypes: [F64, F64], result: VOID }, + "fs.utimesSync": { argTypes: [DYN, DYN, DYN], result: VOID }, + "fs.futimesSync": { argTypes: [DYN, DYN, DYN], result: VOID }, + "fs.lutimesSync": { argTypes: [DYN, DYN, DYN], result: VOID }, + "fs.readvSync": { argTypes: [F64, arrayOf(BYTES_U8), F64], result: F64 }, + "fs.writevSync": { argTypes: [F64, arrayOf(BYTES_U8), F64], result: F64 }, + "fs.linkSync": { argTypes: [DYN, DYN], result: VOID }, + "fs.symlinkSync": { argTypes: [DYN, DYN, DYN], result: VOID }, + /** fs.cpSync / fs/promises cp (scr_fs_cp.c): src, dest and options are + * dyn values the runtime validates in Node's order. */ + "fs.cpSync": { argTypes: [DYN, DYN, DYN], result: VOID }, + "fs.readlinkSync": { argTypes: [DYN, DYN], result: DYN }, + "fs.statfsSync": { argTypes: [DYN, DYN], result: DYN }, + "fs.readlinkSyncStr": { argTypes: [DYN, DYN], result: STRING }, + "fs.readlinkSyncBuffer": { argTypes: [DYN, DYN], result: BYTES_U8 }, + /** fs.openSync(path, flags) → the raw fd as f64; fs.readSync/fs.writeSync + * over Buffer windows perform sequential I/O when position is -1 and + * offset-preserving positioned I/O otherwise; fs.writeStrSync is the + * utf8 string twin; and fs.closeSync(fd) closes the descriptor. This is + * the fd slice behind spawn/log-processing forms. + * flags is Node's string + * grammar ("r", "w", "a" and the +/x variants; unknown flags throw + * Node's ERR_INVALID_ARG_VALUE TypeError text). All throw Node-shaped + * fs errors (openSync ENOENT/EACCES..., readSync EBADF/range errors, + * closeSync EBADF). */ + "fs.openSync": { argTypes: [STRING, STRING], result: F64 }, + "fs.openNumericSync": { argTypes: [STRING, F64, F64], result: F64 }, + "fs.readSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 }, + "fs.readSyncDyn": { argTypes: [DYN, DYN, DYN, DYN, DYN], result: F64 }, + "fs.writeSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 }, + "fs.writeStrSync": { argTypes: [F64, STRING, F64, STRING], result: F64 }, + // fs.watch's callback func type is program-dependent (zero params, or + // the eventType string) — the slot pins arity and the path/receiver. + /** fs.watch(path, listener?) → an FSWatcher handle (scr_watch.c — + * linked, and scr_watch_install() emitted, only when these appear on + * the IR; moduleUsesFsWatch is the switch). The path opens NOW — + * failure throws Node's fs error synchronously ("ENOENT: ..., watch + * 'x'", the polling-fallback catch shape) — and the unit's kqueue + * (EVFILT_VNODE) delivers "rename"/"change" through the loop's watch + * hook. The callback (nullable) MOVES in with an adapter per listener + * shape (zero-param, or the (eventType: string) form — runtime- + * provided); an open watcher keeps the loop alive until watcher.close() + * (idempotent, receiver borrowed, statement position only). */ + "fs.watch": { argTypes: [STRING], result: FSWATCHER_T }, + "fs.watchCb": { argTypes: [STRING, null], result: FSWATCHER_T }, + "watcher.close": { argTypes: [FSWATCHER_T], result: VOID }, + /** The composed `new crypto.X509Certificate(data).fingerprint` read + * (scr_lib.c — no certificate handle exists): the SHA-1 of the DER, + * uppercase colon-separated, over PEM or raw-DER Buffer input; other + * inputs throw Node's ERR_OSSL_PEM_NO_START_LINE Error (may-throw). */ + "crypto.x509Fingerprint": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509FingerprintStr": { argTypes: [STRING], result: STRING }, + /** The certificate's Validity window (validFrom / validTo reads — + * scr_lib.c's minimal ASN.1 walk to the TBSCertificate validity + * SEQUENCE): UTCTime and GeneralizedTime render in Node's exact + * ASN1_TIME_print shape ("Jul 1 00:00:00 2026 GMT" — %2d space-padded + * day). Same input contract and PEM error as the fingerprint pair + * (may-throw). */ + "crypto.x509ValidFrom": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509ValidFromStr": { argTypes: [STRING], result: STRING }, + "crypto.x509ValidTo": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509ValidToStr": { argTypes: [STRING], result: STRING }, + /** The certificate's DER as a fresh Buffer (the raw field), decoded + * once from the constructor's input; the same PEM error (may-throw). */ + "crypto.x509Raw": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "crypto.x509RawDer": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "crypto.x509RawStr": { argTypes: [STRING], result: BYTES_U8 }, + /** The remaining X509Certificate fields over that DER, in Node's + * renderings (scr_lib.c). subjectAltName, infoAccess and + * signatureAlgorithm answer "" where Node answers undefined. Borrowed; + * never throw. */ + "crypto.x509Subject": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509Issuer": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509SubjectAltName": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509InfoAccess": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509SerialNumber": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509Fingerprint256": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509Fingerprint512": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509Ca": { argTypes: [BYTES_U8], result: BOOL }, + "crypto.x509SignatureAlgorithm": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509SignatureAlgorithmOid": { argTypes: [BYTES_U8], result: STRING }, + /** X509Certificate methods over its raw DER. checkHost/checkEmail/checkIP + * answer the matching name or "" (undefined), checkIssued compares two + * certificates, x509Pem is toString's PEM (scr_x509.c); publicKey, + * verify and checkPrivateKey read keys (scr_pki.c). */ + "crypto.x509CheckHost": { argTypes: [BYTES_U8, DYN, DYN], result: STRING }, + "crypto.x509CheckEmail": { argTypes: [BYTES_U8, DYN, DYN], result: STRING }, + "crypto.x509CheckIP": { argTypes: [BYTES_U8, DYN, DYN], result: STRING }, + "crypto.x509CheckIssued": { argTypes: [BYTES_U8, BYTES_U8], result: BOOL }, + "crypto.x509LegacyObject": { argTypes: [BYTES_U8], result: DYN }, + "crypto.x509Pem": { argTypes: [BYTES_U8], result: STRING }, + "crypto.x509PublicKey": { argTypes: [BYTES_U8], result: DYN }, + "crypto.x509Verify": { argTypes: [BYTES_U8, DYN], result: BOOL }, + "crypto.x509CheckPrivateKey": { argTypes: [BYTES_U8, DYN], result: BOOL }, + "fs.closeSync": { argTypes: [F64], result: VOID }, + "stats.isFile": { argTypes: [STATS_T], result: BOOL }, + "stats.isDirectory": { argTypes: [STATS_T], result: BOOL }, + "stats.isSymbolicLink": { argTypes: [STATS_T], result: BOOL }, + "stats.size": { argTypes: [STATS_T], result: F64 }, + "stats.dev": { argTypes: [STATS_T], result: F64 }, + "stats.ino": { argTypes: [STATS_T], result: F64 }, + "stats.blocks": { argTypes: [STATS_T], result: F64 }, + "stats.nlink": { argTypes: [STATS_T], result: F64 }, + "stats.atimeMs": { argTypes: [STATS_T], result: F64 }, + "stats.mtimeMs": { argTypes: [STATS_T], result: F64 }, + "stats.ctimeMs": { argTypes: [STATS_T], result: F64 }, + "stats.birthtimeMs": { argTypes: [STATS_T], result: F64 }, + "stats.mode": { argTypes: [STATS_T], result: F64 }, + "stats.uid": { argTypes: [STATS_T], result: F64 }, + "stats.gid": { argTypes: [STATS_T], result: F64 }, + "stats.rdev": { argTypes: [STATS_T], result: F64 }, + "stats.blksize": { argTypes: [STATS_T], result: F64 }, + "stats.isBlockDevice": { argTypes: [STATS_T], result: BOOL }, + "stats.isCharacterDevice": { argTypes: [STATS_T], result: BOOL }, + "stats.isFIFO": { argTypes: [STATS_T], result: BOOL }, + "stats.isSocket": { argTypes: [STATS_T], result: BOOL }, + /** The Date fields: the *Ms value rounded (Node's dateFromMs). */ + "stats.atime": { argTypes: [STATS_T], result: DATE_T }, + "stats.mtime": { argTypes: [STATS_T], result: DATE_T }, + "stats.ctime": { argTypes: [STATS_T], result: DATE_T }, + "stats.birthtime": { argTypes: [STATS_T], result: DATE_T }, + // The wider sync fs slice (unlink/chmod/chown/copyfile and the + // mode-carrying write/mkdir forms). + /** The wider sync fs slice (scr_lib.c), all throwing catchably with + * Node's errno message shapes and `.code` stamped like the rest of + * sync fs. unlink/chmod/chown wrap the syscalls 1:1 (Node reports the + * syscall's own name). copyFileSync copies contents into a fresh (or + * truncated) destination carrying the SOURCE's mode (libuv's + * uv_fs_copyfile behavior); its errors carry BOTH paths — Node's + * "copyfile 'src' -> 'dest'". lstatSync is statSync without following + * a trailing symlink (Node reports lstat); stats.isSymbolicLink / + * stats.dev / ino / blocks / nlink / atimeMs / mtimeMs / ctimeMs are pure reads on the widened + * snapshot (blocks is allocated 512-byte units; the times are milliseconds + * with their sub-second fractions, Node's arithmetic). + * writeFileModeSync is writeFileSync(path, data, { mode }): the mode + * applies at CREATION only (open(2) with O_CREAT, umask applying), + * exactly Node — an existing file keeps its permissions. mkdirModeSync + * / mkdirRecursiveModeSync are the mkdirSync option forms with an + * explicit mode (the recursive walk passes it to every directory it + * creates, like Node's). */ + "fs.unlinkSync": { argTypes: [STRING], result: VOID }, + "fs.chmodSync": { argTypes: [STRING, F64], result: VOID }, + "fs.chownSync": { argTypes: [STRING, F64, F64], result: VOID }, + "fs.lchownSync": { argTypes: [STRING, F64, F64], result: VOID }, + "fs.fchownSync": { argTypes: [F64, F64, F64], result: VOID }, + /** truncateSync opens the path with 'r+', then truncates (len checked and + * clamped like ftruncateSync's) and closes it. */ + "fs.truncateSync": { argTypes: [STRING, F64], result: VOID }, + "fs.copyFileSync": { argTypes: [STRING, STRING], result: VOID }, + /** renameSync is the direct two-path syscall wrapper. renameCb defers + * the operation to the loop and fires a program-shaped Error | null + * callback through a backend-emitted adapter. */ + "fs.renameSync": { argTypes: [STRING, STRING], result: VOID }, + // Callback type is program-dependent (zero params or Error | null). + "fs.renameCb": { argTypes: [STRING, STRING, null], result: VOID }, + "fs.writeFileModeSync": { argTypes: [STRING, STRING, F64], result: VOID }, + "fs.mkdirModeSync": { argTypes: [STRING, F64], result: VOID }, + "fs.mkdirRecursiveModeSync": { argTypes: [STRING, F64], result: VOID }, + "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 }, + "worker.root": { argTypes: [], result: F64 }, + "worker.data": { argTypes: [], result: DYN }, + "worker.parentPort": { argTypes: [], result: DYN }, + /** worker_threads.resourceLimits: {} in the main thread; a refusal in a + * worker (scr_worker_events.c). */ + "worker.resourceLimits": { argTypes: [], result: DYN }, + /** worker_threads.threadName: the Worker's name in a worker, "" in the + * main thread (scr_worker.c). */ + "worker.threadName": { argTypes: [], result: DYN }, + /** new MessageChannel() ({ port1, port2 }) and receiveMessageOnPort(port) + * (scr_worker_events.c; the latter may throw). */ + "worker.channel": { argTypes: [], result: DYN }, + "worker.receiveMessageOnPort": { argTypes: [DYN], result: DYN }, + /** setEnvironmentData(key, value) and getEnvironmentData(key) + * (scr_worker_events.c; arguments borrowed). */ + "worker.setEnvironmentData": { argTypes: [DYN, DYN], result: VOID }, + "worker.getEnvironmentData": { argTypes: [DYN], result: DYN }, + /** markAsUntransferable(value) and isMarkedAsUntransferable(value) + * (scr_worker_events.c; the value borrowed). */ + "worker.markUntransferable": { argTypes: [DYN], result: VOID }, + "worker.isMarkedUntransferable": { argTypes: [DYN], result: BOOL }, + /** markAsUncloneable(value) (scr_json.c; the value borrowed). */ + "dyn.markUncloneable": { argTypes: [DYN], result: VOID }, + /** before new Worker() with the stdout or stderr option: links the stream + * unit that worker.stdout and worker.stderr read through (scr_stream.c). */ + "stream.workerStdio": { argTypes: [], result: VOID }, + /** new BroadcastChannel(...args) (scr_worker_events.c; may throw), after + * event.broadcastInstall, which installs its EventTarget side. */ + "worker.broadcastChannel": { argTypes: [DYN], result: DYN }, + "event.broadcastInstall": { argTypes: [], result: VOID }, + /** child_process.spawnSync (scr_child.c): posix_spawn + waitpid + piped + * utf8 capture — cmd borrowed, args one borrowed string[] (the frontend + * completes an omitted list to an empty literal), result an owned (+1) + * spawnRes. NEVER throws: spawn failure (nonexistent binary, EACCES) is + * data, like Node's error property — status null and empty outputs + * (SEMANTICS.md documents the divergence from Node's null stdout). + * The getters are pure reads: status is the interned `number | null` + * union (null = spawn failure or signal death, type-directed + * construction in the backend like process.envGet); stdout/stderr are + * +1 strings. */ + "cp.spawnSync": { argTypes: [STRING, arrayOf(STRING)], result: SPAWNRES_T }, + // spawnSync's options form: timeout, killSignal name ("" = SIGTERM), + // and the three stdio modes. + /** spawnSync with options (scr_child.c): the cp.spawnSync core plus the + * option slice portless-class CLIs pass — timeout (killSignal fires at + * the deadline and the result carries error: ETIMEDOUT + the signal, + * Node's shape), killSignal (a signal NAME; "" = the SIGTERM default), + * and per-fd stdio modes (in: 0 /dev/null, 1 ignore, 2 inherit; out/ + * err: 0 capture, 1 ignore → "", 2 inherit → ""). NEVER throws — spawn + * failure and timeout are data on the result, like Node's error + * property. spawnRes.signal is the result's termination signal as the + * call site's `Signals | null` union (null = exited normally or spawn + * failure), constructed type-directedly like spawnRes.status. */ + "cp.spawnSyncOpts": { + 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 + * maps the value to the three modes. Args (cmd, argv, timeout, + * killSignal, stdio). Never throws, like the other spawnSync forms. */ + "cp.spawnSyncStdioStr": { + argTypes: [STRING, arrayOf(STRING), F64, STRING, STRING], + result: SPAWNRES_T, + }, + // node:net (scr_net.c). Listener slots are program-dependent closures — + // null slots, checked in the libCall case (the child.onExit precedent); + // the zero-param callbacks pin the exact func type here. The trailing + // BOOL on registrations is the once flag. + /** node:net (scr_net.c — linked, and scr_net_install() emitted, only + * when one of these appears on the IR; moduleUsesNet is the switch). + * Receivers are borrowed; CALLBACKS MOVE into the handle's listener + * registry and are released at settlement (the child.onExit story). + * All listener registrations carry a trailing BOOL once-flag (`on` vs + * `once`) and are void — chaining is fenced. None of these throw: + * listen/connect failures are the async 'error' event, like Node. + * + * net.createServer's optional connection handler and the + * serverOnConnection listeners take the runtime-provided adapters + * (zero-param, or the one-param socket shape); sockOnData's adapters + * are the stdin pair's shapes (zero-param / bytes chunk); the error + * events reuse the child %Error adapters. net.listen/net.listenCb bind + * NOW and defer 'listening' to the next loop turn (Node's next-tick + * emit); net.serverPort is the composed `server.address().port` read. + * net.connect's host argument is a string ("localhost" pins to + * 127.0.0.1 — SEMANTICS.md); the connect callback is once('connect'). */ + "net.createServer": { argTypes: [], result: NETSERVER_T }, + "net.createServerCb": { argTypes: [null], result: NETSERVER_T }, + "net.listen": { argTypes: [NETSERVER_T, F64], result: VOID }, + /** listen(path[, cb]): a Unix-socket server; the socket file is removed + * when the server closes. Failures are the async 'error'. */ + "net.listenPath": { argTypes: [NETSERVER_T, STRING], result: VOID }, + "net.listenPathCb": { + argTypes: [NETSERVER_T, STRING, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.listenCb": { + argTypes: [NETSERVER_T, F64, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + /** listen({ port, host, ipv6Only }[, cb]) — the explicit-interface bind + * (portless's listenOnProxyInterface): args [server, port, host, + * ipv6Only]. host is an IP literal string ("" = the host-less + * dual-stack any default); ipv6Only sets IPV6_V6ONLY before the bind. + * These are the old-shaped calls, retained for omitted reusePort so + * serialized IR remains compatible. Failures are the async 'error', + * message in Node's listen shape with the requested host. Never throws. */ + "net.listenOpts": { argTypes: [NETSERVER_T, F64, STRING, BOOL], result: VOID }, + // The callback slot is a zero-param void closure OR its + // `(() => void) | undefined` optional-binding union (checked specially). + "net.listenOptsCb": { argTypes: [NETSERVER_T, F64, STRING, BOOL, null], result: VOID }, + /** Additive ABI for listen({ ..., reusePort }) — args + * [server, port, host, ipv6Only, reusePort], with the callback following + * those values in the callback form. The final BOOL is the requested + * kernel connection-distribution option; old spellings remain + * four/five-argument calls so serialized IR stays compatible. */ + "net.listenOptsReusePort": { argTypes: [NETSERVER_T, F64, STRING, BOOL, BOOL], result: VOID }, + // The callback is a zero-param void closure OR its optional-binding union; + // the callback follows the reusePort BOOL in this additive ABI. + "net.listenOptsReusePortCb": { + argTypes: [NETSERVER_T, F64, STRING, BOOL, BOOL, null], + result: VOID, + }, + "net.serverPort": { argTypes: [NETSERVER_T], result: F64 }, + "net.serverListening": { argTypes: [NETSERVER_T], result: BOOL }, + /** socket.ref()/unref() and server.ref()/unref(): whether the handle + * keeps the event loop alive. */ + "net.sockSetRef": { argTypes: [NETSOCKET_T, BOOL], result: VOID }, + "net.serverSetRef": { argTypes: [NETSERVER_T, BOOL], result: VOID }, + // net.serverAddress's record result is shape-checked in the libCall case + // (the dgram.address sentinel pattern). + /** server.address() as the full AddressInfo record (the dgram.address + * materialization pattern: the emitter builds the record from the three + * runtime reads; the frontend pinned the shape). Never throws — before + * listen it answers the any-form defaults with port 0 where Node + * answers null (the serverPort stance). */ + "net.serverAddress": { argTypes: [NETSERVER_T], result: VOID }, + "net.serverClose": { argTypes: [NETSERVER_T], result: VOID }, + /** server.close((err) => ...): the callback (a dyn function) gets + * undefined once the server closes, or Node's ERR_SERVER_NOT_RUNNING + * when it was not listening. */ + "net.serverCloseDyn": { argTypes: [NETSERVER_T, DYN], result: VOID }, + /** server.address() in programs that may listen on a Unix socket: the + * path a listening server answers ("" for a TCP server), and the check + * a record-typed address() makes first (throws for a Unix server). */ + "net.serverUnixPath": { argTypes: [NETSERVER_T], result: STRING }, + "net.serverAddressCheck": { argTypes: [NETSERVER_T], result: VOID }, + "net.serverCloseCb": { + argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.serverOnError": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + "net.serverOnClose": { + argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "net.serverOnConnection": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + /** 'secureConnection' — the TLS server's deferred 'connection' list + * (handshake-completion timing); on a server without deferred + * connections (a plain net server) the registration never fires, + * exactly Node's split. */ + "net.serverOnSecureConnection": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + "net.connect": { argTypes: [F64, STRING], result: NETSOCKET_T }, + /** connect with a validated autoSelectFamilyAttemptTimeout option (the + * budget runs Node's validateInt32-from-1 ladder and is then inert — + * the single dial has nothing to time). May-throw. */ + "net.connectAttempt": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, + /** net.connect/createConnection over a RUNTIME option bag (computed + * keys — the invalid-input probes): Node-order validation (the + * objectMode trio's ERR_INVALID_ARG_VALUE, validatePort, host string, + * autoSelectFamily boolean, the attempt budget), then the trailing + * compiler-rendered fence — ALWAYS THROWS (the error.nodeThrow + * polymorphic-result carve-out). May-throw seed. */ + "net.connectOptsChk": { argTypes: [DYN, STRING], result: VOID }, + "net.connectCb": { + argTypes: [F64, STRING, { kind: "func", params: [], ret: VOID }], + result: NETSOCKET_T, + }, + // The lookup's exact func shape is program data (its answer callback's + // union/record types) — checked specially in the libCall case. + /** connect({ port, host, autoSelectFamily: true, lookup }) — the + * caller-resolver dial (portless's createLoopbackConnection): args + * [port, host, lookup]. The runtime creates the (connecting) socket + * handle, invokes the lookup as Node does — lookup(hostname, options, + * callback), options crossing as the dyn undefined — and the answer + * closure (an emitter-synthesized per-shape thunk over a boxed socket, + * the SNI-answer pattern) dials the answered addresses IN ORDER: each + * connect failure tries the next, the LAST failure's message is the + * socket's 'error' (Node's autoSelectFamily aggregate is a documented + * divergence), and a lookup error surfaces as the deferred socket + * 'error'. A synchronous throw INSIDE the lookup propagates out of the + * connect call (may-throw seed). */ + "net.connectLookup": { argTypes: [F64, STRING, null], result: NETSOCKET_T }, + /** connect({ host, port, lookup }) with a lookup of Node's own type, as + * a checked-dynamic function: the runtime calls it with { hints, all: + * true } and a callback taking an address string or list, then dials + * the answers in order. MAY THROW (host validation, a lookup throw). */ + "net.connectLookupDyn": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, + "net.sockWrite": { argTypes: [NETSOCKET_T, STRING], result: VOID }, + "net.sockWriteBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID }, + "net.sockEnd": { argTypes: [NETSOCKET_T], result: VOID }, + "net.sockEndStr": { argTypes: [NETSOCKET_T, STRING], result: VOID }, + "net.sockEndBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID }, + /** write/end with a CHECKED-DYNAMIC chunk (an untyped JS payload into a + * typed socket): the runtime dispatches STR/BYTES and throws Node's + * ERR_INVALID_ARG_TYPE chunk TypeError on any other kind. */ + "net.sockWriteDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, + "net.sockEndDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, + "net.sockDestroy": { argTypes: [NETSOCKET_T], result: VOID }, + "net.sockPipe": { argTypes: [NETSOCKET_T, NETSOCKET_T], result: VOID }, + /** socket.pipe(res) — raw socket chunks into a ServerResponse body + * (the extended-CONNECT bridge leg): each chunk is a body write (the + * response's own framing applies); source EOF end()s the response, + * pipe's default. Borrows both. Never throws. */ + "net.sockPipeRes": { argTypes: [NETSOCKET_T, HTTPRES_T], result: VOID }, + "net.sockOnData": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, + "net.sockOnEnd": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "net.sockOnClose": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "net.sockOnError": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, + "net.sockOnConnect": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + // node:dgram + node:dns (scr_dgram.c). The message/error listeners and + // dns.lookup's callback are program-dependent closures (null slots, + // checked in the libCall case); dgram.address's record result is + // program-dependent too (VOID here is the envGet sentinel — the libCall + // case checks the {address, family, port} shape). + /** node:dgram + node:dns (scr_dgram.c — linked, and + * scr_dgram_install() emitted, only when one of these appears on the + * IR; moduleUsesDgram is the switch — dns.lookup rides the same unit). + * The net listener discipline verbatim: receivers borrowed, CALLBACKS + * MOVE into the handle's registry and release at settlement, `on` vs + * `once` is the trailing bool, registrations are void. bind/connect + * bind NOW and defer 'listening'/'connect' to the next loop turn (the + * net.listen story); their optional-host completions are "" + * (bind → 0.0.0.0) and "127.0.0.1" (connect — udp4's Node default). + * bind/connect/send/close/address THROW Node's state errors ("Socket + * is already bound", "Already connected", "Not running") — may-throw + * seeded. dgram.address returns the AddressInfo RECORD (the frontend + * pins the {address, family, port} shape; the emitter builds it from + * runtime parts). onMessage adapters are emitted per rinfo record + * shape (the child.onExit precedent); onError reuses the child %Error + * adapters. dns.lookup resolves via getaddrinfo AT CALL TIME and + * defers the callback to the next turn (SEMANTICS.md documents the + * blocking divergence); its per-union adapter builds the + * `Error | null` first argument. */ + "dgram.createSocket": { argTypes: [BOOL], result: DGRAMSOCK_T }, + "dgram.bind": { argTypes: [DGRAMSOCK_T, F64, STRING], result: VOID }, + "dgram.bindCb": { + argTypes: [DGRAMSOCK_T, F64, STRING, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "dgram.connect": { argTypes: [DGRAMSOCK_T, F64, STRING], result: VOID }, + "dgram.connectCb": { + argTypes: [DGRAMSOCK_T, F64, STRING, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "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 + * sends and completion callbacks included. Up to six arguments, the + * missing ones undefined. May-throw. */ + "dgram.sendChk": { argTypes: [DGRAMSOCK_T, DYN, DYN, DYN, DYN, DYN, DYN], result: VOID }, + "dgram.address": { argTypes: [DGRAMSOCK_T], result: VOID }, + "dgram.close": { argTypes: [DGRAMSOCK_T], result: VOID }, + "dgram.closeCb": { + argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "dgram.unref": { argTypes: [DGRAMSOCK_T], result: VOID }, + "dgram.ref": { argTypes: [DGRAMSOCK_T], result: VOID }, + /** The socket options (scr_dgram.c): the membership calls take the + * interface and whether it was given. May-throw. */ + "dgram.setBroadcast": { argTypes: [DGRAMSOCK_T, BOOL], result: VOID }, + "dgram.setTTL": { argTypes: [DGRAMSOCK_T, F64], result: F64 }, + "dgram.setMulticastTTL": { argTypes: [DGRAMSOCK_T, F64], result: F64 }, + "dgram.setMulticastLoopback": { argTypes: [DGRAMSOCK_T, BOOL], result: BOOL }, + "dgram.setMulticastInterface": { argTypes: [DGRAMSOCK_T, STRING], result: VOID }, + "dgram.addMembership": { argTypes: [DGRAMSOCK_T, STRING, STRING, BOOL], result: VOID }, + "dgram.dropMembership": { argTypes: [DGRAMSOCK_T, STRING, STRING, BOOL], result: VOID }, + "dgram.getRecvBufferSize": { argTypes: [DGRAMSOCK_T], result: F64 }, + "dgram.getSendBufferSize": { argTypes: [DGRAMSOCK_T], result: F64 }, + "dgram.setRecvBufferSize": { argTypes: [DGRAMSOCK_T, F64], result: VOID }, + "dgram.setSendBufferSize": { argTypes: [DGRAMSOCK_T, F64], result: VOID }, + "dgram.getSendQueueSize": { argTypes: [DGRAMSOCK_T], result: F64 }, + "dgram.getSendQueueCount": { argTypes: [DGRAMSOCK_T], result: F64 }, + "dgram.disconnect": { argTypes: [DGRAMSOCK_T], result: VOID }, + "dgram.onMessage": { argTypes: [DGRAMSOCK_T, null, BOOL], result: VOID }, + "dgram.onError": { argTypes: [DGRAMSOCK_T, null, BOOL], result: VOID }, + "dgram.onListening": { + argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "dgram.onClose": { + argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "dgram.onConnect": { + argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "dns.lookup": { argTypes: [STRING, F64, null], result: VOID }, + /** Node's argument forms with dyn callbacks: lookup(hostname, options, + * callback) and lookupService(address, port, callback). */ + "dns.lookupDyn": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, + "dns.lookupService": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, + /** node:dns/promises: lookup(hostname, options) and lookupService(address, + * port) over the callback forms; the promise fulfills with the dyn + * { address, family } (array for all: true) / { hostname, service }. + * Argument errors throw synchronously (may-throw), unlike the + * util.promisify forms below. */ + "dns.promisesLookup": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "dns.promisesLookupService": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "dns.getServers": { argTypes: [], result: DYN }, + "dns.getDefaultResultOrder": { argTypes: [], result: STRING }, + "dns.setDefaultResultOrder": { argTypes: [DYN], result: VOID }, + // node:test (scr_test.c). Bodies are program-dependent closures (0 or + // 1 testCtx param, void or Promise result — the spoke pinned the + // shape): null slots. sub's result is the settled Promise the + // await consumes. + /** node:test (scr_test.c — linked only when one of these appears on + * the IR; moduleUsesNodeTest is the switch, and the main epilogue asks + * scr_test_exit_code() for the process's exit status). Strings are + * BORROWED, callbacks MOVE. register/suite/hook attach to the runner + * tree (register: name, mode 0|1|2 run/skip/todo, directive message "" + * = none, cb or absent via registerEmpty, flags 1 async | 2 takes-ctx + * | 4 only, "file:line:col"); suite runs its body AT registration + * (Node's collection phase). sub is t.test — runs the subtest INLINE + * on the runner fiber and returns the settled promise the await + * consumes. ctxSkip/ctxTodo mark the running test; ctxDiagnostic + * queues an ℹ line; ctxName reads t.name. Every registration keeps + * the loop-run emitted (usesTimers) so the runner fiber drains. */ + // The trailing three are the { plan, timeout, concurrency } options (plan + // -1 without one; timeout read under flag 8; concurrency read under flag + // 16, -1 for true). An out-of-range timeout or concurrency throws. + "test.register": { + argTypes: [STRING, F64, STRING, null, F64, STRING, F64, F64, F64], + result: VOID, + }, + "test.registerEmpty": { + argTypes: [STRING, F64, STRING, F64, STRING, F64, F64, F64], + result: VOID, + }, + // The suite body takes no parameters or its SuiteContext (flag 2). + "test.suite": { argTypes: [STRING, F64, STRING, null, STRING, F64, F64], result: VOID }, + // The trailing string is the hook call's "file:line:col". + "test.hook": { argTypes: [F64, null, F64, STRING], result: VOID }, + "test.sub": { + argTypes: [TESTCTX_T, STRING, F64, STRING, null, F64, STRING, F64, F64, F64], + result: { kind: "promise", inner: VOID }, + }, + "test.subEmpty": { + argTypes: [TESTCTX_T, STRING, F64, STRING, F64, STRING, F64, F64, F64], + result: VOID, + }, + "test.ctxSkip": { argTypes: [TESTCTX_T, STRING], result: VOID }, + "test.ctxTodo": { argTypes: [TESTCTX_T, STRING], result: VOID }, + "test.ctxDiagnostic": { argTypes: [TESTCTX_T, STRING], result: VOID }, + "test.ctxName": { argTypes: [TESTCTX_T], result: STRING }, + "test.ctxFullName": { argTypes: [TESTCTX_T], result: STRING }, + // t.passed, and t.workerId's Number(NODE_TEST_WORKER_ID) with 0 for + // undefined. + "test.ctxPassed": { argTypes: [TESTCTX_T], result: BOOL }, + "test.workerId": { argTypes: [], result: F64 }, + // getTestContext(): whether a test, suite or hook is running in the + // current async context, and that context. + "test.hasContext": { argTypes: [], result: BOOL }, + "test.context": { argTypes: [], result: TESTCTX_T }, + // t.before/t.after/t.beforeEach/t.afterEach (which: 0 before, 1 after, + // 2 beforeEach, 3 afterEach); a throwing t.before is captured on the test. + "test.ctxHook": { argTypes: [TESTCTX_T, F64, null, F64], result: VOID }, + // t.plan(count), and the count of one t.assert call. + "test.ctxPlan": { argTypes: [TESTCTX_T, F64], result: VOID }, + "test.ctxCount": { argTypes: [TESTCTX_T], result: VOID }, + // The mock trackers: the module's `mock` and t.mock keep each mock's + // restore closure (moved) for restoreAll and reset. + "test.mockTrack": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: VOID }, + "test.mockRestoreAll": { argTypes: [], result: VOID }, + "test.mockReset": { argTypes: [], result: VOID }, + "test.ctxMockTrack": { + argTypes: [TESTCTX_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "test.ctxMockRestoreAll": { argTypes: [TESTCTX_T], result: VOID }, + "test.ctxMockReset": { argTypes: [TESTCTX_T], result: VOID }, + // mock.timers on the module's `mock` and on t.mock. enable takes one + // code per listed API ('T' setTimeout, 'I' setInterval), the `now` + // value and its flags (1 given, 2 from a Date). enable, tick, runAll and + // setTime throw Node's errors; tick and runAll run timer callbacks. + // t.assert.snapshot and fileSnapshot: the escaped, wrapped serialized + // value, and the stored value it is compared with (snapshot: the test, + // the value, the snapshot file; fileSnapshot: the value, the path). + // Both read or, in update mode, record; missing and unreadable + // snapshots throw Node's ERR_INVALID_STATE. + "test.snapshotValue": { argTypes: [STRING], result: STRING }, + "test.ctxSnapshot": { argTypes: [TESTCTX_T, STRING, STRING], result: STRING }, + "test.fileSnapshot": { argTypes: [STRING, STRING], result: STRING }, + // Node's serializeValue over a dyn array of serializers (undefined for + // the default JSON.stringify(value, null, 2)); a throw becomes Node's + // "The provided serializers did not generate a string." ERR_INVALID_STATE. + "test.snapshotSerialize": { argTypes: [DYN, DYN], result: DYN }, + "test.mockTimersEnable": { argTypes: [STRING, F64, F64], result: VOID }, + "test.mockTimersTick": { argTypes: [F64], result: VOID }, + "test.mockTimersRunAll": { argTypes: [], result: VOID }, + "test.mockTimersReset": { argTypes: [], result: VOID }, + "test.mockTimersSetTime": { argTypes: [F64], result: VOID }, + "test.ctxMockTimersEnable": { argTypes: [TESTCTX_T, STRING, F64, F64], result: VOID }, + "test.ctxMockTimersTick": { argTypes: [TESTCTX_T, F64], result: VOID }, + "test.ctxMockTimersRunAll": { argTypes: [TESTCTX_T], result: VOID }, + "test.ctxMockTimersReset": { argTypes: [TESTCTX_T], result: VOID }, + "test.ctxMockTimersSetTime": { argTypes: [TESTCTX_T, F64], result: VOID }, + // node:http (scr_http.c over scr_net.c). The handler and the data + // listeners are program-dependent closures (null slots, checked in the + // libCall case); reqHeader's result is the interned string|undefined + // union, checked like process.envGet. + /** node:http, the server slice (scr_http.c over scr_net.c — linked + * only when these appear on the IR; moduleUsesHttpServer is the + * switch, and any http.* libCall also counts as net use so scr_net.c + * links and installs). http.createServer's handler MOVES in and takes + * the runtime adapters for its (req, res) / (req) / () shapes; req + * body listeners follow the net.sockOnData story (bytes chunks, once + * flags); reqHeader answers the interned `string | undefined` union + * exactly like process.envGet; the res writers are borrowed-argument + * voids with Node's framing decided in the runtime (Content-Length + * for end-before-head, chunked after an explicit writeHead/write). */ + "http.createServer": { argTypes: [null], result: NETSERVER_T }, + /** http.createServer() / http.Server() with no handler — the + * on("request") route; option helpers set parser behavior on a fresh + * server before the constructor/factory result is returned. */ + "http.createServerEmpty": { argTypes: [], result: NETSERVER_T }, + "http.validateHeaderName": { argTypes: [STRING, STRING], result: VOID }, + "http.validateHeaderValue": { argTypes: [STRING, DYN], result: VOID }, + "http.serverJoinDupHeaders": { argTypes: [NETSERVER_T], result: VOID }, + "http.serverAllowMissingHostHeader": { argTypes: [NETSERVER_T], result: VOID }, + /** The five writable numeric http.Server timeout fields use one + * selector ABI: 0 timeout, 1 keepAliveTimeout, 2 headersTimeout, + * 3 requestTimeout, 4 keepAliveTimeoutBuffer. These calls store/read + * the property values; timer enforcement remains outside this surface. + * The constructor-option setter takes dyn so explicit undefined remains + * distinguishable from a numeric value until its Node validation ladder. */ + "http.serverTimeoutGet": { argTypes: [NETSERVER_T, F64], result: F64 }, + "http.serverSetTimeout": { argTypes: [NETSERVER_T, F64], result: VOID }, + "http.serverSetTimeoutCb": { argTypes: [NETSERVER_T, F64, null], result: VOID }, + "http.serverOnTimeout": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + "http.serverCloseAllConnections": { argTypes: [NETSERVER_T], result: VOID }, + "http.serverCloseIdleConnections": { argTypes: [NETSERVER_T], result: VOID }, + "http.serverTimeoutSet": { argTypes: [NETSERVER_T, F64, F64], result: VOID }, + "http.serverTimeoutOptionSet": { argTypes: [NETSERVER_T, F64, DYN], result: VOID }, + /** server.on("listening", cb) — the deferred listen-callback list. */ + "net.serverOnListening": { + argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + /** The ServerResponse member surface: statusCode/statusMessage reads + * and assignments (Node's writable properties), the header CRUD trio + * (getHeader answers `string | undefined` like reqHeader), and + * end(cb)'s callback slot (resOnEndCallback registers, the end call + * follows — the callback fires deferred, just before Node's 'finish' + * emit; resOnFinish is the 'finish' listener slot). writeHead's + * statusMessage forms compose in interned helpers: resStatusMsgSet + * then the ordinary writeHead entry. */ + "http.resStatusGet": { argTypes: [HTTPRES_T], result: F64 }, + "http.resStatusSet": { argTypes: [HTTPRES_T, F64], result: VOID }, + "http.resStatusMsgGet": { argTypes: [HTTPRES_T], result: VOID }, + "http.resStatusMsgSet": { argTypes: [HTTPRES_T, STRING], result: VOID }, + "http.resRequest": { argTypes: [HTTPRES_T], result: HTTPREQ_T }, + "http.resSocket": { argTypes: [HTTPRES_T], result: VOID }, + "http.resWritableFinished": { argTypes: [HTTPRES_T], result: BOOL }, + "http.resSendDateGet": { argTypes: [HTTPRES_T], result: BOOL }, + "http.resSendDateSet": { argTypes: [HTTPRES_T, BOOL], result: VOID }, + "http.resStrictContentLengthGet": { argTypes: [HTTPRES_T], result: BOOL }, + "http.resStrictContentLengthSet": { argTypes: [HTTPRES_T, BOOL], result: VOID }, + "http.resSetTimeout": { argTypes: [HTTPRES_T, F64], result: VOID }, + "http.resSetTimeoutCb": { + argTypes: [HTTPRES_T, F64, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + // resGetHeader answers the interned `string | undefined` union — the + // reqHeader/envGet sentinel pattern (VOID here, checked specially). + "http.resGetHeader": { argTypes: [HTTPRES_T, STRING], result: VOID }, + /** getHeader as Node answers it: the number, string or string[] that was + * set, or undefined, as a checked-dynamic value. */ + "http.resGetHeaderDyn": { argTypes: [HTTPRES_T, STRING], result: DYN }, + "http.resGetHeaderNames": { argTypes: [HTTPRES_T], result: arrayOf(STRING) }, + "http.resGetRawHeaderNames": { argTypes: [HTTPRES_T], result: arrayOf(STRING) }, + "http.resGetHeaders": { argTypes: [HTTPRES_T], result: DYN }, + "http.resHasHeader": { argTypes: [HTTPRES_T, STRING], result: BOOL }, + "http.resRemoveHeader": { argTypes: [HTTPRES_T, STRING], result: VOID }, + "http.resOnFinish": { + argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "http.resOnEndCallback": { + argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "http.reqUrl": { argTypes: [HTTPREQ_T], result: STRING }, + "http.reqMethod": { argTypes: [HTTPREQ_T], result: STRING }, + "http.reqHeader": { argTypes: [HTTPREQ_T, STRING], result: VOID }, + "http.reqTrailer": { argTypes: [HTTPREQ_T, STRING], result: VOID }, + "http.reqHeaderValues": { argTypes: [HTTPREQ_T, STRING], result: VOID }, + "http.reqTrailerValues": { argTypes: [HTTPREQ_T, STRING], result: VOID }, + "http.reqOnData": { argTypes: [HTTPREQ_T, null, BOOL], result: VOID }, + "http.reqOnEnd": { + argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.resSetHeader": { argTypes: [HTTPRES_T, STRING, STRING], result: VOID }, + /** setHeader / appendHeader with a number, string or array value; the + * stored value keeps its type for getHeader/getHeaders. MAY THROW. */ + "http.resSetHeaderDyn": { argTypes: [HTTPRES_T, STRING, DYN], result: VOID }, + "http.resAppendHeader": { argTypes: [HTTPRES_T, STRING, DYN], result: VOID }, + "http.resWriteHead": { argTypes: [HTTPRES_T, F64], result: VOID }, + "http.resWriteHeadN": { + argTypes: [HTTPRES_T, F64, arrayOf(STRING), arrayOf(STRING)], + result: VOID, + }, + /** HTTP/1.1 informational heads; early hints takes flat string pairs, + * with a lowercase `link` key required to send anything. */ + "http.resWriteContinue": { argTypes: [HTTPRES_T], result: VOID }, + "http.resWriteProcessing": { argTypes: [HTTPRES_T], result: VOID }, + "http.resWriteEarlyHints": { argTypes: [HTTPRES_T, arrayOf(STRING)], result: VOID }, + "http.resWrite": { argTypes: [HTTPRES_T, STRING], result: VOID }, + "http.resWriteBytes": { argTypes: [HTTPRES_T, BYTES_U8], result: VOID }, + "http.resEnd": { argTypes: [HTTPRES_T], result: VOID }, + "http.resEndStr": { argTypes: [HTTPRES_T, STRING], result: VOID }, + "http.resEndBytes": { argTypes: [HTTPRES_T, BYTES_U8], result: VOID }, + /** The checked-dynamic chunk twins (the net.sockWriteDyn story). */ + "http.resWriteDyn": { argTypes: [HTTPRES_T, DYN], result: VOID }, + "http.resEndDyn": { argTypes: [HTTPRES_T, DYN], result: VOID }, + "http.resHeadersSent": { argTypes: [HTTPRES_T], result: BOOL }, + "http.resWritableEnded": { argTypes: [HTTPRES_T], result: BOOL }, + "http.resFlushHeaders": { argTypes: [HTTPRES_T], result: VOID }, + "http.resAddTrailers": { argTypes: [HTTPRES_T, arrayOf(STRING)], result: VOID }, + "http.resCork": { argTypes: [HTTPRES_T], result: VOID }, + "http.resUncork": { argTypes: [HTTPRES_T], result: VOID }, + "http.resWritableCorked": { argTypes: [HTTPRES_T], result: F64 }, + // The member follow-ups: reqStatusCode's `number | undefined` and + // sockRemoteAddress's `string | undefined` results are shape-checked in + // the special cases in validate.ts (like reqHeader/columns). + /** The server-surface member follow-ups: reqStatusCode answers the + * interned `number | undefined` union (negative = the undefined arm — + * a SERVER request, where Node's statusCode is undefined; every client + * response carries a real status); reqSocket is the underlying + * connection (+1, the same handle net.connect would give); + * sockRemoteAddress answers `string | undefined` (NULL after the fd + * closed, Node's destroyed-socket undefined; a dual-stack accept of an + * IPv4 peer reads "::ffff:a.b.c.d" like Node). reqResume/reqPause/reqDestroy and + * the req error/close listener slots complete the IncomingMessage + * surface portless's client responses use; resDestroy/resOnClose and + * resWriteHeadPairs ([k0,v0,k1,v1,...] — the env.pairs helper's flat + * shape) complete ServerResponse. sockSetTimeout arms the idle + * EVFILT_TIMER ('timeout' fires after ms of inactivity, once per idle + * period, never destroying the socket — Node's semantics). */ + "http.reqStatusCode": { argTypes: [HTTPREQ_T], result: VOID }, + // reqStatusMessage's `string | undefined` is program-interned like + // reqHeader's (the VOID here is the envGet sentinel). + "http.reqStatusMessage": { argTypes: [HTTPREQ_T], result: VOID }, + /** req.rawHeaders — [name, value, name, value, ...] in arrival order, + * names in their ORIGINAL case (Node's shape); a fresh string[] per + * read. reqStatusMessage answers the interned `string | undefined` + * union — the reason phrase on client responses ("" when the status + * line carried none), the undefined arm on server requests (the + * statusCode split). sockDestroyed is socket.destroyed — true once the + * fd is gone (destroy() or full close). */ + "http.reqRawHeaders": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, + /** The `{ ...req.headers }` snapshot feed: [lowercased name, value, + * ...] pairs in arrival order — the interned %headers.snapshot helper + * builds the record over it, exactly the process.envPairs pattern. */ + "http.reqHeaderPairs": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, + /** Every value of one header in arrival order (empty when absent): the + * set-cookie array of a `{ ...req.headers }` snapshot. */ + "http.reqHeaderList": { argTypes: [HTTPREQ_T, STRING], result: arrayOf(STRING) }, + "http.reqRawTrailers": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, + "http.reqTrailerPairs": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) }, + "net.sockDestroyed": { argTypes: [NETSOCKET_T], result: BOOL }, + /** socket.writable — the write half is open: no end() yet, no FIN sent, + * fd alive (connecting sockets answer true; writes queue). Node's + * stream flag. Borrows; never throws. */ + "net.sockWritable": { argTypes: [NETSOCKET_T], result: BOOL }, + // The 'upgrade' registrations: the callback shapes are program-typed + // ((req, socket, head) and shorter prefixes — the libCall case checks). + /** The 'upgrade' events, both sides (SEMANTICS.md — the WebSocket + * proxying surface): serverOnUpgrade registers (req, socket, head) + * listeners fired INSTEAD of 'request' for Connection: upgrade + * requests (the parser steps aside; the socket is raw; `head` carries + * bytes past the request head; no listener = the socket destroys, + * Node's default). clientOnUpgrade is the client twin: a 101 response + * fires (res, socket, head) INSTEAD of 'response'. */ + "http.serverOnUpgrade": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + /** server.on("connect", ...) — HTTP CONNECT tunneling, the 'upgrade' + * machinery's twin: (req, socket, head) fired INSTEAD of 'request' for + * CONNECT-method requests (no listener = the socket destroys, Node's + * default). The h2 compat server's 'connect' (portless's RFC 8441 + * handler) only ever sees the HTTP/1.1 arm under the allowHTTP1 + * lowering, so a listener whose second parameter is a UNION with a + * netSocket arm takes the socket wrapped at that arm (an emitted + * per-shape adapter — the tags are program data). */ + "http.serverOnConnect": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + "http.clientOnUpgrade": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, + "http.reqSocket": { argTypes: [HTTPREQ_T], result: NETSOCKET_T }, + // reqH2Stream's program-interned Http2Stream | undefined result is + // checked in the special libCall cases in validate.ts. + "http.reqH2Stream": { argTypes: [HTTPREQ_T], result: VOID }, + "http.reqH2StreamOrThrow": { argTypes: [HTTPREQ_T, STRING], result: HTTP2STREAM_T }, + /** req.pipe(dest) — the IncomingMessage body streaming into a + * ServerResponse (the proxy's response leg), a ClientRequest (the + * request-body forward), or a raw socket (the upgrade-rejection leg); + * chunk-for-chunk, natural end ends the destination (Node's pipe + * default; no backpressure — divergence 54's stream model). */ + "http.reqPipeRes": { argTypes: [HTTPREQ_T, HTTPRES_T], result: VOID }, + "http.reqPipeClient": { argTypes: [HTTPREQ_T, HTTPCLIENTREQ_T], result: VOID }, + "http.reqPipeSock": { argTypes: [HTTPREQ_T, NETSOCKET_T], result: VOID }, + "http.reqResume": { argTypes: [HTTPREQ_T], result: VOID }, + "http.reqPause": { argTypes: [HTTPREQ_T], result: VOID }, + "http.reqDestroy": { argTypes: [HTTPREQ_T], result: VOID }, + "http.reqOnError": { argTypes: [HTTPREQ_T, null, BOOL], result: VOID }, + "http.reqOnClose": { + argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.reqOnAborted": { + argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.reqHttpVersion": { argTypes: [HTTPREQ_T], result: STRING }, + "http.reqHttpVersionMajor": { argTypes: [HTTPREQ_T], result: F64 }, + "http.reqHttpVersionMinor": { argTypes: [HTTPREQ_T], result: F64 }, + "http.reqAborted": { argTypes: [HTTPREQ_T], result: BOOL }, + "http.reqComplete": { argTypes: [HTTPREQ_T], result: BOOL }, + "http.resDestroy": { argTypes: [HTTPRES_T], result: VOID }, + "http.resOnClose": { + argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.resWriteHeadPairs": { argTypes: [HTTPRES_T, F64, arrayOf(STRING)], result: VOID }, + /** writeHead(status, headers) with a checked-dynamic headers value — + * the runtime OBJ walk (string/number values; other values throw + * Node's invalid-argument TypeError; may throw). */ + "http.resWriteHeadDyn": { argTypes: [HTTPRES_T, F64, DYN], result: VOID }, + "net.sockSetTimeout": { argTypes: [NETSOCKET_T, F64], result: VOID }, + /** setEncoding('utf8') — 'data' delivers strings inside the chunk-encoding window; other real encodings throw the "not supported yet" Error, unknown names throw ERR_UNKNOWN_ENCODING (may throw). */ + "net.sockSetEncoding": { argTypes: [NETSOCKET_T, STRING], result: VOID }, + "http.reqSetEncoding": { argTypes: [HTTPREQ_T, STRING], result: VOID }, + "net.sockOnTimeout": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "net.sockRemoteAddress": { argTypes: [NETSOCKET_T], result: VOID }, + /** socket.encrypted — `boolean | undefined`: the true arm iff the + * socket carries a TLS transport (Node types `encrypted: true` on + * TLSSocket; plain sockets answer undefined — the proxy.ts isEncrypted + * idiom reads it through a cast). */ + "net.sockEncrypted": { argTypes: [NETSOCKET_T], result: VOID }, + // node:http, the client slice. The response callback shapes are + // program-dependent (checked by validate.ts); header pairs arrive flat. + /** node:http, the CLIENT slice (http.request/http.get over the net + * client machinery): request/requestCb take (host, port, path, method, + * timeoutMs, headerPairs, autoEnd[, responseCb]) — headerPairs is the + * flat [k0,v0,...] array (empty for none), autoEnd true is http.get's + * eager end(). The handle owns one dialed connection (NO pooling — the + * wire still carries Node's exact head: user headers, then Host, + * Connection: keep-alive, and the framing header; the socket closes + * when the response completes). The response delivered to responseCb / + * 'response' listeners IS an httpReq (IncomingMessage), status and + * headers parsed from the wire, body via reqOnData/reqOnEnd. Errors are + * Node-shaped ('connect ECONNREFUSED ip:port', 'socket hang up') and + * fire 'error' then 'close'; an unhandled 'error' exits 1 like every + * net handle. clientWrite before end commits to chunked framing unless + * the caller set content-length; clientEnd(data) before any write sends + * Content-Length exactly like Node. */ + "http.request": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL], + result: HTTPCLIENTREQ_T, + }, + "http.requestCb": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, null], + result: HTTPCLIENTREQ_T, + }, + /** new http.Agent(opts) / new https.Agent(opts): (secure, keepAlive, + * keepAliveMsecs, maxSockets, maxFreeSockets, timeoutMs, port) — the + * numeric options arrive < 0 for "unset" (Infinity/256/none; port + * seeds the settable defaultPort, Node's option merge). Returns the + * Agent as a checked-dynamic HANDLE (getName/destroy and the + * sockets/requests/freeSockets counters dispatch through the dyn + * handle ops). keepAlive: true THROWS the named construction fence — + * socket POOLING is not modeled (one dial per request); maxSockets + * accounting is real: over-limit requests defer their dial and queue. + * MAY THROW. */ + "http.agentNew": { argTypes: [BOOL, BOOL, F64, F64, F64, F64, F64, DYN], result: DYN }, + /** http.globalAgent / https.globalAgent (true): Node's keepAlive global + * agent, the one requests without an agent option use. */ + "http.globalAgent": { argTypes: [BOOL], result: DYN }, + /** http.request's `signal` option over the fresh request: an aborted + * signal destroys it with Node's AbortError now, a later abort event + * does the same. A falsy value is ignored; a non-signal throws + * ERR_INVALID_ARG_TYPE. */ + "http.clientSignal": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID }, + /** options.signal checked before the request is built (ERR_INVALID_ARG_TYPE). */ + "http.clientSignalCheck": { argTypes: [DYN], result: VOID }, + /** https.request's servername option, stored for the construction call + * that follows: the TLS name sent and verified, and part of the pooled + * socket key. The "https." prefix keeps the TLS unit linked. */ + "https.nextServername": { argTypes: [STRING], result: VOID }, + /** The maxHeaderSize option of createServer / http.request: Node's + * validateInteger (ERR_OUT_OF_RANGE) then the header size bound. + * MAY THROW. */ + "http.serverMaxHeaderSize": { argTypes: [NETSERVER_T, F64], result: VOID }, + "http.clientMaxHeaderSize": { argTypes: [HTTPCLIENTREQ_T, F64], result: VOID }, + /** req.destroy(err): err becomes the request's 'error'. */ + "http.clientDestroyError": { + argTypes: [HTTPCLIENTREQ_T, { kind: "object", className: "%Error" }], + result: VOID, + }, + /** The agent-threaded request rows: the http.request/https.request + * shape with a trailing `agent` dyn argument (an Agent handle, false — + * the one-shot Connection: close dial — or null/undefined for the + * default path). port < 0 means "no port option": the agent's settable + * defaultPort, then the scheme's. MAY THROW (a non-Agent value is + * Node's ERR_INVALID_ARG_TYPE). */ + "http.requestAgent": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, DYN], + result: HTTPCLIENTREQ_T, + }, + "http.requestAgentCb": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, DYN, null], + result: HTTPCLIENTREQ_T, + }, + /** request/get with a URL-STRING first argument: the runtime parses it + * (WHATWG) and dials — throws catchably on an unparsable input or a + * non-http scheme. */ + "http.requestUrl": { argTypes: [STRING, STRING, BOOL], result: HTTPCLIENTREQ_T }, + "http.requestUrlCb": { argTypes: [STRING, STRING, BOOL, null], result: HTTPCLIENTREQ_T }, + /** request/get with a runtime options record: a RequestOptions + * variable, or a literal that spreads one or sets socketPath. Args + * (options, autoEnd[, cb]). The runtime reads the record in the + * ClientRequest constructor's order (host/port or socketPath, path, + * method, headers, timeout, agent, signal, maxHeaderSize) with Node's + * errors, and documented options it does not implement throw the + * runtime fence. The "https." pair adds the TLS options. MAY THROW. */ + "http.requestDyn": { argTypes: [DYN, BOOL], result: HTTPCLIENTREQ_T }, + "http.requestDynCb": { argTypes: [DYN, BOOL, null], result: HTTPCLIENTREQ_T }, + "https.requestDyn": { argTypes: [DYN, BOOL], result: HTTPCLIENTREQ_T }, + "https.requestDynCb": { argTypes: [DYN, BOOL, null], result: HTTPCLIENTREQ_T }, + /** The paused-mode demux surface (portless's first-byte TLS peek: + * once('readable') + read(1) + unshift + emit('connection')). + * sockOnReadable registers a zero-param listener (a consumer: arrived + * bytes buffer instead of flowing, and EOF announces too); sockRead + * answers `Buffer | null` (exactly n buffered bytes, or null — Node's + * less-than-n answer; n <= 0 drains everything); sockUnshift returns + * bytes to the front of the stream; serverEmitConnection routes a + * socket into another server's protocol layer (a TLS target's + * 'connection' waits for its handshake). */ + "net.sockOnReadable": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, + // sockRead's result is the interned `Buffer | null` union — checked + // specially in validate.ts (the reqHeader/envGet pattern; VOID here is a + // placeholder the special case overrides). + "net.sockRead": { argTypes: [NETSOCKET_T, F64], result: VOID }, + "net.sockUnshift": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID }, + /** Socket flow control and the compat surface: pause/resume (reads + * gate off/on — kernel backpressure holds paused bytes; resume flows + * and discards sans listeners) and setNoDelay answer the SOCKET (+1, + * Node's chaining); destroySoon ends now and destroys once the FIN + * flushed; bytesWritten counts accepted bytes; readable is true until + * the read half ends. */ + "net.sockPause": { argTypes: [NETSOCKET_T], result: NETSOCKET_T }, + "net.sockResume": { argTypes: [NETSOCKET_T], result: NETSOCKET_T }, + "net.sockSetNoDelay": { argTypes: [NETSOCKET_T, BOOL], result: NETSOCKET_T }, + "net.sockDestroySoon": { argTypes: [NETSOCKET_T], result: VOID }, + /** RST close, answers the socket; throws ERR_INVALID_HANDLE_TYPE on TLS. */ + "net.sockResetAndDestroy": { argTypes: [NETSOCKET_T], result: NETSOCKET_T }, + /** socket.address(): { address, family, port } or {} as a dyn. */ + "net.sockAddress": { argTypes: [NETSOCKET_T], result: DYN }, + "net.sockBytesWritten": { argTypes: [NETSOCKET_T], result: F64 }, + /** remotePort (0), remoteFamily (1), localAddress (2), localPort (3), + * localFamily (4): dyn values, undefined when unknown. */ + "net.sockAddrField": { argTypes: [NETSOCKET_T, F64], result: DYN }, + "net.sockBytesRead": { argTypes: [NETSOCKET_T], result: F64 }, + "net.sockConnecting": { argTypes: [NETSOCKET_T], result: BOOL }, + "net.sockPending": { argTypes: [NETSOCKET_T], result: BOOL }, + "net.sockReadyState": { argTypes: [NETSOCKET_T], result: STRING }, + "net.sockSetKeepAlive": { argTypes: [NETSOCKET_T, BOOL, F64], result: NETSOCKET_T }, + "net.sockOnReady": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + /** Listeners with a parameter, as dyn functions: close(hadError), + * write's cb(err) and end's cb. */ + "net.sockOnCloseDyn": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID }, + /** socket.on/once('lookup', listener): the listener (a dyn function) + * gets (err, address, family, host) once per resolved address, or once + * with the lookup error, before the connect. */ + "net.sockOnLookup": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID }, + "net.sockOnWriteFlush": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.sockOnWriteFlushDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, + "net.sockOnFinishDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, + "net.sockSetTimeoutCb": { + argTypes: [NETSOCKET_T, F64, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.isIP": { argTypes: [DYN], result: F64 }, + "net.isIPv4": { argTypes: [DYN], result: BOOL }, + "net.isIPv6": { argTypes: [DYN], result: BOOL }, + "net.serverMaxConnectionsGet": { argTypes: [NETSERVER_T], result: DYN }, + "net.serverMaxConnectionsSet": { argTypes: [NETSERVER_T, DYN], result: VOID }, + "net.serverDropMaxConnectionGet": { argTypes: [NETSERVER_T], result: DYN }, + "net.serverDropMaxConnectionSet": { argTypes: [NETSERVER_T, DYN], result: VOID }, + /** net.SocketAddress and net.BlockList (scr_net.c): the constructors + * take the argument array; the handles are dyn values. */ + "net.socketAddressNew": { argTypes: [DYN], result: DYN }, + "net.socketAddressParse": { argTypes: [DYN], result: DYN }, + "net.isSocketAddress": { argTypes: [DYN], result: BOOL }, + "net.blockListNew": { argTypes: [DYN], result: DYN }, + "net.isBlockList": { argTypes: [DYN], result: BOOL }, + "net.blockListPrivateRanges": { argTypes: [], result: DYN }, + "net.serverOnDrop": { argTypes: [NETSERVER_T, DYN, BOOL], result: VOID }, + "net.serverGetConnections": { argTypes: [NETSERVER_T, DYN], result: VOID }, + "net.sockReadable": { argTypes: [NETSOCKET_T], result: BOOL }, + /** socket.on('finish', cb) / end(cb): fires once when the FIN goes out + * (sweep-deferred, never the registering stack). */ + "net.sockOnFinish": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.serverEmitConnection": { argTypes: [NETSERVER_T, NETSOCKET_T], result: VOID }, + // tls/https: cert/key/ca PEM arguments are strings OR Buffers (null = + // both accepted; the emitter passes data+len either way). + /** node:tls + node:https (scr_tls.c over scr_net.c/scr_http.c, with + * the vendored mbedTLS archive — linked only when one of these appears + * on the IR; moduleUsesTls is the switch, and every tls/https libCall + * also counts as net AND http use so both units link and install). + * tls.createServer takes (cert, key[, handler]) — cert/key are PEM + * strings or Buffers (the emitter passes data+len for either); the + * handler is Node's 'secureConnection' (fires post-handshake with a + * socket that behaves exactly like a net socket, the same adapters as + * net.createServer). https.createServer is (cert, key, handler) with + * the http request-handler adapters. https.request/requestCb extend + * the http client row with (…, rejectUnauthorized: bool, ca: PEM + * string/Buffer — "" for none) and default port 443; everything else + * (write/end/destroy/events, the response surface) IS the http client + * 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). + /** RUNTIME options records (the divergence-66 stance): the *Dyn + * creators take a checked-dynamic (dyn) options value whose members + * read at runtime — cert/key extract like the literal path, members + * whose literal forms fence THROW the catchable fence at runtime, and + * undocumented keys drop like Node drops them. tls.pemDyn is the + * literal walk's runtime-valued cert/key extraction: (value, whatLit) + * → PEM bytes (strings/Buffers/one-element arrays of those) or the + * thrown fence. All of them may throw. */ + "tls.pemDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, + "tls.createServerDyn": { argTypes: [DYN], result: NETSERVER_T }, + "tls.createServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T }, + "https.createServerDyn": { argTypes: [DYN], result: NETSERVER_T }, + "https.createServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T }, + /** createSecureServer(options, handler) — the eager COMPAT handler as + * the first 'request' listener (Node's exact route), on both literal + * flavors: Req is the dual ALPN allowHTTP1 server, H2Req the ALPN=h2-only + * server (both use compat handles over h2 streams). Args are + * (cert, key, cb, enableConnectProtocol). */ + "http2.createSecureServerReq": { argTypes: [null, null, null, BOOL], result: NETSERVER_T }, + "http2.createSecureServerH2Req": { argTypes: [null, null, null, BOOL], result: NETSERVER_T }, + /** createSecureServer with a RUNTIME options record (the divergence-66 + * stance): allowHTTP1/cert/key read at runtime — allowHTTP1 picks the + * flavor, the TLS server walk fences its out-of-bounds members with + * the catchable runtime fence, and h2 session-tuning keys drop exactly + * like the literal walk ignores them. May throw. The Cb form carries + * the eager compat handler. */ + "http2.createSecureServerDyn": { argTypes: [DYN], result: NETSERVER_T }, + "http2.createSecureServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T }, + // tls.connect(port, host, opts[, cb]) — port -1 / host "" read the + // options record; the callback fires post-handshake (secureConnect). + /** tls.connect — the TLS client socket: (port, host, opts[, cb]) where + * port -1 reads options.port, host "" reads options.host, and opts is + * the runtime (dyn) options record (rejectUnauthorized/ca/servername + * implemented; other documented members throw the runtime fence). The + * callback fires post-handshake — Node's secureConnect timing. */ + "tls.connect": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, + "tls.connectCb": { argTypes: [F64, STRING, DYN, null], result: NETSOCKET_T }, + // The TLSSocket member surface (authError's `string | null` union is + // shape-checked in the special cases in validate.ts — the reqHeader pattern). + /** The TLSSocket member surface on the socket kind: authorized (bool — + * Node's verify verdict), authorizationError (the verify-failure CODE + * STRING or null), the 'secureConnect' registration (a TLS socket's + * conn list fires at establishment; plain sockets never fire it), and + * the 'session' registration (fires once with the serialized session — + * a Buffer; the received-ticket event). */ + "tls.sockAuthorized": { argTypes: [NETSOCKET_T], result: BOOL }, + /** getProtocol() (string | null), getCipher() (CipherNameAndProtocol, + * undefined before the handshake, null after close) and alpnProtocol + * (string | false | null), as dyn values. */ + "tls.sockGetProtocol": { argTypes: [NETSOCKET_T], result: DYN }, + "tls.sockGetCipher": { argTypes: [NETSOCKET_T], result: DYN }, + "tls.sockAlpnProtocol": { argTypes: [NETSOCKET_T], result: DYN }, + /** getPeerCertificate(detailed) and getCertificate() (the legacy object, + * {} or null; scr_tls_x509.c), and the DER behind getPeerX509Certificate() + * and getX509Certificate() (a Buffer, empty without a certificate). */ + "tls.sockGetPeerCertificate": { argTypes: [NETSOCKET_T, DYN], result: DYN }, + "tls.sockGetCertificate": { argTypes: [NETSOCKET_T], result: DYN }, + "tls.sockPeerCertDer": { argTypes: [NETSOCKET_T], result: BYTES_U8 }, + "tls.sockOwnCertDer": { argTypes: [NETSOCKET_T], result: BYTES_U8 }, + "tls.sockAuthError": { argTypes: [NETSOCKET_T], result: VOID }, + "tls.sockOnSecureConnect": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + 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 + * answer with; cert/key are PEM strings or Buffers like createServer's. */ + "tls.createSecureContext": { argTypes: [null, null], result: SECURECTX_T }, + /** createSecureContext over a RUNTIME options record (the checked- + * dynamic lane): Node's typed option validations first (the ciphers/ + * passphrase/engine/version/timeout/ticketKeys ladders), then the pem + * walk — a validated { cert, key } bag builds the real context. + * May-throw. */ + "tls.createSecureContextDyn": { argTypes: [DYN], result: SECURECTX_T }, + /** server.addContext(servername, options | SecureContext), + * server.setSecureContext(options) and the 'tlsClientError' listener of + * a tls or https server (scr_tls.c). */ + "tls.serverAddContext": { argTypes: [NETSERVER_T, DYN, DYN], result: VOID }, + "tls.serverAddContextCtx": { argTypes: [NETSERVER_T, DYN, SECURECTX_T], result: VOID }, + "tls.serverSetSecureContext": { argTypes: [NETSERVER_T, DYN], result: VOID }, + "tls.serverOnClientError": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + /** tls.getCACertificates(type): validateString + the documented name + * set, then the trailing compiler-rendered fence — ALWAYS THROWS (the + * error.nodeThrow polymorphic-result carve-out). May-throw seed. */ + "tls.caCertsChk": { argTypes: [DYN, STRING], result: VOID }, + // The CA-store introspection unit (scr_tls_ca.c): per-type cached PEM + // string arrays and the default-set replacement. + /** The CA-store introspection surface (scr_tls_ca.c — its own unit and + * link gate, "tlsca." NOT "tls.", so a getCACertificates-only binary + * never pulls mbedTLS): tlsca.get is tls.getCACertificates(type) — the + * cached per-type string[] of PEM blocks (identity-stable across calls, + * Node's caching; an unknown type throws Node's ERR_INVALID_ARG_VALUE + * TypeError); tlsca.root is the tls.rootCertificates value read; + * tlsca.set is tls.setDefaultCACertificates(certs) — replaces the + * default set (deduped) and the anchors the TLS client verifies + * against, throwing Node's ERR_CRYPTO_OPERATION_FAILED when no entry + * carries a certificate block. */ + "tlsca.get": { argTypes: [STRING], result: arrayOf(STRING) }, + "tlsca.root": { argTypes: [], result: arrayOf(STRING) }, + "tlsca.set": { argTypes: [arrayOf(STRING)], result: VOID }, + "https.createServer": { argTypes: [null, null, null], result: NETSERVER_T }, + // http2's allowHTTP1 compatibility server (divergence 57): cert/key + // like tls.createServer; the 'request' handler arrives separately via + // http.serverOnRequest (shape checked in the libCall case, like + // http.createServer's). sessionError callback shape is checked by validate.ts. + /** node:http2 allowHTTP1: one TLS server advertises h2 + http/1.1 and + * dispatches to the matching parser after ALPN. Request listeners mirror + * into both compatibility lists; the remaining session/req.stream rows + * below retain their explicitly limited behavior. */ + "http2.createSecureServer": { argTypes: [null, null, BOOL], result: NETSERVER_T }, + // The SNI-callback form: arg 2 is the JS SNICallback closure — a + // `(servername, cb) => void` func, or its `| undefined` union from the + // conditional-spread spelling (the libCall case checks the shape). + /** createSecureServer with an SNI callback: args are (cert, key, sniCb) + * where sniCb is the JS SNICallback — a `(servername, cb) => void` + * closure, or the `SNICallback | undefined` union from the conditional- + * spread spelling (`...(x ? { SNICallback: x } : {})`; the emitter + * unwraps the union — the undefined arm means "no callback", exactly + * the no-SNI server). The runtime parses each connection's ClientHello + * for the server_name extension BEFORE the TLS handshake begins, calls + * the callback with (servername, answer-closure), and resumes the + * handshake when the answer arrives — cb(err) tears the socket down + * without an error event (Node's 'tlsClientError' default), cb(null, ctx) serves + * ctx's cert/key, cb(null, undefined) serves the default pair. */ + "http2.createSecureServerSni": { argTypes: [null, null, null, BOOL], result: NETSERVER_T }, + // The ALPN=h2 server (createSecureServer without allowHTTP1): the real + // h2 session machinery behind the TLS handshake. + /** The REAL h2-over-TLS server (createSecureServer WITHOUT allowHTTP1): + * scr_http2_create_secure_server — the h2c session machinery behind an + * mbedTLS handshake whose ALPN advertises h2 alone (an http/1.1-only + * client fails the handshake with no_application_protocol, Node's + * h2-only split). args are (cert, key) — PEM strings or Buffers. */ + "http2.createSecureServerH2": { argTypes: [null, null, BOOL], result: NETSERVER_T }, + "http.serverOnRequest": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + /** server.on('checkContinue' | 'checkExpectation', (req, res)): the + * 'request' listener shape; the last argument picks checkExpectation. */ + "http.serverOnCheck": { argTypes: [NETSERVER_T, null, BOOL, BOOL], result: VOID }, + "http2.serverOnSessionError": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + /** The guarded absent-session compatibility call remains a no-op. */ + "http2.streamNoop": { argTypes: [], result: VOID }, + /** The UNGUARDED h2-only stream call (`req.stream.on(...)`): stream is + * undefined on every connection the allowHTTP1 lowering accepts — and + * on every HTTP/1.1 connection of Node's own allowHTTP1 server — so the + * call IS Node's member read on undefined: throws the exact catchable + * TypeError ("Cannot read properties of undefined (reading 'on')"). + * One arg: the read member's name (a string literal). Never returns. */ + "http2.streamUndefCall": { argTypes: [STRING], result: VOID }, + // The REAL h2c surface (scr_http2.c). Callback slots are null (the + // emitter picks the thunk); pairs slots are string arrays; the + // endStream tri-state and event flags ride as f64. + /** node:http2, the REAL h2c surface (scr_http2.c — frame codec + HPACK + * over the net loop; the design note atop that file has the story). + * Sessions and streams are first-class handle kinds; 'stream'/'response' + * payloads cross as flat [name, value, ...] pairs arrays and an EMITTED + * adapter closure builds the program-side headers record (the response + * :status rides separately as a number). */ + "http2.createServer": { argTypes: [], result: NETSERVER_T }, + "http2.createServerReq": { argTypes: [null], result: NETSERVER_T }, + "http2.serverOnStream": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + "http2.serverOnSession": { argTypes: [NETSERVER_T, null, BOOL], result: VOID }, + // connect(authority, reject, ca[, cb]): the TLS client knobs an https + // authority reads (inert on http ones, exactly Node); ca is a PEM + // string/Buffer ("" = the system anchors). + /** connect(authority[, listener]) — h2c prior knowledge; the listener + * closure (if any) is the 'connect' once-listener. */ + "http2.connect": { argTypes: [STRING, BOOL, null], result: HTTP2SESSION_T }, + "http2.connectCb": { argTypes: [STRING, BOOL, null, null], result: HTTP2SESSION_T }, + /** session.request(pairs?, endStream) — endStream is a tri-state f64: + * -1 the method's payload-meaningless default, 0/1 explicit. */ + "http2.sessionRequest": { + argTypes: [HTTP2SESSION_T, arrayOf(STRING), F64], + result: HTTP2STREAM_T, + }, + "http2.sessionClose": { argTypes: [HTTP2SESSION_T], result: VOID }, + "http2.sessionCloseCb": { argTypes: [HTTP2SESSION_T, null], result: VOID }, + "http2.sessionDestroy": { argTypes: [HTTP2SESSION_T], result: VOID }, + "http2.sessionOnClose": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, + "http2.sessionOnError": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, + "http2.sessionOnConnect": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, + "http2.sessionOnStream": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, + "http2.sessionOnGoaway": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID }, + "http2.sessionClosed": { argTypes: [HTTP2SESSION_T], result: BOOL }, + "http2.sessionDestroyed": { argTypes: [HTTP2SESSION_T], result: BOOL }, + "http2.sessionEncrypted": { argTypes: [HTTP2SESSION_T], result: BOOL }, + "http2.sessionType": { argTypes: [HTTP2SESSION_T], result: F64 }, + "http2.sessionAlpn": { argTypes: [HTTP2SESSION_T], result: STRING }, + "http2.sessionSocket": { argTypes: [HTTP2SESSION_T], result: NETSOCKET_T }, + /** stream.respond(pairs?, endStream) — the server answer. */ + "http2.streamRespond": { argTypes: [HTTP2STREAM_T, arrayOf(STRING), BOOL], result: VOID }, + "http2.streamWrite": { argTypes: [HTTP2STREAM_T, STRING], result: VOID }, + "http2.streamWriteBytes": { argTypes: [HTTP2STREAM_T, BYTES_U8], result: VOID }, + "http2.streamEnd": { argTypes: [HTTP2STREAM_T], result: VOID }, + "http2.streamEndStr": { argTypes: [HTTP2STREAM_T, STRING], result: VOID }, + "http2.streamEndBytes": { argTypes: [HTTP2STREAM_T, BYTES_U8], result: VOID }, + "http2.streamClose": { argTypes: [HTTP2STREAM_T, F64], result: VOID }, + "http2.streamCloseCb": { argTypes: [HTTP2STREAM_T, F64, null], result: VOID }, + "http2.streamDestroy": { argTypes: [HTTP2STREAM_T], result: VOID }, + "http2.sessionSettings0": { argTypes: [HTTP2SESSION_T], result: VOID }, + "http2.sessionSettings": { argTypes: [HTTP2SESSION_T, DYN], result: VOID }, + "http2.sessionSettingsDynCb": { argTypes: [HTTP2SESSION_T, DYN, DYN], result: VOID }, + "http2.sessionSettingsCb0": { + argTypes: [HTTP2SESSION_T, DYN, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "http2.sessionOnSettingsDyn": { argTypes: [HTTP2SESSION_T, DYN, BOOL, BOOL], result: VOID }, + "http2.sessionOnSettings0": { + argTypes: [HTTP2SESSION_T, { kind: "func", params: [], ret: VOID }, BOOL, BOOL], + result: VOID, + }, + "http2.sessionSettingsGet": { argTypes: [HTTP2SESSION_T, BOOL], result: DYN }, + "http2.sessionPendingSettingsAck": { argTypes: [HTTP2SESSION_T], result: BOOL }, + "http2.getDefaultSettings": { argTypes: [], result: DYN }, + "http2.streamSetEncoding": { argTypes: [HTTP2STREAM_T, STRING], result: VOID }, + "http2.streamSetEncodingRet": { argTypes: [HTTP2STREAM_T, STRING], result: HTTP2STREAM_T }, + "http2.streamResume": { argTypes: [HTTP2STREAM_T], result: VOID }, + "http2.streamPause": { argTypes: [HTTP2STREAM_T], result: VOID }, + "http2.streamOnData": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, + "http2.streamOnEnd": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, + "http2.streamOnClose": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, + "http2.streamOnAborted": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, + "http2.streamOnError": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, + "http2.streamOnResponse": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID }, + "http2.streamId": { argTypes: [HTTP2STREAM_T], result: F64 }, + "http2.streamRstCode": { argTypes: [HTTP2STREAM_T], result: F64 }, + "http2.streamDestroyed": { argTypes: [HTTP2STREAM_T], result: BOOL }, + "http2.streamClosed": { argTypes: [HTTP2STREAM_T], result: BOOL }, + "http2.streamAborted": { argTypes: [HTTP2STREAM_T], result: BOOL }, + "http2.streamPending": { argTypes: [HTTP2STREAM_T], result: BOOL }, + "http2.streamSession": { argTypes: [HTTP2STREAM_T], result: HTTP2SESSION_T }, + // The createConnection forms: arg 0 is the dialer closure (() => + // Socket — the libCall case checks), then path/method/timeout/headers/ + // autoEnd like http.request. + /** The createConnection form (the proxy's own dialer): args are + * (connCb, path, method, timeout, headers, autoEnd[, cb]) — connCb is + * a `() => net.Socket` closure the runtime invokes ONCE, synchronously + * (Node's onSocket timing); everything else matches http.request. The + * Host header defaults to "localhost" — a headers.host entry wins + * verbatim, the proxy shape. */ + "http.requestConn": { + argTypes: [null, STRING, STRING, F64, arrayOf(STRING), BOOL], + result: HTTPCLIENTREQ_T, + }, + "http.requestConnCb": { + argTypes: [null, STRING, STRING, F64, arrayOf(STRING), BOOL, null], + result: HTTPCLIENTREQ_T, + }, + "https.request": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null], + result: HTTPCLIENTREQ_T, + }, + "https.requestCb": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, null], + result: HTTPCLIENTREQ_T, + }, + /** The URL-string first argument, the http.requestUrl row over TLS: + * no options means Node's defaults (verification on, default trust + * anchors), and a non-https scheme is ERR_INVALID_PROTOCOL. */ + "https.requestUrl": { argTypes: [STRING, STRING, BOOL], result: HTTPCLIENTREQ_T }, + "https.requestUrlCb": { argTypes: [STRING, STRING, BOOL, null], result: HTTPCLIENTREQ_T }, + "https.requestAgent": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, DYN], + result: HTTPCLIENTREQ_T, + }, + "https.requestAgentCb": { + argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, DYN, null], + result: HTTPCLIENTREQ_T, + }, + // The requestFn binding's runtime-secure rows: https.request's shape + // with the leading `secure` bool. + /** A call through a `const requestFn = tls ? https.request : + * http.request` binding — the module-function-as-value ternary between + * the two known clients: the https.request row with a leading `secure` + * bool that picks the dial at RUNTIME (true = the TLS client, exactly + * https.request; false = the plain client, exactly http.request — + * rejectUnauthorized/ca ignored there like Node ignores TLS options on + * http.request). The "https." prefix keeps the TLS unit linked. */ + "https.requestFn": { + argTypes: [BOOL, STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null], + result: HTTPCLIENTREQ_T, + }, + "https.requestFnCb": { + argTypes: [BOOL, STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, null], + result: HTTPCLIENTREQ_T, + }, + "http.clientWrite": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, + "http.clientSetHeader": { argTypes: [HTTPCLIENTREQ_T, STRING, STRING], result: VOID }, + "http.clientGetHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, + "http.clientHasHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: BOOL }, + "http.clientRemoveHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, + "http.clientGetHeaderNames": { argTypes: [HTTPCLIENTREQ_T], result: arrayOf(STRING) }, + "http.clientGetRawHeaderNames": { argTypes: [HTTPCLIENTREQ_T], result: arrayOf(STRING) }, + "http.clientGetHeaders": { argTypes: [HTTPCLIENTREQ_T], result: DYN }, + "http.clientWriteBytes": { argTypes: [HTTPCLIENTREQ_T, BYTES_U8], result: VOID }, + "http.clientEnd": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, + "http.clientEndStr": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, + "http.clientEndBytes": { argTypes: [HTTPCLIENTREQ_T, BYTES_U8], result: VOID }, + /** The checked-dynamic chunk twins (the net.sockWriteDyn story). */ + "http.clientWriteDyn": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID }, + "http.clientEndDyn": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID }, + "http.clientFlushHeaders": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, + "http.clientAddTrailers": { argTypes: [HTTPCLIENTREQ_T, arrayOf(STRING)], result: VOID }, + "http.clientCork": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, + "http.clientUncork": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, + "http.clientWritableCorked": { argTypes: [HTTPCLIENTREQ_T], result: F64 }, + "http.clientMethod": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, + "http.clientPath": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, + "http.clientHost": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, + "http.clientProtocol": { argTypes: [HTTPCLIENTREQ_T], result: STRING }, + "http.clientHeadersSent": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, + "http.clientWritableEnded": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, + "http.clientWritableFinished": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, + "http.clientSocket": { argTypes: [HTTPCLIENTREQ_T], result: NETSOCKET_T }, + "http.clientReusedSocket": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, + "http.clientSetNoDelay": { argTypes: [HTTPCLIENTREQ_T, BOOL], result: VOID }, + "http.clientSetSocketKeepAlive": { argTypes: [HTTPCLIENTREQ_T, BOOL, F64], result: VOID }, + "http.clientSetTimeout": { argTypes: [HTTPCLIENTREQ_T, F64], result: VOID }, + "http.clientSetTimeoutCb": { + argTypes: [HTTPCLIENTREQ_T, F64, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "http.statusCodes": { argTypes: [], result: DYN }, + "http.methods": { argTypes: [], result: arrayOf(STRING) }, + "http.reqSetTimeout": { argTypes: [HTTPREQ_T, F64], result: VOID }, + "http.reqSetTimeoutCb": { + argTypes: [HTTPREQ_T, F64, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "http.clientDestroy": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, + "http.clientAbort": { argTypes: [HTTPCLIENTREQ_T], result: VOID }, + "http.clientAborted": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, + "http.clientDestroyed": { argTypes: [HTTPCLIENTREQ_T], result: BOOL }, + "http.clientOnResponse": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, + "http.clientOnSocket": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, + "http.clientOnFinish": { + argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.clientOnError": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID }, + "http.clientOnTimeout": { + argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.clientOnClose": { + argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.clientOnAbort": { + argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + "http.clientOnContinue": { + argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + /** execFileSync/execSync as ONE entry (args: cmd, argv, shell, input, + * cwd, hasEnv, envPairs, timeoutMs, stdoutMode, stderrMode, buffers — + * see scr_child.h). Throws Node's exact errors: "Command failed: " + * (+ captured stderr) on non-zero exit or signal death, "spawnSync + * ENOENT" on spawn failure, "spawnSync ETIMEDOUT" after + * the SIGTERM timeout, with Node's status, signal, output, pid, stdout + * and stderr properties (Buffer outputs when buffers is set: no + * encoding option). Result is the captured utf8 stdout (+1). */ + "cp.execSync": { + argTypes: [ + STRING, + arrayOf(STRING), + BOOL, + STRING, + BOOL, + STRING, + BOOL, + arrayOf(STRING), + F64, + F64, + F64, + BOOL, + ], + result: STRING, + }, + /** The promisified-execFile/exec capture (args: cmd, argv, shell, cwd, + * hasEnv, envPairs, timeoutMs; shell marks exec's /bin/sh -c argv, as + * for execSync, so failures display the command string): the same exec + * core in the async shape — both streams captured, no echo, Node's ASYNC messages on the throw paths + * ("Command failed: \n" with the unconditional newline, + * "spawn ENOENT" with .code, timeouts reporting as ordinary + * SIGTERM command failures — never ETIMEDOUT). Result reuses the + * ScrSpawnRes container (+1; stdout/stderr strings, status unused). + * Called only from the frontend's interned %execFileAsync and + * %execAsync ASYNC helpers, whose fiber turns the throw into the + * rejection. */ + "cp.execCapture": { + argTypes: [STRING, arrayOf(STRING), BOOL, STRING, BOOL, arrayOf(STRING), F64], + result: SPAWNRES_T, + }, + /** child_process.spawn (scr_child.c + the scr_async.c loop): posix_spawnp + * with per-slot ignore/inherit/pipe modes (and number output fds), the + * all-piped Node default, and the child registered with the event loop, + * which polls waitpid(WNOHANG) at quiescence like timers. + * NEVER throws: spawn failure defers to the "error" event, Node-exact + * (the error message is Node's "spawn "; an "error" + * event with no listener prints it and exits 1 like an EventEmitter). + * cmd/args borrowed; result an owned (+1) child handle. The loop will + * not exhaust while any spawned child is unreaped — Node's keep-alive. + * + * child.onExit / child.onClose / child.onError — terminal listener + * registration through child.on/once. Close uses the exit adapter shape + * but fires only after every piped stdio handle reaches EOF. The + * receiver is borrowed, the CALLBACK MOVES into the child's listener + * registry (released after the terminal event fires, or at reap for + * the event that never fires). Both are void (chaining is fenced). + * onExit's third emitted ingredient is an ADAPTER the backend interns + * per callback shape: the runtime invokes adapter(cb, has_code, code) + * and the adapter builds the `number | null` union (tags are program- + * dependent) or ignores the code for a zero-param callback. onError's + * adapters are runtime-provided (zero-param, or the %Error one-param + * shape — scr_error_new needs no program types). "exit" fires once + * with the code (f64 arm) or null (signal death); "error" fires only + * for spawn failure, exactly Node's split. */ + "cp.spawn": { argTypes: [STRING, arrayOf(STRING)], result: CHILD_T }, + /** Static-native fork startup and IPC. process.forkTarget initializes a + * re-executed child's inherited channel and returns its embedded target id + * (-1 in the parent). The remaining entries mirror Node's JSON channel. */ + "process.forkTarget": { argTypes: [F64], result: F64 }, + "cp.fork": { + argTypes: [F64, arrayOf(STRING), F64, F64, F64, BOOL, arrayOf(STRING), STRING], + result: CHILD_T, + }, + "child.connected": { argTypes: [CHILD_T], result: BOOL }, + "child.hasChannel": { argTypes: [CHILD_T], result: BOOL }, + "child.send": { argTypes: [CHILD_T, STRING], result: BOOL }, + "child.sendCb": { argTypes: [CHILD_T, STRING, null], result: BOOL }, + "child.disconnect": { argTypes: [CHILD_T], result: VOID }, + "child.onMessage": { argTypes: [CHILD_T, null, BOOL], result: VOID }, + "child.onDisconnect": { argTypes: [CHILD_T, null, BOOL], result: VOID }, + "child.onSpawn": { argTypes: [CHILD_T, null], result: VOID }, + /** Rest listeners use the checked function bridge to pack event arguments. */ + "child.onDyn": { argTypes: [CHILD_T, STRING, DYN], result: VOID }, + // Own properties JavaScript adds to children (handle-properties.ts). + "child.propsInstall": { argTypes: [], result: VOID }, + "child.propsDefine": { argTypes: [DYN, DYN, DYN], result: DYN }, + "child.propsDescriptor": { argTypes: [DYN, DYN], result: DYN }, + "child.propsResolve": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "child.propsOnDyn": { argTypes: [CHILD_T, STRING, DYN], result: VOID }, + "process.connected": { argTypes: [], result: BOOL }, + /** Whether this fork child opened its channel; part of process.send's + * presence in programs with fork targets. Never throws. */ + "process.ipcOpen": { argTypes: [], result: BOOL }, + "process.send": { argTypes: [STRING], result: BOOL }, + "process.sendCb": { argTypes: [STRING, null], result: BOOL }, + "process.disconnect": { argTypes: [], result: VOID }, + "process.onMessage": { argTypes: [null, BOOL], result: VOID }, + "process.onDisconnect": { argTypes: [null, BOOL], result: VOID }, + /** child_process.execFile's callback slice: starts an all-piped child, + * captures stdout/stderr, and moves the error-first callback into the + * child registry. The callback shape is program-dependent and checked + * by the validator/backends. The last argument is exec's shell flag: + * the argv is ["-c", command] and failure messages show the command. */ + "cp.execFile": { argTypes: [STRING, arrayOf(STRING), null, BOOL], result: CHILD_T }, + // spawn's options form: per-slot stdio modes (0 ignore / 1 inherit / + // 2 fd) with the out/err fds for mode 2, detached, env replacement + // pairs, cwd ("" = inherit). + /** spawn with options (scr_child.c): the cp.spawn core plus PER-SLOT + * stdio — args are (cmd, argv, inMode, outMode, errMode, outFd, errFd, + * detached, hasEnv, envPairs, cwd); modes 0 ignore (/dev/null), 1 + * inherit, 2 fd (out/err only — the fd dup2s into the child's slot, + * Node's stdio fd form; the daemon-log idiom ["ignore", logFd, logFd]). + * detached is POSIX_SPAWN_SETSID (the child gets its own session and + * process group, Node's semantics), env is a REPLACEMENT ([k,v,...] + * pairs like cp.execSync's), cwd ""=inherit. Same event/loop story as + * cp.spawn. */ + "cp.spawnOpts": { + argTypes: [ + STRING, + arrayOf(STRING), + F64, + F64, + F64, + F64, + F64, + BOOL, + BOOL, + BOOL, + arrayOf(STRING), + STRING, + ], + result: CHILD_T, + }, + /** Runtime options normalized into the native spawn core; unsupported + * process features retain explicit refusals. (command, args, options). */ + "cp.spawnDynamic": { argTypes: [STRING, arrayOf(STRING), DYN], result: CHILD_T }, + // The callback's func type is program-dependent (zero params, or the + // `number | null` union / the %Error class) — the libCall case checks + // the shape; the slot here only pins arity and the child receiver. + "child.onExit": { argTypes: [CHILD_T, null], result: VOID }, + "child.onClose": { argTypes: [CHILD_T, null], result: VOID }, + "child.onError": { argTypes: [CHILD_T, null], result: VOID }, + // Like process.envGet, spawnRes.status's result type is program-dependent + // (the interned `number | null` union) — the libCall case checks the arms. + "spawnRes.status": { argTypes: [SPAWNRES_T], result: VOID }, + // spawnRes.error's result is the interned `Error | undefined` union — + // the libCall case checks the arms, the spawnRes.status pattern. + /** Node's spawn-failure carrier `error?: Error`: a fresh +1 %Error + * ("spawnSync ENOENT", `code` stamped) when the spawn itself + * failed, the interned undefined arm otherwise — the result type is the + * call site's `Error | undefined` union, constructed type-directedly in + * the backend (the envGet convention). Never throws. */ + "spawnRes.error": { argTypes: [SPAWNRES_T], result: VOID }, + // spawnRes.signal's result is the interned `string | null` union — + // same pattern. + "spawnRes.signal": { argTypes: [SPAWNRES_T], result: VOID }, + // The ChildProcess lifecycle members: pid/exitCode are program-dependent + // unions (`number | undefined` / `number | null` — the libCall case + // checks the arms, the spawnRes.status pattern). + /** The ChildProcess lifecycle members (scr_child.c), Node's shapes + * exactly (SEMANTICS.md has the pinned matrix). child.pid is the + * checker's `number | undefined` (undefined = spawn failure) and + * child.exitCode its `number | null` (null while running and after a + * signal death; -errno once a spawn failure settled) — both unions are + * type-directed constructions in the backend over a has/get runtime + * pair, the spawnRes.status pattern. child.killed is Node's + * sent-a-signal flag. child.kill sends while the child is un-reaped + * (false after — Node's null-handle answer) and THROWS the + * Unknown-signal TypeError on bad names (may-throw); killNum passes + * numbers through (0 probes; never throws). child.unref drops the + * child from the loop's keep-alive set — it is still reaped while the + * loop runs for other reasons, and one the loop never reaps is left to + * the OS at exit. All receivers borrowed. */ + "child.pid": { argTypes: [CHILD_T], result: VOID }, + "child.exitCode": { argTypes: [CHILD_T], result: VOID }, + // The close-override pair: the bound close's result func type and the + // override's func argument are program-dependent (the callback union's + // tags) — the slots pin the server receiver; the libCall case checks + // the bound value's shape. + /** The close-override pair (the portless close-proxy idiom). + * serverCloseBind is `wrapper.close.bind(wrapper)` as a VALUE: a + * compiler-emitted closure over the server whose invocation runs the + * REAL close (scr_net_server_close_direct — never the override), so + * the override body's `origClose(cb)` cannot recurse; its callback + * argument (the `((err?: Error) => void) | undefined` union) registers + * as a once-'close' listener, a one-param callback wrapped by an + * emitted zero-arg trampoline firing the undefined arm (a clean close + * carries no error). serverSetCloseOverride is `wrapper.close = fn`: + * the override MOVES in behind an emitted zero-arg wrapper (it invokes + * the user function with the undefined-arm callback — tags are program + * data), and server.close() consults it before closing. */ + "net.serverCloseBind": { argTypes: [NETSERVER_T], result: VOID }, + "net.serverSetCloseOverride": { argTypes: [NETSERVER_T, null], result: VOID }, + // The piped-output stream reads: `Readable | null` unions (the libCall + // case checks the arms — the child.pid pattern with a ref arm). + /** The piped-output streams (stdio mode 3 — scr_child.c's stream + * slice). child.stdout/child.stderr answer the checker's + * `Readable | null` union (type-directed construction in the backend + * over the +1-or-NULL runtime pair — the child.pid pattern with a ref + * arm). stream.onData/onEnd register 'data'/'end' listeners (receiver + * borrowed, CALLBACK MOVES, trailing once-flag, void — chaining + * fenced): 'data' fires one Buffer chunk per read (zero-param and + * Buffer/string adapters are runtime-provided; setEncoding threads + * split multibyte sequences through the shared StringDecoder core. A + * union-param listener — + * ngrok's `Buffer | string` — gets a compiler-emitted adapter wrapping + * the chunk at its Buffer arm), 'end' fires once at EOF, always BEFORE + * the child's 'exit' (the pinned ordering). A flowing stream keeps the + * loop alive: usesTimers. */ + "child.stdout": { argTypes: [CHILD_T], result: VOID }, + "child.stderr": { argTypes: [CHILD_T], result: VOID }, + /** The piped-input writer (stdio mode 3 on fd 0). child.stdin answers + * `Writable | null`; writes copy borrowed data into the nonblocking + * queue, end/destroy settle it, writable is a pure state read, and + * listener callbacks move into the writer registry. */ + "child.stdin": { argTypes: [CHILD_T], result: VOID }, + /** Under the general stream representation (general-streams.ts), a + * stdio read (slot 0, 1 or 2) answers the child's bridged runtime stream + * — `Writable | null` for stdin, `Readable | null` otherwise; the bridge + * throws when the program lacks the stream unit's boxing helpers. */ + "child.stdioStream": { argTypes: [CHILD_T, F64], result: VOID }, + // Listener registrations: callback func shapes are program-dependent + // (zero-param / Buffer / string / unknown / Buffer-armed union) — the slots pin arity, + // the stream receiver, and the once-flag. + "stream.onData": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID }, + "stream.onDataStr": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID }, + "stream.onEnd": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID }, + "stream.childSetEncoding": { argTypes: [CHILDSTREAM_T, STRING], result: CHILDSTREAM_T }, + "writer.writeString": { argTypes: [CHILDWRITER_T, STRING], result: BOOL }, + "writer.writeBytes": { argTypes: [CHILDWRITER_T, BYTES_U8], result: BOOL }, + "writer.end": { argTypes: [CHILDWRITER_T], result: VOID }, + "writer.destroy": { argTypes: [CHILDWRITER_T], result: VOID }, + "writer.writable": { argTypes: [CHILDWRITER_T], result: BOOL }, + "writer.onDrain": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID }, + "writer.onFinish": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID }, + "writer.onError": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID }, + /** The first-class WritableStream write (`output.write(line)` — the + * prefixStream idiom): the receiver IS the fd scalar (process.stdout/ + * stderr reads mint 1/2), dispatched onto the exact stdoutWrite/ + * stderrWrite paths so buffering and ordering stay identical. Data + * borrowed; the bool is Node's always-true backpressure signal. */ + "procStream.write": { argTypes: [PROCSTREAM_T, STRING], result: BOOL }, + /** process.stdout/stderr flowing into a runtime Writable slot under the + * general stream representation: an fd-backed Writable (+1) whose + * writes take process.stdout.write's synchronous path. */ + /** A pipeline() stage that is a checked-dynamic value: a boxed native + * stream unwraps by identity (+1); anything else throws the given + * not-supported message. Typed as a Duplex so every stage position + * accepts it; the runtime pipeline checks the stream's real sides. */ + "stream.unboxDyn": { + argTypes: [DYN, STRING], + result: { kind: "object", className: "%Duplex" }, + }, + /** Whether pipeline() pumps a checked-dynamic first stage as an + * iterable (a string, or a value with an iterator method that is not a + * stream with pipe() and on()) instead of piping it as a stream. */ + "stream.dynIterableSource": { argTypes: [DYN], result: BOOL }, + /** 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; + * the result union is `%Readable | null` or `%Writable | null`. */ + "stream.dynNative": { argTypes: [DYN, F64], result: VOID }, + /** stream.dynNative for a pipe or unpipe destination: also a + * constructor-function stream's runtime stream, in programs that link + * that unit, unless it overrides write() or end(). */ + "stream.dynNativePipe": { argTypes: [DYN, F64], result: VOID }, + /** readable.pipe(destination, end) into a checked-dynamic destination + * that is not a native stream: [source, the source boxed, destination, + * end]. Node's pipe listener protocol over the destination's members + * (scr_stream_dyn.c); throws what those members throw. */ + "stream.pipeDyn": { argTypes: [null, DYN, DYN, BOOL], result: VOID }, + /** readable.unpipe(destination) for a checked-dynamic destination that + * is not a native stream: [source, destination]. Unpipes the pipe + * stream.pipeDyn registered into it, if any (scr_stream_dyn.c). */ + "stream.unpipeDyn": { argTypes: [null, DYN], result: VOID }, + "stream.stdioWritable": { + argTypes: [PROCSTREAM_T], + result: { kind: "object", className: "%Writable" }, + }, + "child.killed": { argTypes: [CHILD_T], result: BOOL }, + "child.kill": { argTypes: [CHILD_T, STRING], result: BOOL }, + "child.killNum": { argTypes: [CHILD_T, F64], result: BOOL }, + "child.unref": { argTypes: [CHILD_T], result: VOID }, + "child.ref": { argTypes: [CHILD_T], result: VOID }, + "spawnRes.stdout": { argTypes: [SPAWNRES_T], result: STRING }, + "spawnRes.stderr": { argTypes: [SPAWNRES_T], result: STRING }, + /** fs/promises (scr_lib.c over scr_async.c's settled minting): the SAME + * sync syscalls, wrapped in an ALREADY-SETTLED promise — failure + * REJECTS (catchable at the await) instead of throwing, so none of + * these are in the may-throw seed. readFile is utf8-fenced like + * readFileSync. The non-interleaving divergence is documented in + * SEMANTICS.md. */ + /** node:crypto randomness: randomUUID plus randomBytes as either a real + * Buffer or the fused randomBytes(n).toString("hex"|"base64") path. + * The size-taking forms throw Node's RangeError on invalid values. */ + "crypto.randomUUID": { argTypes: [], result: STRING }, + "crypto.randomBytesToString": { argTypes: [F64, STRING], result: STRING }, + /** crypto.randomBytes(n) → a real u8 Buffer (+1). THROWS Node's + * RangeError on out-of-range sizes, exactly like the composed + * randomBytesToString (which keeps its one-libCall lowering — the two + * coexist: the composed form never materializes the Buffer). */ + "crypto.randomBytes": { argTypes: [F64], result: BYTES_U8 }, + /** The fused createHash(alg).update(data).digest(enc) fast path and the + * one-shot crypto.hash implementation. First-class Hash/Hmac handles use + * the entries below; all paths share the incremental runtime core. */ + "crypto.hashDigestStr": { argTypes: [STRING, STRING, STRING], result: STRING }, + "crypto.hashDigestBytes": { argTypes: [STRING, BYTES_U8, STRING], result: STRING }, + /** First-class static Hash/Hmac handles. Constructors validate the + * runtime algorithm string (every digest Node 24 lists); update returns the same + * handle by retained identity, copy snapshots Hash state, and digest + * finalizes the handle and returns either a Buffer or encoded string. */ + "crypto.native": { argTypes: [], result: DYN }, + "crypto.hashNew": { argTypes: [STRING], result: CRYPTOHASH_T }, + /** The Transform a Hash or Hmac reads and writes as a stream (+1, + * created on first use and owned by the value; scr_stream.c). The Piped + * form, for pipe destinations and pipeline stages, also keeps the value + * alive until the stream flushes or is destroyed. */ + "crypto.hashStream": { argTypes: [null], result: { kind: "object", className: "%Transform" } }, + "crypto.hashStreamPiped": { + argTypes: [null], + result: { kind: "object", className: "%Transform" }, + }, + /** createCipheriv (false) / createDecipheriv (true) over checked-dynamic + * cipher, key, iv and options arguments (scr_cipher.c): a native + * Cipheriv/Decipheriv handle whose methods dispatch through dynInvoke. */ + "crypto.cipherNew": { argTypes: [BOOL, DYN, DYN, DYN, DYN], result: DYN }, + /** createECDH(curveName) over a checked-dynamic curve (scr_ecdh.c): a + * native ECDH handle whose methods dispatch through dynInvoke. */ + "crypto.ecdhNew": { argTypes: [DYN], result: DYN }, + /** 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 }, + /** 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 }, + /** getCipherInfo(nameOrNid, options) over checked-dynamic arguments + * (scr_cipher_info.c): the info object or undefined. */ + "crypto.getCipherInfo": { argTypes: [DYN, DYN], result: DYN }, + "crypto.checkPrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, + "crypto.generatePrimeSync": { argTypes: [DYN, DYN], result: DYN }, + "crypto.generatePrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, + "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 }, + "crypto.hashUpdateBytes": { argTypes: [CRYPTOHASH_T, BYTES_U8], result: CRYPTOHASH_T }, + "crypto.hmacUpdateStr": { argTypes: [CRYPTOHMAC_T, STRING], result: CRYPTOHMAC_T }, + "crypto.hmacUpdateBytes": { argTypes: [CRYPTOHMAC_T, BYTES_U8], result: CRYPTOHMAC_T }, + "crypto.hashCopy": { argTypes: [CRYPTOHASH_T], result: CRYPTOHASH_T }, + "crypto.hashDigestString": { argTypes: [CRYPTOHASH_T, STRING], result: STRING }, + "crypto.hashDigestBuffer": { argTypes: [CRYPTOHASH_T], result: BYTES_U8 }, + "crypto.hmacDigestString": { argTypes: [CRYPTOHMAC_T, STRING], result: STRING }, + "crypto.hmacDigestBuffer": { argTypes: [CRYPTOHMAC_T], result: BYTES_U8 }, + /** The option and any-value forms, with Node's checks at run time. The + * Dyn digests return a string or a Buffer, and crypto.hash's Dyn form + * either one by its outputEncoding. */ + "crypto.hashNewOpts": { argTypes: [STRING, DYN], result: CRYPTOHASH_T }, + "crypto.hmacNewDyn": { argTypes: [STRING, DYN, DYN], result: CRYPTOHMAC_T }, + "crypto.hashCopyOpts": { argTypes: [CRYPTOHASH_T, DYN], result: CRYPTOHASH_T }, + "crypto.hashUpdateEnc": { argTypes: [CRYPTOHASH_T, STRING, STRING], result: CRYPTOHASH_T }, + "crypto.hmacUpdateEnc": { argTypes: [CRYPTOHMAC_T, STRING, STRING], result: CRYPTOHMAC_T }, + "crypto.hashUpdateDyn": { argTypes: [CRYPTOHASH_T, DYN, DYN], result: CRYPTOHASH_T }, + "crypto.hmacUpdateDyn": { argTypes: [CRYPTOHMAC_T, DYN, DYN], result: CRYPTOHMAC_T }, + "crypto.hashDigestDyn": { argTypes: [CRYPTOHASH_T, DYN], result: DYN }, + "crypto.hmacDigestDyn": { argTypes: [CRYPTOHMAC_T, DYN], result: DYN }, + "crypto.hashOneshot": { argTypes: [STRING, DYN, DYN], result: DYN }, + /** Views of any element kind (the validator checks both are views). */ + "crypto.timingSafeEqual": { argTypes: [null, null], result: BOOL }, + "crypto.timingSafeEqualDyn": { argTypes: [DYN, DYN], result: BOOL }, + "crypto.randomFill": { argTypes: [BYTES_U8, F64, F64], result: BYTES_U8 }, + "crypto.randomFillRest": { argTypes: [BYTES_U8, F64], result: BYTES_U8 }, + "crypto.randomInt": { argTypes: [F64, F64], result: F64 }, + "crypto.randomIntMax": { argTypes: [F64], result: F64 }, + /** randomFillSync over any view returns that view at its own type (the + * validator checks the result against the first argument). The Dyn form + * takes ArrayBuffers and other values. The callback forms carry + * randomFill's argument count before the callback; randomIntCb carries + * whether min was given. */ + "crypto.randomFillView": { argTypes: [null, F64, F64], result: BYTES_U8 }, + "crypto.randomFillViewRest": { argTypes: [null, F64], result: BYTES_U8 }, + /** crypto.getRandomValues: a view returns itself at its own type (the + * float/size DOMExceptions are thrown at run time); any other value + * takes the checked form. */ + "crypto.getRandomValues": { argTypes: [null], result: BYTES_U8 }, + "crypto.getRandomValuesDyn": { argTypes: [DYN], result: DYN }, + "crypto.randomFillSyncDyn": { argTypes: [DYN, DYN, DYN], result: DYN }, + "crypto.randomFillCb": { argTypes: [null, F64, F64, F64, null], result: VOID }, + "crypto.randomFillCbDyn": { argTypes: [DYN, DYN, DYN, F64, null], result: VOID }, + "crypto.randomIntCb": { argTypes: [F64, F64, F64, null], result: VOID }, + "crypto.randomUUIDOpts": { argTypes: [DYN], result: STRING }, + /** Secret KeyObjects: native handles whose getters and methods dispatch + * through the handle. keyObjectInput reads a key's bytes for hkdf. */ + "crypto.createSecretKey": { argTypes: [DYN, DYN], result: DYN }, + "crypto.generateKeySync": { argTypes: [DYN, DYN], result: DYN }, + "crypto.generateKeyCb": { argTypes: [DYN, DYN, null], result: VOID }, + "crypto.keyObjectInput": { argTypes: [DYN], result: BYTES_U8 }, + "crypto.isKeyObject": { argTypes: [DYN], result: BOOL }, + /** 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.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 } }, + "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 }, + "crypto.pbkdf2Cb": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING, null], result: VOID }, + "crypto.hkdf": { argTypes: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64], result: DYN }, + "crypto.hkdfCb": { argTypes: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64, null], result: VOID }, + "crypto.scrypt": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN], result: BYTES_U8 }, + "crypto.scryptCb": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN, null], result: VOID }, + /** util.promisify over the callback forms: the callback form's + * arguments without the callback → a promise the callback form settles, + * or rejected with an argument error; never throws. The result's value + * type is the declared one (a Buffer, the filled view, a number, or a + * checked-dynamic value); the validator checks it. */ + "crypto.randomBytesPromise": { argTypes: [F64], result: VOID }, + "crypto.pbkdf2Promise": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: VOID }, + "crypto.scryptPromise": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN], result: VOID }, + "crypto.hkdfPromise": { argTypes: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64], result: VOID }, + "crypto.randomFillPromise": { argTypes: [null, F64, F64, F64], result: VOID }, + "crypto.randomFillPromiseDyn": { argTypes: [DYN, DYN, DYN, F64], result: VOID }, + "crypto.randomIntPromise": { argTypes: [F64, F64, F64], result: VOID }, + "crypto.generateKeyPromise": { argTypes: [DYN, DYN], result: VOID }, + /** util.promisify(dns.lookup) and util.promisify(dns.lookupService): + * [hostname, options] and [address, port] → a promise of the + * checked-dynamic result object (the address array with options.all), + * or rejected with the error; never throws. */ + "dns.lookupPromise": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "dns.lookupServicePromise": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + // The Buffer statics and the fs/zlib Buffer forms: fixed always-u8 + // signatures (Buffer IS a Uint8Array — one bytes kind). + /** The Buffer statics with fixed (always-u8) signatures. fromStr is + * `Buffer.from(string, enc)` — the frontend completes an omitted + * encoding to "utf8" and fences non-literal/unsupported ones; hex and + * base64 decode Node-leniently, so it never throws. concat takes ONE + * bytes[] arg (the list) and returns a fresh copy. `Buffer.from(u8)` + * and `Buffer.alloc(n)` need no libFn — they lower to bytesNew. */ + "buffer.fromStr": { argTypes: [STRING, STRING], result: BYTES_U8 }, + "buffer.fromStrChecked": { argTypes: [STRING, STRING], result: BYTES_U8 }, + "buffer.brand": { argTypes: [BYTES_U8], result: BYTES_U8 }, + /** Buffer.isBuffer over a typed view: whether the view carries the + * Buffer brand (any element kind; only Buffers do). Never throws. */ + "buffer.isBranded": { argTypes: [null], result: BOOL }, + /** Buffer.from on checked-native strings, bytes, arrays and data-only + * array-like/Buffer-JSON objects. The encoding is a normalized literal; + * non-string inputs ignore it. Copies the input and may throw during + * element coercion or argument validation. Custom object valueOf and + * opaque reference inputs retain explicit runtime refusals. */ + "buffer.fromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, + "buffer.concat": { argTypes: [arrayOf(BYTES_U8)], result: BYTES_U8 }, + "buffer.concatDyn": { argTypes: [DYN], result: BYTES_U8 }, + /** The pooled Buffer factories (Node's allocate/fromStringFast/ + * fromArrayLike): allocUnsafe, Buffer.from of strings, Uint8Arrays, + * number[] and checked values, and concat return views into the + * per-thread pool slab below Buffer.poolSize >>> 1 bytes. alloc and + * allocUnsafeSlow never pool. The size forms validate like Node + * (ERR_OUT_OF_RANGE). poolSize reads and writes Buffer.poolSize. */ + "buffer.alloc": { argTypes: [F64], result: BYTES_U8 }, + /** Buffer.alloc as a function value: the size, fill and encoding arrive + * unchecked, and the runtime fills as Node's alloc does. */ + "buffer.allocValue": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, + "buffer.allocUnsafe": { argTypes: [F64], result: BYTES_U8 }, + "buffer.allocUnsafeSlow": { argTypes: [F64], result: BYTES_U8 }, + "buffer.poolFromStr": { argTypes: [STRING, STRING], result: BYTES_U8 }, + "buffer.poolFromStrChecked": { argTypes: [STRING, STRING], result: BYTES_U8 }, + "buffer.poolFromBytes": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "buffer.poolFromArray": { argTypes: [arrayOf(F64)], result: BYTES_U8 }, + "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.from(value, byteOffset, length): an ArrayBuffer's range with + * Node's fromArrayBuffer checks; other values as poolFromDynOffset. */ + "buffer.poolFromDynRange": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, + "buffer.poolSize": { argTypes: [], result: F64 }, + "buffer.setPoolSize": { argTypes: [F64], result: VOID }, + /** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like + * fromStr's — and Buffer.isEncoding(name) over a runtime string + * (case-insensitive against Node's alias set). Pure; never throw. */ + "buffer.byteLenDyn": { argTypes: [DYN, STRING], result: F64 }, + /** Buffer.byteLength as a function value: the encoding arrives at run time. */ + "buffer.byteLenValue": { argTypes: [DYN, DYN], result: F64 }, + "buffer.byteLenStr": { argTypes: [STRING, STRING], result: F64 }, + "buffer.isEncoding": { argTypes: [STRING], result: BOOL }, + /** Buffer.concat(list, totalLength): the concatenation truncated or + * zero-padded to the total. THROWS Node's 'length' RangeError on a + * negative/non-integer total (may-throw seed). */ + "buffer.concatLen": { argTypes: [arrayOf(BYTES_U8), F64], result: BYTES_U8 }, + "buffer.concatLenDyn": { argTypes: [DYN, F64], result: BYTES_U8 }, + // The checked-dynamic compare/equals validators (Node's argument + // ladders over dyn-boxed invalid-input probes). + /** The checked-dynamic compare/equals validators (scr_bytes_io.c) — + * the lowered form when an argument is NOT statically bytes (the + * invalid-input probes; a dyn from an untyped JS helper): Node's own + * argument ladders run at runtime — ERR_INVALID_ARG_TYPE with the + * API's argument name ("buf1"/"buf2", "otherBuffer", "target"; offsets + * "of type number"), validateOffset's ERR_OUT_OF_RANGE for bad + * numbers, undefined offsets taking their Node defaults — and a + * well-typed value still computes the real answer. All args borrowed + * dyn; compareChk's four offset slots pass the undefined dyn when + * syntactically absent. May-throw seeds. */ + "buffer.compareChk": { argTypes: [DYN, DYN], result: F64 }, + "bytes.equalsChk": { argTypes: [BYTES_U8, DYN], result: BOOL }, + "bytes.compareChk": { argTypes: [BYTES_U8, DYN, DYN, DYN, DYN, DYN], result: F64 }, + /** The deprecated `new Buffer(number, 'enc')` string-arm rejection: + * always throws Node's ERR_INVALID_ARG_TYPE ("The \"string\" argument + * must be of type string. Received ..."). Borrowed dyn; may-throw. */ + "buffer.newStringFail": { argTypes: [DYN], result: BYTES_U8 }, + "buffer.deprecation": { argTypes: [], result: VOID }, + "buffer.deprecationCount": { argTypes: [], result: VOID }, + "buffer.constructor": { argTypes: [], result: DYN }, + "buffer.installValue": { argTypes: [], result: VOID }, + "buffer.staticValue": { argTypes: [STRING], result: DYN }, + "buffer.prototypeMember": { argTypes: [DYN], result: DYN }, + /** fs._toUnixTimestamp(time) over a dyn value: numeric strings and + * finite numbers coerce (negatives answer now/1000, Node's shape); + * everything else throws Node's ERR_INVALID_ARG_TYPE. May-throw. */ + "fs.toUnixTimestamp": { argTypes: [DYN], result: F64 }, + /** The fs argument-validation ladders (checked-dynamic lane): each Chk + * replicates its API's Node-order validation over dyn values (Node's + * exact typed errors — ERR_INVALID_ARG_TYPE/VALUE, ERR_OUT_OF_RANGE), + * and a full pass meets the trailing compiler-rendered SC2020 fence + * string — so the ALWAYS-THROW forms take the error.nodeThrow + * polymorphic-result carve-out. mkdtempSyncChk and the lchmod sync/ + * promise pair run the REAL operation on a validated pass instead + * (macOS lchmod(2); non-APPLE answers Node's not-a-function / + * ERR_METHOD_NOT_IMPLEMENTED shapes). May-throw seeds, all of them. */ + "fs.existsChk": { argTypes: [DYN, DYN], result: DYN }, + "fs.mkdtempChk": { argTypes: [DYN, DYN, STRING], result: VOID }, + "fs.mkdtempSyncChk": { argTypes: [DYN, DYN, STRING], result: STRING }, + "fs.readFileChk": { argTypes: [DYN, DYN, DYN, STRING], result: VOID }, + "fs.opendirChk": { argTypes: [DYN, DYN, STRING], result: VOID }, + "fs.watchFileChk": { argTypes: [DYN, DYN, STRING], result: VOID }, + "fs.lchmodChk": { argTypes: [DYN, DYN, DYN, STRING], result: VOID }, + "fs.lchmodSyncChk": { argTypes: [DYN, DYN], result: DYN }, + "fsp.lchmodChk": { argTypes: [DYN, DYN], result: { kind: "promise", inner: VOID } }, + "fs.readChk": { argTypes: [DYN, DYN, DYN, DYN, DYN, STRING], result: VOID }, + "fs.streamOptsChk": { argTypes: [DYN, DYN, STRING], result: VOID }, + /** fs.ReadStream / fs.WriteStream (scr_fs_stream.c): the constructors + * take the path and options as dyn values and throw Node's option + * errors; the members read bytesRead/bytesWritten, pending and the path + * value, and close() / close(cb) (a `() => void` callback). */ + "fs.createReadStream": { + argTypes: [DYN, DYN], + result: { kind: "object", className: "%Readable" }, + }, + "fs.createWriteStream": { + argTypes: [DYN, DYN], + result: { kind: "object", className: "%Writable" }, + }, + "fs.fsStreamBytes": { argTypes: [null], result: F64 }, + "fs.fsStreamPending": { argTypes: [null], result: BOOL }, + "fs.fsStreamPath": { argTypes: [null], result: DYN }, + "fs.fsStreamPropDyn": { argTypes: [null, STRING], result: DYN }, + "fileHandle.createReadStream": { + argTypes: [FILEHANDLE_T, DYN], + result: { kind: "object", className: "%Readable" }, + }, + "fileHandle.createWriteStream": { + argTypes: [FILEHANDLE_T, DYN], + result: { kind: "object", className: "%Writable" }, + }, + "fs.fsStreamClose": { argTypes: [null], result: VOID }, + "fs.fsStreamCloseCb": { argTypes: [null, funcOf([], VOID)], result: VOID }, + /** The compiler-resolved ERR_INVALID_ARG_TYPE throw with a RUNTIME- + * rendered Received tail: args [argname, "of type ..." clause, the + * offending dyn value]. ALWAYS THROWS; polymorphic result (the + * error.nodeThrow pattern). May-throw seed. */ + "error.argTypeThrow": { argTypes: [STRING, STRING, DYN], result: VOID }, + /** The property flavor of argTypeThrow ("The \"options.x\" property + * must be ..."): the option-bag ladders' provably-invalid arms. ALWAYS + * THROWS; polymorphic result. May-throw seed. */ + "error.propTypeThrow": { argTypes: [STRING, STRING, DYN], result: VOID }, + /** The checked-dynamic max-listeners ladders: setMaxChk is the + * instance form over a dyn n (non-numbers ERR_INVALID_ARG_TYPE, + * negatives/NaN ERR_OUT_OF_RANGE; +1 receiver back — chaining); + * setDefaultMaxChk is the static/property form, its second argument + * naming the message slot ("setMaxListeners" for the static call, + * "defaultMaxListeners" for the module-property assignment). */ + "emitter.setMaxChk": { argTypes: [null, DYN], result: VOID }, + "emitter.setDefaultMaxChk": { argTypes: [DYN, STRING], result: VOID }, + /** The Buffer forms of fs: readFileSync(path) with NO + * encoding → bytes (+1), writeFileSync/appendFileSync(path, bytes), and the + * fs/promises readFile(path) no-encoding form (an already-settled + * promise, rejecting on failure like the other fsp members). The sync + * forms THROW catchably on failure exactly like the utf8 forms. */ + "fs.readFileSyncBytes": { argTypes: [STRING], result: BYTES_U8 }, + "fs.writeFileSyncBytes": { argTypes: [STRING, BYTES_U8], result: VOID }, + "fs.appendFileSyncBytes": { argTypes: [STRING, BYTES_U8], result: VOID }, + "fsp.readFileBytes": { argTypes: [STRING], result: { kind: "promise", inner: BYTES_U8 } }, + /** node:zlib (scr_zlib.c — native-toolchain.ts compiles/links it ONLY when these + * appear on the IR, the regex/libcurl gating precedent): one-shot zlib, + * raw-DEFLATE, gzip, and auto-detect codecs over u8 bytes with Node's + * default options, or a validated literal compression level from -1 to 9. + * Compression never throws (OOM aborts); decompression + * of corrupt input THROWS Node's error catchably. */ + "zlib.deflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "zlib.inflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "zlib.deflateRawSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, + /** bytes, mode (0 zlib / 1 raw / 2 gzip), validated compression level. */ + "zlib.deflateLevelSync": { argTypes: [BYTES_U8, F64, F64], result: BYTES_U8 }, + "zlib.inflateRawSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "zlib.gzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "zlib.gunzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "zlib.unzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, + /** The default-options callback twins run on the executable's shared + * native work pool and deliver (Error | null, Buffer) on a later loop + * turn. crc32 validates its optional uint32 seed synchronously. */ + "zlib.deflateCb": { argTypes: [BYTES_U8, null], result: VOID }, + "zlib.inflateCb": { argTypes: [BYTES_U8, null], result: VOID }, + "zlib.deflateRawCb": { argTypes: [BYTES_U8, null], result: VOID }, + "zlib.inflateRawCb": { argTypes: [BYTES_U8, null], result: VOID }, + "zlib.gzipCb": { argTypes: [BYTES_U8, null], result: VOID }, + "zlib.gunzipCb": { argTypes: [BYTES_U8, null], result: VOID }, + "zlib.unzipCb": { argTypes: [BYTES_U8, null], result: VOID }, + "zlib.crc32": { argTypes: [BYTES_U8, F64], result: F64 }, + /** The one-shot forms with an options value: node_zlib_mode, the input, + * the options (validated at runtime with Node's ladder). The Cb form + * takes the program-shaped (Error | null, Buffer) callback, which moves. */ + "zlib.syncOpts": { argTypes: [F64, BYTES_U8, DYN], result: BYTES_U8 }, + "zlib.cbOpts": { argTypes: [F64, BYTES_U8, DYN, null], result: VOID }, + /** The checked-dynamic input twins (the JS lane's untyped data). */ + "zlib.syncOptsDyn": { argTypes: [F64, DYN, DYN], result: BYTES_U8 }, + "zlib.cbOptsDyn": { argTypes: [F64, DYN, DYN, null], result: VOID }, + /** util.promisify over the callback one-shots: [mode, data, options] + * like zlib.cbOpts → a promise the worker-pool run settles with the + * Buffer or the callback form's Error. An options or input error rejects + * it; never throws. The Dyn twin takes the JS lane's untyped data. */ + "zlib.promise": { + argTypes: [F64, BYTES_U8, DYN], + result: { kind: "promise", inner: BYTES_U8 }, + }, + "zlib.promiseDyn": { argTypes: [F64, DYN, DYN], result: { kind: "promise", inner: BYTES_U8 } }, + /** The stream classes (scr_zlib_stream.c, linked with the stream unit): + * streamNew takes Node's node_zlib_mode number and the options value + * (undefined when absent), validates it with Node's ladder, and answers + * the +1 %Transform stream. flush takes the kind value, params the level + * and strategy values; the Cb forms move their () => void callback. The + * receivers are borrowed. */ + /** Connects scr_brotli.c to the zlib engine (%main, before any module). */ + "zlib.brotliInstall": { argTypes: [], result: VOID }, + "zlib.streamNew": { argTypes: [F64, DYN], result: VOID }, + "zlib.streamBytesWritten": { argTypes: [null], result: F64 }, + /** `stream instanceof zlib.Gzip`: the borrowed stream and the class's + * node_zlib_mode; true when the stream is a zlib stream of that mode. */ + "zlib.streamIs": { argTypes: [null, F64], result: BOOL }, + "zlib.streamFlush": { argTypes: [null, DYN], result: VOID }, + "zlib.streamFlushCb": { argTypes: [null, DYN, null], result: VOID }, + "zlib.streamParams": { argTypes: [null, DYN, DYN], result: VOID }, + "zlib.streamParamsCb": { argTypes: [null, DYN, DYN, null], result: VOID }, + "zlib.streamClose": { argTypes: [null], result: VOID }, + "zlib.streamCloseCb": { argTypes: [null, null], result: VOID }, + "zlib.streamReset": { argTypes: [null], result: VOID }, + /** The Buffer overloads of the raw stream writes — same promptly + * submitted streams as process.stdoutWrite/stderrWrite, constantly true. + * The encoding arg is evaluated but ignored for bytes, like Node. The Cb + * forms move their program-shaped completion callback to the tick queue. */ + "process.stdoutWriteBytes": { argTypes: [BYTES_U8, STRING], result: BOOL }, + "process.stderrWriteBytes": { argTypes: [BYTES_U8, STRING], result: BOOL }, + // Completion callback is program-dependent: zero params, checked-dynamic, + // or an optional Error | null slot (same success shape as fs.rename). + "process.stdoutWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL }, + "process.stderrWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL }, + /** The value of a FULFILLED promise, read without a microtask turn (+1 + * for references): the tuple form of Promise.all reads its entries this + * way once the combinator fulfills, each entry widened to Promise + * and read at the type it was built with (program-dependent). */ + "promise.fulfilledValue": { argTypes: [null], result: VOID }, + /** Promise.all's tuple form as a value: the entries (each widened to + * Promise) join the runtime combinator, and when it fulfills the + * function reads the fulfilled entries into the tuple inside that + * settlement, so the result settles on the combinator's turn; a + * rejection settles it the same way. The result is the tuple's promise + * (program-dependent). */ + "promise.allTuple": { argTypes: [null, null], result: VOID }, + "fsp.readFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: STRING } }, + "fsp.writeFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, + /** fs.promises.writeFile(path, data, { mode }): the settled-promise + * twin of fs.writeFileModeSync. */ + "fsp.writeFileMode": { + argTypes: [STRING, STRING, F64], + result: { kind: "promise", inner: VOID }, + }, + "fsp.mkdir": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, + /** The fs/promises option/member tail the certs pipeline uses: mkdir's + * literal { recursive?, mode? } options (the mkdirSync matrix behind + * settled promises), unlink, chmod. Failures REJECT (catchable at the + * await), like the rest of the fsp family. */ + "fsp.mkdirMode": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, + "fsp.mkdirRecursive": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, + "fsp.mkdirRecursiveMode": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, + "fsp.unlink": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, + "fsp.chmod": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, + "fsp.chown": { argTypes: [STRING, F64, F64], result: { kind: "promise", inner: VOID } }, + "fsp.lchown": { argTypes: [STRING, F64, F64], result: { kind: "promise", inner: VOID } }, + "fsp.access": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, + "fsp.truncate": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } }, + "fsp.appendFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, + "fsp.appendFileBytes": { + argTypes: [STRING, BYTES_U8], + result: { kind: "promise", inner: VOID }, + }, + "fsp.copyFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, + "fsp.mkdtemp": { argTypes: [STRING], result: { kind: "promise", inner: STRING } }, + /** readFile(path, encoding | { encoding }) for any runtime encoding name. */ + "fsp.readFileEncoded": { + argTypes: [STRING, STRING], + result: { kind: "promise", inner: STRING }, + }, + "fsp.rmdir": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, + "fsp.link": { argTypes: [DYN, DYN], result: { kind: "promise", inner: VOID } }, + "fsp.symlink": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, + "fsp.cp": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, + "fsp.readlink": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "fsp.statfs": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + "fsp.readlinkStr": { argTypes: [DYN, DYN], result: { kind: "promise", inner: STRING } }, + "fsp.readlinkBuffer": { argTypes: [DYN, DYN], result: { kind: "promise", inner: BYTES_U8 } }, + "fsp.utimes": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, + "fsp.lutimes": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } }, + "fsp.rename": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, + "fsp.readdir": { argTypes: [STRING], result: { kind: "promise", inner: arrayOf(STRING) } }, + // Result is promise; the libCall case + // validates the program-dependent record shape in validate.ts. + /** `fs.promises.readdir(path, { withFileTypes: true })` — the settled- + * promise twin of fs.readdirTypesSync. The backends assemble the same + * call-site-shaped Dirent rows, then move the array into a promise; + * scandir failure becomes its rejection. */ + "fsp.readdirTypes": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, + "fsp.rm": { argTypes: [STRING], result: { kind: "promise", inner: VOID } }, + /** `fs.promises.rm(path, options)`: fs.rmOptsSync and fs.rmRetrySync + * behind a settled promise; their errors become its rejection. */ + "fsp.rmOpts": { argTypes: [STRING, BOOL, BOOL], result: { kind: "promise", inner: VOID } }, + "fsp.rmRetry": { + argTypes: [STRING, BOOL, BOOL, F64, F64], + result: { kind: "promise", inner: VOID }, + }, + "fsp.stat": { argTypes: [STRING], result: { kind: "promise", inner: STATS_T } }, + "fsp.realpath": { argTypes: [STRING], result: { kind: "promise", inner: STRING } }, + "fsp.lstat": { argTypes: [STRING], result: { kind: "promise", inner: STATS_T } }, + /** fs/promises.open and the statically represented FileHandle surface. + * Every operation returns an already-settled promise; syscall failures + * become rejections rather than escaping synchronously. read/write + * results are call-site record shapes ({ bytesRead/bytesWritten, + * buffer }) assembled by the backends around the fixed runtime ABI. */ + "fsp.open": { + argTypes: [STRING, STRING, F64], + result: { kind: "promise", inner: FILEHANDLE_T }, + }, + "fileHandle.fd": { argTypes: [FILEHANDLE_T], result: F64 }, + "fileHandle.close": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: VOID } }, + "fileHandle.sync": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: VOID } }, + "fileHandle.datasync": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: VOID } }, + "fileHandle.truncate": { + argTypes: [FILEHANDLE_T, F64], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.chmod": { argTypes: [FILEHANDLE_T, F64], result: { kind: "promise", inner: VOID } }, + "fileHandle.utimes": { + argTypes: [FILEHANDLE_T, DYN, DYN], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.readv": { + argTypes: [FILEHANDLE_T, arrayOf(BYTES_U8), F64], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.writev": { + argTypes: [FILEHANDLE_T, arrayOf(BYTES_U8), F64], + result: { kind: "promise", inner: VOID }, + }, + // read/write carry call-site result record shapes; the validator checks + // those in validate.ts, so promise is only a table sentinel. + "fileHandle.read": { + argTypes: [FILEHANDLE_T, BYTES_U8, F64, F64, F64, BOOL], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.writeBytes": { + argTypes: [FILEHANDLE_T, BYTES_U8, F64, F64, F64, BOOL], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.writeStr": { + argTypes: [FILEHANDLE_T, STRING, F64, STRING], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.readFile": { + argTypes: [FILEHANDLE_T, STRING], + result: { kind: "promise", inner: STRING }, + }, + "fileHandle.readFileBytes": { + argTypes: [FILEHANDLE_T, STRING], + result: { kind: "promise", inner: BYTES_U8 }, + }, + "fileHandle.writeFile": { + argTypes: [FILEHANDLE_T, STRING, STRING], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.writeFileBytes": { + argTypes: [FILEHANDLE_T, BYTES_U8, STRING], + result: { kind: "promise", inner: VOID }, + }, + "fileHandle.stat": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: STATS_T } }, + "process.argv": { argTypes: [], result: arrayOf(STRING) }, + // The Node runtime flag a node:test binary took out of process.argv + // (--test-update-snapshots), or [] — a fresh array per read. + "process.execArgv": { argTypes: [], result: arrayOf(STRING) }, + "process.platform": { argTypes: [], result: STRING }, + // The one libCall whose result type is program-dependent (union ids are + // per-module): the `result` here is a placeholder — the libCall case + // checks the union's ARMS ([string, undefinedT] in canonical order) + // against the module's registry instead. + /** getenv(3): one string key arg → the interned `string | undefined` + * union (present: +1 string wrapped into the string arm; absent: the + * interned undefined-arm instance). BOTH source forms — `process.env.FOO` + * and `process.env[expr]` — lower here. Purely static; never throws. */ + "process.envGet": { argTypes: [STRING], result: VOID }, + /** setenv(3): (name, value) string args → void. Later envGet reads and + * spawned children observe the write, like Node (values are strings — + * the frontend fences non-string RHS). Never throws. */ + "process.envSet": { argTypes: [STRING, STRING], result: VOID }, + /** `delete process.env[NAME]` — unsetenv(3): the mutation is visible to + * every later read (process.envGet asks getenv fresh) and inherited by + * spawned children, exactly Node. Statement position only (JS's boolean + * result is constant true there). Borrows the name; never throws. */ + "process.envUnset": { argTypes: [STRING], result: VOID }, + /** The whole environment as alternating [k0, v0, k1, v1, ...] strings in + * environ order — the raw material of the process.env SNAPSHOT record + * (the frontend's interned %env.snapshot helper keyed-writes the pairs + * into a fresh `{ [k: string]: string | undefined }` record). Fresh +1 + * array; never throws. */ + "process.envPairs": { argTypes: [], result: arrayOf(STRING) }, + "process.exit": { argTypes: [F64], result: VOID }, + /** Numeric process.exitCode write (integer validation, implicit exit status). */ + "process.setExitCode": { argTypes: [F64], result: VOID }, + /** The process.exitCode write of a program that also reads the + * property: the same validation, and the code counts as assigned. */ + "process.setExitCodeTracked": { argTypes: [F64], result: VOID }, + /** The code for process.exit() with no argument, or zero when unset. */ + "process.currentExitCode": { argTypes: [], result: F64 }, + /** Whether process.exitCode reads a code rather than undefined. Never throws. */ + "process.hasExitCode": { argTypes: [], result: BOOL }, + "process.cwd": { argTypes: [], result: STRING }, + /** getpid(2) / getuid(2): zero args → f64. POSIX-only target, so both + * always answer (the checker's `getuid?` optionality covers Windows — + * `process.getuid?.()` lowers as the plain call). Never throw. */ + "process.pid": { argTypes: [], result: F64 }, + "dyn.this": { argTypes: [], result: DYN }, + "dyn.generatorThis": { argTypes: [], result: DYN }, + "process.getuid": { argTypes: [], result: F64 }, + "process.getgid": { argTypes: [], result: F64 }, + /** geteuid(2) / getegid(2) / getgroups(2) (the effective gid appended + * when the list lacks it, like Node). Windows has no such members: the + * calls throw the TypeError a missing member does (MAY THROW). */ + "process.geteuid": { argTypes: [], result: F64 }, + "process.getegid": { argTypes: [], result: F64 }, + "process.getgroups": { argTypes: [], result: arrayOf(F64) }, + /** process.ppid: the parent's pid at the read. Never throws. */ + "process.ppid": { argTypes: [], result: F64 }, + /** process.title: libuv's process title (argv[0] until set); setTitle + * writes it (truncated to the original argument area on POSIX, the + * console title on Windows). Never throw. */ + "process.title": { argTypes: [], result: STRING }, + "process.setTitle": { argTypes: [STRING], result: VOID }, + /** process.argv0: the original argv[0]. Never throws. */ + "process.argv0": { argTypes: [], result: STRING }, + /** process.release: the compatibility target's stable release object + * (name, lts, sourceUrl, headersUrl, and libUrl on Windows). +1. */ + "process.release": { argTypes: [], result: DYN }, + /** process.execPath: the compiled binary's own resolved absolute path + * (one interned string, +1 per read) — the honest answer where Node's + * is the node executable's (SEMANTICS.md divergence 12). Never throws. */ + "process.execPath": { argTypes: [], result: STRING }, + /** process.arch: the compiled binary's OWN architecture ("arm64", + * "x64") — Node's answer for its own build on the same machine. + * Interned; +1 per read. Never throws. */ + "process.arch": { argTypes: [], result: STRING }, + /** Stable native version dictionary. node/openssl identify compatibility + * targets; components absent from the native runtime are omitted. */ + "process.versions": { argTypes: [], result: DYN }, + "process.builtinId": { argTypes: [DYN, arrayOf(STRING)], result: STRING }, + "process.builtinModule": { argTypes: [STRING, DYN], result: DYN }, + "process.builtinUnsupported": { argTypes: [STRING, STRING], result: DYN }, + /** A module namespace object as a checked-dynamic value + * (scr_module_namespace_value): the module id, its sorted export names + * and the compiled getter. One object per module. Never throws. */ + "module.namespaceValue": { argTypes: [STRING, arrayOf(STRING), DYN], result: DYN }, + "fs.callbackValue": { argTypes: [STRING], result: DYN }, + "fs.callbackCall": { argTypes: [STRING, DYN], result: DYN }, + "process.hrtimeValue": { argTypes: [], result: DYN }, + /** process.versions.node: the runtime's Node COMPATIBILITY TARGET — + * there is no Node under the binary, so this reports the version whose + * semantics the runtime implements (SEMANTICS.md divergence 60, the + * execPath stance). Interned; +1 per read. Never throws. */ + "process.versionsNode": { argTypes: [], result: STRING }, + "process.versionsOpenssl": { argTypes: [], result: STRING }, + /** kill(2) with Node's exact semantics and error shapes: the pid must be + * an int32 (else the ERR_INVALID_ARG_TYPE TypeError text), the named + * form resolves Node's signal-name table (unknown names throw the + * ERR_UNKNOWN_SIGNAL TypeError), an omitted signal completes to + * "SIGTERM" in the frontend, signal 0 probes, and a kill(2) failure + * throws Node's `kill ESRCH`/`kill EPERM` Error. Result is Node's + * constant true. */ + "process.kill": { argTypes: [F64, STRING], result: BOOL }, + "process.killNum": { argTypes: [F64, F64], result: BOOL }, + /** The raw byte writes: one borrowed string arg → bool (constantly true + * — this synchronous runtime never queues backpressure). stdoutWrite + * shares console.log's stream, each call is submitted promptly, and + * interleaved output keeps source order. No newline, no formatting, + * never throws. */ + "process.stdoutWrite": { argTypes: [STRING], result: BOOL }, + "process.stdio": { argTypes: [F64], result: DYN }, + "process.stderrWrite": { argTypes: [STRING], result: BOOL }, + // error.new's result and the receiver slots are builtin-error classes — + // program-dependent object types, checked in the libCall case. + /** The runtime-provided Error hierarchy's entry points (scr_error.c). + * error.new: one borrowed string arg (the message), result an owned (+1) + * builtin error instance — the result TYPE names which builtin class + * (backends derive the runtime kind from it). error.ctor: the + * super(message) call of an `extends Error` constructor — borrowed + * receiver (already allocated by the derived class's new) + borrowed + * message, void; the receiver's type names the builtin class whose name + * field to stamp. error.toString: borrowed `%Error`-typed receiver, +1 + * string in Node's "name: message" shape. Formatting can invoke a user + * message accessor, which can throw. */ + "error.new": { argTypes: [STRING], result: VOID }, + /** ECMAScript constructors with raw checked-dynamic message/options. + * Options retain cause presence; constructor calls borrow their args. + * Message coercion may throw before the cause is installed. */ + "error.newOptions": { argTypes: [DYN, DYN], result: VOID }, + "error.ctorOptions": { argTypes: [null, DYN, DYN], result: VOID }, + /** Borrowed Error receiver. cause returns an owned dyn value (undefined + * when absent); hasCause distinguishes absence from present undefined. */ + "error.cause": { argTypes: [null], result: DYN }, + "error.hasCause": { argTypes: [null], result: BOOL }, + /** Assignment borrows both operands and retains the new cause. */ + "error.setCause": { argTypes: [null, DYN], result: VOID }, + /** `err.code = s` on an error-hierarchy receiver: the code slot that + * error.code reads and coded throws stamp. Borrows both; never throws. */ + "error.setCode": { argTypes: [null, STRING], result: VOID }, + /** `err.code = v` with a value that is not statically a string: strings + * take the code slot, anything else clears it and sets the dynamic own + * property. Borrows both; never throws. */ + "error.setCodeDyn": { argTypes: [null, DYN], result: VOID }, + /** `Object.assign(err, ...sources)` onto an error-hierarchy receiver + * (args: the error, a dyn array of the sources): the own-member copy + * onto its checked-dynamic node, then the typed slots (name, message, + * stack, cause, code) re-read from it. Throws what the copy throws. */ + "error.assign": { argTypes: [null, DYN], result: VOID }, + /** `key in err` for a key no compiled class of the receiver declares + * (args: the error, the key): a runtime-set own property (Object.assign, + * throw-site fields, dynamic writes) or an Object.prototype member. */ + "error.hasKey": { argTypes: [null, STRING], result: BOOL }, + "error.hasCode": { argTypes: [null], result: BOOL }, + /** Whether the error's own `name` or `message` (the key) is an accessor + * a reflective definition installed. Borrows both; never throws. */ + "error.ownAccessor": { argTypes: [null, STRING], result: BOOL }, + /** That accessor's getter called on the error (owned result), and its + * setter called with the value (V8's TypeError without one). Borrow + * their arguments; throw what the accessor throws. */ + "error.accessorGet": { argTypes: [null, STRING], result: DYN }, + "error.accessorSet": { argTypes: [null, STRING, DYN], result: VOID }, + "error.defineCause": { argTypes: [null, DYN], result: VOID }, + "error.deleteCause": { argTypes: [null], result: VOID }, + /** The compiler-resolved Node-parity throw for always-throwing lowered + * arms (ERR_INVALID_THIS receivers, ERR_MISSING_ARGS arity ladders, + * the symbol-to-string TypeError): args are [error-kind f64 (the + * SCR_ERR_* index: 0 Error, 1 TypeError, 2 RangeError), code (empty = + * no code slot), message]. ALWAYS THROWS catchably; the result type is + * the replaced expression's own (never materialized — the + * global.undefRead pattern). May-throw seed. */ + "error.nodeThrow": { argTypes: [F64, STRING, STRING], result: VOID }, + /** JS ToString over a dyn value WITH the object protocol (a user + * toString/valueOf member is CALLED and its throw propagates; + * exhaustion throws "Cannot convert object to primitive value"; units + * render "null"/"undefined") — the WHATWG USVString conversions + * (URLSearchParams names/values). Borrowed dyn; +1 string. May-throw. */ + "dyn.toStringCoerce": { argTypes: [DYN], result: STRING }, + "dyn.stringConstructor": { argTypes: [DYN], result: STRING }, + "dyn.propertyKey": { argTypes: [DYN], result: DYN }, + "dyn.toNumeric": { argTypes: [DYN], result: DYN }, + "dyn.increment": { argTypes: [DYN, BOOL], result: DYN }, + /** JS ToNumber over a dyn value WITH the object protocol (number-hint + * valueOf/toString ordering; user throws propagate). Borrowed dyn; + * f64 result, or a throw. Used by statically lowered numeric coercions + * whose checker type remained any. */ + "dyn.numberConstructor": { argTypes: [DYN], result: F64 }, + "dyn.bigintConstructor": { argTypes: [DYN], result: BIGINT_T }, + "dyn.toNumberCoerce": { argTypes: [DYN], result: F64 }, + "dyn.add": { argTypes: [DYN, DYN], result: DYN }, + "dyn.arithmetic": { argTypes: [DYN, DYN, STRING], result: DYN }, + "dyn.compare": { argTypes: [DYN, DYN, STRING], result: BOOL }, + "dyn.bitwise": { argTypes: [DYN, DYN, STRING], result: DYN }, + "dyn.proxyNew": { argTypes: [DYN, DYN], result: DYN }, + /* Dynamic-tier values (jsval; two-tier design stage S3). The jsval pass + * (ir/jsval.ts) introduces these in dynamic-tier functions only; the + * runtime is scr_js_value.c. Arguments are borrowed and results owned, + * like every libCall. Boundary conversions: js.fromDyn (a cell keeps its + * identity), js.toDyn (an immediate is boxed), js.fromF64/fromBool/ + * fromStr/fromBigInt (the design's toJs: immediates, and strings and + * BigInts under a tag without a copy) and js.toF64 (fromJs to number: dynCheck's + * number check and TypeError). The operators are the dyn.* operators + * over jsvals, with identical JS semantics. */ + "js.fromDyn": { argTypes: [DYN], result: JSVAL }, + "js.toDyn": { argTypes: [JSVAL], result: DYN }, + "js.fromF64": { argTypes: [F64], result: JSVAL }, + "js.fromBool": { argTypes: [BOOL], result: JSVAL }, + "js.fromStr": { argTypes: [STRING], result: JSVAL }, + "js.fromBigInt": { argTypes: [BIGINT_T], result: JSVAL }, + "js.toF64": { argTypes: [JSVAL], result: F64 }, + "js.add": { argTypes: [JSVAL, JSVAL], result: JSVAL }, + "js.arithmetic": { argTypes: [JSVAL, JSVAL, STRING], result: JSVAL }, + "js.bitwise": { argTypes: [JSVAL, JSVAL, STRING], result: JSVAL }, + "js.compare": { argTypes: [JSVAL, JSVAL, STRING], result: BOOL }, + "js.toNumeric": { argTypes: [JSVAL], result: JSVAL }, + "js.increment": { argTypes: [JSVAL, BOOL], result: JSVAL }, + "js.toNumberCoerce": { argTypes: [JSVAL], result: F64 }, + // Always throws; the result is the READ's declared type (a typed dummy + // the unwind abandons) — the libCall case skips the result check. + /** A read of a `declare`d const NOTHING defines (the bundler-define + * pattern — __VERSION__): always throws the catchable ReferenceError + * Node raises at the access (" is not defined"). args[0] is the + * name; the result type is the read's declared type (a typed dummy the + * unwind abandons — the value never exists). */ + "global.undefRead": { argTypes: [STRING], result: VOID }, + "console.native": { argTypes: [], result: DYN }, + /** node:console Console instances (scr_console_native.c): new mints the + * handle over the stdout/stderr fds (1/2; 0 = a stream binds next) and + * the ignoreErrors flag; bind attaches a borrowed writable stream to + * target 0 (stdout) or 1 (stderr); write prints `text\n` to the + * selected target of an instance or the global console handle. */ + "console.new": { argTypes: [F64, F64, BOOL], result: DYN }, + "console.bind": { argTypes: [DYN, F64, null], result: VOID }, + "console.write": { argTypes: [DYN, STRING, BOOL], result: VOID }, + /** The global console's remaining members (scr_console_native.c; the + * group indentation itself lives with the writers in scr_console.c). + * Arguments are borrowed; string results are owned. noop is the + * emitter-dropped tail of a lowering whose effects sit in statements. + * label is `${label}` over a defaulted label (undefined -> "default"; + * a Symbol throws Node's TypeError). count prints "label: n". + * countReset answers the warning text for an unknown (or non-string) + * label, "" otherwise. time answers the duplicate-label warning text + * or ""; timeEnd/timeLog answer Node's formatted elapsed time, or "" + * for an unknown label (timeEnd drops the timer). group adds (+1) or + * removes (-1) one two-space indentation step. clear writes the + * cursor-home and erase-down sequences when stdout is a terminal. + * table renders and prints Node's table for a dyn value (undefined + * properties = all). dirDyn is console.dir's inspect of a dyn value + * (custom inspection off: a Buffer anywhere inside throws the fence). */ + "console.noop": { argTypes: [], result: VOID }, + "console.label": { argTypes: [DYN], result: STRING }, + "console.count": { argTypes: [STRING], result: VOID }, + "console.countReset": { argTypes: [DYN], result: STRING }, + "console.time": { argTypes: [STRING], result: STRING }, + "console.timeEnd": { argTypes: [STRING], result: STRING }, + "console.timeLog": { argTypes: [STRING], result: STRING }, + "console.group": { argTypes: [F64], result: VOID }, + "console.clear": { argTypes: [], result: VOID }, + "console.table": { argTypes: [DYN, DYN], result: VOID }, + "console.dirDyn": { argTypes: [DYN, F64], result: STRING }, + /** A native reference to the global object; known names retain value fences. */ + "global.native": { argTypes: [arrayOf(STRING)], result: DYN }, + // `X.name` through a class value: the arg is a program-dependent + // classval (a null slot; the libCall case checks the kind). + /** `X.name` through a class VALUE (scr_object.c): args[0] is a borrowed + * classval; the result is the class object's stored .name string, + * retained (+1 — the string is an interned immortal, so the retain is a + * no-op, kept for ownership uniformity). Never throws. A direct + * `C.name` on the class name itself folds to a strLit instead. */ + "class.name": { argTypes: [null], result: STRING }, + "error.ctor": { argTypes: [null, STRING], result: VOID }, + "error.toString": { argTypes: [null], result: STRING }, + /** SameValue over checked values, including NaN, signed zero and reference identity. */ + "dyn.sameValue": { argTypes: [DYN, DYN], result: BOOL }, + "error.stack": { argTypes: [null], result: STRING }, + "error.setStack": { argTypes: [null, STRING], result: VOID }, + "error.stackLimitGet": { argTypes: [], result: F64 }, + "error.stackLimitSet": { argTypes: [F64], result: VOID }, + // Receiver (any error-hierarchy object) and the program-dependent + // `string | undefined` result are checked in the libCall case. + /** NodeJS.ErrnoException's `.code` read: borrowed error-hierarchy + * receiver → the interned `string | undefined` union (type-directed + * construction in the backend, the process.envGet pattern) — the errno + * name where a throw site stamped one (fs, exec spawn/timeout, + * process.kill, the spawn 'error' event), the undefined arm everywhere + * else. Never throws. */ + "error.code": { argTypes: [null], result: VOID }, + // DOMException: construction takes the two dyn args (WebIDL's + // resolution runs in the runtime); the %DOMException result is a + // program-dependent object type, checked in the libCall case. The + // read surface takes the %DOMException receiver (a null slot — the + // libCall case checks the class name). + /** `new DOMException(message?, nameOrOptions?)` (scr_error.c): both args + * are borrowed dyn values (the lowering passes the dyn undefined for an + * absent argument, so WebIDL's optionality lives in one place). The + * runtime ToStrings the message ("" for absent/undefined), resolves the + * name — absent/undefined → "Error", a non-null object → ToString of its + * `name` member plus the `cause` own-property record, anything else → + * ToString — and stamps the legacy code from the WebIDL name table (0 + * off-table). Result is an owned (+1) %DOMException. Never throws (dyn + * ToString is total). */ + "error.newDom": { argTypes: [DYN, DYN], result: VOID }, + /** DOMException's own read surface (scr_error.c; %DOMException receivers + * only — borrowed). domCode: the legacy numeric code. domHasCause: the + * options form's own-property record (`'cause' in e`). domCause: the + * cause value, +1 (the dyn undefined when absent — matching Node's + * undefined read). None throws. */ + "error.domCode": { argTypes: [null], result: F64 }, + "error.domHasCause": { argTypes: [null], result: BOOL }, + "error.domCause": { argTypes: [null], result: DYN }, + /** structuredClone of a %DOMException receiver (scr_error.c): WebIDL + * serialization — name/message copy, the legacy code re-derives, cause + * does not serialize. args are the borrowed receiver and the borrowed + * options dyn value (the dyn undefined when absent — the shared + * validation throws Node's exact option errors; any non-empty transfer + * list throws DataCloneError, nothing static is transferable). Result + * +1 %DOMException. */ + "error.domClone": { argTypes: [null, DYN], result: VOID }, + /** `new AggregateError(errors, message?, options?)` (scr_json.c), in + * spec order: newAggregate installs the message and cause (both dyn, + * borrowed — the message conversion can throw) and returns an owned + * %AggregateError; aggregateSetErrors then stores the iterated list + * (borrowed receiver, the dyn array borrowed and retained). + * aggregateErrors reads the list (+1, the same array every read). */ + "error.newAggregate": { + argTypes: [DYN, DYN], + result: { kind: "object", className: "%AggregateError" }, + }, + "error.aggregateSetErrors": { argTypes: [null, DYN], result: VOID }, + "error.aggregateErrors": { argTypes: [null], result: DYN }, + /** `new SuppressedError(error, suppressed, message?)` (scr_json.c): all + * three dyn and borrowed; the message conversion can throw. Returns an + * owned %SuppressedError. */ + "error.newSuppressed": { + argTypes: [DYN, DYN, DYN], + result: { kind: "object", className: "%SuppressedError" }, + }, + /** `d instanceof TypeError` (and the other BUILTIN error classes) on a + * checked-dynamic value (scr_json.c): the from_error cache holds the + * dyn↔error identity edge, so the test resolves the runtime error and + * asks its vtable's stamped preorder interval — exact for every error + * that crossed the boundary. A dyn object that never came from an + * error (a hand-built {%error} literal) answers false: subclass + * identity is unknowable there (the root keeps dynTest's marker + * answer). args are the borrowed dyn and the SCR_ERR_* kind literal. + * Never throws. */ + "dyn.errInstanceof": { argTypes: [DYN, F64], result: BOOL }, + "weakMap.is": { argTypes: [DYN], result: BOOL }, + "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, 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 + * pushed (Node's listener receiver, a dyn OBJ method's object, an + * apply/call thisArg), or the undefined dyn singleton with none bound + * (the strict-mode plain-call answer, the old constant). Zero args → + * dyn (+1). Never throws. */ + "dyn.dataViewIs": { argTypes: [DYN], result: BOOL }, + "dyn.construct": { argTypes: [DYN, DYN, STRING], result: DYN }, + "arrayBuffer.constructor": { argTypes: [], result: DYN }, + "sharedArrayBuffer.new": { argTypes: [DYN], result: DYN }, + "sharedArrayBuffer.is": { argTypes: [DYN], result: BOOL }, + "arrayBuffer.new": { argTypes: [DYN], result: DYN }, + "ffi.argument": { argTypes: [DYN, STRING], result: DYN }, + "ffi.memoryModule": { argTypes: [DYN], result: DYN }, + "intl.segmenterNew": { argTypes: [], result: DYN }, + "intl.segmenterRefusing": { argTypes: [STRING], result: DYN }, + "arrayBuffer.is": { argTypes: [DYN], result: BOOL }, + "arrayBuffer.isView": { argTypes: [DYN], result: BOOL }, + "arrayBuffer.byteLengthGetter": { argTypes: [], result: F64 }, + "arrayBuffer.byteLengthDescriptor": { argTypes: [DYN], result: DYN }, + "arrayBuffer.viewU8C": { argTypes: [DYN, DYN, DYN], result: bytesOf("u8c") }, + "arrayBuffer.viewI8": { argTypes: [DYN, DYN, DYN], result: bytesOf("i8") }, + "arrayBuffer.viewU16": { argTypes: [DYN, DYN, DYN], result: bytesOf("u16") }, + "arrayBuffer.viewI16": { argTypes: [DYN, DYN, DYN], result: bytesOf("i16") }, + "arrayBuffer.viewU8": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, + "arrayBuffer.viewU32": { argTypes: [DYN, DYN, DYN], result: bytesOf("u32") }, + "arrayBuffer.viewI32": { argTypes: [DYN, DYN, DYN], result: bytesOf("i32") }, + "arrayBuffer.viewF16": { argTypes: [DYN, DYN, DYN], result: bytesOf("f16") }, + "arrayBuffer.viewF32": { argTypes: [DYN, DYN, DYN], result: bytesOf("f32") }, + "arrayBuffer.viewF64": { argTypes: [DYN, DYN, DYN], result: bytesOf("f64") }, + "arrayBuffer.viewDV": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, + /** Object.keys/values/entries over a CHECKED-DYNAMIC receiver + * (scr_json.c): the runtime walks the dyn node's own members in JS + * own-key order (array-index keys ascending first, then insertion + * order) and answers a dyn array (entries: an array of [key, value] + * pairs; values RETAIN the member nodes — reference semantics, like + * JS). Strings/arrays/bytes answer their index keys; other scalars an + * empty array; null/undefined throw Node's catchable TypeError + * ("Cannot convert undefined or null to object"). */ + "dyn.objKeys": { argTypes: [DYN], result: DYN }, + "dyn.forInKeys": { argTypes: [DYN], result: DYN }, + "dyn.forInKeyStrings": { argTypes: [DYN], result: arrayOf(STRING) }, + "dyn.forInStable": { argTypes: [DYN], result: BOOL }, + "dyn.objKeyStrings": { argTypes: [DYN], result: arrayOf(STRING) }, + /** Object.entries as a native [string, any][] (VOID: the tuple row's + * shape is program-dependent; the validator checks it). */ + "dyn.objEntryPairs": { argTypes: [DYN], result: VOID }, + "dyn.hasOwn": { argTypes: [DYN, STRING], result: BOOL }, + "dyn.propertyIsEnumerable": { argTypes: [DYN, STRING], result: BOOL }, + "dyn.assign": { argTypes: [DYN, DYN], result: DYN }, + "dyn.copyDataProperties": { argTypes: [DYN, DYN], result: DYN }, + "dyn.objectRest": { argTypes: [DYN, DYN], result: DYN }, + /** Variadic Object.assign over CHECKED-DYNAMIC targets (`Object.assign( + * {}, ...arr.map(f), tail)` — the option-table merge): the lowering + * builds one fresh dyn pack of sources (packPush retains a plain source + * in; packPushSpread flattens a spread source through the spread-call + * walk — V8's exact TypeError texts, the string arg spelling the spread + * expression for the nullish form), so every source evaluates and + * flattens BEFORE any copying (JS's ArgumentListEvaluation), then + * assignAll copies each pack element's own enumerable keys onto the + * target left to right and answers the TARGET (+1) — identity, like JS. + * assignAll throws Node's ToObject TypeError on a nullish target. */ + "dyn.packPush": { argTypes: [DYN, DYN], result: VOID }, + "dyn.packPushSpread": { argTypes: [DYN, DYN, STRING], result: VOID }, + /** The iterated-path spread twin: V8 spells the optimized apply-path + * texts (packPushSpread's — the expression named for nullish sources) + * only for the SINGLE LAST argument's spread; any other spread position + * drives the real iterator protocol, whose failure describes the VALUE + * ("object null is not iterable (cannot read property + * Symbol(Symbol.iterator))"). The frontend picks by position. */ + "dyn.packPushSpreadIter": { argTypes: [DYN, DYN], result: VOID }, + "dyn.reflectApply": { argTypes: [DYN, DYN, DYN], result: DYN }, + "dyn.reflectGet": { argTypes: [DYN, DYN, DYN], result: DYN }, + /** `using` / `await using` with a dyn value (scr_json.c): usingMethod + * runs the declaration's checks and answers what usingDispose calls at + * scope exit (undefined for a null or undefined value). The bool selects + * `await using`. Both throw Node's TypeErrors or the method's error. */ + "dyn.usingMethod": { argTypes: [DYN, BOOL], result: DYN }, + "dyn.usingDispose": { argTypes: [DYN, DYN], result: DYN }, + /** DisposableStack (scr_symbol.c): a new stack handle; whether a value is + * one; and `stack.use(value)` for a class instance, registering the + * compiler's disposal function for it. The last throws Node's + * ReferenceError on a disposed stack. */ + "disposable.stackNew": { argTypes: [], result: DYN }, + "disposable.stackIs": { argTypes: [DYN], result: BOOL }, + "disposable.stackUseCallback": { argTypes: [DYN, DYN, DYN], result: BOOL }, + "dyn.reflectSet": { argTypes: [DYN, DYN, DYN, DYN], result: BOOL }, + "dyn.reflectDefine": { argTypes: [DYN, DYN, DYN], result: BOOL }, + "dyn.abstractEq": { argTypes: [DYN, DYN], result: BOOL }, + "dyn.assignAll": { argTypes: [DYN, DYN], result: DYN }, - "dyn.objCreateNullProto": { argTypes: [], result: DYN }, - "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 }, - "dyn.classBasePrototype": { argTypes: [DYN], result: DYN }, - "dyn.classInherit": { argTypes: [DYN, DYN], result: DYN }, - /** EventEmitter (0), Stream (1), Readable (2), Writable (3), Duplex (4), - * Transform (5) and PassThrough (6) as untyped constructor values, one - * per thread (scr_dyn_builtin_class.c). */ - "dyn.builtinClass": { argTypes: [F64], result: DYN }, - /** The stream classes (2-6 above) with their stream half: constructor - * functions over them get native stream state - * (scr_dyn_builtin_stream.c). */ - "dyn.builtinStreamClass": { argTypes: [F64], result: DYN }, - /** `value instanceof C` for those constructors, over the value's - * prototype chain. */ - "dyn.builtinInstanceOf": { argTypes: [DYN, F64], result: BOOL }, - /** `value instanceof f` for an untyped constructor function: the - * prototype chain against f.prototype. */ - "dyn.functionInstanceOf": { argTypes: [DYN, DYN], result: BOOL }, - "dyn.classSuper": { argTypes: [DYN, DYN, DYN], result: VOID }, - "dyn.assignPrototype": { argTypes: [DYN, DYN, DYN], result: DYN }, - "dyn.definePrototypeProps": { argTypes: [DYN, DYN, DYN], result: DYN }, - "dyn.bagGet": { argTypes: [DYN, STRING, DYN], result: DYN }, - /** A class field's creation in its instance's property bag: adds the - * string key as a default data property when absent, answering false - * (nothing changed) when the bag needs the general defineProperty path. */ - "dyn.bagCreateField": { argTypes: [DYN, STRING], result: BOOL }, - "dyn.bagSet": { argTypes: [DYN, STRING, DYN, DYN], result: VOID }, - "dyn.objCreate": { argTypes: [DYN], result: DYN }, - // Names a compiled class's prototype object (args[0]) after the class - // (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.getPrototype for walks that compare each prototype with a program - // class's prototype: native builtin values end the walk (null). - "dyn.getPrototypeForIdentity": { argTypes: [DYN], result: DYN }, - "dyn.setPrototype": { argTypes: [DYN, DYN], result: DYN }, - "dyn.getOwnPropertyNames": { argTypes: [DYN], result: DYN }, - "dyn.ownKeys": { argTypes: [DYN], result: DYN }, - "dyn.getOwnPropertySymbols": { argTypes: [DYN], result: DYN }, - "dyn.getOwnPropertyDescriptors": { argTypes: [DYN], result: DYN }, - "dyn.preventExtensions": { argTypes: [DYN], result: DYN }, - "dyn.isExtensible": { argTypes: [DYN], result: BOOL }, - "dyn.seal": { argTypes: [DYN], result: DYN }, - "dyn.isSealed": { argTypes: [DYN], result: BOOL }, - "dyn.objValues": { argTypes: [DYN], result: DYN }, - /** Object.values as a native vector of checked values, for results the - * program consumes directly (for-of, index reads, read-only methods). */ - "dyn.objValueList": { argTypes: [DYN], result: arrayOf(DYN) }, - "dyn.objEntries": { argTypes: [DYN], result: DYN }, - /** structuredClone over the checked-dynamic tree (scr_json.c): the JSON-safe subset plus - * bytes (a fresh copy — a Buffer clones as a plain Uint8Array, like - * Node), deep. Functions and handle kinds throw the spec's catchable - * DataCloneError; CYCLES throw the scriptc fence (the checked-dynamic tree cannot - * represent them — Node clones cycles; documented divergence). The - * options dyn value validates with Node's exact errors (dictionary - * conversion, the transfer-sequence member; any non-empty transfer - * list throws DataCloneError). dyn.cloneMissing is the zero-argument - * call: always throws Node's TypeError [ERR_MISSING_ARGS] with Node's - * own (verbatim, doubly-wrapped) message. */ - "dyn.structuredClone": { argTypes: [DYN, DYN], result: DYN }, - /** The declaration-typed boundary's adoption (decl-projection.ts): - * `source` is the dynamic value a boundary cast just converted, and - * `capsule` a live capsule of the native value built from it. Every - * object or array of `source` that something else still references - * becomes, in place, a live capsule of the native value built from it. - * Throws the path TypeError, changing nothing, when a converted object - * is not plain data (a prototype, accessors, symbols, frozen state). */ - "dyn.declAdopt": { argTypes: [DYN, DYN], result: VOID }, - "dyn.cloneMissing": { argTypes: [], result: DYN }, - /** structuredClone with a NON-EMPTY transfer array of static values: - * nothing static is transferable, so the call always throws Node's - * catchable DataCloneError ("Found invalid value in transferList.") — - * lowered directly (the list's values need no dyn representation to - * fail). */ - "dyn.cloneTransferFail": { argTypes: [], result: DYN }, - /** The same refusal given the transfer list's first member, an - * ArrayBuffer: a Buffer pool slab gets Node's untransferable message. */ - "dyn.cloneTransferFailWith": { argTypes: [DYN], result: DYN }, - /** `new RegExp(pattern, flags?)` (scr_regex.c): a heap regex over the - * same libregexp engine the literals use. The pattern compiles EAGERLY - * so an invalid pattern (or an unknown flag letter) throws Node's - * catchable SyntaxError at construction — Node's message shape with - * libregexp's detail text (approximate fidelity; e.name exact). An - * empty pattern stores the spec's "(?:)" source. Both args borrowed - * strings (the lowering completes an absent flags to ""); result +1. - * The result TYPE is the regex kind, so the link switch pulls the - * engine exactly like a literal. */ - "regex.new": { argTypes: [STRING, STRING], result: REGEX }, - "regex.resetLastIndex": { argTypes: [REGEX, F64], result: VOID }, - "regex.newChecked": { argTypes: [DYN, DYN], result: REGEX }, - "regex.replaceCallback": { argTypes: [STRING, REGEX, DYN, BOOL], result: STRING }, - // Reads of capture args[1] of an execSpans/matchGroups result: - // `capture === args[2]`, Number(capture) and parseInt(capture, - // args[2]). Borrowed; never throw. - "regex.groupEquals": { argTypes: [arrayOf(STRING), F64, STRING], result: BOOL }, - "regex.groupNumber": { argTypes: [arrayOf(STRING), F64], result: F64 }, - "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) }, - // 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 }, - // The key of an element access whose index is undefined (the property - // named "undefined"; frontend/lowering/undefined-index.ts). - "array.undefinedKey": { argTypes: [], result: F64 }, - // node:events EventEmitter: receivers are emitter-hierarchy objects and - // the chaining forms (on/off/removeAll/setMax) return the receiver's - // own class — program-dependent object types, checked in the libCall - // case. emitter.emit is the one VARIADIC libCall: args beyond - // (recv, name) are the event's frontend-unified tuple, any borrowable - // kind — the count check admits a longer list for it alone. - /** node:events EventEmitter (scr_events_emitter.c, link-gated by - * moduleUsesEmitter). The receiver of every instance form is a borrowed - * emitter-hierarchy object (`%EventEmitter` or a user subclass — the - * backend reinterprets to ScrEmitter*, the identical prefix); event - * names are borrowed strings (compile-time literals — the frontend - * fences non-literals and unifies each event's argument tuple program- - * wide). The chaining forms (on/off/removeAll/setMax) return the - * receiver +1 typed as its static class, Node's `return this`. - * - * emitter.new: `new EventEmitter()` → a +1 bare emitter. emitter.ctor: - * super() into the prefix of an emitted subclass (borrowed receiver, - * void — allocation already initialized the prefix). emitter.on: - * (recv, name, cb /moves/, once, prepend) — the backend synthesizes the - * per-signature va_list invoke adapter from the cb's func type. - * emitter.emit: (recv, name, ...tuple) — VARIADIC, the one libCall - * whose arg count exceeds its signature; args are borrowed, result is - * the had-listeners bool. emitter.emitError: emit('error', err) — - * throws err when unhandled. count/countFn/names/listeners/getMax/ - * setMax/setDefaultMax/getDefaultMax are the introspection surface. */ - "emitter.new": { argTypes: [], result: VOID }, - /** `x.constructor.name` of an emitter or stream (a borrowed object of - * EventEmitter's hierarchy): the +1 class name the instance records. */ - "emitter.className": { argTypes: [null], result: STRING }, - "emitter.ctor": { argTypes: [null], result: VOID }, - "emitter.on": { argTypes: [null, STRING, null, BOOL, BOOL], result: VOID }, - "emitter.off": { argTypes: [null, STRING, null], result: VOID }, - // The dyn-adapted registration family (JS-lane checked-dynamic - // listeners): checkListener has NO receiver (it validates the listener - // argument alone); onDyn carries (recv, name, cb dyn, adapter func, - // once, prepend), offDyn (recv, name, cb dyn). - "emitter.checkListener": { argTypes: [DYN], result: VOID }, - "emitter.onDyn": { argTypes: [null, STRING, DYN, null, BOOL, BOOL], result: VOID }, - "emitter.onFlex": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID }, - /** on/once/prepend* and emit through an untyped value (the class - * dispatch of compiled emitters): onFlex/emitFlex plus runtime typed - * adapters under 'error' and the meta events, and Node's unhandled-'error' - * contract. emitAny is VARIADIC like emitFlex. */ - "emitter.onAny": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID }, - "emitter.offDyn": { argTypes: [null, STRING, DYN], result: VOID }, - "emitter.removeAll": { argTypes: [null, STRING, BOOL], result: VOID }, - "emitter.emit": { argTypes: [null, STRING], result: BOOL }, - "emitter.emitFlex": { argTypes: [null, STRING], result: BOOL }, - "emitter.emitAny": { argTypes: [null, STRING], result: BOOL }, - /** An untyped emit of an event with a fixed tuple: (recv, name, k, then - * k checked-dynamic arguments as given, then the event's converted - * tuple) — VARIADIC like emitter.emit. hasTyped answers whether a - * listener of the name needs the converted tuple. */ - "emitter.emitMixed": { argTypes: [null, STRING, F64], result: BOOL }, - "emitter.hasTyped": { argTypes: [null, STRING], result: BOOL }, - "emitter.emitError": { argTypes: [null, STRING, null], result: BOOL }, - "emitter.count": { argTypes: [null, STRING], result: F64 }, - "emitter.countFn": { argTypes: [null, STRING, null], result: F64 }, - "emitter.countDyn": { argTypes: [null, STRING, DYN], result: F64 }, - "emitter.names": { argTypes: [null], result: VOID }, - "emitter.listeners": { argTypes: [null, STRING], result: VOID }, - /** listeners()/rawListeners() through an untyped value: a checked- - * dynamic array (throws when a listener has typed parameters). */ - "emitter.listenersDyn": { argTypes: [null, STRING], result: DYN }, - /** Stream-'data' registration twins of emitter.on/onDyn (same args): - * chosen at registration sites whose receiver is stream-rooted, so the - * backend emits DATA thunks — the runtime's 'data' emission carries - * BOTH payload slots (bytes, string; exactly one non-NULL — encoded - * streams deliver strings), and the thunk unwraps the listener's - * declared side (typed) or boxes by tag (dyn). emitter.emitData is the - * user-emit form: (recv, name, chunk) with a bytes OR string chunk. */ - "emitter.onData": { argTypes: [null, STRING, null, BOOL, BOOL], result: VOID }, - "emitter.onDataDyn": { argTypes: [null, STRING, DYN, null, BOOL, BOOL], result: VOID }, - "emitter.emitData": { argTypes: [null, STRING, null], result: BOOL }, - "emitter.setMax": { argTypes: [null, F64], result: VOID }, - "emitter.getMax": { argTypes: [null], result: F64 }, - "emitter.setDefaultMax": { argTypes: [F64], result: VOID }, - "emitter.getDefaultMax": { argTypes: [], result: F64 }, - // The ES module namespace's copy of defaultMaxListeners, and - // module.syncBuiltinESMExports() refreshing it. - "emitter.esmDefaultMax": { argTypes: [], result: F64 }, - "emitter.esmSync": { argTypes: [], result: VOID }, - /** The events.errorMonitor event key: an immortal string the runtime - * registry matches by identity, so no string event name reaches it. */ - "emitter.errorMonitor": { argTypes: [], result: STRING }, - // node:stream: receivers are stream-hierarchy objects and several - // results are program-dependent (the receiver's class, unions) — the - // libCall case owns those checks. The constructors and a few methods - // are VARIADIC (trailing option callbacks / the optional chunk+cb tail): - // argTypes here is the fixed prefix, VARIADIC_LIB_FNS admits the rest. - /** node:stream (scr_stream.c, link-gated by moduleUsesStream — which - * implies the emitter unit: stream events dispatch through the embedded - * ScrEmitter registry). Receivers are borrowed stream-class objects - * (`%Readable`/`%Writable`/`%Duplex`/`%Transform`/`%PassThrough` — one - * runtime layout, reinterpreted by side). - * - * Constructors (`readable.new` et al): args are [hwmR, hwmW, flags] - * followed by the PRESENT user callbacks in canonical order (read, - * write, final, destroy, transform, flush — the flags f64 is a bitmask - * naming which follow; absent ones emit NULL). Every callback closure - * MOVES and carries a leading `this` param (the stream), invoked - * through compiler-emitted adapters. Results are +1. - * - * readable.push / readable.pushStr / readable.pushNull: Node's push — - * buffers or delivers (bytes chunk borrowed; string converted utf8); - * returns the below-hwm answer. readable.read: (recv, size — -1 for - * absent) → Buffer|null union. pause/resume return recv +1 (`this`); - * isPaused answers the flag. readable.pipe: (recv, dst, end) → dst +1, - * fires 'pipe' on dst; readable.pipeGeneral: the same, with `end` off - * when dst is the converted process.stdout / process.stderr; - * 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.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 → - * bool|null union. All may leave a listener's exception pending - * (dispatch runs user code synchronously, like emit). */ - "readable.new": { argTypes: [F64, BOOL, BOOL, F64], result: VOID }, - "writable.new": { argTypes: [F64, BOOL, BOOL, F64], result: VOID }, - "duplex.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, - "transform.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, - "passthrough.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, - // The subclass-initialization twins: the borrowed receiver leads, then - // the .new tail (variadic callbacks likewise). - // The dyn-options twins: newDyn takes the record, initDyn the borrowed - // receiver + record + fallback flags (trailing wrapper closures ride - // the variadic tail like the static forms). - /** The underscore-method assignment surface (`r._read = fn` after - * construction — Node's own-property shadow of the prototype method): - * args [stream receiver (borrowed), callback closure (+1 moves)]. The - * runtime slot the matching option callback fills swaps its closure - * and invoke thunk; the next dispatch uses it (Node's timing). The - * setters themselves never throw. */ - "stream.setRead": { argTypes: [null, null], result: VOID }, - "stream.setWrite": { argTypes: [null, null], result: VOID }, - "stream.setFinal": { argTypes: [null, null], result: VOID }, - "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.replaceConstruct": { argTypes: [null, null], result: VOID }, - "stream.setWritev": { argTypes: [null, null], result: VOID }, - "stream.setWritevDefault": { argTypes: [null, null], result: VOID }, - /** The dyn-options twins (a checked-dynamic options record — the JS - * lane's `super(options)` forwarding and `new Readable(dynVar)`): the - * option walk runs at RUNTIME with Node's reading rules. newDyn: - * (optsDyn) → the fresh stream; initDyn: (recv, optsDyn, flags, - * ...fallback wrapper closures in canonical order — the flags literal - * names which ride, exactly the .init callback ABI). MAY THROW (a - * consumed-but-unlowered option is the compile fence's runtime twin). */ - /** stream.finished(s, cb) — the callback form: the watcher fires once - * at the terminal point with the finish status; the result is the +1 - * cleanup closure. stream.pipeline(count, s1..sn, cb): chains pipes, - * propagates the first error by destroying the rest, calls cb after the - * last 'close'; answers the destination +1. The Dyn twins take the - * callback as a checked-dynamic VALUE (mustCall wrappers). */ - /** stream.addAbortSignal: [stream, signal] — the constructors' signal - * option, chained over the fresh stream (falsy signals are ignored); - * stream.addAbortSignalFn: [signal, stream] — addAbortSignal(signal, - * stream). Both answer the stream (+1) and throw ERR_INVALID_ARG_TYPE - * for a value that is not an AbortSignal; they link the event-target - * unit (addAbortListener). */ - "stream.addAbortSignal": { argTypes: [null, DYN], result: VOID }, - "stream.addAbortSignalFn": { argTypes: [DYN, null], result: VOID }, - "stream.finished": { argTypes: [null, null], result: VOID }, - "stream.finishedDyn": { argTypes: [null, DYN], result: VOID }, - "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 - * no longer readable or writable). */ - "stream.compose": { argTypes: [F64], result: VOID }, - /** node:stream/promises — the promise forms over the same machinery: - * sp.finished: (s) → a pending void promise the terminal watcher - * settles; sp.pipeline: (count, s1..sn) → the callback pipeline's - * chaining/destroyer semantics settling a void promise (fulfilled on a - * clean finish, rejected with the finish status otherwise). */ - "sp.finished": { argTypes: [null], result: { kind: "promise", inner: VOID } }, - /** The options forms: finished(s, { readable, writable, error, cleanup, - * signal }) as [s, readable, writable, error, cleanup, signal?] — - * readable/writable -1 (absent), 0 or 1 — and pipeline(...streams, - * { signal, end }) as [count, s1..sn, end, signal?]. The Signal forms - * link the event-target unit (addAbortListener). The callback form - * stream.finishedOpts: [s, readable, writable, error, signal?, cb] → - * the cleanup closure. */ - "sp.finishedOpts": { - argTypes: [null, F64, F64, BOOL, BOOL], - result: { kind: "promise", inner: VOID }, - }, - "sp.finishedOptsSignal": { - argTypes: [null, F64, F64, BOOL, BOOL, DYN], - result: { kind: "promise", inner: VOID }, - }, - "sp.pipelineOpts": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, - "sp.pipelineOptsSignal": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, - "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 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 }, - }, - /** 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 - * parsed dyn — malformed input rejects with the parse's SyntaxError, - * sc.buffer: the concatenated bytes, sc.arrayBuffer: an ArrayBuffer - * dyn over them) or rejected with the stream's - * error / ERR_STREAM_PREMATURE_CLOSE. */ - "sc.text": { argTypes: [null], result: { kind: "promise", inner: STRING } }, - "sc.json": { argTypes: [null], result: { kind: "promise", inner: DYN } }, - "sc.buffer": { argTypes: [null], result: { kind: "promise", inner: BYTES_U8 } }, - "sc.arrayBuffer": { argTypes: [null], result: { kind: "promise", inner: DYN } }, - "readable.newDyn": { argTypes: [DYN], result: VOID }, - "writable.newDyn": { argTypes: [DYN], result: VOID }, - "duplex.newDyn": { argTypes: [DYN], result: VOID }, - "transform.newDyn": { argTypes: [DYN], result: VOID }, - "passthrough.newDyn": { argTypes: [DYN], result: VOID }, - "readable.initDyn": { argTypes: [null, DYN, F64], result: VOID }, - "writable.initDyn": { argTypes: [null, DYN, F64], result: VOID }, - "duplex.initDyn": { argTypes: [null, DYN, F64], result: VOID }, - "transform.initDyn": { argTypes: [null, DYN, F64], result: VOID }, - "passthrough.initDyn": { argTypes: [null, DYN, F64], result: VOID }, - /** Subclass initialization (`super(options?)` in a user `extends - * Readable` constructor): same tail as the `.new` forms, prefixed with - * the BORROWED receiver (the emitted subclass allocation — vtable and - * display name stamped, state NULL until here). Overridden underscore - * methods arrive as synthesized wrapper closures dispatching through - * the vtable. Void result. */ - "readable.init": { argTypes: [null, F64, BOOL, BOOL, F64], result: VOID }, - "writable.init": { argTypes: [null, F64, BOOL, BOOL, F64], result: VOID }, - "duplex.init": { argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, - "transform.init": { argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, - "passthrough.init": { - argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], - result: VOID, - }, - "readable.push": { argTypes: [null, BYTES_U8], result: BOOL }, - "readable.pushStr": { argTypes: [null, STRING], result: BOOL }, - "readable.pushNull": { argTypes: [null], result: BOOL }, - "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 }, - "readable.read": { argTypes: [null, F64], result: VOID }, - "readable.pause": { argTypes: [null], result: VOID }, - "readable.setEncoding": { argTypes: [null, STRING], result: VOID }, - /** push(chunk, enc) with a literal non-utf8 encoding, and the - * defaultEncoding option's push side (how push(string) decodes — - * 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 }, - "stream.decodeStrings": { argTypes: [null, BOOL], result: VOID }, - /** Runtime decodeStrings and defaultEncoding option values, applied - * after construction (answers the receiver +1); the encoding was - * validated by stream.checkEncodingOption before construction. */ - "stream.optionValues": { argTypes: [null, DYN, DYN], result: VOID }, - "stream.checkEncodingOption": { argTypes: [DYN], result: VOID }, - "stream.duplexFromGen": { argTypes: [null, null, null, null, null, null], result: VOID }, - "stream.duplexFromDyn": { argTypes: [DYN], result: VOID }, - /** Duplex.from over a lone Readable or Writable, or a { readable, - * writable } pair of streams (duplexify.js's _duplexify). */ - "stream.duplexifyReadable": { argTypes: [null], result: VOID }, - "stream.duplexifyWritable": { argTypes: [null], result: VOID }, - "stream.duplexifyPair": { argTypes: [null, null], result: VOID }, - /** Duplex.from(promise): the promise and its box helper. */ - "stream.duplexFromPromise": { argTypes: [null, null], result: VOID }, - /** Duplex.from(asyncFunction): the stage, its source helper and the - * promise's box helper. */ - "stream.duplexFromAsyncFn": { argTypes: [null, null, null], result: VOID }, - /** The same for a body that declares the options argument: then the - * AbortController's abort() and its signal. */ - "stream.duplexFromGenSignal": { - argTypes: [null, null, null, null, null, null, null, DYN], - result: VOID, - }, - "stream.duplexFromAsyncFnSignal": { argTypes: [null, null, null, null, DYN], result: VOID }, - "writable.writeEncoding": { argTypes: [null, STRING], result: VOID }, - "writable.setDefaultEncoding": { argTypes: [null, DYN], result: VOID }, - /** Whether a readable.nextChunkDyn step answered the end (the done - * 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 - * error. The Dyn twin boxes chunks by runtime tag (the JS lane). - * readable.fromArr is Readable.from over a typed array, string or - * Buffer: [array, SCR_FROM_* flags, highWaterMark]. readable.fromDyn is - * Readable.from over a checked-dynamic source: [source, options]. - * readable.fromIter is Readable.from over a class iterator, already - * opened: [checked-dynamic { next } object, flags, highWaterMark]; - * readable.fromAsyncIter is the async class iterator's twin. */ - "readable.nextChunk": { argTypes: [null], result: VOID }, - "readable.nextChunkDyn": { argTypes: [null], result: VOID }, - "readable.fromArr": { argTypes: [null, F64, F64], result: VOID }, - "readable.fromDyn": { argTypes: [DYN, DYN], result: VOID }, - "readable.fromIter": { argTypes: [DYN, F64, F64], result: VOID }, - "readable.fromAsyncIter": { argTypes: [DYN, F64, F64], result: VOID }, - /** Readable.from over a compiled generator: [generator, next, return, - * throw, flags, highWaterMark] (sync) or [generator, next, read, - * return, throw, flags, highWaterMark] (async), the helpers being the - * frontend's interned closures. readable.fromDone is the done marker - * the helpers answer. */ - "readable.fromGen": { argTypes: [null, null, null, null, F64, F64], result: VOID }, - "readable.fromAsyncGen": { argTypes: [null, null, null, null, null, F64, F64], result: VOID }, - "readable.fromDone": { argTypes: [], result: DYN }, - /** The Readable operators (scr_stream_ops.c): [stream, ...] with every - * other argument a checked-dynamic value (the callback, the count, the - * options). map/filter/flatMap/drop/take answer the object-mode output - * stream (validated as a stream class; THROW Node's validation errors); - * toArray settles a dyn array, find a dyn value, some/every a boolean, - * forEach nothing; reduce: [stream, fn, init, hasInit, options, kind] - * with the payload kind (0 dyn, 1 number, 2 boolean, 3 string). The - * promise-returning ones reject instead of throwing. */ - "readable.opMap": { argTypes: [null, DYN, DYN, DYN], result: VOID }, - "readable.opFilter": { argTypes: [null, DYN, DYN, DYN], result: VOID }, - "readable.opFlatMap": { argTypes: [null, DYN, DYN, DYN], result: VOID }, - /** flatMap whose callback returns a compiled generator: the helpers - * over its type follow (async: next, read, return, throw; sync: next, - * return, throw). */ - "readable.opFlatMapAsyncGen": { - argTypes: [null, DYN, DYN, DYN, null, null, null, null], - result: VOID, - }, - "readable.opFlatMapSyncGen": { argTypes: [null, DYN, DYN, DYN, null, null, null], result: VOID }, - "readable.opDrop": { argTypes: [null, DYN, DYN], result: VOID }, - "readable.opTake": { argTypes: [null, DYN, DYN], result: VOID }, - "readable.opToArray": { argTypes: [null, DYN], result: { kind: "promise", inner: DYN } }, - "readable.opForEach": { - argTypes: [null, DYN, DYN, DYN], - result: { kind: "promise", inner: VOID }, - }, - "readable.opSome": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: BOOL } }, - "readable.opEvery": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: BOOL } }, - "readable.opFind": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: DYN } }, - /** The map family's last argument is the callback's { signal } (or - * undefined). reduceSignal adds ac.abort() (a closure) and - * { signal: ac.signal } for a reducer that reads them. */ - "readable.opReduce": { argTypes: [null, DYN, DYN, BOOL, DYN, F64], result: VOID }, - "readable.opReduceSignal": { - argTypes: [null, DYN, DYN, BOOL, DYN, F64, null, DYN], - result: VOID, - }, - "readable.opSignalList": { argTypes: [DYN], result: DYN }, - "readable.opSignalOpts": { argTypes: [DYN], result: DYN }, - "readable.opSignalOf": { argTypes: [DYN], result: DYN }, - "readable.opOnceOptions": { argTypes: [], result: DYN }, - "readable.resume": { argTypes: [null], result: VOID }, - "readable.isPaused": { argTypes: [null], result: BOOL }, - "readable.pipe": { argTypes: [null, null, BOOL], result: VOID }, - "readable.pipeGeneral": { argTypes: [null, null, BOOL], result: VOID }, - "readable.unpipe": { argTypes: [null], result: VOID }, - "readable.flowing": { argTypes: [null], result: VOID }, - "writable.write": { argTypes: [null, BYTES_U8], result: BOOL }, - "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 }, - "stream.destroy": { argTypes: [null], result: VOID }, - "stream.destroyErr": { argTypes: [null, null], result: VOID }, - /** AsyncIteratorClose for Readable[Symbol.asyncIterator](): destroy with - * Node's AbortError/ABORT_ERR payload while the iterator's internal error - * consumer prevents an unhandled-error crash. */ - "stream.iteratorClose": { argTypes: [null], result: VOID }, - "stream.prop": { argTypes: [null, STRING], result: VOID }, - "stream.errored": { argTypes: [null], result: VOID }, - // node:assert: pass/negated/deep/hasMsg are frontend-computed bools; the - // message slot always carries a string ("" when hasMsg is false). - /** node:assert (scr_assert.c; assert.match in scr_regex.c — every call - * site carries a regex value, so the regex link switch is already on). - * Failures throw a catchable AssertionError — a runtime %Error whose - * name is "AssertionError" and whose code slot is "ERR_ASSERTION" — so - * `instanceof Error`, `.name`, `.message`, and `.code` all answer like - * Node's. Generated messages are Node's assertion_error.js scalar forms - * byte-exactly (the short `a !== b` form, the stacked `+ actual - * - expected` diff with the string `^` indicator, the inline-vs-block - * not-equal split); composite deep failures carry the header line - * without the rendered inspect diff (documented divergence). - * - * assert.ok: (pass, message) — the frontend computed the truthiness AND - * the full message (the user's, or the compile-time source-text form — - * assert.fail lowers here too with pass=false). assert.eqF64/eqBigInt/ - * eqStr/eqBool: (a, b, negated, deep, msg, hasMsg) — Object.is comparison, - * covering strictEqual/notStrictEqual and the scalar deepStrictEqual - * pair; msg is a typed dummy ("" literal) when hasMsg is false (Node - * distinguishes an omitted message from an empty one per operator). - * assert.deepResult: (equal, negated, msg, hasMsg) — the verdict of a - * frontend-synthesized structural comparison, turned into Node's throw. - * assert.sameValue: Object.is over doubles (the deep-equal helpers' - * number leaf; never throws). assert.match: (s, regex, negated, msg, - * hasMsg) — a fresh exec from index 0. assert.throwsNone: (rejection, - * ename, hasEname, msg, hasMsg) — the "Missing expected - * exception|rejection" throw of assert.throws/rejects whose callback - * returned (fulfilled) normally, with Node's ` (${expected.name})` - * detail when the expected class or shape carries a name. - * assert.throwsMismatch: (expectedName, error) — the wrong-class throw - * of the assert.throws(fn, ErrorClass) form. assert.eqSym: - * (a, b, negated, deep, msg, hasMsg) — strict equality over symbol - * values, pointer identity with v24's "Symbol(desc)" stacked-diff - * messages (scr_symbol.c, the assert.match pattern — symbol-typed - * slots already flip the symbol link switch). assert.eqDyn: - * (a, b, negated, deep, msg, hasMsg) — the whole quartet over - * checked-dynamic operands (both slots dyn; the frontend boxes a - * static side with dynFrom first): SameValue for the strict pair over - * the dyn kinds (boxed-closure identity for functions), the structural - * dyn walk for the deep pair, with assertion_error.js's messages — - * scalar forms byte-exact, composites rendered compact:false/sorted - * through the checked-dynamic tree and diffed with the real myers line printer. - * - * The assert.throws(fn, {name/code/message}) shape check - * (expectedException over the static error surface): shapeBegin - * stashes the caught error, shapeStr/shapeRe add one expected key each - * (key ids 0 code / 1 message / 2 name; shapeRe lives in scr_regex.c — - * its regex argument flips the regex link switch — and tests eagerly - * so scr_assert.c stays libregexp-free), then shapeEnd throws Node's - * deep-equal Comparison diff BYTE-EXACTLY (the bounded key set makes - * the rendering enumerable) or the custom message. - * assert.throwsRegex: (regex, error, msg, hasMsg) — the - * assert.throws(fn, /re/) check over String(error), Node's - * regex-mismatch message. assert.regexErrTest: doesNotReject's - * regex predicate over String(error) (never throws). - * assert.unwantedRejection: (error, msg, hasMsg) — doesNotReject's - * "Got unwanted rejection" throw. - * assert.ifErrorErr/F64/Str/Bool: assert.ifError's per-type throws - * ("ifError got unwanted exception: " + the error's message/name or - * the value's inspection) — the frontend routes null/undefined to a - * no-op and everything else here (Node throws for falsy values too). - * All arguments are borrowed. */ - "assert.ok": { argTypes: [BOOL, STRING, BOOL], result: VOID }, - /** The next AssertionError's actual, expected and operator, recorded on - * a failure branch. */ - "assert.record": { argTypes: [DYN, DYN, STRING], result: VOID }, - "assert.errorNew": { argTypes: [DYN], result: { kind: "object", className: "%AssertionError" } }, - "assert.eqF64": { argTypes: [F64, F64, BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.eqBigInt": { argTypes: [BIGINT_T, BIGINT_T, BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.eqStr": { argTypes: [STRING, STRING, BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.eqBool": { argTypes: [BOOL, BOOL, BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.deepResult": { argTypes: [BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.looseDeepResult": { argTypes: [BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.looseDeepDyn": { argTypes: [DYN, DYN, BOOL, STRING, BOOL], result: VOID }, - "assert.partialDeepDyn": { argTypes: [DYN, DYN, STRING, BOOL], result: VOID }, - "assert.partialDeepResult": { argTypes: [BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.partialEqDyn": { argTypes: [DYN, DYN], result: BOOL }, - "assert.looseF64Eq": { argTypes: [F64, F64], result: BOOL }, - "assert.sameValue": { argTypes: [F64, F64], result: BOOL }, - // The deep-equality pair memo: both slots are program-dependent - // (any cycle-capable record/array/map type). - /* deepStrictEqual's pair memo over cycle-capable types: enter answers - * true for a pair already being compared (Node's memo — equal cyclic - * structures compare true); leave pops. */ - "assert.deqEnter": { argTypes: [null, null], result: BOOL }, - "assert.deqLeave": { argTypes: [], result: VOID }, - "assert.match": { argTypes: [STRING, REGEX, BOOL, STRING, BOOL], result: VOID }, - "assert.throwsNone": { argTypes: [BOOL, STRING, BOOL, STRING, BOOL], result: VOID }, - "assert.throwsMismatch": { - argTypes: [STRING, { kind: "object", className: "%Error" }, STRING, BOOL, BOOL], - result: VOID, - }, - "assert.throwsRegex": { - argTypes: [REGEX, { kind: "object", className: "%Error" }, STRING, BOOL, BOOL], - result: VOID, - }, - // Symbol strict equality (pointer identity; scr_symbol.c). - "assert.eqSym": { - argTypes: [{ kind: "symbol" }, { kind: "symbol" }, BOOL, BOOL, STRING, BOOL], - result: VOID, - }, - // The equality quartet over checked-dynamic operands (the frontend - // boxes a static side into the checked-dynamic tree first). - "assert.eqDyn": { argTypes: [DYN, DYN, BOOL, BOOL, STRING, BOOL], result: VOID }, - /** A message argument that is not a string, registered for the next - * failing assertion (scr_assert_message_value). */ - "assert.messageValue": { argTypes: [DYN, BOOL], result: VOID }, - /** An assert method as a function value, by canonical name. */ - "assert.methodValue": { argTypes: [STRING], result: DYN }, - "assert.messageClear": { argTypes: [], result: VOID }, - "assert.looseResult": { argTypes: [BOOL, BOOL, STRING, STRING, STRING, BOOL], result: VOID }, - // The throws(fn, {shape}) accumulator: begin/slot calls never throw; - // shapeEnd throws the Comparison diff. The error slot is the - // %Error-narrowed caught value. - "assert.shapeBegin": { argTypes: [{ kind: "object", className: "%Error" }], result: VOID }, - "assert.shapeStr": { argTypes: [F64, STRING], result: VOID }, - "assert.shapeRe": { argTypes: [F64, REGEX], result: VOID }, - "assert.shapeEnd": { argTypes: [STRING, BOOL, BOOL], result: VOID }, - "assert.regexErrTest": { - argTypes: [REGEX, { kind: "object", className: "%Error" }], - result: BOOL, - }, - "assert.unwantedRejection": { - argTypes: [{ kind: "object", className: "%Error" }, STRING, BOOL], - result: VOID, - }, - "assert.unwantedError": { argTypes: [STRING, BOOL, STRING, BOOL], result: VOID }, - "assert.noErrorPredicate": { argTypes: [DYN, DYN], result: BOOL }, - "assert.regexDynTest": { argTypes: [REGEX, DYN], result: BOOL }, - /** Node's expectsError over an error-INSTANCE expected (assert.throws/ - * rejects second argument): walk the expected dyn error's keys (name, - * message, code — the %error marker skipped) and deep-compare each - * against the caught value's; a mismatch throws the deep-equal - * AssertionError (scr_assert.c). MAY THROW by design. */ - "assert.expectsErrDyn": { argTypes: [DYN, DYN, STRING, BOOL], result: VOID }, - /** A validation function against the caught value (Node's expectedException). */ - "assert.throwsValidate": { argTypes: [DYN, DYN, STRING, STRING, BOOL], result: VOID }, - // assert.ifError's typed entries (always throw; unit args never - // lower). The error slot is program-dependent — any %Error-hierarchy - // class, root or subclass (the insp.error precedent, null slot). - "assert.ifErrorErr": { argTypes: [null], result: VOID }, - "assert.ifErrorF64": { argTypes: [F64], result: VOID }, - "assert.ifErrorStr": { argTypes: [STRING], result: VOID }, - "assert.ifErrorBool": { argTypes: [BOOL], result: VOID }, - "assert.ifErrorDyn": { argTypes: [DYN], result: VOID }, - // Bytes equality: the two value slots are any bytes kind (the frontend - // gates both sides to ONE static bytes type; the libCall case checks - // the kind and the pair below). - "assert.refEqBytes": { argTypes: [null, null, BOOL, BOOL, STRING, BOOL], result: VOID }, - "assert.refEqFn": { argTypes: [null, null, BOOL, STRING, BOOL], result: VOID }, - "assert.bytesDeepEq": { argTypes: [null, null, BOOL], result: BOOL }, - "assert.looseBytesEq": { argTypes: [null, null], result: BOOL }, - // util.inspect: the error receiver is program-dependent (a builtin or - // user error class — the error.toString precedent, null slot). + "dyn.objCreateNullProto": { argTypes: [], result: DYN }, + "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 }, + "dyn.classBasePrototype": { argTypes: [DYN], result: DYN }, + "dyn.classInherit": { argTypes: [DYN, DYN], result: DYN }, + /** EventEmitter (0), Stream (1), Readable (2), Writable (3), Duplex (4), + * Transform (5) and PassThrough (6) as untyped constructor values, one + * per thread (scr_dyn_builtin_class.c). */ + "dyn.builtinClass": { argTypes: [F64], result: DYN }, + /** The stream classes (2-6 above) with their stream half: constructor + * functions over them get native stream state + * (scr_dyn_builtin_stream.c). */ + "dyn.builtinStreamClass": { argTypes: [F64], result: DYN }, + /** `value instanceof C` for those constructors, over the value's + * prototype chain. */ + "dyn.builtinInstanceOf": { argTypes: [DYN, F64], result: BOOL }, + /** `value instanceof f` for an untyped constructor function: the + * prototype chain against f.prototype. */ + "dyn.functionInstanceOf": { argTypes: [DYN, DYN], result: BOOL }, + "dyn.classSuper": { argTypes: [DYN, DYN, DYN], result: VOID }, + "dyn.assignPrototype": { argTypes: [DYN, DYN, DYN], result: DYN }, + "dyn.definePrototypeProps": { argTypes: [DYN, DYN, DYN], result: DYN }, + "dyn.bagGet": { argTypes: [DYN, STRING, DYN], result: DYN }, + /** A class field's creation in its instance's property bag: adds the + * string key as a default data property when absent, answering false + * (nothing changed) when the bag needs the general defineProperty path. */ + "dyn.bagCreateField": { argTypes: [DYN, STRING], result: BOOL }, + "dyn.bagSet": { argTypes: [DYN, STRING, DYN, DYN], result: VOID }, + "dyn.objCreate": { argTypes: [DYN], result: DYN }, + // Names a compiled class's prototype object (args[0]) after the class + // (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.getPrototype for walks that compare each prototype with a program + // class's prototype: native builtin values end the walk (null). + "dyn.getPrototypeForIdentity": { argTypes: [DYN], result: DYN }, + "dyn.setPrototype": { argTypes: [DYN, DYN], result: DYN }, + "dyn.getOwnPropertyNames": { argTypes: [DYN], result: DYN }, + "dyn.ownKeys": { argTypes: [DYN], result: DYN }, + "dyn.getOwnPropertySymbols": { argTypes: [DYN], result: DYN }, + "dyn.getOwnPropertyDescriptors": { argTypes: [DYN], result: DYN }, + "dyn.preventExtensions": { argTypes: [DYN], result: DYN }, + "dyn.isExtensible": { argTypes: [DYN], result: BOOL }, + "dyn.seal": { argTypes: [DYN], result: DYN }, + "dyn.isSealed": { argTypes: [DYN], result: BOOL }, + "dyn.objValues": { argTypes: [DYN], result: DYN }, + /** Object.values as a native vector of checked values, for results the + * program consumes directly (for-of, index reads, read-only methods). */ + "dyn.objValueList": { argTypes: [DYN], result: arrayOf(DYN) }, + "dyn.objEntries": { argTypes: [DYN], result: DYN }, + /** structuredClone over the checked-dynamic tree (scr_json.c): the JSON-safe subset plus + * bytes (a fresh copy — a Buffer clones as a plain Uint8Array, like + * Node), deep. Functions and handle kinds throw the spec's catchable + * DataCloneError; CYCLES throw the scriptc fence (the checked-dynamic tree cannot + * represent them — Node clones cycles; documented divergence). The + * options dyn value validates with Node's exact errors (dictionary + * conversion, the transfer-sequence member; any non-empty transfer + * list throws DataCloneError). dyn.cloneMissing is the zero-argument + * call: always throws Node's TypeError [ERR_MISSING_ARGS] with Node's + * own (verbatim, doubly-wrapped) message. */ + "dyn.structuredClone": { argTypes: [DYN, DYN], result: DYN }, + /** The declaration-typed boundary's adoption (decl-projection.ts): + * `source` is the dynamic value a boundary cast just converted, and + * `capsule` a live capsule of the native value built from it. Every + * object or array of `source` that something else still references + * becomes, in place, a live capsule of the native value built from it. + * Throws the path TypeError, changing nothing, when a converted object + * is not plain data (a prototype, accessors, symbols, frozen state). */ + "dyn.declAdopt": { argTypes: [DYN, DYN], result: VOID }, + "dyn.cloneMissing": { argTypes: [], result: DYN }, + /** structuredClone with a NON-EMPTY transfer array of static values: + * nothing static is transferable, so the call always throws Node's + * catchable DataCloneError ("Found invalid value in transferList.") — + * lowered directly (the list's values need no dyn representation to + * fail). */ + "dyn.cloneTransferFail": { argTypes: [], result: DYN }, + /** The same refusal given the transfer list's first member, an + * ArrayBuffer: a Buffer pool slab gets Node's untransferable message. */ + "dyn.cloneTransferFailWith": { argTypes: [DYN], result: DYN }, + /** `new RegExp(pattern, flags?)` (scr_regex.c): a heap regex over the + * same libregexp engine the literals use. The pattern compiles EAGERLY + * so an invalid pattern (or an unknown flag letter) throws Node's + * catchable SyntaxError at construction — Node's message shape with + * libregexp's detail text (approximate fidelity; e.name exact). An + * empty pattern stores the spec's "(?:)" source. Both args borrowed + * strings (the lowering completes an absent flags to ""); result +1. + * The result TYPE is the regex kind, so the link switch pulls the + * engine exactly like a literal. */ + "regex.new": { argTypes: [STRING, STRING], result: REGEX }, + "regex.resetLastIndex": { argTypes: [REGEX, F64], result: VOID }, + "regex.newChecked": { argTypes: [DYN, DYN], result: REGEX }, + "regex.replaceCallback": { argTypes: [STRING, REGEX, DYN, BOOL], result: STRING }, + // Reads of capture args[1] of an execSpans/matchGroups result: + // `capture === args[2]`, Number(capture) and parseInt(capture, + // args[2]). Borrowed; never throw. + "regex.groupEquals": { argTypes: [arrayOf(STRING), F64, STRING], result: BOOL }, + "regex.groupNumber": { argTypes: [arrayOf(STRING), F64], result: F64 }, + "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) }, + // 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 }, + // The key of an element access whose index is undefined (the property + // named "undefined"; frontend/lowering/undefined-index.ts). + "array.undefinedKey": { argTypes: [], result: F64 }, + // node:events EventEmitter: receivers are emitter-hierarchy objects and + // the chaining forms (on/off/removeAll/setMax) return the receiver's + // own class — program-dependent object types, checked in the libCall + // case. emitter.emit is the one VARIADIC libCall: args beyond + // (recv, name) are the event's frontend-unified tuple, any borrowable + // kind — the count check admits a longer list for it alone. + /** node:events EventEmitter (scr_events_emitter.c, link-gated by + * moduleUsesEmitter). The receiver of every instance form is a borrowed + * emitter-hierarchy object (`%EventEmitter` or a user subclass — the + * backend reinterprets to ScrEmitter*, the identical prefix); event + * names are borrowed strings (compile-time literals — the frontend + * fences non-literals and unifies each event's argument tuple program- + * wide). The chaining forms (on/off/removeAll/setMax) return the + * receiver +1 typed as its static class, Node's `return this`. + * + * emitter.new: `new EventEmitter()` → a +1 bare emitter. emitter.ctor: + * super() into the prefix of an emitted subclass (borrowed receiver, + * void — allocation already initialized the prefix). emitter.on: + * (recv, name, cb /moves/, once, prepend) — the backend synthesizes the + * per-signature va_list invoke adapter from the cb's func type. + * emitter.emit: (recv, name, ...tuple) — VARIADIC, the one libCall + * whose arg count exceeds its signature; args are borrowed, result is + * the had-listeners bool. emitter.emitError: emit('error', err) — + * throws err when unhandled. count/countFn/names/listeners/getMax/ + * setMax/setDefaultMax/getDefaultMax are the introspection surface. */ + "emitter.new": { argTypes: [], result: VOID }, + /** `x.constructor.name` of an emitter or stream (a borrowed object of + * EventEmitter's hierarchy): the +1 class name the instance records. */ + "emitter.className": { argTypes: [null], result: STRING }, + "emitter.ctor": { argTypes: [null], result: VOID }, + "emitter.on": { argTypes: [null, STRING, null, BOOL, BOOL], result: VOID }, + "emitter.off": { argTypes: [null, STRING, null], result: VOID }, + // The dyn-adapted registration family (JS-lane checked-dynamic + // listeners): checkListener has NO receiver (it validates the listener + // argument alone); onDyn carries (recv, name, cb dyn, adapter func, + // once, prepend), offDyn (recv, name, cb dyn). + "emitter.checkListener": { argTypes: [DYN], result: VOID }, + "emitter.onDyn": { argTypes: [null, STRING, DYN, null, BOOL, BOOL], result: VOID }, + "emitter.onFlex": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID }, + /** on/once/prepend* and emit through an untyped value (the class + * dispatch of compiled emitters): onFlex/emitFlex plus runtime typed + * adapters under 'error' and the meta events, and Node's unhandled-'error' + * contract. emitAny is VARIADIC like emitFlex. */ + "emitter.onAny": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID }, + "emitter.offDyn": { argTypes: [null, STRING, DYN], result: VOID }, + "emitter.removeAll": { argTypes: [null, STRING, BOOL], result: VOID }, + "emitter.emit": { argTypes: [null, STRING], result: BOOL }, + "emitter.emitFlex": { argTypes: [null, STRING], result: BOOL }, + "emitter.emitAny": { argTypes: [null, STRING], result: BOOL }, + /** An untyped emit of an event with a fixed tuple: (recv, name, k, then + * k checked-dynamic arguments as given, then the event's converted + * tuple) — VARIADIC like emitter.emit. hasTyped answers whether a + * listener of the name needs the converted tuple. */ + "emitter.emitMixed": { argTypes: [null, STRING, F64], result: BOOL }, + "emitter.hasTyped": { argTypes: [null, STRING], result: BOOL }, + "emitter.emitError": { argTypes: [null, STRING, null], result: BOOL }, + "emitter.count": { argTypes: [null, STRING], result: F64 }, + "emitter.countFn": { argTypes: [null, STRING, null], result: F64 }, + "emitter.countDyn": { argTypes: [null, STRING, DYN], result: F64 }, + "emitter.names": { argTypes: [null], result: VOID }, + "emitter.listeners": { argTypes: [null, STRING], result: VOID }, + /** listeners()/rawListeners() through an untyped value: a checked- + * dynamic array (throws when a listener has typed parameters). */ + "emitter.listenersDyn": { argTypes: [null, STRING], result: DYN }, + /** Stream-'data' registration twins of emitter.on/onDyn (same args): + * chosen at registration sites whose receiver is stream-rooted, so the + * backend emits DATA thunks — the runtime's 'data' emission carries + * BOTH payload slots (bytes, string; exactly one non-NULL — encoded + * streams deliver strings), and the thunk unwraps the listener's + * declared side (typed) or boxes by tag (dyn). emitter.emitData is the + * user-emit form: (recv, name, chunk) with a bytes OR string chunk. */ + "emitter.onData": { argTypes: [null, STRING, null, BOOL, BOOL], result: VOID }, + "emitter.onDataDyn": { argTypes: [null, STRING, DYN, null, BOOL, BOOL], result: VOID }, + "emitter.emitData": { argTypes: [null, STRING, null], result: BOOL }, + "emitter.setMax": { argTypes: [null, F64], result: VOID }, + "emitter.getMax": { argTypes: [null], result: F64 }, + "emitter.setDefaultMax": { argTypes: [F64], result: VOID }, + "emitter.getDefaultMax": { argTypes: [], result: F64 }, + // The ES module namespace's copy of defaultMaxListeners, and + // module.syncBuiltinESMExports() refreshing it. + "emitter.esmDefaultMax": { argTypes: [], result: F64 }, + "emitter.esmSync": { argTypes: [], result: VOID }, + /** The events.errorMonitor event key: an immortal string the runtime + * registry matches by identity, so no string event name reaches it. */ + "emitter.errorMonitor": { argTypes: [], result: STRING }, + // node:stream: receivers are stream-hierarchy objects and several + // results are program-dependent (the receiver's class, unions) — the + // libCall case owns those checks. The constructors and a few methods + // are VARIADIC (trailing option callbacks / the optional chunk+cb tail): + // argTypes here is the fixed prefix, VARIADIC_LIB_FNS admits the rest. + /** node:stream (scr_stream.c, link-gated by moduleUsesStream — which + * implies the emitter unit: stream events dispatch through the embedded + * ScrEmitter registry). Receivers are borrowed stream-class objects + * (`%Readable`/`%Writable`/`%Duplex`/`%Transform`/`%PassThrough` — one + * runtime layout, reinterpreted by side). + * + * Constructors (`readable.new` et al): args are [hwmR, hwmW, flags] + * followed by the PRESENT user callbacks in canonical order (read, + * write, final, destroy, transform, flush — the flags f64 is a bitmask + * naming which follow; absent ones emit NULL). Every callback closure + * MOVES and carries a leading `this` param (the stream), invoked + * through compiler-emitted adapters. Results are +1. + * + * readable.push / readable.pushStr / readable.pushNull: Node's push — + * buffers or delivers (bytes chunk borrowed; string converted utf8); + * returns the below-hwm answer. readable.read: (recv, size — -1 for + * absent) → Buffer|null union. pause/resume return recv +1 (`this`); + * isPaused answers the flag. readable.pipe: (recv, dst, end) → dst +1, + * fires 'pipe' on dst; readable.pipeGeneral: the same, with `end` off + * when dst is the converted process.stdout / process.stderr; + * 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.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 → + * bool|null union. All may leave a listener's exception pending + * (dispatch runs user code synchronously, like emit). */ + "readable.new": { argTypes: [F64, BOOL, BOOL, F64], result: VOID }, + "writable.new": { argTypes: [F64, BOOL, BOOL, F64], result: VOID }, + "duplex.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, + "transform.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, + "passthrough.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, + // The subclass-initialization twins: the borrowed receiver leads, then + // the .new tail (variadic callbacks likewise). + // The dyn-options twins: newDyn takes the record, initDyn the borrowed + // receiver + record + fallback flags (trailing wrapper closures ride + // the variadic tail like the static forms). + /** The underscore-method assignment surface (`r._read = fn` after + * construction — Node's own-property shadow of the prototype method): + * args [stream receiver (borrowed), callback closure (+1 moves)]. The + * runtime slot the matching option callback fills swaps its closure + * and invoke thunk; the next dispatch uses it (Node's timing). The + * setters themselves never throw. */ + "stream.setRead": { argTypes: [null, null], result: VOID }, + "stream.setWrite": { argTypes: [null, null], result: VOID }, + "stream.setFinal": { argTypes: [null, null], result: VOID }, + "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.replaceConstruct": { argTypes: [null, null], result: VOID }, + "stream.setWritev": { argTypes: [null, null], result: VOID }, + "stream.setWritevDefault": { argTypes: [null, null], result: VOID }, + /** The dyn-options twins (a checked-dynamic options record — the JS + * lane's `super(options)` forwarding and `new Readable(dynVar)`): the + * option walk runs at RUNTIME with Node's reading rules. newDyn: + * (optsDyn) → the fresh stream; initDyn: (recv, optsDyn, flags, + * ...fallback wrapper closures in canonical order — the flags literal + * names which ride, exactly the .init callback ABI). MAY THROW (a + * consumed-but-unlowered option is the compile fence's runtime twin). */ + /** stream.finished(s, cb) — the callback form: the watcher fires once + * at the terminal point with the finish status; the result is the +1 + * cleanup closure. stream.pipeline(count, s1..sn, cb): chains pipes, + * propagates the first error by destroying the rest, calls cb after the + * last 'close'; answers the destination +1. The Dyn twins take the + * callback as a checked-dynamic VALUE (mustCall wrappers). */ + /** stream.addAbortSignal: [stream, signal] — the constructors' signal + * option, chained over the fresh stream (falsy signals are ignored); + * stream.addAbortSignalFn: [signal, stream] — addAbortSignal(signal, + * stream). Both answer the stream (+1) and throw ERR_INVALID_ARG_TYPE + * for a value that is not an AbortSignal; they link the event-target + * unit (addAbortListener). */ + "stream.addAbortSignal": { argTypes: [null, DYN], result: VOID }, + "stream.addAbortSignalFn": { argTypes: [DYN, null], result: VOID }, + "stream.finished": { argTypes: [null, null], result: VOID }, + "stream.finishedDyn": { argTypes: [null, DYN], result: VOID }, + "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 + * no longer readable or writable). */ + "stream.compose": { argTypes: [F64], result: VOID }, + /** node:stream/promises — the promise forms over the same machinery: + * sp.finished: (s) → a pending void promise the terminal watcher + * settles; sp.pipeline: (count, s1..sn) → the callback pipeline's + * chaining/destroyer semantics settling a void promise (fulfilled on a + * clean finish, rejected with the finish status otherwise). */ + "sp.finished": { argTypes: [null], result: { kind: "promise", inner: VOID } }, + /** The options forms: finished(s, { readable, writable, error, cleanup, + * signal }) as [s, readable, writable, error, cleanup, signal?] — + * readable/writable -1 (absent), 0 or 1 — and pipeline(...streams, + * { signal, end }) as [count, s1..sn, end, signal?]. The Signal forms + * link the event-target unit (addAbortListener). The callback form + * stream.finishedOpts: [s, readable, writable, error, signal?, cb] → + * the cleanup closure. */ + "sp.finishedOpts": { + argTypes: [null, F64, F64, BOOL, BOOL], + result: { kind: "promise", inner: VOID }, + }, + "sp.finishedOptsSignal": { + argTypes: [null, F64, F64, BOOL, BOOL, DYN], + result: { kind: "promise", inner: VOID }, + }, + "sp.pipelineOpts": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, + "sp.pipelineOptsSignal": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, + "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 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 }, + }, + /** 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 + * parsed dyn — malformed input rejects with the parse's SyntaxError, + * sc.buffer: the concatenated bytes, sc.arrayBuffer: an ArrayBuffer + * dyn over them) or rejected with the stream's + * error / ERR_STREAM_PREMATURE_CLOSE. */ + "sc.text": { argTypes: [null], result: { kind: "promise", inner: STRING } }, + "sc.json": { argTypes: [null], result: { kind: "promise", inner: DYN } }, + "sc.buffer": { argTypes: [null], result: { kind: "promise", inner: BYTES_U8 } }, + "sc.arrayBuffer": { argTypes: [null], result: { kind: "promise", inner: DYN } }, + "readable.newDyn": { argTypes: [DYN], result: VOID }, + "writable.newDyn": { argTypes: [DYN], result: VOID }, + "duplex.newDyn": { argTypes: [DYN], result: VOID }, + "transform.newDyn": { argTypes: [DYN], result: VOID }, + "passthrough.newDyn": { argTypes: [DYN], result: VOID }, + "readable.initDyn": { argTypes: [null, DYN, F64], result: VOID }, + "writable.initDyn": { argTypes: [null, DYN, F64], result: VOID }, + "duplex.initDyn": { argTypes: [null, DYN, F64], result: VOID }, + "transform.initDyn": { argTypes: [null, DYN, F64], result: VOID }, + "passthrough.initDyn": { argTypes: [null, DYN, F64], result: VOID }, + /** Subclass initialization (`super(options?)` in a user `extends + * Readable` constructor): same tail as the `.new` forms, prefixed with + * the BORROWED receiver (the emitted subclass allocation — vtable and + * display name stamped, state NULL until here). Overridden underscore + * methods arrive as synthesized wrapper closures dispatching through + * the vtable. Void result. */ + "readable.init": { argTypes: [null, F64, BOOL, BOOL, F64], result: VOID }, + "writable.init": { argTypes: [null, F64, BOOL, BOOL, F64], result: VOID }, + "duplex.init": { argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID }, + "transform.init": { + argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], + result: VOID, + }, + "passthrough.init": { + argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], + result: VOID, + }, + "readable.push": { argTypes: [null, BYTES_U8], result: BOOL }, + "readable.pushStr": { argTypes: [null, STRING], result: BOOL }, + "readable.pushNull": { argTypes: [null], result: BOOL }, + "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 }, + "readable.read": { argTypes: [null, F64], result: VOID }, + "readable.pause": { argTypes: [null], result: VOID }, + "readable.setEncoding": { argTypes: [null, STRING], result: VOID }, + /** push(chunk, enc) with a literal non-utf8 encoding, and the + * defaultEncoding option's push side (how push(string) decodes — + * 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 }, + "stream.decodeStrings": { argTypes: [null, BOOL], result: VOID }, + /** Runtime decodeStrings and defaultEncoding option values, applied + * after construction (answers the receiver +1); the encoding was + * validated by stream.checkEncodingOption before construction. */ + "stream.optionValues": { argTypes: [null, DYN, DYN], result: VOID }, + "stream.checkEncodingOption": { argTypes: [DYN], result: VOID }, + "stream.duplexFromGen": { argTypes: [null, null, null, null, null, null], result: VOID }, + "stream.duplexFromDyn": { argTypes: [DYN], result: VOID }, + /** Duplex.from over a lone Readable or Writable, or a { readable, + * writable } pair of streams (duplexify.js's _duplexify). */ + "stream.duplexifyReadable": { argTypes: [null], result: VOID }, + "stream.duplexifyWritable": { argTypes: [null], result: VOID }, + "stream.duplexifyPair": { argTypes: [null, null], result: VOID }, + /** Duplex.from(promise): the promise and its box helper. */ + "stream.duplexFromPromise": { argTypes: [null, null], result: VOID }, + /** Duplex.from(asyncFunction): the stage, its source helper and the + * promise's box helper. */ + "stream.duplexFromAsyncFn": { argTypes: [null, null, null], result: VOID }, + /** The same for a body that declares the options argument: then the + * AbortController's abort() and its signal. */ + "stream.duplexFromGenSignal": { + argTypes: [null, null, null, null, null, null, null, DYN], + result: VOID, + }, + "stream.duplexFromAsyncFnSignal": { argTypes: [null, null, null, null, DYN], result: VOID }, + "writable.writeEncoding": { argTypes: [null, STRING], result: VOID }, + "writable.setDefaultEncoding": { argTypes: [null, DYN], result: VOID }, + /** Whether a readable.nextChunkDyn step answered the end (the done + * 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 + * error. The Dyn twin boxes chunks by runtime tag (the JS lane). + * readable.fromArr is Readable.from over a typed array, string or + * Buffer: [array, SCR_FROM_* flags, highWaterMark]. readable.fromDyn is + * Readable.from over a checked-dynamic source: [source, options]. + * readable.fromIter is Readable.from over a class iterator, already + * opened: [checked-dynamic { next } object, flags, highWaterMark]; + * readable.fromAsyncIter is the async class iterator's twin. */ + "readable.nextChunk": { argTypes: [null], result: VOID }, + "readable.nextChunkDyn": { argTypes: [null], result: VOID }, + "readable.fromArr": { argTypes: [null, F64, F64], result: VOID }, + "readable.fromDyn": { argTypes: [DYN, DYN], result: VOID }, + "readable.fromIter": { argTypes: [DYN, F64, F64], result: VOID }, + "readable.fromAsyncIter": { argTypes: [DYN, F64, F64], result: VOID }, + /** Readable.from over a compiled generator: [generator, next, return, + * throw, flags, highWaterMark] (sync) or [generator, next, read, + * return, throw, flags, highWaterMark] (async), the helpers being the + * frontend's interned closures. readable.fromDone is the done marker + * the helpers answer. */ + "readable.fromGen": { argTypes: [null, null, null, null, F64, F64], result: VOID }, + "readable.fromAsyncGen": { argTypes: [null, null, null, null, null, F64, F64], result: VOID }, + "readable.fromDone": { argTypes: [], result: DYN }, + /** The Readable operators (scr_stream_ops.c): [stream, ...] with every + * other argument a checked-dynamic value (the callback, the count, the + * options). map/filter/flatMap/drop/take answer the object-mode output + * stream (validated as a stream class; THROW Node's validation errors); + * toArray settles a dyn array, find a dyn value, some/every a boolean, + * forEach nothing; reduce: [stream, fn, init, hasInit, options, kind] + * with the payload kind (0 dyn, 1 number, 2 boolean, 3 string). The + * promise-returning ones reject instead of throwing. */ + "readable.opMap": { argTypes: [null, DYN, DYN, DYN], result: VOID }, + "readable.opFilter": { argTypes: [null, DYN, DYN, DYN], result: VOID }, + "readable.opFlatMap": { argTypes: [null, DYN, DYN, DYN], result: VOID }, + /** flatMap whose callback returns a compiled generator: the helpers + * over its type follow (async: next, read, return, throw; sync: next, + * return, throw). */ + "readable.opFlatMapAsyncGen": { + argTypes: [null, DYN, DYN, DYN, null, null, null, null], + result: VOID, + }, + "readable.opFlatMapSyncGen": { + argTypes: [null, DYN, DYN, DYN, null, null, null], + result: VOID, + }, + "readable.opDrop": { argTypes: [null, DYN, DYN], result: VOID }, + "readable.opTake": { argTypes: [null, DYN, DYN], result: VOID }, + "readable.opToArray": { argTypes: [null, DYN], result: { kind: "promise", inner: DYN } }, + "readable.opForEach": { + argTypes: [null, DYN, DYN, DYN], + result: { kind: "promise", inner: VOID }, + }, + "readable.opSome": { + argTypes: [null, DYN, DYN, DYN], + result: { kind: "promise", inner: BOOL }, + }, + "readable.opEvery": { + argTypes: [null, DYN, DYN, DYN], + result: { kind: "promise", inner: BOOL }, + }, + "readable.opFind": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: DYN } }, + /** The map family's last argument is the callback's { signal } (or + * undefined). reduceSignal adds ac.abort() (a closure) and + * { signal: ac.signal } for a reducer that reads them. */ + "readable.opReduce": { argTypes: [null, DYN, DYN, BOOL, DYN, F64], result: VOID }, + "readable.opReduceSignal": { + argTypes: [null, DYN, DYN, BOOL, DYN, F64, null, DYN], + result: VOID, + }, + "readable.opSignalList": { argTypes: [DYN], result: DYN }, + "readable.opSignalOpts": { argTypes: [DYN], result: DYN }, + "readable.opSignalOf": { argTypes: [DYN], result: DYN }, + "readable.opOnceOptions": { argTypes: [], result: DYN }, + "readable.resume": { argTypes: [null], result: VOID }, + "readable.isPaused": { argTypes: [null], result: BOOL }, + "readable.pipe": { argTypes: [null, null, BOOL], result: VOID }, + "readable.pipeGeneral": { argTypes: [null, null, BOOL], result: VOID }, + "readable.unpipe": { argTypes: [null], result: VOID }, + "readable.flowing": { argTypes: [null], result: VOID }, + "writable.write": { argTypes: [null, BYTES_U8], result: BOOL }, + "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 }, + "stream.destroy": { argTypes: [null], result: VOID }, + "stream.destroyErr": { argTypes: [null, null], result: VOID }, + /** AsyncIteratorClose for Readable[Symbol.asyncIterator](): destroy with + * Node's AbortError/ABORT_ERR payload while the iterator's internal error + * consumer prevents an unhandled-error crash. */ + "stream.iteratorClose": { argTypes: [null], result: VOID }, + "stream.prop": { argTypes: [null, STRING], result: VOID }, + "stream.errored": { argTypes: [null], result: VOID }, + // node:assert: pass/negated/deep/hasMsg are frontend-computed bools; the + // message slot always carries a string ("" when hasMsg is false). + /** node:assert (scr_assert.c; assert.match in scr_regex.c — every call + * site carries a regex value, so the regex link switch is already on). + * Failures throw a catchable AssertionError — a runtime %Error whose + * name is "AssertionError" and whose code slot is "ERR_ASSERTION" — so + * `instanceof Error`, `.name`, `.message`, and `.code` all answer like + * Node's. Generated messages are Node's assertion_error.js scalar forms + * byte-exactly (the short `a !== b` form, the stacked `+ actual + * - expected` diff with the string `^` indicator, the inline-vs-block + * not-equal split); composite deep failures carry the header line + * without the rendered inspect diff (documented divergence). + * + * assert.ok: (pass, message) — the frontend computed the truthiness AND + * the full message (the user's, or the compile-time source-text form — + * assert.fail lowers here too with pass=false). assert.eqF64/eqBigInt/ + * eqStr/eqBool: (a, b, negated, deep, msg, hasMsg) — Object.is comparison, + * covering strictEqual/notStrictEqual and the scalar deepStrictEqual + * pair; msg is a typed dummy ("" literal) when hasMsg is false (Node + * distinguishes an omitted message from an empty one per operator). + * assert.deepResult: (equal, negated, msg, hasMsg) — the verdict of a + * frontend-synthesized structural comparison, turned into Node's throw. + * assert.sameValue: Object.is over doubles (the deep-equal helpers' + * number leaf; never throws). assert.match: (s, regex, negated, msg, + * hasMsg) — a fresh exec from index 0. assert.throwsNone: (rejection, + * ename, hasEname, msg, hasMsg) — the "Missing expected + * exception|rejection" throw of assert.throws/rejects whose callback + * returned (fulfilled) normally, with Node's ` (${expected.name})` + * detail when the expected class or shape carries a name. + * assert.throwsMismatch: (expectedName, error) — the wrong-class throw + * of the assert.throws(fn, ErrorClass) form. assert.eqSym: + * (a, b, negated, deep, msg, hasMsg) — strict equality over symbol + * values, pointer identity with v24's "Symbol(desc)" stacked-diff + * messages (scr_symbol.c, the assert.match pattern — symbol-typed + * slots already flip the symbol link switch). assert.eqDyn: + * (a, b, negated, deep, msg, hasMsg) — the whole quartet over + * checked-dynamic operands (both slots dyn; the frontend boxes a + * static side with dynFrom first): SameValue for the strict pair over + * the dyn kinds (boxed-closure identity for functions), the structural + * dyn walk for the deep pair, with assertion_error.js's messages — + * scalar forms byte-exact, composites rendered compact:false/sorted + * through the checked-dynamic tree and diffed with the real myers line printer. + * + * The assert.throws(fn, {name/code/message}) shape check + * (expectedException over the static error surface): shapeBegin + * stashes the caught error, shapeStr/shapeRe add one expected key each + * (key ids 0 code / 1 message / 2 name; shapeRe lives in scr_regex.c — + * its regex argument flips the regex link switch — and tests eagerly + * so scr_assert.c stays libregexp-free), then shapeEnd throws Node's + * deep-equal Comparison diff BYTE-EXACTLY (the bounded key set makes + * the rendering enumerable) or the custom message. + * assert.throwsRegex: (regex, error, msg, hasMsg) — the + * assert.throws(fn, /re/) check over String(error), Node's + * regex-mismatch message. assert.regexErrTest: doesNotReject's + * regex predicate over String(error) (never throws). + * assert.unwantedRejection: (error, msg, hasMsg) — doesNotReject's + * "Got unwanted rejection" throw. + * assert.ifErrorErr/F64/Str/Bool: assert.ifError's per-type throws + * ("ifError got unwanted exception: " + the error's message/name or + * the value's inspection) — the frontend routes null/undefined to a + * no-op and everything else here (Node throws for falsy values too). + * All arguments are borrowed. */ + "assert.ok": { argTypes: [BOOL, STRING, BOOL], result: VOID }, + /** The next AssertionError's actual, expected and operator, recorded on + * a failure branch. */ + "assert.record": { argTypes: [DYN, DYN, STRING], result: VOID }, + "assert.errorNew": { + argTypes: [DYN], + result: { kind: "object", className: "%AssertionError" }, + }, + "assert.eqF64": { argTypes: [F64, F64, BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.eqBigInt": { argTypes: [BIGINT_T, BIGINT_T, BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.eqStr": { argTypes: [STRING, STRING, BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.eqBool": { argTypes: [BOOL, BOOL, BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.deepResult": { argTypes: [BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.looseDeepResult": { argTypes: [BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.looseDeepDyn": { argTypes: [DYN, DYN, BOOL, STRING, BOOL], result: VOID }, + "assert.partialDeepDyn": { argTypes: [DYN, DYN, STRING, BOOL], result: VOID }, + "assert.partialDeepResult": { argTypes: [BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.partialEqDyn": { argTypes: [DYN, DYN], result: BOOL }, + "assert.looseF64Eq": { argTypes: [F64, F64], result: BOOL }, + "assert.sameValue": { argTypes: [F64, F64], result: BOOL }, + // The deep-equality pair memo: both slots are program-dependent + // (any cycle-capable record/array/map type). + /* deepStrictEqual's pair memo over cycle-capable types: enter answers + * true for a pair already being compared (Node's memo — equal cyclic + * structures compare true); leave pops. */ + "assert.deqEnter": { argTypes: [null, null], result: BOOL }, + "assert.deqLeave": { argTypes: [], result: VOID }, + "assert.match": { argTypes: [STRING, REGEX, BOOL, STRING, BOOL], result: VOID }, + "assert.throwsNone": { argTypes: [BOOL, STRING, BOOL, STRING, BOOL], result: VOID }, + "assert.throwsMismatch": { + argTypes: [STRING, { kind: "object", className: "%Error" }, STRING, BOOL, BOOL], + result: VOID, + }, + "assert.throwsRegex": { + argTypes: [REGEX, { kind: "object", className: "%Error" }, STRING, BOOL, BOOL], + result: VOID, + }, + // Symbol strict equality (pointer identity; scr_symbol.c). + "assert.eqSym": { + argTypes: [{ kind: "symbol" }, { kind: "symbol" }, BOOL, BOOL, STRING, BOOL], + result: VOID, + }, + // The equality quartet over checked-dynamic operands (the frontend + // boxes a static side into the checked-dynamic tree first). + "assert.eqDyn": { argTypes: [DYN, DYN, BOOL, BOOL, STRING, BOOL], result: VOID }, + /** A message argument that is not a string, registered for the next + * failing assertion (scr_assert_message_value). */ + "assert.messageValue": { argTypes: [DYN, BOOL], result: VOID }, + /** An assert method as a function value, by canonical name. */ + "assert.methodValue": { argTypes: [STRING], result: DYN }, + "assert.messageClear": { argTypes: [], result: VOID }, + "assert.looseResult": { argTypes: [BOOL, BOOL, STRING, STRING, STRING, BOOL], result: VOID }, + // The throws(fn, {shape}) accumulator: begin/slot calls never throw; + // shapeEnd throws the Comparison diff. The error slot is the + // %Error-narrowed caught value. + "assert.shapeBegin": { argTypes: [{ kind: "object", className: "%Error" }], result: VOID }, + "assert.shapeStr": { argTypes: [F64, STRING], result: VOID }, + "assert.shapeRe": { argTypes: [F64, REGEX], result: VOID }, + "assert.shapeEnd": { argTypes: [STRING, BOOL, BOOL], result: VOID }, + "assert.regexErrTest": { + argTypes: [REGEX, { kind: "object", className: "%Error" }], + result: BOOL, + }, + "assert.unwantedRejection": { + argTypes: [{ kind: "object", className: "%Error" }, STRING, BOOL], + result: VOID, + }, + "assert.unwantedError": { argTypes: [STRING, BOOL, STRING, BOOL], result: VOID }, + "assert.noErrorPredicate": { argTypes: [DYN, DYN], result: BOOL }, + "assert.regexDynTest": { argTypes: [REGEX, DYN], result: BOOL }, + /** Node's expectsError over an error-INSTANCE expected (assert.throws/ + * rejects second argument): walk the expected dyn error's keys (name, + * message, code — the %error marker skipped) and deep-compare each + * against the caught value's; a mismatch throws the deep-equal + * AssertionError (scr_assert.c). MAY THROW by design. */ + "assert.expectsErrDyn": { argTypes: [DYN, DYN, STRING, BOOL], result: VOID }, + /** A validation function against the caught value (Node's expectedException). */ + "assert.throwsValidate": { argTypes: [DYN, DYN, STRING, STRING, BOOL], result: VOID }, + // assert.ifError's typed entries (always throw; unit args never + // lower). The error slot is program-dependent — any %Error-hierarchy + // class, root or subclass (the insp.error precedent, null slot). + "assert.ifErrorErr": { argTypes: [null], result: VOID }, + "assert.ifErrorF64": { argTypes: [F64], result: VOID }, + "assert.ifErrorStr": { argTypes: [STRING], result: VOID }, + "assert.ifErrorBool": { argTypes: [BOOL], result: VOID }, + "assert.ifErrorDyn": { argTypes: [DYN], result: VOID }, + // Bytes equality: the two value slots are any bytes kind (the frontend + // gates both sides to ONE static bytes type; the libCall case checks + // the kind and the pair below). + "assert.refEqBytes": { argTypes: [null, null, BOOL, BOOL, STRING, BOOL], result: VOID }, + "assert.refEqFn": { argTypes: [null, null, BOOL, STRING, BOOL], result: VOID }, + "assert.bytesDeepEq": { argTypes: [null, null, BOOL], result: BOOL }, + "assert.looseBytesEq": { argTypes: [null, null], result: BOOL }, + // util.inspect: the error receiver is program-dependent (a builtin or + // user error class — the error.toString precedent, null slot). - "insp.f64": { argTypes: [F64], result: STRING }, - /** util.format %j over a checked-dynamic argument: the runtime dyn - * walk (JS-exact stringify; root undefined/function prints - * "undefined"; a handle in the tree throws the "not supported yet" - * Error). */ - "insp.jsonDyn": { argTypes: [DYN], result: STRING }, - "insp.str": { argTypes: [STRING], result: STRING }, - "insp.regex": { argTypes: [REGEX], result: STRING }, - /** A Date's inspect text: toISOString(), or "Invalid Date". */ - "insp.date": { argTypes: [DATE_T], result: STRING }, - "insp.buffer": { argTypes: [BYTES_U8], result: STRING }, - /** A numeric typed array that is not a Buffer (any element kind). */ - "insp.typedArray": { argTypes: [null, F64, F64], result: STRING }, - /** A u8 byte view in a union arm, element or field: a DataView, Buffer - * or Uint8Array by its own flags. */ - "insp.bytes": { argTypes: [null, F64, F64], result: STRING }, - "insp.error": { argTypes: [null, F64, F64], result: STRING }, - "insp.errorHasCause": { argTypes: [null], result: BOOL }, - "insp.errorBagHasProperties": { argTypes: [DYN], result: BOOL }, - "insp.errorCause": { argTypes: [null, F64, F64], result: STRING }, - "insp.dyn": { argTypes: [DYN, F64, F64], result: STRING }, - "insp.dynS": { argTypes: [DYN, F64], result: STRING }, - /** util.format's %o over a checked-dynamic value: inspect with - * showHidden. Kinds whose hidden entries have no rendering throw the - * 'not supported yet' Error. */ - "insp.dynHidden": { argTypes: [DYN, F64], result: STRING }, - "insp.begin": { argTypes: [F64], result: VOID }, - /** util.inspect's breakLength, compact (0 for false) and sorted for the - * renders that follow; the call site restores 80, 3 and false afterwards. */ - "insp.layout": { argTypes: [F64, F64, BOOL], result: VOID }, - "insp.entry": { argTypes: [STRING, BOOL], result: VOID }, - // Circular references: the receiver slot is program-dependent (any - // cycle-capable record/array/map/class type — the insp.error precedent). - /* Circular references over cycle-capable composites (recursive record/ - * class types): circCheck answers a value's circular id when it is - * already on the traversal stack (0 otherwise), seenPush/refWrap - * bracket the frame (insp.end adds Node's " " prefix to values - * the walk found circular), circular renders "[Circular *N]". */ - "insp.circCheck": { argTypes: [null], result: F64 }, - "insp.seenPush": { argTypes: [null], result: VOID }, - "insp.refWrap": { argTypes: [null, STRING], result: STRING }, - "insp.circular": { argTypes: [F64], result: STRING }, - /* Whether a closed declared-field record kept the undeclared properties - * of its checked source (scr_record_source_note): reflective surfaces - * (inspect, Object.keys/values/entries) then answer through its live - * checked view. The receiver is any such record type. Never throws. */ - "record.sourceHas": { argTypes: [null], result: BOOL }, - /** `record.sourceSpread(built, value)`: the record a literal starting - * with `...value` just built keeps the order of value's kept source - * (key-order-witness.ts ordered spreads). Both are record types. Never - * throws. */ - "record.sourceSpread": { argTypes: [null, null], result: VOID }, - "insp.key": { argTypes: [STRING], result: STRING }, - "insp.moreItems": { argTypes: [F64], result: STRING }, - "insp.end": { argTypes: [STRING, STRING, STRING, F64, BOOL, BOOL], result: STRING }, - /** A nested inspect inside a custom rendering (PerformanceEntry's - * `${name} ${inspect(toJSON)}`): nestedBegin starts a fresh inspect - * context, nestedEnd(prefix, inner) restores the caller's and answers - * `prefix inner` re-indented at the caller's level (+1). */ - "insp.nestedBegin": { argTypes: [], result: VOID }, - "insp.nestedEnd": { argTypes: [STRING, STRING], result: STRING }, - /** Around a top-level render that an inspection refusal can end: mark - * records the inspector's frame and nesting depth, unwind (in the - * render's finally) closes what the render left open above it. */ - "insp.mark": { argTypes: [], result: VOID }, - "insp.unwind": { argTypes: [], result: VOID }, - /** node:string_decoder's utf8 StringDecoder (scr_string.c): the decoder - * value is a one-field record whose f64 PACKS the pending partial - * sequence (count + up to 3 raw bytes — Node buffers at most 3 for - * every encoding); the frontend's interned %strdec helpers thread it, - * with the decoder's CANONICAL encoding name first, through these pure - * functions. write: (enc, pending, chunk) → the decoded complete - * prefix of pending+chunk (+1); next: (enc, pending, chunk) → the - * packed NEW pending; end: (enc, pending) → the buffered partial's - * flush (+1). Node-exact per encoding (oracle-pinned); none throws. */ - "strdec.write": { argTypes: [STRING, F64, BYTES_U8], result: STRING }, - "strdec.next": { argTypes: [STRING, F64, BYTES_U8], result: F64 }, - "strdec.end": { argTypes: [STRING, F64], result: STRING }, - /** A runtime encoding argument's canonical name (Node's normalizeEncoding: - * nullish and "" are utf8); an unknown name throws ERR_UNKNOWN_ENCODING. */ - "strdec.encoding": { argTypes: [DYN], result: STRING }, - // node:readline: the interface handle is f64; the callbacks' func types - // are program-dependent (the emitter picks the adapter by parameter - // count), so those slots are null like child.onExit's. - /** node:readline and node:readline/promises (scr_readline.c, linked - * under the events gate — these fns imply moduleUsesProcessEvents; a - * stream input or output adds scr_stream.c's bridge). The interface - * value is an f64 handle (the Timeout-id precedent); an open interface - * on stdin keeps the loop alive until close/EOF. - * rl.create: [mode, input?, output?, crlfDelay, prompt?] → f64, mode - * bits 1 stream input, 2 stream output, 4 stdout output, 16 prompt - * (lowerReadlineCreateCall); rl.createSignal: the same plus the - * trailing AbortSignal (mode bit 8), which links the event-target unit - * for addAbortListener. rl.question: [handle, query, cb] — the callback - * form; THROWS ERR_USE_AFTER_CLOSE on a closed interface (may-throw). - * rl.questionAsync(Signal): [handle, query, signal?] → promise, - * rejecting instead of throwing. rl.close/pause/resume/prompt: - * [handle] (pause/resume/prompt throw ERR_USE_AFTER_CLOSE when - * closed); rl.setPrompt/getPrompt; rl.write: [handle, data] (throws - * when closed). rl.on: [handle, event, cb, once] — the event number - * (0 'line', 1 'close', 2 'pause', 3 'resume', 4 'error') and the - * listener (moves). rl.iterOpen/iterNext/iterReturn: the async - * iterator — iterNext answers promise of the line or undefined. */ - "rl.create": { argTypes: [F64], result: F64 }, - "rl.createSignal": { argTypes: [F64], result: F64 }, - "rl.question": { argTypes: [F64, STRING, null], result: VOID }, - "rl.questionAsync": { argTypes: [F64, STRING], result: { kind: "promise", inner: STRING } }, - "rl.questionAsyncSignal": { - argTypes: [F64, STRING, DYN], - result: { kind: "promise", inner: STRING }, - }, - /** The question forms with a checked-dynamic query (a JavaScript value - * that need not be a string). */ - "rl.questionDyn": { argTypes: [F64, DYN, null], result: VOID }, - "rl.questionAsyncDyn": { argTypes: [F64, DYN], result: { kind: "promise", inner: STRING } }, - "rl.questionAsyncSignalDyn": { - argTypes: [F64, DYN, DYN], - result: { kind: "promise", inner: STRING }, - }, - "rl.close": { argTypes: [F64], result: VOID }, - "rl.pause": { argTypes: [F64], result: VOID }, - "rl.resume": { argTypes: [F64], result: VOID }, - "rl.prompt": { argTypes: [F64], result: VOID }, - /** prompt(preserveCursor): [handle, preserveCursor] */ - "rl.promptWith": { argTypes: [F64, BOOL], result: VOID }, - "rl.setPrompt": { argTypes: [F64, STRING], result: VOID }, - "rl.setPromptDyn": { argTypes: [F64, DYN], result: VOID }, - "rl.getPrompt": { argTypes: [F64], result: STRING }, - "rl.write": { argTypes: [F64, STRING], result: VOID }, - /** write(data, key): [handle, hasData, data, hasKey, hasName, name, - * ctrl, meta, shift] */ - "rl.writeKey": { - argTypes: [F64, BOOL, STRING, BOOL, BOOL, STRING, BOOL, BOOL, BOOL], - result: VOID, - }, - "rl.on": { argTypes: [F64, F64, null, BOOL], result: VOID }, - "rl.iterOpen": { argTypes: [F64], result: VOID }, - "rl.iterNext": { argTypes: [F64], result: { kind: "promise", inner: DYN } }, - "rl.iterReturn": { argTypes: [F64], result: VOID }, - /** createInterface's history and editing options (scr_rl_check_options: - * [flags, tabSize, escapeCodeTimeout, historySize], may throw; - * rl.setOptions: [handle, flags, historySize, removeHistoryDuplicates, - * tabSize, escapeCodeTimeout]; rl.setHistory: [handle, history]) and - * the member reads (rl.history answers the array, rl.numProp the - * numeric member by number). */ - "rl.checkOptions": { argTypes: [F64, F64, F64, F64], result: VOID }, - "rl.checkCompleter": { argTypes: [DYN], result: VOID }, - "rl.setOptions": { argTypes: [F64, F64, F64, BOOL, F64, F64], result: VOID }, - "rl.setHistory": { argTypes: [F64, arrayOf(STRING)], result: VOID }, - "rl.history": { argTypes: [F64], result: arrayOf(STRING) }, - "rl.numProp": { argTypes: [F64, F64], result: F64 }, - /** Terminal mode (scr_readline_tty.c): rl.terminal: [handle, mode (0 - * the option's value, 2 output.isTTY of process.stdout), terminal, - * completer (undefined when absent), readline/promises], emitted with - * scr_insp_dyn when there is a completer; the line, terminal and cursor - * members. */ - "rl.terminal": { argTypes: [F64, F64, BOOL, DYN, BOOL], result: VOID }, - "rl.line": { argTypes: [F64], result: STRING }, - "rl.isTerminal": { argTypes: [F64], result: BOOL }, - "rl.isPromises": { argTypes: [F64], result: BOOL }, - /** createInterface's input or output as a checked-dynamic value: - * [value, output] — the stream; refused when it is not one. */ - "rl.inputFromDyn": { argTypes: [DYN, BOOL], result: { kind: "object", className: "%Readable" } }, - "rl.outputFromDyn": { - argTypes: [DYN, BOOL], - result: { kind: "object", className: "%Writable" }, - }, - "rl.cursor": { argTypes: [F64], result: F64 }, - /** readline.cursorTo / moveCursor / clearLine / clearScreenDown: rl.seq - * [kind (0-3), the first and second numbers, whether the second is - * given] answers the data to write after the argument checks (may - * throw); rl.moves [dx, dy] says whether moveCursor writes. */ - "rl.seq": { argTypes: [F64, F64, F64, BOOL], result: STRING }, - "rl.moves": { argTypes: [F64, F64], result: BOOL }, - /** getCursorPos() (scr_readline_tty.c, linked through the regex unit - * like rl.terminal): [handle, 0 rows | 1 cols] */ - "rl.cursorPos": { argTypes: [F64, F64], result: F64 }, - /** readline.emitKeypressEvents (scr_readline_tty.c): rl.keypressStdin - * [interface (0: none), escapeCodeTimeout (NaN: the interface's or - * 500)], rl.keypressStream [stream, interface, timeout]. */ - "rl.keypressStdin": { argTypes: [F64, F64], result: VOID }, - "rl.keypressStream": { argTypes: [null, F64, F64], result: VOID }, - /** readline/promises Readline over an f64 handle: rl.uiOpen [fd (1 - * stdout, 2 stderr), autoCommit], rl.uiOpenStream [stream, autoCommit] - * (isWritable, may throw); the actions take [handle, ...numbers] (the - * checks may throw; cursorTo: [handle, x, whether y is given, y]); - * rl.uiCommit answers a promise. */ - "rl.uiOpen": { argTypes: [F64, BOOL], result: F64 }, - "rl.uiOpenStream": { argTypes: [null, BOOL], result: F64 }, - "rl.uiCursorTo": { argTypes: [F64, F64, BOOL, F64], result: VOID }, - "rl.uiMoveCursor": { argTypes: [F64, F64, F64], result: VOID }, - "rl.uiClearLine": { argTypes: [F64, F64], result: VOID }, - "rl.uiClearScreenDown": { argTypes: [F64], result: VOID }, - "rl.uiRollback": { argTypes: [F64], result: VOID }, - "rl.uiCommit": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, - /** node:timers/promises — the promisified pair (scr_async.c, beside - * the timer heap they ride): tp.setTimeout: [ms] → a pending void - * promise a one-shot heap timer fulfills (the loop's timer phase, FIFO - * against equal deadlines like Node); tp.setImmediate: [] → the same - * through the immediate queue (fires before due timers of later loop - * turns, Node's check phase). Neither throws. */ - "tp.setTimeout": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, - // scheduler.wait(delay): tp.setTimeout's promise. It throws Node's - // ERR_INVALID_THIS once mock.timers has restored the real scheduler.wait. - "tp.schedulerWait": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, - "tp.setImmediate": { argTypes: [], result: { kind: "promise", inner: VOID } }, - /** events.once (scr_events_emitter.c): [emitter, name, boxer, - * errorBoxer, listenForError] → a pending promise of any[]; the boxers - * turn the event's and the 'error' tuple into that array. */ - "events.once": { argTypes: [null, STRING, null, null, BOOL], result: VOID }, - /** events.once(emitter, name, { signal }) (scr_events_once_signal.c): - * events.once's arguments plus the checked-dynamic signal (an - * AbortSignal or undefined). */ - "events.onceSignal": { argTypes: [null, STRING, null, null, BOOL, DYN], result: VOID }, - /** The resolve-value forms: [ms?, value, ref] → a pending promise of the - * value's type that copies the borrowed already-resolved `value` promise - * when the timer (or immediate) fires; ref false unrefs it. The result - * type follows the value promise. */ - "tp.setTimeoutValue": { argTypes: [F64, null, BOOL], result: VOID }, - "tp.setImmediateValue": { argTypes: [null, BOOL], result: VOID }, - /** The same with options.signal (a checked-dynamic AbortSignal or - * undefined) last: an abort clears the timer and rejects with an - * AbortError (scr_event_target.c). */ - "tp.setTimeoutSignal": { argTypes: [F64, null, BOOL, DYN], result: VOID }, - "tp.setImmediateSignal": { argTypes: [null, BOOL, DYN], result: VOID }, - /** scheduler.wait(delay, options): tp.setTimeoutValue's and - * tp.setTimeoutSignal's shapes, with tp.schedulerWait's throws. */ - "tp.schedulerWaitValue": { argTypes: [F64, null, BOOL], result: VOID }, - "tp.schedulerWaitSignal": { argTypes: [F64, null, BOOL, DYN], result: VOID }, - /** node:diagnostics_channel (scr_dc.c, linked when any dc.* appears — - * the zlib gating precedent): a process-global name→channel registry; - * channel values are f64 handles (type-mapper.ts maps Channel to F64, the - * readline.Interface pattern). Subscribers are dyn function values — - * identity-compared by unsubscribe, called (message, name) by publish - * over a SNAPSHOT of the list (a subscriber unsubscribing itself - * mid-publish still lets its siblings fire, Node's behavior). dc.publish - * MAY THROW: a subscriber's throw propagates out of publish (catchable - * there) where Node routes it to triggerUncaughtException — the - * documented divergence. subscribe/unsubscribe throw Node's - * ERR_INVALID_ARG_TYPE TypeError for non-function subscribers. */ - "dc.channel": { argTypes: [STRING], result: F64 }, - "dc.subscribe": { argTypes: [STRING, DYN], result: VOID }, - "dc.unsubscribe": { argTypes: [STRING, DYN], result: BOOL }, - "dc.hasSubscribers": { argTypes: [STRING], result: BOOL }, - "dc.publish": { argTypes: [F64, DYN], result: VOID }, - "dc.chanSubscribe": { argTypes: [F64, DYN], result: VOID }, - "dc.chanUnsubscribe": { argTypes: [F64, DYN], result: BOOL }, - "dc.chanHasSubscribers": { argTypes: [F64], result: BOOL }, - "dc.chanName": { argTypes: [F64], result: STRING }, - /** TracingChannel (dc.tracingChannel): a registry entry of the five - * event channels, an f64 handle like Channel (type-mapper.ts). tcSubscribe/ - * tcUnsubscribe walk a dyn handlers object's five event keys (truthy - * non-function slots throw the per-channel ERR_INVALID_ARG_TYPE); - * tcTraceSync/tcTraceCallback/tcTracePromise run Node's publish choreography in C over - * dyn values (fn, ctx, thisArg, args-array) with thisArg bound as the - * ambient receiver — the traced call's throw and any subscriber throw - * both propagate (MAY THROW). tcTraceCallback wraps args[position] in a - * native error/result + asyncStart/asyncEnd publisher and throws Node's - * TypeError when that slot is not callable. tracingChannelOf is the - * five-Channel collection form of the constructor. */ - /** setImmediate as a first-class dyn value (scr_async.c): a minted dyn - * callable scheduling args[0](args[1..]) on the immediate queue — the - * Node-suite traceCallback shape (`traceCallback(setImmediate, ...)`). - * Calling it validates the callback (the dyn call machinery); minting - * never throws. */ - "timers.setImmediateFnValue": { argTypes: [], result: DYN }, - /** `new Promise(setImmediate)` (the Node-suite early-exit shape): a - * fresh promise an immediate fulfills with the undefined dyn value — - * the executor IS setImmediate, so resolve rides the immediate queue - * (scr_async.c). Never throws. */ - "timers.immediatePromise": { argTypes: [], result: { kind: "promise", inner: DYN } }, - "dc.tracingChannel": { argTypes: [STRING], result: F64 }, - "dc.tracingChannelOf": { argTypes: [F64, F64, F64, F64, F64], result: F64 }, - "dc.tcChannel": { argTypes: [F64, F64], result: F64 }, - "dc.tcHasSubscribers": { argTypes: [F64], result: BOOL }, - "dc.tcSubscribe": { argTypes: [F64, DYN], result: VOID }, - "dc.tcUnsubscribe": { argTypes: [F64, DYN], result: BOOL }, - "dc.tcTraceSync": { argTypes: [F64, DYN, DYN, DYN, DYN], result: DYN }, - "dc.tcTraceCallback": { argTypes: [F64, DYN, F64, DYN, DYN, DYN], result: DYN }, - /** tracePromise (scr_dc.c): start publish, the traced call, a wrap of - * non-promise results, the end publish, and a REACTION FIBER that - * awaits the traced promise and publishes asyncStart/asyncEnd (error - * first on rejection) before settling the returned promise with - * the passed-through outcome. MAY THROW (the traced call and the - * synchronous publishes). */ - "dc.tcTracePromise": { - argTypes: [F64, DYN, DYN, DYN, DYN], - result: { kind: "promise", inner: DYN }, - }, - /** process.on/once('unhandledRejection', fn): registers a dyn listener - * the checkpoint report dispatches per never-observed rejection — - * (reason, promise) — instead of printing and exiting 1 (scr_async.c). - * The bool second arg is `once` (auto-removed after one delivery, - * Node's once); off/removeListener remove by closure identity, the - * offWarning stance. Throws Node's ERR_INVALID_ARG_TYPE on a - * non-function. */ - /** Uncaught exception handlers and monitors: [callback dyn, once, monitor]. */ - "process.onUncaughtException": { argTypes: [DYN, BOOL, BOOL], result: VOID }, - "process.offUncaughtException": { argTypes: [DYN, BOOL], result: VOID }, - "process.onUnhandledRejection": { argTypes: [DYN, BOOL], result: VOID }, - "process.offUnhandledRejection": { argTypes: [DYN], result: VOID }, - /** process.on/once/off('rejectionHandled', fn): the sibling registry. - * A handler attached after unhandledRejection delivery fires the event - * once. Dispatch is synchronous at the attach, with the promise, - * Node's payload. */ - "process.onRejectionHandled": { argTypes: [DYN, BOOL], result: VOID }, - "process.offRejectionHandled": { argTypes: [DYN], result: VOID }, - /** process warnings (scr_async_dyn.c): onWarning/offWarning register - * dyn listeners; emitWarning applies Node's full argument grammar over - * the call's dyn argument vector (ERR_INVALID_ARG_TYPE TypeErrors — MAY - * THROW) and emits on the next tick, like Node: Node's stderr report, - * then the listeners. */ - "process.onWarning": { argTypes: [DYN], result: VOID }, - "process.offWarning": { argTypes: [DYN], result: VOID }, - "process.emitWarning": { argTypes: [DYN], result: VOID }, - /** A Warning-typed process warning with this message (the console - * members' warnings), emitted on the next tick as Node's emitWarning - * does (so the loop runs); an empty message emits nothing. */ - "process.emitWarningText": { argTypes: [STRING], result: VOID }, - /** process.noDeprecation (0) / process.throwDeprecation (1): the stored - * value (undefined until assigned) and its assignment. util.deprecate's - * warnings and DeprecationWarning emissions read their truthiness. */ - "process.deprecationFlag": { argTypes: [F64], result: DYN }, - "process.setDeprecationFlag": { argTypes: [F64, DYN], result: VOID }, - "process.setEmitWarning": { argTypes: [DYN], result: VOID }, - /** AsyncLocalStorage (node:async_hooks — scr_async.c): stores are f64 - * handles; contexts are immutable fiber-carried snapshots (spawned - * fibers inherit the spawner's — Node's init-time capture). run/exit - * enter (or clear) the store, call the dyn function with forwarded - * arguments, and restore (the finally); getStore answers the current - * dyn value or undefined; enterWith installs with no restore point. - * run/exit MAY THROW (the callback's own throws propagate). */ - /** `await v` over a checked-dynamic VALUE (scr_async.c): a dyn promise - * adopts (rejections re-throw — MAY THROW), anything else takes JS's - * one-microtask non-thenable await and answers itself. Only emitted - * inside async bodies (the frontend's isAsync gate). */ - "async.awaitDyn": { argTypes: [DYN], result: DYN }, - /** The bare one-microtask hop (scr_await_hop): `await v` over a typed - * NON-promise value — JS awaits non-thenables through exactly one - * microtask turn and yields the value itself. Never throws. */ - "async.hop": { argTypes: [], result: VOID }, - "als.new": { argTypes: [], result: F64 }, - "als.get": { argTypes: [F64], result: DYN }, - "als.run": { argTypes: [F64, DYN, DYN, DYN], result: DYN }, - "als.exitRun": { argTypes: [F64, DYN, DYN], result: DYN }, - "als.enterWith": { argTypes: [F64, DYN], result: VOID }, - "als.disable": { argTypes: [F64], result: VOID }, - /** new AsyncLocalStorage(options) (Node's object check throws; MAY - * THROW) and the store's name option (dyn; "" without one). */ - "als.newOptions": { argTypes: [DYN], result: F64 }, - "als.name": { argTypes: [F64], result: DYN }, - /** A function bound to the active context: the completion callbacks of - * callback-style APIs (always false: an empty context leaves the - * callback unbound) and AsyncLocalStorage.bind (always true). The typed - * form answers its closure argument's type; the dyn form throws Node's - * ERR_INVALID_ARG_TYPE for a non-function when always (MAY THROW). */ - "als.bindCallback": { argTypes: [null, BOOL], result: VOID }, - "als.bindCallbackDyn": { argTypes: [DYN, BOOL], result: DYN }, - /** AsyncLocalStorage.snapshot(): a dyn function (cb, ...args) that runs - * cb in the context active at the snapshot. */ - "als.snapshot": { argTypes: [], result: DYN }, - /** Channel.bindStore/unbindStore/runStores (scr_dc.c): the - * AsyncLocalStorage integration — set-semantics bindings per store, - * runStores entering every bound store with transform(data) around the - * publish and the callback (MAY THROW: transforms, subscribers, and - * the callback all run). */ - "dc.chanBindStore": { argTypes: [F64, F64, DYN], result: VOID }, - "dc.chanUnbindStore": { argTypes: [F64, F64], result: BOOL }, - "dc.chanRunStores": { argTypes: [F64, DYN, DYN, DYN, DYN], result: DYN }, - /** The Number statics with a static C implementation (scr_lib.c): one - * f64 arg → bool, JS-exact BY CONSTRUCTION — Number.isFinite/isNaN/ - * isInteger/isSafeInteger never coerce, and the frontend routes only - * f64-typed arguments here (other static types fence). None throws. */ - "number.isFinite": { argTypes: [F64], result: BOOL }, - "number.isNaN": { argTypes: [F64], result: BOOL }, - "number.isInteger": { argTypes: [F64], result: BOOL }, - "number.isSafeInteger": { argTypes: [F64], result: BOOL }, - /** Date (scr_lib.c). Values are TimeClip'd epoch-millisecond scalars: - * construction/store/pass/getters are exact while identity and mutation - * remain fenced. date.now is Node's integer milliseconds since epoch; - * date.toISOString formats one raw f64 millisecond time value and - * date.toISOStringValue formats a stored Date, both with Node's rules (UTC, - * YYYY-MM-DDTHH:mm:ss.sssZ, expanded ±YYYYYY years outside 0–9999, - * ToInteger truncation of fractional ms) and THROWS Node's "Invalid - * time value" RangeError on NaN / out-of-range input (may-throw seed). - * Results: f64 / owned (+1) string. */ - "date.now": { argTypes: [], result: F64 }, - "date.newNow": { argTypes: [], result: DATE_T }, - "date.newMs": { argTypes: [F64], result: DATE_T }, - "date.newString": { argTypes: [STRING], result: DATE_T }, - /** Date.parse(dateString), with its own reach witness for exact fences. */ - "date.parse": { argTypes: [STRING], result: F64 }, - "date.getTime": { argTypes: [DATE_T], result: F64 }, - "date.valueOf": { argTypes: [DATE_T], result: F64 }, - "date.toISOString": { argTypes: [F64], result: STRING }, - "date.toISOStringValue": { argTypes: [DATE_T], result: STRING }, - /** Date.prototype.toUTCString over a stored Date ("Invalid Date" for - * NaN; never throws). */ - "date.toUTCString": { argTypes: [DATE_T], result: STRING }, - "date.getFullYear": { argTypes: [DATE_T], result: F64 }, - "date.getUTCFullYear": { argTypes: [DATE_T], result: F64 }, - "date.getMonth": { argTypes: [DATE_T], result: F64 }, - "date.getUTCMonth": { argTypes: [DATE_T], result: F64 }, - "date.getDate": { argTypes: [DATE_T], result: F64 }, - "date.getUTCDate": { argTypes: [DATE_T], result: F64 }, - "date.getDay": { argTypes: [DATE_T], result: F64 }, - "date.getUTCDay": { argTypes: [DATE_T], result: F64 }, - "date.getHours": { argTypes: [DATE_T], result: F64 }, - "date.getUTCHours": { argTypes: [DATE_T], result: F64 }, - "date.getMinutes": { argTypes: [DATE_T], result: F64 }, - "date.getUTCMinutes": { argTypes: [DATE_T], result: F64 }, - "date.getSeconds": { argTypes: [DATE_T], result: F64 }, - "date.getUTCSeconds": { argTypes: [DATE_T], result: F64 }, - "date.getMilliseconds": { argTypes: [DATE_T], result: F64 }, - "date.getUTCMilliseconds": { argTypes: [DATE_T], result: F64 }, - "date.getTimezoneOffset": { argTypes: [DATE_T], result: F64 }, - /** A Date object for a time value a local holds (date-values.ts): a - * lib-call argument passes the time value itself, and any other use - * creates the Date. */ - "date.fromTime": { argTypes: [F64], result: DATE_T }, - /** Date.prototype's field setters over a time value - * (scr_date_set_fields): time value, setter code, argument count and four - * arguments; answers the new time value. */ - "date.setFields": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: F64 }, - /** TimeClip of a number (setTime's argument). */ - "date.timeClip": { argTypes: [F64], result: F64 }, - /** Stores a time value into a Date object and answers it (a setter's - * write). */ - "date.store": { argTypes: [DATE_T, F64], result: F64 }, - /** Date.prototype.toString / toDateString / toTimeString and String(date): - * V8's local-time formats with the host zone's long name, "Invalid Date" - * for an invalid date. Owned (+1) string; never throws. */ - "date.toString": { argTypes: [DATE_T], result: STRING }, - "date.toDateString": { argTypes: [DATE_T], result: STRING }, - "date.toTimeString": { argTypes: [DATE_T], result: STRING }, - /** The composed `new Date(dateString).getTime()` read: one borrowed - * string, f64 milliseconds since epoch, with V8's date-string grammar - * (the ES5 format, then the legacy forms: RFC 1123/2822, Date#toString, - * "Jan 2, 2024", ...). Zone-less dates are local time; anything V8 - * rejects is NaN, Node's invalid-date getTime. Never throws. */ - "date.parseGetTime": { argTypes: [STRING], result: F64 }, - /** `Date.UTC(...)` — seven f64 arguments (the frontend completes the - * spec's defaults for omitted trailing parts: month 0, date 1, time - * parts 0), the spec's MakeDay/MakeTime/TimeClip exactly: 0–99 years - * map to 1900+year, out-of-range months/dates roll over, non-finite - * parts and out-of-range results answer NaN. Never throws. */ - "date.utc": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: F64 }, - /** `new Date(year, monthIndex[, date, hours, minutes, seconds, ms])` — - * the same seven-f64 completion as date.utc, but the fields are LOCAL - * time: V8's MakeDay/MakeTime, then UTC(t) in the host zone (DST gaps - * read with the offset before the transition, overlaps answer the - * earlier instant), then TimeClip. Never throws. */ - "date.newLocal": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: DATE_T }, - /** WHATWG TextDecoder.decode over u8 bytes (scr_bytes.c): utf-8 with - * default options — the same maximal-subpart replacement decode as - * Buffer.toString("utf8"), with the leading BOM stripped (the one - * behavioral difference; ignoreBOM defaults to false). Inline calls - * lower directly; stored decoder records dispatch by encoding id. - * TextEncoder.encode needs no libFn: its matching forms - * lower to buffer.fromStr(s, "utf8") (identical bytes — both write each - * 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 }, - /** Whether two typed arrays share no byte: their element ranges in - * memory do not overlap. Borrowed args of any element kind; never throws. */ - "bytes.disjoint": { argTypes: [null, null], 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 }, - /** 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.diffieHellman": { argTypes: [DYN, DYN], result: DYN }, - "crypto.verifyOneshot": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, - "crypto.createSign": { argTypes: [DYN], result: DYN }, - "crypto.createVerify": { argTypes: [DYN], result: DYN }, - /** 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 }, - "text.decodeBufferSource": { argTypes: [DYN], result: STRING }, - "text.decodeOptions": { argTypes: [BYTES_U8, BOOL, BOOL], result: STRING }, - "text.decodeStream": { argTypes: [DYN, BYTES_U8, F64, BOOL, BOOL, BOOL], result: STRING }, - "text.decodeLegacyOptions": { argTypes: [BYTES_U8, F64, BOOL, BOOL], result: STRING }, - /** TextDecoder.decode for a compile-time non-UTF-8 WHATWG label. The - * second f64 is the frontend-owned encoding id consumed by scr_bytes.c; - * label aliases/case/ASCII whitespace are canonicalized before IR. The - * mapping tables are generated from the pinned Node 24 oracle, keeping - * output portable without an ICU/iconv dependency. Borrowed args; owned - * (+1) string; never throws. */ - "text.decodeLegacy": { argTypes: [BYTES_U8, F64], result: STRING }, - "text.decoderEncoding": { argTypes: [DYN], result: F64 }, - "text.decoderName": { argTypes: [F64], result: STRING }, - "text.encodeInto": { argTypes: [DYN, DYN], result: DYN }, - /** `value instanceof TextDecoder` (true) or `TextEncoder` (false) on a - * checked-dynamic value: a codec record's handle box (scr_text_codec_dyn.c). - * Borrowed; never throws. */ - "text.codecIs": { argTypes: [DYN, BOOL], result: BOOL }, - "buffer.isAscii": { argTypes: [DYN], result: BOOL }, - "buffer.isUtf8": { argTypes: [DYN], result: BOOL }, - "buffer.transcode": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, - /** The fs option forms and friends (scr_lib.c), all throwing catchably - * like the rest of sync fs. mkdirRecursiveSync is Node's recursive - * algorithm (try mkdir, EEXIST-dir is fine, ENOENT creates the parent - * first — errors report Node's errno at Node's path, EEXIST at a file - * target, ENOTDIR at the full path past a file); the first-created-dir - * return value has no lowering (statement position only, frontend- - * fenced). rmOptsSync is rmSync with (recursive, force) bools: force - * swallows ENOENT, recursive removes trees post-order, a directory - * without recursive throws Node's ERR_FS_EISDIR SystemError. mkdtempSync appends the six X's and returns the - * created path (+1). accessSync takes the F_OK/R_OK/W_OK/X_OK bits as - * one f64 (the frontend bakes fs.constants.* as literals and completes - * an omitted mode to 0). readFdSync/readFdSyncBytes are the - * readFileSync(fd[, "utf8"]) forms — a read(2) loop to EOF on the fd - * (the stdin path); errors carry Node's no-path message shape. */ - "fs.mkdirRecursiveSync": { argTypes: [STRING], result: VOID }, - "fs.rmOptsSync": { argTypes: [STRING, BOOL, BOOL], result: VOID }, - /** rmOptsSync's maxRetries/retryDelay form (path, recursive, force, - * maxRetries, retryDelay): Node's linear-backoff retry on - * EBUSY/EMFILE/ENFILE/ENOTEMPTY/EPERM — the tmpdir-harness shape - * rmSync(p, { maxRetries: 3, recursive: true, force: true }). Throws - * ERR_OUT_OF_RANGE for a retryDelay outside int32 >= 0 or a maxRetries - * outside uint32, before checking the path. */ - "fs.rmRetrySync": { argTypes: [STRING, BOOL, BOOL, F64, F64], result: VOID }, - "fs.mkdtempSync": { argTypes: [STRING], result: STRING }, - "fs.accessSync": { argTypes: [STRING, F64], result: VOID }, - "fs.readFdSync": { argTypes: [F64, STRING], result: STRING }, - "fs.readFdSyncBytes": { argTypes: [F64], result: BYTES_U8 }, - /** Validated isatty(3) probe. Stream property lowering maps false to - * undefined; tty.isatty exposes the boolean result directly. */ - "process.isTTY": { argTypes: [F64], result: BOOL }, - // Like process.envGet: the result is the module's interned - // `number | undefined` union — checked by arms in the libCall case. - /** Terminal width over an fd literal (1/2 — process.stdout/stderr - * .columns): ioctl(TIOCGWINSZ). The result is the module's interned - * `number | undefined` union — a non-TTY stream (or an ioctl refusal) - * yields the undefined arm, exactly Node's missing `.columns`. Never - * throws. */ - "process.columns": { argTypes: [F64], result: VOID }, - /** Terminal height, with the same fd and optional-number contract. */ - "process.rows": { argTypes: [F64], result: VOID }, - /** process.stdin.destroy(): a deliberate no-op (no stream machinery - * exists to tear down; documented in SEMANTICS.md). */ - "process.stdinDestroy": { argTypes: [], result: VOID }, - /** process.stdin.setRawMode(mode) — one borrowed bool arg, void. On a - * TTY stdin: termios raw mode on/off (libuv's UV_TTY_MODE_RAW flag set, - * the mode Node's setRawMode(true) applies; false restores the entry - * state). On a NON-TTY stdin Node's process.stdin has no such method at - * all, so the call throws Node's exact catchable TypeError - * ("process.stdin.setRawMode is not a function") — the may-throw seed - * carries it. */ - "process.stdinSetRawMode": { argTypes: [BOOL], result: VOID }, - // Arg 0 is a packed f64[] OR a bytes value (the spread-typed-array - // form) — checked in the libCall case. - /** fromCharCode takes one packed f64[] or bytes arg and builds a string - * from UTF-16 code units. Adjacent surrogate pairs combine; lone - * surrogates follow the runtime's replacement policy. */ - "string.fromCharCode": { argTypes: [null], result: STRING }, - /** Numeric code points, with catchable RangeError for non-integers or - * values outside 0..0x10ffff. Uses the same UTF-8 surrogate policy. */ - "string.fromCodePoint": { argTypes: [null], result: STRING }, - /** lastIndexOf returns the last UTF-16 start index, or -1. The two-arg - * form searches at or before its numeric position; NaN starts at the end. - * String arguments are borrowed and neither form throws. */ - "string.lastIndexOf": { argTypes: [STRING, STRING], result: F64 }, - "string.lastIndexOfFrom": { argTypes: [STRING, STRING, F64], result: F64 }, - /** String.raw(template, ...subs): the raw literals array (a string[] - * read off the template record) interleaved with the PRE-STRINGIFIED - * substitutions (the frontend applies the static ToString and packs - * them into one string[] literal) — extra substitutions drop, missing - * ones skip, the spec's loop exactly (scr_array.c). Borrowed args; - * +1 string; never throws. */ - "string.raw": { argTypes: [arrayOf(STRING), arrayOf(STRING)], result: STRING }, -}); + "insp.f64": { argTypes: [F64], result: STRING }, + /** util.format %j over a checked-dynamic argument: the runtime dyn + * walk (JS-exact stringify; root undefined/function prints + * "undefined"; a handle in the tree throws the "not supported yet" + * Error). */ + "insp.jsonDyn": { argTypes: [DYN], result: STRING }, + "insp.str": { argTypes: [STRING], result: STRING }, + "insp.regex": { argTypes: [REGEX], result: STRING }, + /** A Date's inspect text: toISOString(), or "Invalid Date". */ + "insp.date": { argTypes: [DATE_T], result: STRING }, + "insp.buffer": { argTypes: [BYTES_U8], result: STRING }, + /** A numeric typed array that is not a Buffer (any element kind). */ + "insp.typedArray": { argTypes: [null, F64, F64], result: STRING }, + /** A u8 byte view in a union arm, element or field: a DataView, Buffer + * or Uint8Array by its own flags. */ + "insp.bytes": { argTypes: [null, F64, F64], result: STRING }, + "insp.error": { argTypes: [null, F64, F64], result: STRING }, + "insp.errorHasCause": { argTypes: [null], result: BOOL }, + "insp.errorBagHasProperties": { argTypes: [DYN], result: BOOL }, + "insp.errorCause": { argTypes: [null, F64, F64], result: STRING }, + "insp.dyn": { argTypes: [DYN, F64, F64], result: STRING }, + "insp.dynS": { argTypes: [DYN, F64], result: STRING }, + /** util.format's %o over a checked-dynamic value: inspect with + * showHidden. Kinds whose hidden entries have no rendering throw the + * 'not supported yet' Error. */ + "insp.dynHidden": { argTypes: [DYN, F64], result: STRING }, + "insp.begin": { argTypes: [F64], result: VOID }, + /** util.inspect's breakLength, compact (0 for false) and sorted for the + * renders that follow; the call site restores 80, 3 and false afterwards. */ + "insp.layout": { argTypes: [F64, F64, BOOL], result: VOID }, + "insp.entry": { argTypes: [STRING, BOOL], result: VOID }, + // Circular references: the receiver slot is program-dependent (any + // cycle-capable record/array/map/class type — the insp.error precedent). + /* Circular references over cycle-capable composites (recursive record/ + * class types): circCheck answers a value's circular id when it is + * already on the traversal stack (0 otherwise), seenPush/refWrap + * bracket the frame (insp.end adds Node's " " prefix to values + * the walk found circular), circular renders "[Circular *N]". */ + "insp.circCheck": { argTypes: [null], result: F64 }, + "insp.seenPush": { argTypes: [null], result: VOID }, + "insp.refWrap": { argTypes: [null, STRING], result: STRING }, + "insp.circular": { argTypes: [F64], result: STRING }, + /* Whether a closed declared-field record kept the undeclared properties + * of its checked source (scr_record_source_note): reflective surfaces + * (inspect, Object.keys/values/entries) then answer through its live + * checked view. The receiver is any such record type. Never throws. */ + "record.sourceHas": { argTypes: [null], result: BOOL }, + /** `record.sourceSpread(built, value)`: the record a literal starting + * with `...value` just built keeps the order of value's kept source + * (key-order-witness.ts ordered spreads). Both are record types. Never + * throws. */ + "record.sourceSpread": { argTypes: [null, null], result: VOID }, + "insp.key": { argTypes: [STRING], result: STRING }, + "insp.moreItems": { argTypes: [F64], result: STRING }, + "insp.end": { argTypes: [STRING, STRING, STRING, F64, BOOL, BOOL], result: STRING }, + /** A nested inspect inside a custom rendering (PerformanceEntry's + * `${name} ${inspect(toJSON)}`): nestedBegin starts a fresh inspect + * context, nestedEnd(prefix, inner) restores the caller's and answers + * `prefix inner` re-indented at the caller's level (+1). */ + "insp.nestedBegin": { argTypes: [], result: VOID }, + "insp.nestedEnd": { argTypes: [STRING, STRING], result: STRING }, + /** Around a top-level render that an inspection refusal can end: mark + * records the inspector's frame and nesting depth, unwind (in the + * render's finally) closes what the render left open above it. */ + "insp.mark": { argTypes: [], result: VOID }, + "insp.unwind": { argTypes: [], result: VOID }, + /** node:string_decoder's utf8 StringDecoder (scr_string.c): the decoder + * value is a one-field record whose f64 PACKS the pending partial + * sequence (count + up to 3 raw bytes — Node buffers at most 3 for + * every encoding); the frontend's interned %strdec helpers thread it, + * with the decoder's CANONICAL encoding name first, through these pure + * functions. write: (enc, pending, chunk) → the decoded complete + * prefix of pending+chunk (+1); next: (enc, pending, chunk) → the + * packed NEW pending; end: (enc, pending) → the buffered partial's + * flush (+1). Node-exact per encoding (oracle-pinned); none throws. */ + "strdec.write": { argTypes: [STRING, F64, BYTES_U8], result: STRING }, + "strdec.next": { argTypes: [STRING, F64, BYTES_U8], result: F64 }, + "strdec.end": { argTypes: [STRING, F64], result: STRING }, + /** A runtime encoding argument's canonical name (Node's normalizeEncoding: + * nullish and "" are utf8); an unknown name throws ERR_UNKNOWN_ENCODING. */ + "strdec.encoding": { argTypes: [DYN], result: STRING }, + // node:readline: the interface handle is f64; the callbacks' func types + // are program-dependent (the emitter picks the adapter by parameter + // count), so those slots are null like child.onExit's. + /** node:readline and node:readline/promises (scr_readline.c, linked + * under the events gate — these fns imply moduleUsesProcessEvents; a + * stream input or output adds scr_stream.c's bridge). The interface + * value is an f64 handle (the Timeout-id precedent); an open interface + * on stdin keeps the loop alive until close/EOF. + * rl.create: [mode, input?, output?, crlfDelay, prompt?] → f64, mode + * bits 1 stream input, 2 stream output, 4 stdout output, 16 prompt + * (lowerReadlineCreateCall); rl.createSignal: the same plus the + * trailing AbortSignal (mode bit 8), which links the event-target unit + * for addAbortListener. rl.question: [handle, query, cb] — the callback + * form; THROWS ERR_USE_AFTER_CLOSE on a closed interface (may-throw). + * rl.questionAsync(Signal): [handle, query, signal?] → promise, + * rejecting instead of throwing. rl.close/pause/resume/prompt: + * [handle] (pause/resume/prompt throw ERR_USE_AFTER_CLOSE when + * closed); rl.setPrompt/getPrompt; rl.write: [handle, data] (throws + * when closed). rl.on: [handle, event, cb, once] — the event number + * (0 'line', 1 'close', 2 'pause', 3 'resume', 4 'error') and the + * listener (moves). rl.iterOpen/iterNext/iterReturn: the async + * iterator — iterNext answers promise of the line or undefined. */ + "rl.create": { argTypes: [F64], result: F64 }, + "rl.createSignal": { argTypes: [F64], result: F64 }, + "rl.question": { argTypes: [F64, STRING, null], result: VOID }, + "rl.questionAsync": { argTypes: [F64, STRING], result: { kind: "promise", inner: STRING } }, + "rl.questionAsyncSignal": { + argTypes: [F64, STRING, DYN], + result: { kind: "promise", inner: STRING }, + }, + /** The question forms with a checked-dynamic query (a JavaScript value + * that need not be a string). */ + "rl.questionDyn": { argTypes: [F64, DYN, null], result: VOID }, + "rl.questionAsyncDyn": { argTypes: [F64, DYN], result: { kind: "promise", inner: STRING } }, + "rl.questionAsyncSignalDyn": { + argTypes: [F64, DYN, DYN], + result: { kind: "promise", inner: STRING }, + }, + "rl.close": { argTypes: [F64], result: VOID }, + "rl.pause": { argTypes: [F64], result: VOID }, + "rl.resume": { argTypes: [F64], result: VOID }, + "rl.prompt": { argTypes: [F64], result: VOID }, + /** prompt(preserveCursor): [handle, preserveCursor] */ + "rl.promptWith": { argTypes: [F64, BOOL], result: VOID }, + "rl.setPrompt": { argTypes: [F64, STRING], result: VOID }, + "rl.setPromptDyn": { argTypes: [F64, DYN], result: VOID }, + "rl.getPrompt": { argTypes: [F64], result: STRING }, + "rl.write": { argTypes: [F64, STRING], result: VOID }, + /** write(data, key): [handle, hasData, data, hasKey, hasName, name, + * ctrl, meta, shift] */ + "rl.writeKey": { + argTypes: [F64, BOOL, STRING, BOOL, BOOL, STRING, BOOL, BOOL, BOOL], + result: VOID, + }, + "rl.on": { argTypes: [F64, F64, null, BOOL], result: VOID }, + "rl.iterOpen": { argTypes: [F64], result: VOID }, + "rl.iterNext": { argTypes: [F64], result: { kind: "promise", inner: DYN } }, + "rl.iterReturn": { argTypes: [F64], result: VOID }, + /** createInterface's history and editing options (scr_rl_check_options: + * [flags, tabSize, escapeCodeTimeout, historySize], may throw; + * rl.setOptions: [handle, flags, historySize, removeHistoryDuplicates, + * tabSize, escapeCodeTimeout]; rl.setHistory: [handle, history]) and + * the member reads (rl.history answers the array, rl.numProp the + * numeric member by number). */ + "rl.checkOptions": { argTypes: [F64, F64, F64, F64], result: VOID }, + "rl.checkCompleter": { argTypes: [DYN], result: VOID }, + "rl.setOptions": { argTypes: [F64, F64, F64, BOOL, F64, F64], result: VOID }, + "rl.setHistory": { argTypes: [F64, arrayOf(STRING)], result: VOID }, + "rl.history": { argTypes: [F64], result: arrayOf(STRING) }, + "rl.numProp": { argTypes: [F64, F64], result: F64 }, + /** Terminal mode (scr_readline_tty.c): rl.terminal: [handle, mode (0 + * the option's value, 2 output.isTTY of process.stdout), terminal, + * completer (undefined when absent), readline/promises], emitted with + * scr_insp_dyn when there is a completer; the line, terminal and cursor + * members. */ + "rl.terminal": { argTypes: [F64, F64, BOOL, DYN, BOOL], result: VOID }, + "rl.line": { argTypes: [F64], result: STRING }, + "rl.isTerminal": { argTypes: [F64], result: BOOL }, + "rl.isPromises": { argTypes: [F64], result: BOOL }, + /** createInterface's input or output as a checked-dynamic value: + * [value, output] — the stream; refused when it is not one. */ + "rl.inputFromDyn": { + argTypes: [DYN, BOOL], + result: { kind: "object", className: "%Readable" }, + }, + "rl.outputFromDyn": { + argTypes: [DYN, BOOL], + result: { kind: "object", className: "%Writable" }, + }, + "rl.cursor": { argTypes: [F64], result: F64 }, + /** readline.cursorTo / moveCursor / clearLine / clearScreenDown: rl.seq + * [kind (0-3), the first and second numbers, whether the second is + * given] answers the data to write after the argument checks (may + * throw); rl.moves [dx, dy] says whether moveCursor writes. */ + "rl.seq": { argTypes: [F64, F64, F64, BOOL], result: STRING }, + "rl.moves": { argTypes: [F64, F64], result: BOOL }, + /** getCursorPos() (scr_readline_tty.c, linked through the regex unit + * like rl.terminal): [handle, 0 rows | 1 cols] */ + "rl.cursorPos": { argTypes: [F64, F64], result: F64 }, + /** readline.emitKeypressEvents (scr_readline_tty.c): rl.keypressStdin + * [interface (0: none), escapeCodeTimeout (NaN: the interface's or + * 500)], rl.keypressStream [stream, interface, timeout]. */ + "rl.keypressStdin": { argTypes: [F64, F64], result: VOID }, + "rl.keypressStream": { argTypes: [null, F64, F64], result: VOID }, + /** readline/promises Readline over an f64 handle: rl.uiOpen [fd (1 + * stdout, 2 stderr), autoCommit], rl.uiOpenStream [stream, autoCommit] + * (isWritable, may throw); the actions take [handle, ...numbers] (the + * checks may throw; cursorTo: [handle, x, whether y is given, y]); + * rl.uiCommit answers a promise. */ + "rl.uiOpen": { argTypes: [F64, BOOL], result: F64 }, + "rl.uiOpenStream": { argTypes: [null, BOOL], result: F64 }, + "rl.uiCursorTo": { argTypes: [F64, F64, BOOL, F64], result: VOID }, + "rl.uiMoveCursor": { argTypes: [F64, F64, F64], result: VOID }, + "rl.uiClearLine": { argTypes: [F64, F64], result: VOID }, + "rl.uiClearScreenDown": { argTypes: [F64], result: VOID }, + "rl.uiRollback": { argTypes: [F64], result: VOID }, + "rl.uiCommit": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, + /** node:timers/promises — the promisified pair (scr_async.c, beside + * the timer heap they ride): tp.setTimeout: [ms] → a pending void + * promise a one-shot heap timer fulfills (the loop's timer phase, FIFO + * against equal deadlines like Node); tp.setImmediate: [] → the same + * through the immediate queue (fires before due timers of later loop + * turns, Node's check phase). Neither throws. */ + "tp.setTimeout": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, + // scheduler.wait(delay): tp.setTimeout's promise. It throws Node's + // ERR_INVALID_THIS once mock.timers has restored the real scheduler.wait. + "tp.schedulerWait": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, + "tp.setImmediate": { argTypes: [], result: { kind: "promise", inner: VOID } }, + /** events.once (scr_events_emitter.c): [emitter, name, boxer, + * errorBoxer, listenForError] → a pending promise of any[]; the boxers + * turn the event's and the 'error' tuple into that array. */ + "events.once": { argTypes: [null, STRING, null, null, BOOL], result: VOID }, + /** events.once(emitter, name, { signal }) (scr_events_once_signal.c): + * events.once's arguments plus the checked-dynamic signal (an + * AbortSignal or undefined). */ + "events.onceSignal": { argTypes: [null, STRING, null, null, BOOL, DYN], result: VOID }, + /** The resolve-value forms: [ms?, value, ref] → a pending promise of the + * value's type that copies the borrowed already-resolved `value` promise + * when the timer (or immediate) fires; ref false unrefs it. The result + * type follows the value promise. */ + "tp.setTimeoutValue": { argTypes: [F64, null, BOOL], result: VOID }, + "tp.setImmediateValue": { argTypes: [null, BOOL], result: VOID }, + /** The same with options.signal (a checked-dynamic AbortSignal or + * undefined) last: an abort clears the timer and rejects with an + * AbortError (scr_event_target.c). */ + "tp.setTimeoutSignal": { argTypes: [F64, null, BOOL, DYN], result: VOID }, + "tp.setImmediateSignal": { argTypes: [null, BOOL, DYN], result: VOID }, + /** scheduler.wait(delay, options): tp.setTimeoutValue's and + * tp.setTimeoutSignal's shapes, with tp.schedulerWait's throws. */ + "tp.schedulerWaitValue": { argTypes: [F64, null, BOOL], result: VOID }, + "tp.schedulerWaitSignal": { argTypes: [F64, null, BOOL, DYN], result: VOID }, + /** node:diagnostics_channel (scr_dc.c, linked when any dc.* appears — + * the zlib gating precedent): a process-global name→channel registry; + * channel values are f64 handles (type-mapper.ts maps Channel to F64, the + * readline.Interface pattern). Subscribers are dyn function values — + * identity-compared by unsubscribe, called (message, name) by publish + * over a SNAPSHOT of the list (a subscriber unsubscribing itself + * mid-publish still lets its siblings fire, Node's behavior). dc.publish + * MAY THROW: a subscriber's throw propagates out of publish (catchable + * there) where Node routes it to triggerUncaughtException — the + * documented divergence. subscribe/unsubscribe throw Node's + * ERR_INVALID_ARG_TYPE TypeError for non-function subscribers. */ + "dc.channel": { argTypes: [STRING], result: F64 }, + "dc.subscribe": { argTypes: [STRING, DYN], result: VOID }, + "dc.unsubscribe": { argTypes: [STRING, DYN], result: BOOL }, + "dc.hasSubscribers": { argTypes: [STRING], result: BOOL }, + "dc.publish": { argTypes: [F64, DYN], result: VOID }, + "dc.chanSubscribe": { argTypes: [F64, DYN], result: VOID }, + "dc.chanUnsubscribe": { argTypes: [F64, DYN], result: BOOL }, + "dc.chanHasSubscribers": { argTypes: [F64], result: BOOL }, + "dc.chanName": { argTypes: [F64], result: STRING }, + /** TracingChannel (dc.tracingChannel): a registry entry of the five + * event channels, an f64 handle like Channel (type-mapper.ts). tcSubscribe/ + * tcUnsubscribe walk a dyn handlers object's five event keys (truthy + * non-function slots throw the per-channel ERR_INVALID_ARG_TYPE); + * tcTraceSync/tcTraceCallback/tcTracePromise run Node's publish choreography in C over + * dyn values (fn, ctx, thisArg, args-array) with thisArg bound as the + * ambient receiver — the traced call's throw and any subscriber throw + * both propagate (MAY THROW). tcTraceCallback wraps args[position] in a + * native error/result + asyncStart/asyncEnd publisher and throws Node's + * TypeError when that slot is not callable. tracingChannelOf is the + * five-Channel collection form of the constructor. */ + /** setImmediate as a first-class dyn value (scr_async.c): a minted dyn + * callable scheduling args[0](args[1..]) on the immediate queue — the + * Node-suite traceCallback shape (`traceCallback(setImmediate, ...)`). + * Calling it validates the callback (the dyn call machinery); minting + * never throws. */ + "timers.setImmediateFnValue": { argTypes: [], result: DYN }, + /** `new Promise(setImmediate)` (the Node-suite early-exit shape): a + * fresh promise an immediate fulfills with the undefined dyn value — + * the executor IS setImmediate, so resolve rides the immediate queue + * (scr_async.c). Never throws. */ + "timers.immediatePromise": { argTypes: [], result: { kind: "promise", inner: DYN } }, + "dc.tracingChannel": { argTypes: [STRING], result: F64 }, + "dc.tracingChannelOf": { argTypes: [F64, F64, F64, F64, F64], result: F64 }, + "dc.tcChannel": { argTypes: [F64, F64], result: F64 }, + "dc.tcHasSubscribers": { argTypes: [F64], result: BOOL }, + "dc.tcSubscribe": { argTypes: [F64, DYN], result: VOID }, + "dc.tcUnsubscribe": { argTypes: [F64, DYN], result: BOOL }, + "dc.tcTraceSync": { argTypes: [F64, DYN, DYN, DYN, DYN], result: DYN }, + "dc.tcTraceCallback": { argTypes: [F64, DYN, F64, DYN, DYN, DYN], result: DYN }, + /** tracePromise (scr_dc.c): start publish, the traced call, a wrap of + * non-promise results, the end publish, and a REACTION FIBER that + * awaits the traced promise and publishes asyncStart/asyncEnd (error + * first on rejection) before settling the returned promise with + * the passed-through outcome. MAY THROW (the traced call and the + * synchronous publishes). */ + "dc.tcTracePromise": { + argTypes: [F64, DYN, DYN, DYN, DYN], + result: { kind: "promise", inner: DYN }, + }, + /** process.on/once('unhandledRejection', fn): registers a dyn listener + * the checkpoint report dispatches per never-observed rejection — + * (reason, promise) — instead of printing and exiting 1 (scr_async.c). + * The bool second arg is `once` (auto-removed after one delivery, + * Node's once); off/removeListener remove by closure identity, the + * offWarning stance. Throws Node's ERR_INVALID_ARG_TYPE on a + * non-function. */ + /** Uncaught exception handlers and monitors: [callback dyn, once, monitor]. */ + "process.onUncaughtException": { argTypes: [DYN, BOOL, BOOL], result: VOID }, + "process.offUncaughtException": { argTypes: [DYN, BOOL], result: VOID }, + "process.onUnhandledRejection": { argTypes: [DYN, BOOL], result: VOID }, + "process.offUnhandledRejection": { argTypes: [DYN], result: VOID }, + /** process.on/once/off('rejectionHandled', fn): the sibling registry. + * A handler attached after unhandledRejection delivery fires the event + * once. Dispatch is synchronous at the attach, with the promise, + * Node's payload. */ + "process.onRejectionHandled": { argTypes: [DYN, BOOL], result: VOID }, + "process.offRejectionHandled": { argTypes: [DYN], result: VOID }, + /** process warnings (scr_async_dyn.c): onWarning/offWarning register + * dyn listeners; emitWarning applies Node's full argument grammar over + * the call's dyn argument vector (ERR_INVALID_ARG_TYPE TypeErrors — MAY + * THROW) and emits on the next tick, like Node: Node's stderr report, + * then the listeners. */ + "process.onWarning": { argTypes: [DYN], result: VOID }, + "process.offWarning": { argTypes: [DYN], result: VOID }, + "process.emitWarning": { argTypes: [DYN], result: VOID }, + /** A Warning-typed process warning with this message (the console + * members' warnings), emitted on the next tick as Node's emitWarning + * does (so the loop runs); an empty message emits nothing. */ + "process.emitWarningText": { argTypes: [STRING], result: VOID }, + /** process.noDeprecation (0) / process.throwDeprecation (1): the stored + * value (undefined until assigned) and its assignment. util.deprecate's + * warnings and DeprecationWarning emissions read their truthiness. */ + "process.deprecationFlag": { argTypes: [F64], result: DYN }, + "process.setDeprecationFlag": { argTypes: [F64, DYN], result: VOID }, + "process.setEmitWarning": { argTypes: [DYN], result: VOID }, + /** AsyncLocalStorage (node:async_hooks — scr_async.c): stores are f64 + * handles; contexts are immutable fiber-carried snapshots (spawned + * fibers inherit the spawner's — Node's init-time capture). run/exit + * enter (or clear) the store, call the dyn function with forwarded + * arguments, and restore (the finally); getStore answers the current + * dyn value or undefined; enterWith installs with no restore point. + * run/exit MAY THROW (the callback's own throws propagate). */ + /** `await v` over a checked-dynamic VALUE (scr_async.c): a dyn promise + * adopts (rejections re-throw — MAY THROW), anything else takes JS's + * one-microtask non-thenable await and answers itself. Only emitted + * inside async bodies (the frontend's isAsync gate). */ + "async.awaitDyn": { argTypes: [DYN], result: DYN }, + /** The bare one-microtask hop (scr_await_hop): `await v` over a typed + * NON-promise value — JS awaits non-thenables through exactly one + * microtask turn and yields the value itself. Never throws. */ + "async.hop": { argTypes: [], result: VOID }, + "als.new": { argTypes: [], result: F64 }, + "als.get": { argTypes: [F64], result: DYN }, + "als.run": { argTypes: [F64, DYN, DYN, DYN], result: DYN }, + "als.exitRun": { argTypes: [F64, DYN, DYN], result: DYN }, + "als.enterWith": { argTypes: [F64, DYN], result: VOID }, + "als.disable": { argTypes: [F64], result: VOID }, + /** new AsyncLocalStorage(options) (Node's object check throws; MAY + * THROW) and the store's name option (dyn; "" without one). */ + "als.newOptions": { argTypes: [DYN], result: F64 }, + "als.name": { argTypes: [F64], result: DYN }, + /** A function bound to the active context: the completion callbacks of + * callback-style APIs (always false: an empty context leaves the + * callback unbound) and AsyncLocalStorage.bind (always true). The typed + * form answers its closure argument's type; the dyn form throws Node's + * ERR_INVALID_ARG_TYPE for a non-function when always (MAY THROW). */ + "als.bindCallback": { argTypes: [null, BOOL], result: VOID }, + "als.bindCallbackDyn": { argTypes: [DYN, BOOL], result: DYN }, + /** AsyncLocalStorage.snapshot(): a dyn function (cb, ...args) that runs + * cb in the context active at the snapshot. */ + "als.snapshot": { argTypes: [], result: DYN }, + /** Channel.bindStore/unbindStore/runStores (scr_dc.c): the + * AsyncLocalStorage integration — set-semantics bindings per store, + * runStores entering every bound store with transform(data) around the + * publish and the callback (MAY THROW: transforms, subscribers, and + * the callback all run). */ + "dc.chanBindStore": { argTypes: [F64, F64, DYN], result: VOID }, + "dc.chanUnbindStore": { argTypes: [F64, F64], result: BOOL }, + "dc.chanRunStores": { argTypes: [F64, DYN, DYN, DYN, DYN], result: DYN }, + /** The Number statics with a static C implementation (scr_lib.c): one + * f64 arg → bool, JS-exact BY CONSTRUCTION — Number.isFinite/isNaN/ + * isInteger/isSafeInteger never coerce, and the frontend routes only + * f64-typed arguments here (other static types fence). None throws. */ + "number.isFinite": { argTypes: [F64], result: BOOL }, + "number.isNaN": { argTypes: [F64], result: BOOL }, + "number.isInteger": { argTypes: [F64], result: BOOL }, + "number.isSafeInteger": { argTypes: [F64], result: BOOL }, + /** Date (scr_lib.c). Values are TimeClip'd epoch-millisecond scalars: + * construction/store/pass/getters are exact while identity and mutation + * remain fenced. date.now is Node's integer milliseconds since epoch; + * date.toISOString formats one raw f64 millisecond time value and + * date.toISOStringValue formats a stored Date, both with Node's rules (UTC, + * YYYY-MM-DDTHH:mm:ss.sssZ, expanded ±YYYYYY years outside 0–9999, + * ToInteger truncation of fractional ms) and THROWS Node's "Invalid + * time value" RangeError on NaN / out-of-range input (may-throw seed). + * Results: f64 / owned (+1) string. */ + "date.now": { argTypes: [], result: F64 }, + "date.newNow": { argTypes: [], result: DATE_T }, + "date.newMs": { argTypes: [F64], result: DATE_T }, + "date.newString": { argTypes: [STRING], result: DATE_T }, + /** Date.parse(dateString), with its own reach witness for exact fences. */ + "date.parse": { argTypes: [STRING], result: F64 }, + "date.getTime": { argTypes: [DATE_T], result: F64 }, + "date.valueOf": { argTypes: [DATE_T], result: F64 }, + "date.toISOString": { argTypes: [F64], result: STRING }, + "date.toISOStringValue": { argTypes: [DATE_T], result: STRING }, + /** Date.prototype.toUTCString over a stored Date ("Invalid Date" for + * NaN; never throws). */ + "date.toUTCString": { argTypes: [DATE_T], result: STRING }, + "date.getFullYear": { argTypes: [DATE_T], result: F64 }, + "date.getUTCFullYear": { argTypes: [DATE_T], result: F64 }, + "date.getMonth": { argTypes: [DATE_T], result: F64 }, + "date.getUTCMonth": { argTypes: [DATE_T], result: F64 }, + "date.getDate": { argTypes: [DATE_T], result: F64 }, + "date.getUTCDate": { argTypes: [DATE_T], result: F64 }, + "date.getDay": { argTypes: [DATE_T], result: F64 }, + "date.getUTCDay": { argTypes: [DATE_T], result: F64 }, + "date.getHours": { argTypes: [DATE_T], result: F64 }, + "date.getUTCHours": { argTypes: [DATE_T], result: F64 }, + "date.getMinutes": { argTypes: [DATE_T], result: F64 }, + "date.getUTCMinutes": { argTypes: [DATE_T], result: F64 }, + "date.getSeconds": { argTypes: [DATE_T], result: F64 }, + "date.getUTCSeconds": { argTypes: [DATE_T], result: F64 }, + "date.getMilliseconds": { argTypes: [DATE_T], result: F64 }, + "date.getUTCMilliseconds": { argTypes: [DATE_T], result: F64 }, + "date.getTimezoneOffset": { argTypes: [DATE_T], result: F64 }, + /** A Date object for a time value a local holds (date-values.ts): a + * lib-call argument passes the time value itself, and any other use + * creates the Date. */ + "date.fromTime": { argTypes: [F64], result: DATE_T }, + /** Date.prototype's field setters over a time value + * (scr_date_set_fields): time value, setter code, argument count and four + * arguments; answers the new time value. */ + "date.setFields": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: F64 }, + /** TimeClip of a number (setTime's argument). */ + "date.timeClip": { argTypes: [F64], result: F64 }, + /** Stores a time value into a Date object and answers it (a setter's + * write). */ + "date.store": { argTypes: [DATE_T, F64], result: F64 }, + /** Date.prototype.toString / toDateString / toTimeString and String(date): + * V8's local-time formats with the host zone's long name, "Invalid Date" + * for an invalid date. Owned (+1) string; never throws. */ + "date.toString": { argTypes: [DATE_T], result: STRING }, + "date.toDateString": { argTypes: [DATE_T], result: STRING }, + "date.toTimeString": { argTypes: [DATE_T], result: STRING }, + /** The composed `new Date(dateString).getTime()` read: one borrowed + * string, f64 milliseconds since epoch, with V8's date-string grammar + * (the ES5 format, then the legacy forms: RFC 1123/2822, Date#toString, + * "Jan 2, 2024", ...). Zone-less dates are local time; anything V8 + * rejects is NaN, Node's invalid-date getTime. Never throws. */ + "date.parseGetTime": { argTypes: [STRING], result: F64 }, + /** `Date.UTC(...)` — seven f64 arguments (the frontend completes the + * spec's defaults for omitted trailing parts: month 0, date 1, time + * parts 0), the spec's MakeDay/MakeTime/TimeClip exactly: 0–99 years + * map to 1900+year, out-of-range months/dates roll over, non-finite + * parts and out-of-range results answer NaN. Never throws. */ + "date.utc": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: F64 }, + /** `new Date(year, monthIndex[, date, hours, minutes, seconds, ms])` — + * the same seven-f64 completion as date.utc, but the fields are LOCAL + * time: V8's MakeDay/MakeTime, then UTC(t) in the host zone (DST gaps + * read with the offset before the transition, overlaps answer the + * earlier instant), then TimeClip. Never throws. */ + "date.newLocal": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: DATE_T }, + /** WHATWG TextDecoder.decode over u8 bytes (scr_bytes.c): utf-8 with + * default options — the same maximal-subpart replacement decode as + * Buffer.toString("utf8"), with the leading BOM stripped (the one + * behavioral difference; ignoreBOM defaults to false). Inline calls + * lower directly; stored decoder records dispatch by encoding id. + * TextEncoder.encode needs no libFn: its matching forms + * lower to buffer.fromStr(s, "utf8") (identical bytes — both write each + * 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 }, + /** Whether two typed arrays share no byte: their element ranges in + * memory do not overlap. Borrowed args of any element kind; never throws. */ + "bytes.disjoint": { argTypes: [null, null], 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 }, + /** 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.diffieHellman": { argTypes: [DYN, DYN], result: DYN }, + "crypto.verifyOneshot": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, + "crypto.createSign": { argTypes: [DYN], result: DYN }, + "crypto.createVerify": { argTypes: [DYN], result: DYN }, + /** 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 }, + "text.decodeBufferSource": { argTypes: [DYN], result: STRING }, + "text.decodeOptions": { argTypes: [BYTES_U8, BOOL, BOOL], result: STRING }, + "text.decodeStream": { argTypes: [DYN, BYTES_U8, F64, BOOL, BOOL, BOOL], result: STRING }, + "text.decodeLegacyOptions": { argTypes: [BYTES_U8, F64, BOOL, BOOL], result: STRING }, + /** TextDecoder.decode for a compile-time non-UTF-8 WHATWG label. The + * second f64 is the frontend-owned encoding id consumed by scr_bytes.c; + * label aliases/case/ASCII whitespace are canonicalized before IR. The + * mapping tables are generated from the pinned Node 24 oracle, keeping + * output portable without an ICU/iconv dependency. Borrowed args; owned + * (+1) string; never throws. */ + "text.decodeLegacy": { argTypes: [BYTES_U8, F64], result: STRING }, + "text.decoderEncoding": { argTypes: [DYN], result: F64 }, + "text.decoderName": { argTypes: [F64], result: STRING }, + "text.encodeInto": { argTypes: [DYN, DYN], result: DYN }, + /** `value instanceof TextDecoder` (true) or `TextEncoder` (false) on a + * checked-dynamic value: a codec record's handle box (scr_text_codec_dyn.c). + * Borrowed; never throws. */ + "text.codecIs": { argTypes: [DYN, BOOL], result: BOOL }, + "buffer.isAscii": { argTypes: [DYN], result: BOOL }, + "buffer.isUtf8": { argTypes: [DYN], result: BOOL }, + "buffer.transcode": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, + /** The fs option forms and friends (scr_lib.c), all throwing catchably + * like the rest of sync fs. mkdirRecursiveSync is Node's recursive + * algorithm (try mkdir, EEXIST-dir is fine, ENOENT creates the parent + * first — errors report Node's errno at Node's path, EEXIST at a file + * target, ENOTDIR at the full path past a file); the first-created-dir + * return value has no lowering (statement position only, frontend- + * fenced). rmOptsSync is rmSync with (recursive, force) bools: force + * swallows ENOENT, recursive removes trees post-order, a directory + * without recursive throws Node's ERR_FS_EISDIR SystemError. mkdtempSync appends the six X's and returns the + * created path (+1). accessSync takes the F_OK/R_OK/W_OK/X_OK bits as + * one f64 (the frontend bakes fs.constants.* as literals and completes + * an omitted mode to 0). readFdSync/readFdSyncBytes are the + * readFileSync(fd[, "utf8"]) forms — a read(2) loop to EOF on the fd + * (the stdin path); errors carry Node's no-path message shape. */ + "fs.mkdirRecursiveSync": { argTypes: [STRING], result: VOID }, + "fs.rmOptsSync": { argTypes: [STRING, BOOL, BOOL], result: VOID }, + /** rmOptsSync's maxRetries/retryDelay form (path, recursive, force, + * maxRetries, retryDelay): Node's linear-backoff retry on + * EBUSY/EMFILE/ENFILE/ENOTEMPTY/EPERM — the tmpdir-harness shape + * rmSync(p, { maxRetries: 3, recursive: true, force: true }). Throws + * ERR_OUT_OF_RANGE for a retryDelay outside int32 >= 0 or a maxRetries + * outside uint32, before checking the path. */ + "fs.rmRetrySync": { argTypes: [STRING, BOOL, BOOL, F64, F64], result: VOID }, + "fs.mkdtempSync": { argTypes: [STRING], result: STRING }, + "fs.accessSync": { argTypes: [STRING, F64], result: VOID }, + "fs.readFdSync": { argTypes: [F64, STRING], result: STRING }, + "fs.readFdSyncBytes": { argTypes: [F64], result: BYTES_U8 }, + /** Validated isatty(3) probe. Stream property lowering maps false to + * undefined; tty.isatty exposes the boolean result directly. */ + "process.isTTY": { argTypes: [F64], result: BOOL }, + // Like process.envGet: the result is the module's interned + // `number | undefined` union — checked by arms in the libCall case. + /** Terminal width over an fd literal (1/2 — process.stdout/stderr + * .columns): ioctl(TIOCGWINSZ). The result is the module's interned + * `number | undefined` union — a non-TTY stream (or an ioctl refusal) + * yields the undefined arm, exactly Node's missing `.columns`. Never + * throws. */ + "process.columns": { argTypes: [F64], result: VOID }, + /** Terminal height, with the same fd and optional-number contract. */ + "process.rows": { argTypes: [F64], result: VOID }, + /** process.stdin.destroy(): a deliberate no-op (no stream machinery + * exists to tear down; documented in SEMANTICS.md). */ + "process.stdinDestroy": { argTypes: [], result: VOID }, + /** process.stdin.setRawMode(mode) — one borrowed bool arg, void. On a + * TTY stdin: termios raw mode on/off (libuv's UV_TTY_MODE_RAW flag set, + * the mode Node's setRawMode(true) applies; false restores the entry + * state). On a NON-TTY stdin Node's process.stdin has no such method at + * all, so the call throws Node's exact catchable TypeError + * ("process.stdin.setRawMode is not a function") — the may-throw seed + * carries it. */ + "process.stdinSetRawMode": { argTypes: [BOOL], result: VOID }, + // Arg 0 is a packed f64[] OR a bytes value (the spread-typed-array + // form) — checked in the libCall case. + /** fromCharCode takes one packed f64[] or bytes arg and builds a string + * from UTF-16 code units. Adjacent surrogate pairs combine; lone + * surrogates follow the runtime's replacement policy. */ + "string.fromCharCode": { argTypes: [null], result: STRING }, + /** Numeric code points, with catchable RangeError for non-integers or + * values outside 0..0x10ffff. Uses the same UTF-8 surrogate policy. */ + "string.fromCodePoint": { argTypes: [null], result: STRING }, + /** lastIndexOf returns the last UTF-16 start index, or -1. The two-arg + * form searches at or before its numeric position; NaN starts at the end. + * String arguments are borrowed and neither form throws. */ + "string.lastIndexOf": { argTypes: [STRING, STRING], result: F64 }, + "string.lastIndexOfFrom": { argTypes: [STRING, STRING, F64], result: F64 }, + /** String.raw(template, ...subs): the raw literals array (a string[] + * read off the template record) interleaved with the PRE-STRINGIFIED + * substitutions (the frontend applies the static ToString and packs + * them into one string[] literal) — extra substitutions drop, missing + * ones skip, the spec's loop exactly (scr_array.c). Borrowed args; + * +1 string; never throws. */ + "string.raw": { argTypes: [arrayOf(STRING), arrayOf(STRING)], result: STRING }, + }); +} + +let libFnSignatureTable: ReturnType | undefined; + +/** The runtime call slots of every native builtin, keyed by name. The table + * is built on the first call, so runs that neither lower nor validate a + * module do not construct it. */ +export function libFnSignatures(): ReturnType { + libFnSignatureTable ??= buildLibFnSignatures(); + return libFnSignatureTable; +} /** A builtin name exists exactly when its runtime signature exists. */ -export type IrLibFn = keyof typeof LIB_FN_SIGS; +export type IrLibFn = keyof ReturnType; /** The function-stage runs of a pipeline of `total` stages that a stream * follows (positions bit j: stage j is a function), and an async diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 251d14904..655288945 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -3940,7 +3940,7 @@ export type IrExpr = } /** Standard-library call (`process` members, node:fs functions). `fn` is a * closed union; arg/result types are fixed per member (validated against - * LIB_FN_SIGS). Property READS (`process.argv`, `process.platform`) are + * libFnSignatures()). Property READS (`process.argv`, `process.platform`) are * zero-arg libCalls. Args are BORROWED by the operation (frame temps * release at statement end); refcounted results come back owned (+1) — * `process.argv` returns +1 on ONE interned array (JS identity: diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index eba319f85..f27d11f5b 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -66,7 +66,7 @@ import { 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 { libFnSignatures, pipelinePumpedRuns } from "./builtin-signatures.js"; import { ALWAYS_THROW_LIB_FNS } from "./builtin-effects.js"; /** The promisified crypto calls' value kinds besides checked-dynamic. */ @@ -4104,7 +4104,7 @@ function validateFunction( } if (e.fn === "worker.new" && !workers) errors.push({ message: "Worker construction requires worker execution mode", loc: e.loc }); - const sig = LIB_FN_SIGS[e.fn]; + const sig = libFnSignatures()[e.fn]; if (!sig) { err(`libCall of unknown library function "${e.fn as string}"`, e.loc); return; diff --git a/packages/compiler/src/library/fence-eval.ts b/packages/compiler/src/library/fence-eval.ts index 8b0450857..a950b3990 100644 --- a/packages/compiler/src/library/fence-eval.ts +++ b/packages/compiler/src/library/fence-eval.ts @@ -56,9 +56,9 @@ import { } from "../coverage/surface-manifest.js"; import { libFenceDiag, type ScrDiagnostic } from "../diagnostics/diagnostic.js"; import { - AMBIENT_SURFACE_FNS, + ambientSurfaceFns, BUILTIN_MODULE_CONSTS, - BUILTIN_MODULE_FNS, + builtinModuleFns, BUILTIN_MODULE_FN_ALIASES, STATIC_MATH_FNS, STATIC_MATH_PROPS, @@ -125,7 +125,7 @@ interface FenceTaxonomy { /** node-builtin constant ids: reads fold to literals at compile time. */ foldedIds: Set; /** Ambient dedicated-path surfaces (Date/perf_hooks/process): manifest - * id → the libCall spellings witnessing reach (AMBIENT_SURFACE_FNS — + * id → the libCall spellings witnessing reach (ambientSurfaceFns() — * the same rows the manifest projects the entries from). */ ambientFns: Map; } @@ -148,7 +148,7 @@ function fenceTaxonomy(): FenceTaxonomy { const builtinFns = new Map(); const builtinRoots = new Map(); const foldedIds = new Set(); - for (const [mod, members] of Object.entries(BUILTIN_MODULE_FNS)) { + for (const [mod, members] of Object.entries(builtinModuleFns())) { const root = new Set(); for (const [member, entry] of Object.entries(members!)) { if (entry === undefined) continue; @@ -156,7 +156,7 @@ function fenceTaxonomy(): FenceTaxonomy { // The bare-path and url tables rebind per TARGET platform // (builtinModuleFnsOf): a fence on the bare id covers both flavors. if (mod === "path") { - const w = BUILTIN_MODULE_FNS["path/win32"]?.[member]; + const w = builtinModuleFns()["path/win32"]?.[member]; if (w !== undefined) fns.add(w.fn); } if (mod === "url" && member === "pathToFileURL") fns.add("url.pathToFileURLWin32"); @@ -173,7 +173,7 @@ function fenceTaxonomy(): FenceTaxonomy { } for (const member of Object.keys(STATIC_MATH_PROPS)) foldedIds.add(`stdlib.math.${member}`); const ambientFns = new Map( - AMBIENT_SURFACE_FNS.map((row) => [row.id, row.fns as readonly string[]]), + ambientSurfaceFns().map((row) => [row.id, row.fns as readonly string[]]), ); taxonomy = { byId, diff --git a/packages/compiler/test/compat-exports.test.ts b/packages/compiler/test/compat-exports.test.ts new file mode 100644 index 000000000..57ee41339 --- /dev/null +++ b/packages/compiler/test/compat-exports.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from "vitest"; +import { + LIB_FN_SIGS, + libFnSignatures, + NODE24_FETCH_COMPAT_PROFILE, + node24FetchCompatProfile, +} from "../src/index.js"; + +test("the table constants name the tables their functions build", () => { + expect(LIB_FN_SIGS).toBe(libFnSignatures()); + expect(NODE24_FETCH_COMPAT_PROFILE).toBe(node24FetchCompatProfile()); +}); diff --git a/packages/compiler/test/node-pin.test.ts b/packages/compiler/test/node-pin.test.ts index 7f032c69e..4d4c4a7e8 100644 --- a/packages/compiler/test/node-pin.test.ts +++ b/packages/compiler/test/node-pin.test.ts @@ -5,7 +5,7 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { expect, test } from "vitest"; -import { NODE24_FETCH_COMPAT_PROFILE } from "../src/compat/fetch-profile.js"; +import { node24FetchCompatProfile } from "../src/compat/fetch-profile.js"; import { NODE_COMPAT_VERSION } from "../src/frontend/lowering/builtins/process.js"; import { CRYPTO_CONSTANTS } from "../src/frontend/lowering/crypto-tables.js"; import { ZLIB_CONSTANTS } from "../src/frontend/lowering/zlib-constants.js"; @@ -23,7 +23,7 @@ test("the Node pins agree", () => { expect(compat.tag).toBe(`v${pinned}`); expect(read("Dockerfile.sandbox")).toMatch(new RegExp(`^ARG NODE_VERSION=${pinned}$`, "m")); expect(NODE_COMPAT_VERSION).toBe(pinned); - expect(NODE24_FETCH_COMPAT_PROFILE.target.node).toBe(pinned); + expect(node24FetchCompatProfile().target.node).toBe(pinned); expect(read("packages/runtime/src/scr_lib.c")).toContain( `#define SCR_NODE_COMPAT_VERSION "${pinned}"`, ); diff --git a/tests/harness/README.md b/tests/harness/README.md index 3922c72cc..66a528f4e 100644 --- a/tests/harness/README.md +++ b/tests/harness/README.md @@ -52,7 +52,7 @@ that may be newer than the installed declarations). Every item is classified: The selected adjacent interface families excluded from the census carry reasons too. A Node upgrade that adds/removes a public member or dictionary key fails the focused suite until that member is classified. The static and unsupported rows project into the shipped surface manifest; -filter `NODE24_FETCH_COMPAT_PROFILE.inventory.entries` by `status`/`owner` for +filter `node24FetchCompatProfile().inventory.entries` by `status`/`owner` for the next cohesive implementation queue. When Node changes, update `.node-version` and the profile's Node/Undici tuple diff --git a/tests/harness/code-quality.test.ts b/tests/harness/code-quality.test.ts index deff9afe0..9a683e50a 100644 --- a/tests/harness/code-quality.test.ts +++ b/tests/harness/code-quality.test.ts @@ -93,7 +93,7 @@ test("IR imports and the explicit parser islands remain usable", () => { source( work, "packages/compiler/src/frontend/lowering/probe.ts", - 'export { LIB_FN_SIGS } from "../../ir/builtin-signatures.js";\nexport { STR_INTRINSIC_SIGS } from "../../ir/intrinsic-signatures.js";\n', + 'export { libFnSignatures } from "../../ir/builtin-signatures.js";\nexport { STR_INTRINSIC_SIGS } from "../../ir/intrinsic-signatures.js";\n', ); source( work, diff --git a/tests/harness/fetch-conformance-program.ts b/tests/harness/fetch-conformance-program.ts index 0858f5bfe..322443e73 100644 --- a/tests/harness/fetch-conformance-program.ts +++ b/tests/harness/fetch-conformance-program.ts @@ -429,7 +429,7 @@ export function generateFetchConformanceProgram( const traceCount = options.traceCount ?? 12; const scenarios = generatedScenarioIds(profile); const sections: string[] = [ - `// Generated from NODE24_FETCH_COMPAT_PROFILE. Seed: 0x${seed.toString(16)}.`, + `// Generated from node24FetchCompatProfile(). Seed: 0x${seed.toString(16)}.`, ]; for (const scenario of scenarios) { if (!Object.hasOwn(SCENARIO_GENERATORS, scenario)) { diff --git a/tests/harness/fetch-conformance.test.ts b/tests/harness/fetch-conformance.test.ts index 5b6d62f08..abeac58ae 100644 --- a/tests/harness/fetch-conformance.test.ts +++ b/tests/harness/fetch-conformance.test.ts @@ -17,7 +17,7 @@ import { promisify } from "node:util"; import { beforeAll, describe, expect, test } from "vitest"; import { compile, - NODE24_FETCH_COMPAT_PROFILE, + node24FetchCompatProfile, renderDiagnostics, type FetchCompatEvidence, } from "@scriptc/compiler"; @@ -32,7 +32,7 @@ const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); const fixturesRoot = join(repoRoot, "tests/fixtures/fetch"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; -const profile = NODE24_FETCH_COMPAT_PROFILE; +const profile = node24FetchCompatProfile(); function configuredInteger(name: string, fallback: number, maximum: number): number { const raw = process.env[name]; if (raw === undefined || raw === "") return fallback; diff --git a/tests/harness/surface-manifest.test.ts b/tests/harness/surface-manifest.test.ts index 58958c102..8a838aac6 100644 --- a/tests/harness/surface-manifest.test.ts +++ b/tests/harness/surface-manifest.test.ts @@ -6,7 +6,7 @@ import { analyze, generateSurfaceManifest, ir, - LIB_FN_SIGS, + libFnSignatures, renderSurfaceManifest, resolveLibraryFences, type SurfaceManifest, @@ -563,7 +563,7 @@ describe("determinism attestation ↔ fence parity", () => { // initializer is the Date expression the fences witness. "date.fromTime", ]); - const missing = Object.keys(LIB_FN_SIGS).filter( + const missing = Object.keys(libFnSignatures()).filter( (fn) => ir.LIB_NONDETERMINISTIC_PREFIXES.some(([prefix]) => fn.startsWith(prefix)) && !covered.has(fn) && @@ -571,7 +571,7 @@ describe("determinism attestation ↔ fence parity", () => { ); expect( missing, - "attestation-demoting spellings no declarable fence can deny — add a manifest projection (AMBIENT_SURFACE_FNS) or a member alias (BUILTIN_MODULE_FN_ALIASES)", + "attestation-demoting spellings no declarable fence can deny — add a manifest projection (ambientSurfaceFns()) or a member alias (BUILTIN_MODULE_FN_ALIASES)", ).toEqual([]); // The excuse list stays honest in the other direction too: an excused // spelling that gains detector coverage must leave the list. From 0e92be2bf79e9e244e32ffd3aef52d05cb9e5a93 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 05:41:44 -0500 Subject: [PATCH 200/273] Poll worker cancellation between blocks of typed-array loop iterations In a worker program, every loop without a pending test on each iteration decremented a cancellation budget in its body and polled the exception flag every 64 iterations. The poll's early return made each loop an early-exit loop, which LLVM's loop vectorizer does not vectorize. A counted for loop (integer counter, invariant limit), or a loop to a typed array's length, whose body reads or writes a typed-array element outside nested loops and closures, now runs as blocks of at most 64 iterations: the outer header tests the counter against the limit, the inner loop runs to the nearer of the limit and 64 steps on, and the poll follows each block. A typed array's length is fixed, so each block takes it as its limit. The loop polls as often as the budget did, and the inner loop's only exit besides break and return is its bound. The block bound lives in one slot shared by the loop's speculative copies, which resume iterations in each other's bodies. Other loops keep the budget, which costs less in loops of a few iterations. An elementwise Float64Array loop in a worker-enabled program vectorizes again: 949 ms to 691 ms. A byte-classifying loop over a 100M-byte Uint8Array takes 13 ms instead of 141 ms. New corpus program: native-worker-terminate-counted-loops.ts. --- .../src/backend/llvm/emit-statements.ts | 157 +++++++++++++++++- packages/compiler/src/backend/llvm/emitter.ts | 27 ++- packages/compiler/src/backend/loop-polls.ts | 31 ++++ .../test/counted-loop-emission.test.ts | 80 ++++++++- ...ive-worker-terminate-counted-loops.ts.json | 6 + .../native-worker-terminate-counted-loops.ts | 66 ++++++++ ...ive-worker-terminate-counted-loops.ts.json | 1 + 7 files changed, 358 insertions(+), 10 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-counted-loops.ts.json create mode 100644 tests/corpus/native-worker-terminate-counted-loops.ts create mode 100644 tests/harness/corpus-costs/native-worker-terminate-counted-loops.ts.json diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index cbdeb0558..2efe2cf3a 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -91,6 +91,7 @@ import { } from "./string-construction.js"; import { emitStringInputs } from "./string-lifetimes.js"; import { LOOPS } from "./loop-state.js"; +import { loopAccessesTypedArrays } from "../loop-polls.js"; /** A callback loop version copies the loop; keep the copies small. */ const MAX_CALLBACK_VERSION_COST = 400; @@ -1211,9 +1212,67 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.line(`store ptr ${fresh}, ptr ${slot}`); } }; - const workerBudget = emitter.workerLoopBudget(s); + // A counted loop over typed arrays, or a loop to a typed array's + // length, polls between blocks of WORKER_POLL_BLOCK iterations instead + // of counting a budget in its body: the inner loop over a block then + // has no exit besides its own bound, which LLVM can vectorize. A typed + // array's length is fixed, so each block can take it as its limit. + const stripMined = + capturedLets.length === 0 && + integerSlot !== null && + ((countedLoop !== null && + countedLimit !== null && + Math.abs(countedLoop.step) * WORKER_POLL_BLOCK <= 2 ** 52) || + (integerLoop !== null && integerArrayLoop === null)) && + emitter.workerLoopPolls(s) && + loopAccessesTypedArrays(s); + const workerBudget = stripMined ? null : emitter.workerLoopBudget(s); + // One slot for every copy of the loop: a speculative copy resumes an + // iteration in another copy's body, which then reads this bound. + let blockBound: string | null = null; + if (stripMined) { + blockBound = B.slot(); + B.entryAllocas.push( + `${blockBound} = alloca ${countedLoop ? "i64" : emitter.sizeType} ; cancellation poll block bound`, + ); + } freshenBindings(); emitter.emitLoopCopies(s, capturedLets.length === 0, le, (exit) => { + if (stripMined) { + const counter: StripMinedCounter = countedLoop + ? { + slot: integerSlot!, + type: "i64", + signed: true, + step: countedLoop.step, + followers: countedLoop.followers.map((f, i) => ({ + slot: followerSlots[i]!, + step: f.step, + })), + limit: () => countedLimit!, + } + : { + slot: integerSlot!, + type: emitter.sizeType, + signed: false, + step: 1, + followers: [], + limit: () => { + const receiver = emitter.emitStableReceiver(integerLoop!.limitReceiver, []); + const lenPtr = B.tmp(); + const len = B.tmp(); + B.line( + `${lenPtr} = getelementptr inbounds %ScrBytes, ptr ${receiver.name}, i64 0, i32 1`, + ); + B.line( + `${len} = load ${emitter.sizeType}, ptr ${lenPtr}${bytesHeaderAlias(emitter)}`, + ); + return len; + }, + }; + emitStripMinedCountedLoop(emitter, s, counter, blockBound!, exit); + return; + } const lc = B.newLabel("loop.c"); const lb = B.newLabel("loop.b"); const lu = @@ -2549,6 +2608,102 @@ function emitInt32KindLoop( /** 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. */ +/** Iterations between cancellation polls of a strip-mined counted loop. */ +const WORKER_POLL_BLOCK = 64; + +/** The counter of a strip-mined loop: its slot, the LLVM integer type and + * signedness of its values, its step, the counters stepped beside it, and + * the limit, emitted in the outer header before each block. */ +interface StripMinedCounter { + slot: string; + type: string; + signed: boolean; + step: number; + followers: { slot: string; step: number }[]; + limit: () => string; +} + +/** A counted `for` loop of a worker program as blocks of WORKER_POLL_BLOCK + * iterations, with the cancellation poll between blocks: the outer header + * tests the counter against the loop's limit, each block runs the body up + * to the nearer of the limit and WORKER_POLL_BLOCK steps on, and the poll + * follows the block. It polls as often as the budget it replaces, and the + * inner loop's only exit besides `break` and `return` is its bound. */ +function emitStripMinedCountedLoop( + emitter: LlEmitter, + s: Extract, + counter: StripMinedCounter, + boundSlot: string, + exit: string, +): void { + const B = emitter.B; + const { slot, type, signed, step } = counter; + const ascending = step > 0; + const compare = !signed ? "ult" : ascending ? "slt" : "sgt"; + const saturating = `llvm.${signed ? "sadd" : "uadd"}.sat.${type}`; + const nearer = `llvm.${!signed ? "umin" : ascending ? "smin" : "smax"}.${type}`; + const wrap = signed ? "nsw" : "nuw"; + const lc = B.newLabel("loop.c"); + const block = B.newLabel("loop.block"); + const bc = B.newLabel("loop.bc"); + const lb = B.newLabel("loop.b"); + const lu = B.newLabel("loop.u"); + const poll = B.newLabel("loop.poll"); + emitter.declare(`declare ${type} @${saturating}(${type}, ${type})`); + emitter.declare(`declare ${type} @${nearer}(${type}, ${type})`); + B.br(lc); + B.startBlock(lc); + enterLoopHeader(emitter, lc); + const start = B.tmp(); + const open = B.tmp(); + B.line(`${start} = load ${type}, ptr ${slot}`); + const limit = counter.limit(); + B.line(`${open} = icmp ${compare} ${type} ${start}, ${limit}`); + B.condBr(open, block, exit); + B.startBlock(block); + const stepped = B.tmp(); + const bound = B.tmp(); + B.line( + `${stepped} = call ${type} @${saturating}(${type} ${start}, ${type} ${step * WORKER_POLL_BLOCK})`, + ); + B.line(`${bound} = call ${type} @${nearer}(${type} ${stepped}, ${type} ${limit})`); + B.line(`store ${type} ${bound}, ptr ${boundSlot}`); + B.br(bc); + B.startBlock(bc); + const index = B.tmp(); + const end = B.tmp(); + const inBlock = B.tmp(); + B.line(`${index} = load ${type}, ptr ${slot}`); + B.line(`${end} = load ${type}, ptr ${boundSlot}`); + B.line(`${inBlock} = icmp ${compare} ${type} ${index}, ${end}`); + B.condBr(inBlock, lb, poll); + B.startBlock(lb); + emitter.jumpTargets.push({ + kind: "loop", + brkLabel: exit, + contLabel: lu, + labels: s.labels, + frameDepth: emitter.frames.length, + scopeDepth: emitter.scopes.length, + finallyDepth: emitter.finallyStack.length, + }); + emitter.emitBlock(s.body); + emitter.jumpTargets.pop(); + B.br(lu); + B.startBlock(lu); + for (const { slot: stepSlot, step: by } of [{ slot, step }, ...counter.followers]) { + const old = B.tmp(); + const next = B.tmp(); + B.line(`${old} = load ${type}, ptr ${stepSlot}`); + B.line(`${next} = add ${wrap} ${type} ${old}, ${by}`); + B.line(`store ${type} ${next}, ptr ${stepSlot}`); + } + B.br(bc); + B.startBlock(poll); + emitter.emitWorkerPoll(); + B.br(lc); +} + function enterLoopHeader(emitter: LlEmitter, header: string): void { const dispatch = emitter.speculationHeader; emitter.speculationHeader = null; diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index d1f4ebcca..c1ea7bf35 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -3932,13 +3932,19 @@ export class LlEmitter { } } + /** Whether `loop` needs a cancellation poll: in a worker program, unless + * the loop already runs a pending test on every iteration, which observes + * a terminate request itself (a separate poll would only add work). */ + workerLoopPolls(loop: Extract): boolean { + return ( + this.mod.workers === true && !loopTestsEveryIteration(loop, (e) => this.callTestsPending(e)) + ); + } + workerLoopBudget( loop: Extract, ): string | null { - if (!this.mod.workers) return null; - // A loop that runs a pending test on every iteration observes a - // terminate request through it; a separate poll would only add work. - if (loopTestsEveryIteration(loop, (e) => this.callTestsPending(e))) return null; + if (!this.workerLoopPolls(loop)) return null; const slot = this.B.slot(); this.B.entryAllocas.push(`${slot} = alloca i32 ; worker cancellation budget`); this.B.line(`store i32 0, ptr ${slot}`); @@ -4056,14 +4062,19 @@ export class LlEmitter { B.condBr(due, poll, next); B.startBlock(poll); B.line(`store i32 63, ptr ${slot}`); - // Calls and throwing operations retain their own checks. Amortize the - // extra cancellation poll across bounded loop batches so scalar work - // does not cross the runtime ABI on every iteration. - this.emitPendingCheck(undefined, true); + this.emitWorkerPoll(); B.br(next); B.startBlock(next); } + /** The cancellation poll itself. Calls and throwing operations retain + * their own checks. Loops amortize this extra poll across bounded + * batches of iterations so scalar work does not cross the runtime ABI on + * every iteration. */ + emitWorkerPoll(): void { + this.emitPendingCheck(undefined, true); + } + /** Moves an already-evaluated value into the runtime's exception cell — * the `throw` statement's kind dispatch (stmts.ts's), shared with * every synthetic thrower. Ownership of a refcounted payload must have diff --git a/packages/compiler/src/backend/loop-polls.ts b/packages/compiler/src/backend/loop-polls.ts index 14c2b049c..c582a4974 100644 --- a/packages/compiler/src/backend/loop-polls.ts +++ b/packages/compiler/src/backend/loop-polls.ts @@ -106,3 +106,34 @@ export function loopTestsEveryIteration(loop: Loop, tested: (call: IrExpr) => bo } return false; } + +/** True when the body of `loop`, outside its nested loops and closures, + * reads or writes a typed-array element: the loops whose cancellation poll + * the emitter moves between blocks of iterations, so that LLVM can + * vectorize them. Other loops keep the budget in their body, which costs + * less in a loop of a few iterations. */ +export function loopAccessesTypedArrays(loop: Loop): boolean { + const inExpr = (e: IrExpr): boolean => { + if (e.kind === "bytesIntrinsic" && e.method === "get") return true; + let found = false; + everyExprChild( + e, + (child) => !(found = inExpr(child)), + (child) => !(found = inStmt(child)), + ); + return found; + }; + const inStmt = (s: IrStmt): boolean => { + if (s.kind === "bytesSet") return true; + if (s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf") + return false; + let found = false; + everyStmtChild( + s, + (child) => !(found = inExpr(child)), + (child) => !(found = inStmt(child)), + ); + return found; + }; + return loop.body.some(inStmt); +} diff --git a/packages/compiler/test/counted-loop-emission.test.ts b/packages/compiler/test/counted-loop-emission.test.ts index 7a3fc1f44..206a3c57e 100644 --- a/packages/compiler/test/counted-loop-emission.test.ts +++ b/packages/compiler/test/counted-loop-emission.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; @@ -45,3 +45,81 @@ test("counted loops guard exact induction and preserve wide remainders on both p await rm(dir, { recursive: true, force: true }); } }); + +test("worker programs poll counted loops over typed arrays between blocks of iterations", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-counted-poll-")); + try { + const source = (workers: boolean): string => + [ + workers ? 'import { isMainThread, Worker } from "node:worker_threads";' : "", + "export function scale(a: Float64Array, b: Float64Array, n: number): void {", + " for (let i = 0; i < n; i++) b[i] = a[i]! * 2;", + "}", + "export function codes(a: Uint8Array): void {", + " for (let i = 0; i < a.length; ++i) a[i] = a[i] === 97 ? 0 : 3;", + "}", + "export function mix(n: number): number {", + " let t = 0;", + " for (let i = 0; i < n; i++) t = (t * 31 + i) % 1000003;", + " return t;", + "}", + "const a = new Float64Array(8).fill(1);", + "const b = new Float64Array(8);", + "scale(a, b, 8);", + "const c = new Uint8Array(8).fill(97);", + "codes(c);", + "console.log(b[7], mix(8), c[0]);", + workers ? "if (!isMainThread) new Worker(new URL(import.meta.url));" : "", + ].join("\n"); + const emit = async (workers: boolean): Promise => { + const entry = join(dir, `${workers ? "worker" : "plain"}.ts`); + await writeFile(entry, source(workers)); + const outPath = join(dir, `${workers ? "worker" : "plain"}.ir.json`); + const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + return emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + }; + // Every emitted version of a function (the number and int32 entries). + const bodies = (ll: string, name: string): string => + [ + ...ll.matchAll( + new RegExp( + `define internal [^\\n]+ @sc_b?f_${name}(?:\\.i32)?\\([^\\n]*\\n([\\s\\S]*?)\\n\\}`, + "g", + ), + ), + ] + .map((match) => match[1]!) + .join("\n"); + const workerModule = await emit(true); + const worker = bodies(workerModule, "scale"); + const blocks = worker.split(/\n(?=[\w.]+:)/); + // No budget in the body: the inner loop's latch only steps the counter, + // the poll runs after each block, and the block's bound is the nearer of + // the limit and 64 steps on. + expect(worker).not.toContain("worker cancellation budget"); + expect(worker).toContain("@llvm.sadd.sat.i64(i64 %"); + expect(worker).toMatch(/@llvm\.smin\.i64\(i64 %\w+, i64 %\w+\)/); + for (const latch of blocks.filter((block) => block.startsWith("loop.u"))) + expect(latch).not.toContain("scr_exc_flag"); + const polls = blocks.filter((block) => block.startsWith("loop.poll")); + expect(polls.length).toBeGreaterThan(0); + for (const poll of polls) expect(poll).toContain("@scr_exc_flag"); + // A counted loop without typed-array accesses keeps the budget. + // A loop to a typed array's length is strip-mined the same way, with the + // length as each block's limit. + const codes = bodies(workerModule, "codes"); + expect(codes).not.toContain("worker cancellation budget"); + expect(codes).toMatch(/@llvm\.uadd\.sat\.i64\(i64 %\w+, i64 64\)/); + expect(codes).toMatch(/@llvm\.umin\.i64\(i64 %\w+, i64 %\w+\)/); + expect(codes).toMatch(/^loop\.poll\d+:$/m); + const mix = bodies(workerModule, "mix"); + expect(mix).toContain("worker cancellation budget"); + expect(mix).not.toContain("loop.poll"); + const plain = bodies(await emit(false), "scale"); + expect(plain).not.toContain("loop.poll"); + expect(plain).not.toContain("@scr_exc_flag"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-counted-loops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-counted-loops.ts.json new file mode 100644 index 000000000..5f7c54075 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-counted-loops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/native-worker-terminate-counted-loops.ts" + ], + "diags": [] +} diff --git a/tests/corpus/native-worker-terminate-counted-loops.ts b/tests/corpus/native-worker-terminate-counted-loops.ts new file mode 100644 index 000000000..55972c4f6 --- /dev/null +++ b/tests/corpus/native-worker-terminate-counted-loops.ts @@ -0,0 +1,66 @@ +// worker.terminate() stops a worker in counted loops over a typed array +// with long bounds: ascending and descending counters, a step above one, a +// second counter that steps with the first, and loops to a typed array's +// length run again and again. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +function ascending(values: Float64Array, limit: number): number { + let acc = 0; + for (let i = 0; i < limit; i++) { + acc = (acc + values[i & 1023]!) % 1000003; + if (i === 5000) parentPort!.postMessage("ready"); + } + return acc; +} + +function descending(values: Float64Array, limit: number): number { + let acc = 0; + for (let i = limit; i > 0; i -= 3) { + acc = (acc * 31 + i + values[i & 1023]!) % 1000003; + if (i === limit - 15000) parentPort!.postMessage("ready"); + } + return acc; +} + +function paired(values: Float64Array, limit: number): number { + let acc = 0; + for (let i = 0, j = 7; i < limit; i++, j += 2) { + acc = (acc + i * j + values[j & 1023]!) % 1000003; + if (i === 5000) parentPort!.postMessage("ready"); + } + return acc; +} + +function lengths(bytes: Uint8Array): number { + let acc = 0; + for (let round = 0; ; round++) { + for (let i = 0; i < bytes.length; ++i) bytes[i] = bytes[i] === 97 ? 0 : (bytes[i]! + round) & 127; + for (let i = 0; i < bytes.length; i++) acc = (acc + bytes[i]!) % 1000003; + if (round === 50) parentPort!.postMessage("ready"); + } + return acc; +} + +if (isMainThread) { + for (const kind of ["ascending", "descending", "paired", "lengths"]) { + await new Promise((resolve) => { + const worker = new Worker(new URL(import.meta.url), { workerData: kind }); + worker.once("message", () => { + void worker.terminate().then((code: number) => console.log(kind, "terminate", code)); + }); + worker.once("exit", (code: number) => { + console.log(kind, "exit", code); + resolve(); + }); + }); + } +} else { + const values = new Float64Array(1024).map((_, i) => i); + const limit = 1e15; + let result = 0; + if (workerData === "ascending") result = ascending(values, limit); + else if (workerData === "descending") result = descending(values, limit); + else if (workerData === "paired") result = paired(values, limit); + else result = lengths(new Uint8Array(1 << 16).fill(97)); + console.log("unexpected result", result); +} diff --git a/tests/harness/corpus-costs/native-worker-terminate-counted-loops.ts.json b/tests/harness/corpus-costs/native-worker-terminate-counted-loops.ts.json new file mode 100644 index 000000000..66a146275 --- /dev/null +++ b/tests/harness/corpus-costs/native-worker-terminate-counted-loops.ts.json @@ -0,0 +1 @@ +{ "plain": 3594, "san": 23593 } From 1e9afec1d412240843aa865a84173f7dafd82f1c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 05:42:02 -0500 Subject: [PATCH 201/273] Reclaim completed socket writes before growing the pending-write queue The queue of writes with buffered bytes dropped completed entries only when the buffer emptied. On a socket whose buffer stays nonempty while old writes complete and new ones arrive, it kept every write since the buffer last emptied. Appending to a full queue now first moves the pending entries to the front, so its size follows the writes still pending. --- packages/runtime/src/scr_net.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 0fe06cedc..85c7b7db7 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -1139,6 +1139,13 @@ static void scr_net_sock_buffer(ScrNetSocket *s, const char *data, size_t len, s } memcpy(s->wbuf + s->wlen, data, len); s->wlen += len; + if (s->wq_n == s->wq_cap && s->wq_head > 0) { + /* reclaim the completed writes before growing: a buffer that never + * empties completes old writes while new ones arrive */ + memmove(s->wq, s->wq + s->wq_head, (s->wq_n - s->wq_head) * sizeof *s->wq); + s->wq_n -= s->wq_head; + s->wq_head = 0; + } if (s->wq_n == s->wq_cap) { s->wq_cap = s->wq_cap ? s->wq_cap * 2 : 8; s->wq = realloc(s->wq, s->wq_cap * sizeof *s->wq); From b3c98139e40d5b2c4bd57ec2dd9b06768baaf6d8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 05:48:40 -0500 Subject: [PATCH 202/273] Move small number-key maps to the direct index from the second key A number-keyed Map or Set with keys the direct index allows now moves to it at its second entry rather than its fifth, so a map over a few small integer keys (a histogram over 0..3) reads through the inline direct index instead of the out-of-line linear scan. --- ...6310-small-number-map-direct-index.ts.json | 6 ++ packages/runtime/src/scr_map.c | 8 +- packages/runtime/src/scr_map.test.c | 8 +- packages/runtime/test/test_map.c | 23 +++--- .../6310-small-number-map-direct-index.ts | 77 +++++++++++++++++++ 5 files changed, 106 insertions(+), 16 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6310-small-number-map-direct-index.ts.json create mode 100644 tests/corpus/6310-small-number-map-direct-index.ts diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6310-small-number-map-direct-index.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6310-small-number-map-direct-index.ts.json new file mode 100644 index 000000000..72472aa95 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6310-small-number-map-direct-index.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6310-small-number-map-direct-index.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 1cb77b0f6..ccfd4b907 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -1205,9 +1205,11 @@ static __attribute__((noinline)) void scr_map_set_num(ScrMap *m, uint64_t hash, } /* A number-key map with at least this many entries moves to the direct - * index when its keys allow it, so small integer keys are indexed before - * the linear scan ends. */ -#define SCR_MAP_DENSE_AFTER 4 + * index when its keys allow it: from its second key, so a map of a few + * small integer keys (a histogram over 0..3) reads through the inline + * direct index rather than the out-of-line linear scan. A map with one + * key keeps its linear storage. */ +#define SCR_MAP_DENSE_AFTER 1 /* Whether appending one more entry would grow the lookup index rather than * reuse tombstones: scr_map_reserve_append compacts a linear map with a diff --git a/packages/runtime/src/scr_map.test.c b/packages/runtime/src/scr_map.test.c index 9916c7406..e04e0dac9 100644 --- a/packages/runtime/src/scr_map.test.c +++ b/packages/runtime/src/scr_map.test.c @@ -184,9 +184,11 @@ static void iterate_while_mutating(void) { static void modes(void) { ScrMap *map = scr_map_new(SCR_MAP_KEY_F64, SCR_MAP_VAL_F64, NULL); - for (int i = 1; i <= 4; i++) scr_map_set_f64_f64(map, i, i); - assert(!map->ndense && !map->nbuckets); /* still a linear scan */ - scr_map_set_f64_f64(map, 5, 5); + scr_map_set_f64_f64(map, 1, 1); + assert(!map->ndense && !map->nbuckets); /* one key: still a linear scan */ + scr_map_set_f64_f64(map, 2, 2); + assert(map->ndense >= 16 && !map->nbuckets); /* from the second small integer key */ + for (int i = 3; i <= 5; i++) scr_map_set_f64_f64(map, i, i); assert(map->ndense >= 16 && !map->nbuckets); for (int i = 6; i < 1000; i++) scr_map_set_f64_f64(map, i, i); assert(map->ndense >= 1000 && map->ndense <= 1000 + 1000 / 4 + 64); /* near the largest key */ diff --git a/packages/runtime/test/test_map.c b/packages/runtime/test/test_map.c index 46805da1e..e23618f77 100644 --- a/packages/runtime/test/test_map.c +++ b/packages/runtime/test/test_map.c @@ -422,12 +422,14 @@ static void test_live_iteration_indices(void) { /* A small (linear) map that must grow its lookup index while an iteration * is active may not compact: entry indices, tombstones and the append * position stay put. Integer keys in a compact range move to the direct - * index (dense) from the fifth entry, any other number keys to the bucket + * index (dense) from the second entry, any other number keys to the bucket * table from the ninth; both transitions are covered by running the same - * script with `offset` 0 and 0.5. */ + * script with `offset` 0 and 0.5. The walk deletes `victim`, an existing + * key, then appends the key that makes the index grow. */ static void run_small_storage_transitions(double offset) { bool dense = offset == 0; - int n = dense ? 4 : 8; /* entries a linear map holds before its index grows */ + int n = dense ? 1 : 8; /* entries a linear map holds before its index grows */ + int victim = dense ? 0 : 1; ScrMap *m = scr_map_new(SCR_MAP_KEY_F64, SCR_MAP_VAL_F64, NULL); for (int i = 0; i < n; i++) scr_map_set_f64_f64(m, i + offset, i + 10); check(m->nbuckets == 0 && m->buckets == NULL && m->ndense == 0, @@ -441,22 +443,23 @@ static void run_small_storage_transitions(double offset) { ScrMap *copy = scr_map_clone(m, false, NULL); check(copy->nbuckets == 0 && copy->ndense == 0, "small clone preserves compact storage"); scr_map_iter_enter(m); - scr_map_delete_f64(m, 1 + offset); + scr_map_delete_f64(m, victim + offset); scr_map_set_f64_f64(m, n + offset, 14); check(dense ? m->ndense > 0 && m->nbuckets == 0 : m->nbuckets > 0 && m->ndense == 0, "live iteration promotes without moving entry indices"); - check(m->nentries == (uint32_t)n + 1 && !scr_map_iter_live(m, 1) && + check(m->nentries == (uint32_t)n + 1 && !scr_map_iter_live(m, victim) && scr_map_iter_key_f64(m, n) == n + offset, "promotion preserves tombstones and append position"); for (int i = n + 1; i < 64; i++) scr_map_set_f64_f64(m, i + offset, i + 10); - check(m->nentries == 64 && !scr_map_iter_live(m, 1) && scr_map_iter_key_f64(m, 63) == 63 + offset, + check(m->nentries == 64 && !scr_map_iter_live(m, victim) && scr_map_iter_key_f64(m, 63) == 63 + offset, "growth during iteration keeps entry indices"); double out = 0; - check(scr_map_get_f64_f64(m, offset, &out) && out == 10, - "original key survives promotion and growth"); - check(!scr_map_has_f64(m, 1 + offset), "deleted small key stays absent after rehash"); + check(dense ? scr_map_get_f64_f64(m, n + offset, &out) && out == 14 + : scr_map_get_f64_f64(m, offset, &out) && out == 10, + "keys written before the promotion survive it and the growth"); + check(!scr_map_has_f64(m, victim + offset), "deleted small key stays absent after rehash"); scr_map_iter_exit(m); - check(scr_map_has_f64(copy, 1 + offset) && !scr_map_has_f64(copy, n + offset), + check(scr_map_has_f64(copy, victim + offset) && !scr_map_has_f64(copy, n + offset), "small clone remains independent"); scr_map_clear(copy); scr_map_iter_enter(copy); diff --git a/tests/corpus/6310-small-number-map-direct-index.ts b/tests/corpus/6310-small-number-map-direct-index.ts new file mode 100644 index 000000000..5614a56a1 --- /dev/null +++ b/tests/corpus/6310-small-number-map-direct-index.ts @@ -0,0 +1,77 @@ +// Number-keyed Maps and Sets with two to four small integer keys use the +// direct index. Results must match Node through overwrites, deletes and +// re-adds, a fractional, negative or large key that moves the map to +// hashing, clear, and mutation during a for-of or forEach walk. + +const hist = new Map(); +const codes = [0, 3, 1, 3, 2, 0, 3, 3, 1]; +for (const c of codes) hist.set(c, (hist.get(c) ?? 0) + 1); +console.log([...hist].map(([k, v]) => `${k}:${v}`).join(" "), hist.size, hist.has(4), hist.get(-0)); + +// Delete and re-add: the re-added key moves to the end. +hist.delete(3); +hist.set(3, 9); +hist.delete(0); +console.log([...hist.keys()].join(","), hist.get(3), hist.has(0)); + +// A key the direct index cannot hold moves the map to hashing. +for (const odd of [0.5, -1, 2 ** 40]) { + const m = new Map([ + [1, "a"], + [2, "b"], + ]); + m.set(odd, "c"); + m.set(1, "A"); + console.log([...m].map(([k, v]) => `${k}=${v}`).join(" "), m.get(2), m.get(odd), m.size); +} + +// Two keys, then clear, then reuse. +const pair = new Map(); +pair.set(5, 50); +pair.set(7, 70); +pair.clear(); +pair.set(7, 1); +pair.set(1, 2); +console.log([...pair].map(([k, v]) => `${k}=${v}`).join(" "), pair.size); + +// A walk that deletes ahead and adds keys. +const walk = new Map([ + [0, 0], + [1, 10], + [2, 20], +]); +const seen: number[] = []; +for (const [k, v] of walk) { + seen.push(v); + if (k === 0) { + walk.delete(2); + walk.set(3, 30); + } + if (k === 3) walk.set(0, -1); +} +console.log(seen.join(","), [...walk].map(([k, v]) => `${k}=${v}`).join(" ")); + +// Sets of small integers, forEach with deletion and addition. +const set = new Set([2, 0]); +set.add(1); +set.add(2); +const order: number[] = []; +set.forEach((v) => { + order.push(v); + if (v === 2) { + set.delete(1); + set.add(3); + } +}); +console.log(order.join(","), [...set].join(","), set.has(1), set.size); + +// Many small maps, each with a few keys. +let total = 0; +for (let i = 0; i < 1000; i++) { + const m = new Map(); + m.set(i % 3, i); + m.set((i + 1) % 3, 1); + if (i % 5 === 0) m.delete(i % 3); + total += (m.get(i % 3) ?? 100) + (m.get((i + 1) % 3) ?? 0) + m.size; +} +console.log(total); From cac5a19fc07a7cb13a8085645723a37d1a110d52 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 05:58:42 -0500 Subject: [PATCH 203/273] Fill Buffers with multi-byte values, swap byte order and decode utf16le a word at a time - fill with a string or Buffer value longer than one byte: writes the value once, then doubles the filled prefix with memcpy, as Node does. The value is copied with memmove, so a value that is a view of the buffer being filled gives Node's result; the previous per-byte loop read such a view while overwriting it. The loop also computed a division per byte. - swap16, swap32 and swap64: one load, byte swap and store per group, through a local data pointer. - toString("utf16le" | "ucs2"): four units below 0x80 copy as four bytes, and other units outside the surrogate range are written inline (a three-byte sequence as one four-byte store). Surrogates keep the previous path. The result records its UTF-16 length (one unit per input unit), or is marked ASCII, so `.length` does not walk it. --- .../8007-buffer-fill-swap-utf16le.ts.json | 6 + packages/runtime/src/scr_bytes.c | 85 ++++++++++++-- packages/runtime/src/scr_string.c | 13 +++ packages/runtime/src/scr_string.h | 3 + tests/corpus/8007-buffer-fill-swap-utf16le.ts | 110 ++++++++++++++++++ .../8007-buffer-fill-swap-utf16le.ts.json | 1 + 6 files changed, 210 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8007-buffer-fill-swap-utf16le.ts.json create mode 100644 tests/corpus/8007-buffer-fill-swap-utf16le.ts create mode 100644 tests/harness/corpus-costs/8007-buffer-fill-swap-utf16le.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8007-buffer-fill-swap-utf16le.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8007-buffer-fill-swap-utf16le.ts.json new file mode 100644 index 000000000..f629dac24 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8007-buffer-fill-swap-utf16le.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8007-buffer-fill-swap-utf16le.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 398e6eb86..df64b2ab2 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -2495,14 +2495,57 @@ static uint32_t scr_bytes_utf16le_cp(const uint8_t *in, size_t units, size_t *u) return cu; } +/* Four units below 0x80 copy as four bytes (one word holds them), and + * other units outside the surrogate range are written inline; surrogates + * take scr_bytes_utf16le_cp. The string has one UTF-16 unit per input + * unit, which is recorded so its length needs no walk. */ static ScrStr *scr_bytes_decode_utf16le(const uint8_t *in, size_t n) { size_t units = n / 2; if (units > SIZE_MAX / 3) scr_bytes_oom(); ScrStr *s = scr_str_alloc_raw(0, units * 3); - size_t o = 0; - for (size_t u = 0; u < units;) o = scr_bytes_put_cp(s->data, o, scr_bytes_utf16le_cp(in, units, &u)); + uint8_t *out = (uint8_t *)s->data; + size_t o = 0, u = 0; + while (u < units) { +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + if (units - u >= 4) { + uint64_t w; + memcpy(&w, in + u * 2, 8); + if (!(w & UINT64_C(0xff80ff80ff80ff80))) { + out[o] = (uint8_t)w; + out[o + 1] = (uint8_t)(w >> 16); + out[o + 2] = (uint8_t)(w >> 32); + out[o + 3] = (uint8_t)(w >> 48); + o += 4; + u += 4; + continue; + } + } +#endif + uint32_t cu = (uint32_t)in[u * 2] | ((uint32_t)in[u * 2 + 1] << 8); + if (cu >= 0x800 && (cu < 0xd800 || cu > 0xdfff)) { + /* Three bytes in one four-byte store; the next write (or the + * terminator) replaces the fourth. */ + uint8_t seq[4] = {(uint8_t)(0xe0 | (cu >> 12)), (uint8_t)(0x80 | ((cu >> 6) & 0x3f)), + (uint8_t)(0x80 | (cu & 0x3f)), 0}; + memcpy(out + o, seq, 4); + o += 3; + u++; + } else if (cu < 0x80) { + out[o++] = (uint8_t)cu; + u++; + } else if (cu < 0x800) { + out[o] = (uint8_t)(0xc0 | (cu >> 6)); + out[o + 1] = (uint8_t)(0x80 | (cu & 0x3f)); + o += 2; + u++; + } else { + o = scr_bytes_put_cp(s->data, o, scr_bytes_utf16le_cp(in, units, &u)); + } + } s->len = o; s->data[o] = '\0'; + if (o == units) scr_str_note_ascii(s); + else scr_str_note_utf16_len(s, units); return s; } @@ -3479,7 +3522,15 @@ static ScrBytes *scr_bytes_fill_core(ScrBytes *b, const uint8_t *pat, size_t pat } else if (patn == 1) { memset(dst, pat[0], e - o); } else { - for (size_t i = o; i < e; i++) dst[i - o] = pat[(i - o) % patn]; + /* Node's fill: the pattern's first bytes (memmove: the pattern may + * be a view of this buffer), then the filled prefix doubled. */ + size_t len = e - o, done = patn < len ? patn : len; + memmove(dst, pat, done); + while (done < len) { + size_t step = done < len - done ? done : len - done; + memcpy(dst + done, dst, step); + done += step; + } } if (b->shared) { scr_shared_store(b->data + o, dst, e - o); @@ -3558,11 +3609,29 @@ ScrBytes *scr_bytes_swap(ScrBytes *b, double width) { scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_INVALID_BUFFER_SIZE"); return NULL; } - for (size_t g = 0; g < b->len; g += w) { - for (size_t i = 0; i < w / 2; i++) { - uint8_t t = b->data[g + i]; - b->data[g + i] = b->data[g + w - 1 - i]; - b->data[g + w - 1 - i] = t; + /* One load, byte swap and store per group. */ + uint8_t *p = b->data; + size_t len = b->len; + if (w == 2) { + for (size_t g = 0; g < len; g += 2) { + uint16_t v; + memcpy(&v, p + g, 2); + v = __builtin_bswap16(v); + memcpy(p + g, &v, 2); + } + } else if (w == 4) { + for (size_t g = 0; g < len; g += 4) { + uint32_t v; + memcpy(&v, p + g, 4); + v = __builtin_bswap32(v); + memcpy(p + g, &v, 4); + } + } else { + for (size_t g = 0; g < len; g += 8) { + uint64_t v; + memcpy(&v, p + g, 8); + v = __builtin_bswap64(v); + memcpy(p + g, &v, 8); } } return scr_bytes_retain(b); diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index d2d397e25..dfd0d23f8 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -2006,6 +2006,19 @@ double scr_str_utf16_len(ScrStr *s) { return (double)(int64_t)units; } +/* Records the UTF-16 length of a string whose producer counted it (latin1 + * text has one unit per byte), so scr_str_utf16_len does not walk it. Only + * strings that scr_str_utf16_len would otherwise record are kept. */ +void scr_str_note_utf16_len(ScrStr *s, size_t units) { + if (s->ascii || s->len <= 64 || s->rc == SIZE_MAX) return; + if (!scr_u16len_tab) { + scr_u16len_tab = calloc(SCR_U16LEN_N, sizeof *scr_u16len_tab); + if (!scr_u16len_tab) return; + scr_sidx_register_cleanup(); + } + *scr_u16len_slot(s) = (ScrU16Len){s, units}; +} + double scr_str_char_code_at(ScrStr *s, double i) { /* A sequential scan of a non-ASCII receiver stays inside the bounded * ASCII span cached at its last resolved position. Its unit offset is an diff --git a/packages/runtime/src/scr_string.h b/packages/runtime/src/scr_string.h index fc2f15562..e269498b1 100644 --- a/packages/runtime/src/scr_string.h +++ b/packages/runtime/src/scr_string.h @@ -83,6 +83,9 @@ static inline void scr_str_hash_forget(ScrStr *s) { /* True only when every byte is proven below 0x80 (see above). */ static inline bool scr_str_known_ascii(const ScrStr *s) { return s->ascii; } +/* Record the UTF-16 length a string's producer counted (scr_string.c). */ +void scr_str_note_utf16_len(ScrStr *s, size_t units); + /* Record a proven-ASCII fact. Immortal strings may live in read-only * memory and are shared across threads, so they are never written. */ static inline void scr_str_note_ascii(ScrStr *s) { diff --git a/tests/corpus/8007-buffer-fill-swap-utf16le.ts b/tests/corpus/8007-buffer-fill-swap-utf16le.ts new file mode 100644 index 000000000..850c5b964 --- /dev/null +++ b/tests/corpus/8007-buffer-fill-swap-utf16le.ts @@ -0,0 +1,110 @@ +// Buffer fill with values of more than one byte, swap16/32/64, and +// toString("utf16le"): pattern lengths that do and do not divide the +// filled range, offsets and ends, patterns that are views of the buffer +// being filled, string encodings, sizes around word and pool boundaries, +// and utf16le text with ASCII runs, other BMP characters, surrogate pairs, +// lone surrogates and an odd last byte. + +function show(buf: Uint8Array): string { + return Buffer.from(buf).toString("hex"); +} + +const fills: string[] = []; +for (const size of [0, 1, 2, 3, 7, 8, 9, 31, 32, 33]) { + const buf = Buffer.alloc(size); + buf.fill("ab"); + fills.push(buf.toString("latin1")); + buf.fill(Buffer.from([1, 2, 3])); + fills.push(show(buf)); +} +console.log(fills.join(" ")); + +const ranged = Buffer.alloc(20); +ranged.fill("xyz", 3, 17); +console.log(ranged.toString("latin1").replace(/\0/g, ".")); +ranged.fill(Buffer.from("0123456789"), 5); +console.log(ranged.toString("latin1").replace(/\0/g, ".")); +ranged.fill("long pattern longer than the range", 2, 6); +console.log(ranged.toString("latin1").replace(/\0/g, ".")); +ranged.fill("é中", 0, 11); +console.log(show(ranged)); +ranged.fill("6869", 0, 7, "hex"); +ranged.fill("aGk=", 10, 15, "base64"); +console.log(show(ranged)); +ranged.fill("éa", 0, 20, "latin1"); +console.log(show(ranged)); +ranged.fill("ab", 0, 9, "utf16le"); +console.log(show(ranged)); + +// Patterns that are views of the buffer being filled. +const self = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); +self.fill(self.subarray(1, 3)); +console.log(show(self)); +const later = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); +later.fill(later.subarray(6, 9), 2); +console.log(show(later)); +const whole = Buffer.from([9, 8, 7, 6]); +whole.fill(whole); +console.log(show(whole)); +const typed = new Uint8Array(12); +typed.set([5, 6, 7], 0); +Buffer.from(typed.buffer).fill(typed.subarray(0, 3), 3); +console.log(show(typed)); + +const big = Buffer.alloc(70001); +big.fill("0123456"); +console.log(big.length, big.toString("latin1", 0, 10), big.toString("latin1", 69990), big.toString("latin1", 35000, 35010)); +try { + Buffer.alloc(4).fill(Buffer.alloc(0)); +} catch (error) { + console.log((error as NodeJS.ErrnoException).code); +} +console.log(show(Buffer.alloc(5).fill(""))); + +// swap16/32/64. +for (const size of [0, 8, 16, 24, 64, 1000]) { + const buf = Buffer.alloc(size); + for (let i = 0; i < size; i++) buf[i] = (i * 37 + 11) & 255; + const a = Buffer.from(buf).swap16(); + const b = Buffer.from(buf).swap32(); + const c = Buffer.from(buf).swap64(); + console.log(size, show(a.subarray(0, 8)), show(b.subarray(0, 8)), show(c.subarray(0, 8)), show(c.subarray(c.length - 8)), a.swap16().equals(buf)); +} +for (const [size, width] of [[3, 16], [6, 32], [12, 64]] as const) { + try { + const buf = Buffer.alloc(size); + if (width === 16) buf.swap16(); + else if (width === 32) buf.swap32(); + else buf.swap64(); + } catch (error) { + console.log((error as NodeJS.ErrnoException).code, (error as Error).message); + } +} +const view = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).subarray(1, 9); +view.swap32(); +console.log(show(view)); + +// toString("utf16le"). +const texts = [ + "plain ascii text", + "abcéÿĀ߿ࠀ中￿", + "ascii run then 中文 and more ascii after it", + "\u{1F600}\u{10FFFF}a\u{10000}", + "lone \ud800 high and \udc00 low and \ud83d", + "x".repeat(4097) + "é" + "y".repeat(3), +]; +for (const text of texts) { + const bytes = Buffer.from(text, "utf16le"); + const back = bytes.toString("utf16le"); + console.log(back === text, back.length, bytes.length, JSON.stringify(back.slice(0, 24)), back.charCodeAt(back.length - 1)); +} +const odd = Buffer.from([0x61, 0, 0x62, 0, 0x63]); +console.log(JSON.stringify(odd.toString("utf16le")), odd.toString("utf16le").length); +const pairSplit = Buffer.from([0x3d, 0xd8, 0x00, 0xde, 0x3d, 0xd8]); +const split = pairSplit.toString("utf16le"); +console.log(split.length, split.codePointAt(0), split.charCodeAt(2)); +const units = Buffer.alloc(40); +for (let i = 0; i < 40; i++) units[i] = (i * 53 + 7) & 255; +const decoded = units.toString("utf16le"); +console.log(decoded.length, [...decoded].map((c) => c.codePointAt(0)).join(",")); +console.log(units.toString("utf16le", 3, 21).length, units.toString("ucs2", 0, 8) === units.toString("utf16le", 0, 8)); diff --git a/tests/harness/corpus-costs/8007-buffer-fill-swap-utf16le.ts.json b/tests/harness/corpus-costs/8007-buffer-fill-swap-utf16le.ts.json new file mode 100644 index 000000000..2c773a1b8 --- /dev/null +++ b/tests/harness/corpus-costs/8007-buffer-fill-swap-utf16le.ts.json @@ -0,0 +1 @@ +{ "plain": 4406, "san": 2190 } From 872c12e247952524485022a9e1af90c89ce05c3e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 05:59:55 -0500 Subject: [PATCH 204/273] Emit dev builds' function bodies on the emit workers --- .../compiler/src/backend/llvm/debug-info.ts | 104 ++++++++++++++++- .../compiler/src/backend/llvm/emit-journal.ts | 44 ++++++- .../compiler/src/backend/llvm/emit-module.ts | 8 +- packages/compiler/src/backend/llvm/emitter.ts | 32 ++++-- .../compiler/test/chunked-emission.test.ts | 108 +++++++++++------- 5 files changed, 232 insertions(+), 64 deletions(-) diff --git a/packages/compiler/src/backend/llvm/debug-info.ts b/packages/compiler/src/backend/llvm/debug-info.ts index 3a75c6263..aaa35d666 100644 --- a/packages/compiler/src/backend/llvm/debug-info.ts +++ b/packages/compiler/src/backend/llvm/debug-info.ts @@ -13,9 +13,37 @@ import { import { SourceLocations } from "../source-locations.js"; import { mangleFunction } from "../mangle.js"; -/** DWARF describes source bindings at their actual native storage locations. */ +/** A request a range emitter made of its debug info that added nodes + * (LlvmDebugInfo.record), with the ids it answered. */ +export type DebugRequest = + | { readonly op: "function"; readonly fn: string; readonly result: string } + | { + readonly op: "local"; + readonly local: IrLocal; + readonly fn: string; + readonly arg: number; + readonly captured: boolean; + readonly result: { variable: string; location: string }; + } + | { + readonly op: "location"; + readonly loc: SrcLoc; + readonly fn: string; + readonly result: string; + }; + +/** DWARF describes source bindings at their actual native storage locations. + * + * Nodes are numbered in request order. A range emitter (emit-module.ts) + * records the requests that added nodes; the emitter that merges the range + * replays them in order, so every node gets the number serial emission + * gives it, and renames the range's text from the ids the range answered to + * its own. A request that added no node answered an id that existed before + * the range started or that an earlier recorded request created. */ export class LlvmDebugInfo { private readonly nodes: string[] = []; + /** The requests that added nodes, while recording (record). */ + private requests: DebugRequest[] | null = null; private readonly files = new Map(); private readonly locations = new Map(); private readonly scopes = new Map(); @@ -73,7 +101,53 @@ export class LlvmDebugInfo { return unit; } + /** Records the requests that add nodes from here on. */ + record(): void { + this.requests = []; + } + + /** The requests recorded since record. */ + recorded(): readonly DebugRequest[] { + return this.requests ?? []; + } + + /** Replays a range's recorded requests in order and adds to `ids` the + * rename from the ids the range answered to this debug info's. */ + replay( + requests: readonly DebugRequest[], + functionNamed: (name: string) => IrFunction, + ids: Map, + ): void { + const own = (id: string, to: string | null | undefined): void => { + if (to === null || to === undefined) + throw new Error(`llvm emitter bug: debug request for ${id} added no node on replay`); + if (to !== id) ids.set(id, to); + }; + for (const request of requests) { + if (request.op === "function") { + own(request.result, this.function(functionNamed(request.fn))); + continue; + } + const fn = ids.get(request.fn) ?? request.fn; + if (request.op === "location") { + own(request.result, this.location(request.loc, fn)); + continue; + } + const local = this.local(request.local, fn, request.arg, request.captured); + own(request.result.variable, local?.variable); + own(request.result.location, local?.location); + } + } + function(fn: IrFunction): string | null { + const before = this.nodes.length; + const id = this.functionNode(fn); + if (this.requests !== null && id !== null && this.nodes.length > before) + this.requests.push({ op: "function", fn: fn.name, result: id }); + return id; + } + + private functionNode(fn: IrFunction): string | null { const pos = this.source.position(fn.loc); if (pos === null) return null; const file = this.file(pos.file); @@ -230,6 +304,26 @@ export class LlvmDebugInfo { fn: string | null, arg = 0, captured = false, + ): { variable: string; expression: string; location: string } | null { + const before = this.nodes.length; + const result = this.localNodes(local, fn, arg, captured); + if (this.requests !== null && fn !== null && result !== null && this.nodes.length > before) + this.requests.push({ + op: "local", + local, + fn, + arg, + captured, + result: { variable: result.variable, location: result.location }, + }); + return result; + } + + private localNodes( + local: IrLocal, + fn: string | null, + arg: number, + captured: boolean, ): { variable: string; expression: string; location: string } | null { if (fn === null || local.source === undefined) return null; const pos = this.source.position(local.source.loc); @@ -279,6 +373,14 @@ export class LlvmDebugInfo { } location(loc: SrcLoc, fn: string | null): string | null { + const before = this.nodes.length; + const id = this.locationNode(loc, fn); + if (this.requests !== null && fn !== null && id !== null && this.nodes.length > before) + this.requests.push({ op: "location", loc, fn, result: id }); + return id; + } + + private locationNode(loc: SrcLoc, fn: string | null): string | null { if (fn === null) return null; const pos = this.source.position(loc); if (pos === null) return null; diff --git a/packages/compiler/src/backend/llvm/emit-journal.ts b/packages/compiler/src/backend/llvm/emit-journal.ts index ad89d2671..d6fed03a6 100644 --- a/packages/compiler/src/backend/llvm/emit-journal.ts +++ b/packages/compiler/src/backend/llvm/emit-journal.ts @@ -5,6 +5,7 @@ * 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 { DebugRequest } from "./debug-info.js"; import type { LlEmitter } from "./emitter.js"; /** One event of a range's emission, in order. */ @@ -97,6 +98,8 @@ export class EmitJournal { * worker process can write it. */ export interface ChunkResult { readonly entries: readonly JournalEntry[]; + /** The requests that added debug metadata nodes (LlvmDebugInfo.record). */ + readonly debugRequests: readonly DebugRequest[]; readonly flags: { readonly usesCoroutines: boolean; readonly usesTimers: boolean; @@ -152,23 +155,56 @@ export function replayJournal( } /** 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. */ + * sigil), field alias tags (`tags`) and debug metadata ids (`debugIds`, + * `!N`). 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, + debugIds: ReadonlyMap = new Map(), ): (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; + if (prefixes.size === 0 && tags.size === 0 && debugIds.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); + if (debugIds.size > 0) out = renameDebugIds(out, debugIds); return out; }; } +/** Renames metadata references `!N` outside string literals. Debug builds + * attach no alias tags, so every such reference in a function's text is a + * debug metadata id. */ +function renameDebugIds(text: string, ids: ReadonlyMap): string { + const parts: string[] = []; + let copied = 0; + // The next quote at or after the scan, and whether the scan is inside a + // string literal. + let quote = text.indexOf('"'); + let quoted = false; + for (let at = text.indexOf("!"); at >= 0; at = text.indexOf("!", at + 1)) { + while (quote >= 0 && quote < at) { + quoted = !quoted; + quote = text.indexOf('"', quote + 1); + } + if (quoted) continue; + if (at > 0 && isNameChar(text.charCodeAt(at - 1))) continue; + let end = at + 1; + while (end < text.length && isDigit(text.charCodeAt(end))) end++; + if (end === at + 1 || (end < text.length && isNameChar(text.charCodeAt(end)))) continue; + const own = ids.get(text.slice(at, end)); + if (own === undefined) continue; + parts.push(text.slice(copied, at), own); + copied = end; + } + if (copied === 0) return text; + parts.push(text.slice(copied)); + return parts.join(""); +} + function renameNames( text: string, names: ReadonlyMap, diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index dfdd95451..6a33924c3 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -134,6 +134,7 @@ function emitFunctionsOnWorkers(emitter: LlEmitter, workers: number): string[] | let code = 1; try { emitter.journal = new EmitJournal(); + emitter.debug?.record(); const texts: string[] = []; for (const fn of functions.slice(range * size, (range + 1) * size)) { texts.push(emitter.emitFunction(fn)); @@ -230,13 +231,10 @@ const PARTITION_GROUP_END = "; scriptc-group"; 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 = - (ranges && chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ?? - (ranges ? emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) : null) ?? + (chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ?? + emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) ?? []; 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 d1f4ebcca..8dc1f912a 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -476,14 +476,13 @@ export interface LlvmTargetOptions { * earlier emission found breaking its rules (UnwindAbiViolation). */ unwindExcluded?: ReadonlySet; /** 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. Debug builds (`debugSources`) - * emit in one emitter. */ + * own emitter, and merge them in function order (emit-journal.ts, and + * debug-info.ts for debug metadata). 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. Debug builds (`debugSources`) - * emit in one emitter. */ + * output is the same as one emitter's. */ functionWorkers?: number; /** The host's process primitives, where it can fork (the native * compiler's). */ @@ -1889,9 +1888,7 @@ export class LlEmitter { this.journal = journal; this.journaled = journal !== null || - ((options.functionWorkers ?? 1) > 1 && - options.forkPrimitives !== undefined && - options.debugSources === undefined); + ((options.functionWorkers ?? 1) > 1 && options.forkPrimitives !== undefined); // One walk of every body, which the analyses below read instead of // walking the bodies themselves. this.nodeIndex = int32Analysis?.nodeIndex ?? new IrNodeIndex(mod); @@ -5264,6 +5261,7 @@ export class LlEmitter { chunkResult(): ChunkResult { return { entries: this.journal!.entries, + debugRequests: this.debug?.recorded() ?? [], flags: { usesCoroutines: this.usesCoroutines, usesTimers: this.usesTimers, @@ -5299,6 +5297,8 @@ export class LlEmitter { // emission left them (a helper set guards the declarations its first // request adds). replayJournal(this, chunk.entries, names, tags); + const debugIds = new Map(); + this.debug?.replay(chunk.debugRequests, (name) => this.functionNamed(name), debugIds); const flags = chunk.flags; this.usesCoroutines ||= flags.usesCoroutines; this.usesTimers ||= flags.usesTimers; @@ -5319,7 +5319,13 @@ export class LlEmitter { 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); + return renameChunk(names, tags, debugIds); + } + + private functionNamed(name: string): IrFunction { + const fn = this.fnByName.get(name); + if (fn === undefined) throw new InternalCompilerError(`llvm emitter bug: no function ${name}`); + return fn; } /** Adds a content-named constant a chunk interned. Its symbol is this @@ -5447,7 +5453,13 @@ export class LlEmitter { /** 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()); + const chunk = new LlEmitter( + this.mod, + { ...this.targetOptions, functionChunks: 1 }, + new EmitJournal(), + ); + chunk.debug?.record(); + return chunk; } /** The double-ABI entry of a function with an i32 entry: every caller diff --git a/packages/compiler/test/chunked-emission.test.ts b/packages/compiler/test/chunked-emission.test.ts index 6d2b2e06a..567a082ef 100644 --- a/packages/compiler/test/chunked-emission.test.ts +++ b/packages/compiler/test/chunked-emission.test.ts @@ -103,57 +103,77 @@ test("a Promise.all tuple adapter requested in a range is defined", async () => } }, 60_000); -// Debug metadata is numbered per emitter (debug-info.ts) and not journaled: -// a debug build emits its function bodies in one emitter. Its one fork -// computes the int32 slot analysis; when the fork fails, the emitter -// computes it itself. -test("debug builds emit function bodies serially, with every metadata reference defined", async () => { +// Debug builds journal their metadata requests (debug-info.ts): ranges +// merged in function order number every node as one emitter does. +const DEBUG_PROGRAMS: { entry: string; sources: string[] }[] = [ + { + entry: "3066-source-debugging/main.ts", + sources: ["3066-source-debugging/main.ts", "3066-source-debugging/helper.ts"], + }, + ...[ + "1020-async-basics.ts", + "7103-fiberless-plain-calls.ts", + "7110-als-completion-callbacks.ts", + "1301-errors-subclass.ts", + "1529-class-with-statics.ts", + "1591-js-closures.js", + "2010-generators-basics.ts", + "2013-generators-sent-values.ts", + "4451-int32-field-storage.ts", + ].map((program) => ({ entry: program, sources: [program] })), +]; + +test("debug builds emitted in ranges merge into the text one emitter gives", 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"); + const corpus = join(import.meta.dirname, "../../../tests/corpus"); + for (const program of DEBUG_PROGRAMS) { + const outPath = join(dir, "main.ir.json"); + const result = await compile(join(corpus, program.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 program.sources) + debugSources.set(join(corpus, name), await readFile(join(corpus, name), "utf8")); + const options: LlvmTargetOptions = { debugSources }; + const serial = text(emitLlvmModuleSource(deserializeModule(ir), options)); + expect(serial, program.entry).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([]); + // A fork that fails leaves every range to the merging emitter. + process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = "1"; + const forkPrimitives = { + fork: (): number => -1, + exit: (): void => {}, + wait: (): number => -1, + }; + for (const variant of [ + { functionChunks: 2 }, + { functionChunks: 5 }, + { functionWorkers: 4, forkPrimitives }, + ] satisfies LlvmTargetOptions[]) { + const emitted = text( + emitLlvmModuleSource(deserializeModule(ir), { ...options, ...variant }), + ); + expect(emitted === serial, `${program.entry} ${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([]); + } + if (minFunctions === undefined) delete process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"]; + else process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = minFunctions; } - expect(forks).toBe(1); } 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); +}, 300_000); From 627ebd86a51546f2fd6d40a82caab8191ceee667 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 06:16:32 -0500 Subject: [PATCH 205/273] Call replace callbacks with unannotated parameters directly TypeScript types the parameters of an inline replace callback after the match `any`. An unannotated parameter that is never reassigned, whose every read means the same for a string or number as for `any`, and that receives a capture every match sets, the offset or the subject now takes that value's type. String-search callbacks get the same treatment for the offset and the subject. scr_regex_replace_fn calls the closure directly in more cases: - checked-dynamic parameters, which receive the boxed value Node passes at their position (a capture or undefined, the offset, the subject, the groups object, then undefined); - `string | undefined` capture parameters, which receive NULL when the match leaves the capture unset; - a checked-dynamic result, converted with ToString; - up to six parameters. Callbacks of the earlier typed form compile to the same call, with a kinds argument of 0, and run the same per-match work. --- benchmarks/runtime/README.md | 1 + .../runtime/workloads/regex-replacers.json | 6 + .../runtime/workloads/regex-replacers.ts | 60 ++++ .../compiler/src/backend/llvm/lib-dispatch.ts | 77 +++-- .../containers/replacer-params.test.ts | 63 ++++ .../lowering/containers/replacer-params.ts | 292 ++++++++++++++++++ .../lowering/containers/string-and-regexp.ts | 7 +- .../lowering/containers/string-replacement.ts | 14 +- .../compiler/test/replacer-params.test.ts | 84 +++++ .../tests/corpus/8118-replacer-params.ts.json | 6 + ...45-replace-callback-untyped-params.ts.json | 6 + ...6-replace-callback-untyped-params.mjs.json | 6 + packages/runtime/src/scr_regex.c | 203 +++++++++--- packages/runtime/src/scr_regex.h | 26 +- tests/corpus/8118-replacer-params.ts | 43 +++ .../8345-replace-callback-untyped-params.ts | 85 +++++ .../8346-replace-callback-untyped-params.mjs | 32 ++ .../corpus-costs/8118-replacer-params.ts.json | 1 + ...45-replace-callback-untyped-params.ts.json | 1 + ...6-replace-callback-untyped-params.mjs.json | 1 + 20 files changed, 941 insertions(+), 73 deletions(-) create mode 100644 benchmarks/runtime/workloads/regex-replacers.json create mode 100644 benchmarks/runtime/workloads/regex-replacers.ts create mode 100644 packages/compiler/src/frontend/lowering/containers/replacer-params.test.ts create mode 100644 packages/compiler/src/frontend/lowering/containers/replacer-params.ts create mode 100644 packages/compiler/test/replacer-params.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8118-replacer-params.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8345-replace-callback-untyped-params.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8346-replace-callback-untyped-params.mjs.json create mode 100644 tests/corpus/8118-replacer-params.ts create mode 100644 tests/corpus/8345-replace-callback-untyped-params.ts create mode 100644 tests/corpus/8346-replace-callback-untyped-params.mjs create mode 100644 tests/harness/corpus-costs/8118-replacer-params.ts.json create mode 100644 tests/harness/corpus-costs/8345-replace-callback-untyped-params.ts.json create mode 100644 tests/harness/corpus-costs/8346-replace-callback-untyped-params.mjs.json diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 520964cf3..af3d3d239 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -30,6 +30,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions | | `records-sort` | cpu | comparator sorts, group-by, `reduce`/`filter` | | `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` | +| `regex-replacers` | cpu | regex `replace`/`replaceAll` with inline callbacks whose parameters have no annotations: dictionary placeholders, snake_case to camelCase, date reordering, optional captures, capitalization, and a string search that reads the offset | | `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` | | `throw-chains` | cpu | exceptions caught 2, 5 and 20 recursive calls above the throw, through direct calls and through a callback, and a backtracking parser that throws on a failed alternative | diff --git a/benchmarks/runtime/workloads/regex-replacers.json b/benchmarks/runtime/workloads/regex-replacers.json new file mode 100644 index 000000000..c01c6c272 --- /dev/null +++ b/benchmarks/runtime/workloads/regex-replacers.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/regex-replacers.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/regex-replacers.ts b/benchmarks/runtime/workloads/regex-replacers.ts new file mode 100644 index 000000000..be3820b89 --- /dev/null +++ b/benchmarks/runtime/workloads/regex-replacers.ts @@ -0,0 +1,60 @@ +// Regex replace and replaceAll with inline callbacks, written as they +// usually are, without parameter annotations: template placeholders looked +// up in a dictionary, snake_case to camelCase, date reordering, optional +// version parts, capitalization and a string search with its offset. +let seed = 7; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} +function pick(items: readonly T[]): T { + return items[Math.floor(random() * items.length)]!; +} + +const words = ["user", "order", "item", "cart", "price", "total", "status", "created", "updated", "id"]; +const names = ["Ada", "Grace", "Linus", "Barbara", "Ken", "Margaret"]; +const vars: Record = { + name: "world", + city: "Lisbon", + day: "Tuesday", + plan: "pro", + count: "12", +}; +const keys = [...Object.keys(vars), "missing"]; + +const scale = Number(process.argv[2] ?? "1"); +const count = Math.floor(20000 * scale); +const templates: string[] = []; +const identifiers: string[] = []; +const dates: string[] = []; +const versions: string[] = []; +const sentences: string[] = []; +for (let i = 0; i < count; i++) { + templates.push(`Hello {${pick(keys)}}, your {${pick(keys)}} plan renews on {${pick(keys)}} in {${pick(keys)}}.`); + identifiers.push(`${pick(words)}_${pick(words)}_${pick(words)}${random() < 0.5 ? "_" + pick(words) : ""}`); + const year = 1990 + Math.floor(random() * 40); + const month = String(1 + Math.floor(random() * 12)).padStart(2, "0"); + const day = String(1 + Math.floor(random() * 28)).padStart(2, "0"); + dates.push(`from ${year}-${month}-${day} to ${year + 1}-${month}-${day} (${pick(names)})`); + versions.push(`v${i % 9}.${i % 13}${random() < 0.5 ? "." + (i % 7) : ""} and v${i % 5}.${i % 3}`); + sentences.push(`${pick(names)} said the ${pick(words)} and the ${pick(words)} were ready for ${pick(names)}`); +} + +let length = 0; +let checksum = 0; +for (let round = 0; round < 4; round++) { + for (let i = 0; i < count; i++) { + const filled = templates[i]!.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`); + const camel = identifiers[i]!.replace(/_([a-z])/g, (_m, c) => c.toUpperCase()); + const reordered = dates[i]!.replace(/(\d{4})-(\d{2})-(\d{2})/g, (_m, y, m, d) => `${d}/${m}/${y}`); + const padded = versions[i]!.replace(/v(\d+)\.(\d+)(?:\.(\d+))?/g, (_m, major, minor, patch) => `v${major}.${minor}.${patch ?? "0"}`); + const titled = sentences[i]!.replace(/\b[a-z]/g, (c) => c.toUpperCase()); + const marked = sentences[i]!.replaceAll("the", (m, at) => (at % 2 === 0 ? m.toUpperCase() : m)); + length += filled.length + camel.length + reordered.length + padded.length + titled.length + marked.length; + checksum = (checksum * 31 + camel.charCodeAt(camel.length - 1) + padded.charCodeAt(padded.length - 1)) >>> 0; + } +} +console.log(templates[0]!.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`)); +console.log(identifiers[1]!.replace(/_([a-z])/g, (_m, c) => c.toUpperCase())); +console.log(versions[2]!.replace(/v(\d+)\.(\d+)(?:\.(\d+))?/g, (_m, major, minor, patch) => `v${major}.${minor}.${patch ?? "0"}`)); +console.log("length", length, "checksum", checksum); diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 8a98fb154..4af8182a7 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -843,35 +843,68 @@ function emitAssertFailing( * `a.name.localeCompare(b.name)` reads both fields without retaining). */ const READ_ONLY_STRING_LIB_FNS: ReadonlySet = new Set(["str.localeCompare"]); -/** The parameter count of a replace callback the runtime can call through - * the closure ABI (scr_regex_replace_fn), or null when it needs the dyn - * box. Each declared parameter must receive a value of its declared kind - * in every match: the match string at 0; with more than one parameter the +/** The most parameters scr_regex_replace_fn calls a replacer with + * (SCR_REPLACER_MAX_ARITY), the first bit of its string-or-undefined + * parameters (SCR_REPLACER_OPTIONAL), and its flag for a checked-dynamic + * result (SCR_REPLACER_BOXED_RESULT). */ +const REPLACER_MAX_ARITY = 6; +const REPLACER_OPTIONAL = 2 ** 8; +const REPLACER_BOXED_RESULT = 2 ** 31; + +/** `string | undefined` held as the string pointer, NULL for undefined. */ +function optionalStringType(host: LlvmEmitterContext, type: IrType): boolean { + if (type.kind !== "union") return false; + const nullable = host.nullableUnions.get(type.unionId); + if (nullable?.repr !== "single" || nullable.arm.kind !== "string" || nullable.nullTag >= 0) + return false; + return host.unionsById.get(type.unionId)?.arms[nullable.unitTag]?.kind === "undefinedT"; +} + +/** The parameter count and kinds (scr_regex_replace_fn's `kinds`) of a + * replace callback the runtime can call through the closure ABI, or null + * when it needs the dyn box. A checked-dynamic parameter receives whatever + * Node passes at its position. Each typed parameter must receive a value of + * its declared kind in every match: the match string at 0; past 0 the * pattern must be a literal whose captures are known, so position i is a - * capture that every match sets (a string), the offset (a number) right - * after the captures, or the subject (a string) after that. */ -function typedReplacerArity(callback: IrType, pattern: IrExpr): number | null { + * capture (a string when every match sets it, otherwise `string | + * undefined`), the offset (a number) right after the captures, or the + * subject (a string) after that. The result is a string or a + * checked-dynamic value. */ +function typedReplacerShape( + host: LlvmEmitterContext, + callback: IrType, + pattern: IrExpr, +): { arity: number; kinds: number } | null { if (callback.kind !== "func" || callback.rest || callback.argumentsAll) return null; - if (callback.ret.kind !== "string") return null; + if (callback.ret.kind !== "string" && callback.ret.kind !== "dyn") return null; const params = callback.params; - if (params.length > 4) return null; - if (params.length === 0) return 0; - if (params[0]!.kind !== "string") return null; - if (params.length === 1) return 1; - if (pattern.kind !== "regexLit") return null; - const captures = regexLiteralCaptures(pattern.pattern, pattern.flags); - if (!captures) return null; - for (let i = 1; i < params.length; i++) { + if (params.length > REPLACER_MAX_ARITY) return null; + let kinds = callback.ret.kind === "dyn" ? REPLACER_BOXED_RESULT : 0; + let captures: { count: number; always: boolean[] } | null | undefined; + for (let i = 0; i < params.length; i++) { const kind = params[i]!.kind; + if (kind === "dyn") { + kinds += 2 ** i; + continue; + } + const optional = optionalStringType(host, params[i]!); + if (i === 0) { + if (kind !== "string") return null; + continue; + } + if (pattern.kind !== "regexLit") return null; + captures ??= regexLiteralCaptures(pattern.pattern, pattern.flags); + if (!captures) return null; const ok = i < captures.count - ? kind === "string" && captures.always[i] === true + ? (kind === "string" && captures.always[i] === true) || optional : i === captures.count ? kind === "f64" : i === captures.count + 1 && kind === "string"; if (!ok) return null; + if (optional) kinds += REPLACER_OPTIONAL * 2 ** i; } - return params.length; + return { arity: params.length, kinds }; } /** `s.replace(re, fn)` / `replaceAll` with a typed callback the runtime can @@ -881,16 +914,16 @@ function typedReplacerArity(callback: IrType, pattern: IrExpr): number | null { function emitTypedRegexReplace(host: LlvmEmitterContext, e: LibCallExpr): LlValue | null { const [subject, pattern, callback, all] = e.args; if (!subject || !pattern || callback?.kind !== "dynFrom" || all?.kind !== "boolLit") return null; - const arity = typedReplacerArity(callback.value.type, pattern); - if (arity === null) return null; + const shape = typedReplacerShape(host, callback.value.type, pattern); + if (shape === null) return null; const B = host.B; const s = host.emitExpr(subject); const re = host.emitExpr(pattern); const fn = host.emitExpr(callback.value); - host.declare(`declare ptr @scr_regex_replace_fn(ptr, ptr, ptr, i32, i1 zeroext)`); + host.declare(`declare ptr @scr_regex_replace_fn(ptr, ptr, ptr, i32, i32, i1 zeroext)`); const result = B.tmp(); B.line( - `${result} = call ptr @scr_regex_replace_fn(ptr ${s.name}, ptr ${re.name}, ptr ${fn.name}, i32 ${arity}, i1 zeroext ${all.value})`, + `${result} = call ptr @scr_regex_replace_fn(ptr ${s.name}, ptr ${re.name}, ptr ${fn.name}, i32 ${shape.arity}, i32 ${shape.kinds | 0}, i1 zeroext ${all.value})`, ); const output = host.own({ name: result, type: e.type }); host.emitPendingCheck(); diff --git a/packages/compiler/src/frontend/lowering/containers/replacer-params.test.ts b/packages/compiler/src/frontend/lowering/containers/replacer-params.test.ts new file mode 100644 index 000000000..1d5d376e9 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/containers/replacer-params.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, test } from "vitest"; +import { regexCaptureParticipation } from "./replacer-params.js"; + +describe("regex capture participation", () => { + test("counts captures and marks the ones every match sets", () => { + expect(regexCaptureParticipation("\\{(\\w+)\\}", "g")).toEqual({ + count: 2, + always: [true, true], + }); + expect(regexCaptureParticipation("(\\d{4})-(\\d{2})(?:-(\\d{2}))?", "g")).toEqual({ + count: 4, + always: [true, true, true, false], + }); + expect(regexCaptureParticipation("(a)|(b)", "")).toEqual({ + count: 3, + always: [true, false, false], + }); + expect(regexCaptureParticipation("((a)|b)", "")).toEqual({ + count: 3, + always: [true, true, false], + }); + expect(regexCaptureParticipation("(?:(a)|b)(c)", "")).toEqual({ + count: 3, + always: [true, false, true], + }); + expect(regexCaptureParticipation("(a)*(b)+(c){0,2}(d){1,3}(e){00}", "")).toEqual({ + count: 6, + always: [true, false, true, false, true, false], + }); + expect(regexCaptureParticipation("(?=(a))(?!(b))(?<=(c))(?e)", "")).toEqual({ + count: 6, + always: [true, false, false, false, false, true], + }); + }); + + test("skips escapes, classes and non-capturing groups", () => { + expect(regexCaptureParticipation("\\((a)\\)[(|)\\]](?:b)(?i:c)", "")).toEqual({ + count: 2, + always: [true, true], + }); + expect(regexCaptureParticipation("[](a)", "")).toEqual({ count: 2, always: [true, true] }); + expect(regexCaptureParticipation("(a)[", "")).toBeNull(); + expect(regexCaptureParticipation("(a", "")).toBeNull(); + expect(regexCaptureParticipation("a)", "")).toBeNull(); + expect(regexCaptureParticipation("(a)", "v")).toBeNull(); + }); + test("marks captures in alternations, optional groups and lookarounds", () => { + const always = (pattern: string): boolean[] | null => + regexCaptureParticipation(pattern, "")?.always ?? null; + expect(always("tag(\\d+)")).toEqual([true, true]); + expect(always("((a)b)+c")).toEqual([true, true, true]); + expect(always("(?\\d)(x)")).toEqual([true, true, true]); + expect(always("(a){2,3}")).toEqual([true, true]); + expect(always("(?:x|y)(a)")).toEqual([true, true]); + expect(always("(?:(a)|b)c")).toEqual([true, false]); + expect(always("(x)?(\\d)")).toEqual([true, false, true]); + expect(always("(?:(t)a)*g")).toEqual([true, false]); + expect(always("((a)?)")).toEqual([true, true, false]); + expect(always("(?<=(-))(\\w)")).toEqual([true, false, true]); + expect(always("[\\]|(](b)")).toEqual([true, true]); + expect(always("[^)](c)\\|")).toEqual([true, true]); + }); +}); diff --git a/packages/compiler/src/frontend/lowering/containers/replacer-params.ts b/packages/compiler/src/frontend/lowering/containers/replacer-params.ts new file mode 100644 index 000000000..e84ca1461 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/containers/replacer-params.ts @@ -0,0 +1,292 @@ +import * as ts from "../../ts7/adapter.js"; +import { F64, STRING, type IrExpr, type IrType } from "../../../ir/ir.js"; +import type { Lowerer } from "../lowerer.js"; +import { bindingNeverReassigned } from "../binding-analysis.js"; + +/** The captures of a regex literal's pattern (the whole match included) + * and, per capture, whether every successful match sets it. A capture may + * be unset when an enclosing group has alternatives, when an enclosing + * group or the capture itself is quantified with a minimum of zero, or + * when it sits inside a lookaround. Null for the `v` flag (nested + * classes) and for a pattern the scan does not finish. */ +export function regexCaptureParticipation( + pattern: string, + flags: string, +): { count: number; always: boolean[] } | null { + if (flags.includes("v")) return null; + const always: boolean[] = [true]; + /** Open groups: the group's own capture (-1 when it does not capture), + * the captures nested in it, whether it has alternatives, and whether it + * is a lookaround. */ + const frames: { own: number; nested: number[]; alternatives: boolean; lookaround: boolean }[] = [ + { own: -1, nested: [], alternatives: false, lookaround: false }, + ]; + let i = 0; + while (i < pattern.length) { + const c = pattern[i]!; + if (c === "\\") { + i += 2; + continue; + } + if (c === "[") { + // The first unescaped `]` ends the class (`[]` is an empty class). + i++; + while (i < pattern.length && pattern[i] !== "]") i += pattern[i] === "\\" ? 2 : 1; + if (i >= pattern.length) return null; + i++; + continue; + } + if (c === "|") { + frames[frames.length - 1]!.alternatives = true; + i++; + continue; + } + if (c === "(") { + let capture = false; + let lookaround = false; + if (pattern[i + 1] !== "?") { + capture = true; + i++; + } else if (pattern[i + 2] === "<" && pattern[i + 3] !== "=" && pattern[i + 3] !== "!") { + const gt = pattern.indexOf(">", i + 3); + if (gt < 0) return null; + capture = true; + i = gt + 1; + } else { + const kind = pattern[i + 2] === "<" ? pattern[i + 3] : pattern[i + 2]; + lookaround = kind === "=" || kind === "!"; + i += 2; + } + frames.push({ + own: capture ? always.length : -1, + nested: [], + alternatives: false, + lookaround, + }); + if (capture) always.push(true); + continue; + } + if (c === ")") { + if (frames.length === 1) return null; + const frame = frames.pop()!; + i++; + const next = pattern[i]; + const optional = + next === "?" || next === "*" || (next === "{" && /^\{0+[,}]/.test(pattern.slice(i))); + const parent = frames[frames.length - 1]!; + // Alternatives inside the group leave its nested captures unset, but + // not the group's own capture. + for (const index of frame.nested) { + if (optional || frame.lookaround || frame.alternatives) always[index] = false; + parent.nested.push(index); + } + if (frame.own >= 0) { + if (optional || frame.lookaround) always[frame.own] = false; + parent.nested.push(frame.own); + } + continue; + } + i++; + } + if (frames.length !== 1) return null; + if (frames[0]!.alternatives) for (const index of frames[0]!.nested) always[index] = false; + return { count: always.length, always }; +} + +/** String members a refined parameter may use: these lower from the + * receiver's type whatever the checker resolved for an `any` receiver. */ +const STRING_MEMBERS: ReadonlySet = new Set([ + "length", + "toUpperCase", + "toLowerCase", + "trim", + "charCodeAt", + "codePointAt", + "slice", + "substring", + "repeat", + "padStart", + "padEnd", + "includes", + "startsWith", + "endsWith", + "indexOf", +]); + +/** Whether every read of `symbol` in `body` is one that means the same + * for an `any` value and for a `kind` value, and lowers for both: a + * template span, a Number/String/parseInt/parseFloat argument, a listed + * string member, `+`, strict equality, arithmetic and comparisons of + * numbers, a returned value, and a string key of an element access on a + * string-indexed record or an `any` value. Anything else keeps the + * parameter `any`. */ +function readsFit( + lowerer: Lowerer, + body: ts.Node, + symbol: ts.Symbol, + kind: "string" | "f64", +): boolean { + const name = ts.symbolName(symbol); + const primitive = (e: ts.Expression): boolean => { + const flags = lowerer.checker.getTypeAtLocation(e).flags; + return (flags & (ts.TypeFlags.NumberLike | ts.TypeFlags.StringLike)) !== 0; + }; + const fits = (id: ts.Identifier): boolean => { + let node: ts.Node = id; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + if (ts.isTemplateSpan(parent) || ts.isReturnStatement(parent)) return true; + if (ts.isElementAccessExpression(parent)) { + if (kind !== "string" || parent.argumentExpression !== node) return false; + const receiver = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(parent.expression)); + if (receiver?.kind === "dyn") return true; + const shape = receiver?.kind === "record" ? lowerer.shapes.get(receiver.shapeId) : undefined; + return shape?.indexValue !== undefined && !shape.tuple; + } + if (ts.isArrowFunction(parent)) return parent.body === node; + if (ts.isCallExpression(parent)) + return ( + parent.expression !== node && + ["Number", "String", "parseInt", "parseFloat"].some((global) => + lowerer.isStdlibGlobal(parent.expression, global), + ) + ); + if (ts.isPropertyAccessExpression(parent)) { + const member = parent.name.text; + if (kind !== "string" || parent.expression !== node || !STRING_MEMBERS.has(member)) + return false; + return ( + member === "length" || + (ts.isCallExpression(parent.parent) && parent.parent.expression === parent) + ); + } + if (ts.isBinaryExpression(parent)) { + const op = parent.operatorToken.kind; + if ( + op === ts.SyntaxKind.EqualsEqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsEqualsToken + ) + return true; + const other = parent.left === node ? parent.right : parent.left; + if (op === ts.SyntaxKind.PlusToken) return kind === "string" || primitive(other); + return ( + kind === "f64" && + (op === ts.SyntaxKind.MinusToken || + op === ts.SyntaxKind.AsteriskToken || + op === ts.SyntaxKind.SlashToken || + op === ts.SyntaxKind.PercentToken || + op === ts.SyntaxKind.LessThanToken || + op === ts.SyntaxKind.GreaterThanToken || + op === ts.SyntaxKind.LessThanEqualsToken || + op === ts.SyntaxKind.GreaterThanEqualsToken) && + (lowerer.checker.getTypeAtLocation(other).flags & ts.TypeFlags.NumberLike) !== 0 + ); + } + return false; + }; + let ok = true; + const visit = (n: ts.Node): void => { + if (!ok) return; + if (ts.isIdentifier(n) && n.text === name && lowerer.resolveValueSymbol(n) === symbol) { + if (!fits(n)) ok = false; + return; + } + ts.forEachChild(n, visit); + }; + visit(body); + return ok; +} + +/** The capture layout of a string search: the match, then the offset and + * the subject. */ +export const PLAIN_SEARCH_CAPTURES: { count: number; always: boolean[] } = { + count: 1, + always: [true], +}; + +/** The captures of a regex literal argument (regexCaptureParticipation), + * or null for any other pattern expression. */ +export function regexLiteralCapturesOf( + pattern: ts.Expression, +): { count: number; always: boolean[] } | null { + let regex = pattern; + while (ts.isParenthesizedExpression(regex)) regex = regex.expression; + if (!ts.isRegularExpressionLiteral(regex)) return null; + const text = regex.text; + const slash = text.lastIndexOf("/"); + return regexCaptureParticipation(text.slice(1, slash), text.slice(slash + 1)); +} + +/** Lowers an inline replace callback. JavaScript passes it the match, the + * captures, the match offset and the subject (`captures` gives their + * layout; null lowers the callback as written). TypeScript types the + * parameters after the match `any`, so an unannotated parameter that is + * never reassigned, whose every read fits its value (readsFit), and that + * receives a capture every match sets, the offset or the subject takes + * that value's type: a string or a number. The closure can then be called + * directly (typed replacers in lib-dispatch.ts) with fewer boxed values. */ +export function lowerReplacerCallback( + lowerer: Lowerer, + callback: ts.Expression, + captures: { count: number; always: boolean[] } | null, +): IrExpr { + let node = callback; + while (ts.isParenthesizedExpression(node)) node = node.expression; + // A named function expression can call itself with other arguments. + const literal = + ts.isArrowFunction(node) || (ts.isFunctionExpression(node) && !node.name) ? node : null; + if (!literal || !captures) return lowerer.lowerExpr(callback); + const bound: { symbol: ts.Symbol; previous: IrType | undefined }[] = []; + const previousImplicit = lowerer.implicitParamTypes; + let implicit: Map | null = null; + const params = literal.parameters.nodes; + // A declared `this` parameter takes no argument position. + const first = + params[0] && ts.isIdentifier(params[0].name) && params[0].name.text === "this" ? 1 : 0; + params.slice(first).forEach((param, i) => { + const type = + i === 0 || i > captures.count + 1 + ? null + : i < captures.count + ? captures.always[i] === true + ? STRING + : null + : i === captures.count + ? F64 + : STRING; + if ( + type === null || + !ts.isIdentifier(param.name) || + param.type || + param.initializer || + param.dotDotDotToken || + param.questionToken + ) + return; + if ((lowerer.checker.getTypeAtLocation(param.name).flags & ts.TypeFlags.Any) === 0) return; + const symbol = lowerer.checker.getSymbolAtLocation(param.name); + if ( + !symbol || + !bindingNeverReassigned(lowerer, symbol, param) || + !readsFit(lowerer, literal.body, symbol, type === STRING ? "string" : "f64") + ) + return; + bound.push({ symbol, previous: lowerer.runtimeOptionalBindingTypes.get(symbol) }); + lowerer.runtimeOptionalBindingTypes.set(symbol, type); + implicit ??= new Map(previousImplicit ?? []); + implicit.set( + symbol, + type === STRING ? lowerer.checker.getStringType() : lowerer.checker.getNumberType(), + ); + }); + if (implicit !== null) lowerer.implicitParamTypes = implicit; + try { + return lowerer.lowerExpr(callback); + } finally { + for (const { symbol, previous } of bound) { + if (previous === undefined) lowerer.runtimeOptionalBindingTypes.delete(symbol); + else lowerer.runtimeOptionalBindingTypes.set(symbol, previous); + } + lowerer.implicitParamTypes = previousImplicit; + } +} 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 d55f71ff0..12885418b 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -33,6 +33,7 @@ import { isSafeToDiscard } from "../expressions/evaluation-safety.js"; import { STRING_INDEX_METHODS, STRING_REPLACE_METHODS, STR_METHODS } from "../surfaces.js"; import { execResultCaptureMask } from "./exec-result-uses.js"; import { lowerStringReplacement } from "./string-replacement.js"; +import { lowerReplacerCallback, regexLiteralCapturesOf } from "./replacer-params.js"; import { coerceStringSearchValue, defaultAfterUndefined, @@ -1025,7 +1026,11 @@ export function lowerRegexMethodCall( : [ lowerer.lowerExprExpecting(arg0, { kind: "regex" }), call.arguments.nodes[1] - ? lowerer.lowerExpr(call.arguments.nodes[1]) + ? lowerReplacerCallback( + lowerer, + call.arguments.nodes[1], + regexLiteralCapturesOf(arg0), + ) : dynUndefinedExpr(loc), ]; if (name !== "split") { diff --git a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts index 156f74869..c21808464 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts @@ -18,6 +18,7 @@ import { import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import { locOf } from "../../program.js"; +import { PLAIN_SEARCH_CAPTURES, lowerReplacerCallback } from "./replacer-params.js"; import type { Lowerer } from "../lowerer.js"; import { coerceStringSearchValue, @@ -48,11 +49,18 @@ export function lowerStringReplacement( type.kind === "bool" || type.kind === "bigint" || isUnitType(type); - const argument = (node: ts.Expression | undefined, callback: boolean): IrExpr => { + const argument = ( + node: ts.Expression | undefined, + callback: boolean, + plainSearch = false, + ): IrExpr => { if (!node) return strLit("undefined", loc); const absent = lowerStaticallyUndefinedArgument(lowerer, node); if (absent) return defaultAfterUndefined(absent, strLit("undefined", loc)); - const value = lowerer.lowerExpr(node); + // A plain search passes (match, offset, subject). + const value = plainSearch + ? lowerReplacerCallback(lowerer, node, PLAIN_SEARCH_CAPTURES) + : lowerer.lowerExpr(node); if (value.type.kind === "dyn" || (callback && value.type.kind === "func")) return value; if (!primitive(value.type)) { lowerer.noLowering( @@ -64,7 +72,7 @@ export function lowerStringReplacement( return coerceStringSearchValue(lowerer, value, node, loc); }; const search = argument(args[0], false); - const replacement = argument(args[1], true); + const replacement = argument(args[1], true, search.type.kind !== "dyn"); if (search.type.kind === "dyn" || replacement.type.kind === "dyn") { // Evaluate all inputs once before choosing the search protocol. Regexes // retain their native pattern; plain values use the string-search helper. diff --git a/packages/compiler/test/replacer-params.test.ts b/packages/compiler/test/replacer-params.test.ts new file mode 100644 index 000000000..bb5549a86 --- /dev/null +++ b/packages/compiler/test/replacer-params.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 { afterEach, describe, expect, test } from "vitest"; +import { compile } from "../src/index.js"; +import { regexLiteralCaptures } from "../src/backend/llvm/regex-matchers.js"; +import { regexCaptureParticipation } from "../src/frontend/lowering/containers/replacer-params.js"; + +// The frontend binds a replace callback parameter as a string only for a +// capture every match sets; the backend then calls the callback directly +// only when its own parse agrees. +describe("replace callback captures", () => { + test("never claims a capture the backend's parse can leave unset", () => { + const patterns = [ + "(\\w+)=(\\d)+", + "(?a)(b)?|(c)", + "x(?:(a)|(b))y(c)*(d){0,2}(e){1,3}", + "((a)(b)?)+", + "(a(b|c))d", + "(?:a|(b))(c)", + ]; + for (const pattern of patterns) { + const frontend = regexCaptureParticipation(pattern, ""); + const backend = regexLiteralCaptures(pattern, ""); + expect(frontend?.count).toBe(backend?.count); + frontend!.always.forEach((always, index) => { + if (always) expect(backend!.always[index]).toBe(true); + }); + } + }); +}); + +const directories: string[] = []; +afterEach(async () => { + await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +async function emit(source: string): Promise { + const outDir = await mkdtemp(join(tmpdir(), "scriptc-replacer-")); + directories.push(outDir); + const entry = join(outDir, "main.ts"); + await writeFile(entry, source); + const outPath = join(outDir, "program.ll"); + const result = await compile(entry, { entry, outDir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result)).toBe(true); + return readFile(outPath, "utf8"); +} + +/** The (arity, kinds) arguments of each direct replacer call, and the + * number of calls through the boxed argument array. */ +function replacerCalls(llvm: string): { direct: string[]; boxed: number } { + const lines = llvm.split("\n").filter((line) => line.includes("call ")); + return { + direct: lines + .filter((line) => line.includes("@scr_regex_replace_fn(")) + .map((line) => /i32 (-?\d+), i32 (-?\d+), i1/.exec(line)!.slice(1, 3).join(":")), + boxed: lines.filter((line) => line.includes("@scr_regex_replace_callback(")).length, + }; +} + +describe("replace callback calls", () => { + test("unannotated parameters take the kinds the replace passes", async () => { + const llvm = await emit(` +const vars: Record = { a: "1" }; +const text = String(Date.now() > 0 ? "{a} {b}" : ""); +console.log(text.replace(/\\{(\\w+)\\}/g, (_m, key) => vars[key] ?? "")); +console.log(text.replace(/\\{(\\w)/g, (_m, c) => c.toUpperCase())); +console.log(text.replace(/(a)|(b)/g, (m, a, b) => \`\${m}\${a}\${b}\`)); +console.log(text.replace(/(a)(b)?/g, (m: string, a: string, b: string | undefined, at: number) => m + a + (b ?? "") + at)); +const half = (n: number): number => n / 2; +console.log(text.replace(/b/g, (m, at) => String(half(at)))); +console.log(text.replace(/(a)(b)(c)/g, (m, a, b, c, at, all, extra) => m + a + b + c + at + all + extra)); +`); + expect(replacerCalls(llvm)).toEqual({ + // Typed parameters and result (0); a checked-dynamic result (bit 31); + // two captures a match can leave unset (bits 1 and 2); + // \`string | undefined\` at capture 2 (bit 8 + 2); an offset passed to a + // number parameter keeps its checked-dynamic box (bit 1). + direct: ["2:0", "2:-2147483648", "3:6", "4:1024", "2:2"], + // Seven parameters exceed the direct calls' limit. + boxed: 1, + }); + }); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8118-replacer-params.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8118-replacer-params.ts.json new file mode 100644 index 000000000..4de6ead6b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8118-replacer-params.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8118-replacer-params.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8345-replace-callback-untyped-params.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8345-replace-callback-untyped-params.ts.json new file mode 100644 index 000000000..9a4c2b510 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8345-replace-callback-untyped-params.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8345-replace-callback-untyped-params.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8346-replace-callback-untyped-params.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8346-replace-callback-untyped-params.mjs.json new file mode 100644 index 000000000..301203783 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8346-replace-callback-untyped-params.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8346-replace-callback-untyped-params.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index 158697a14..ab323c288 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -1529,49 +1529,101 @@ ScrStr *scr_regex_replace_callback(ScrStr *s, ScrRegex *re, ScrDyn *callback, bo return scr_jb_finish(&output); } -/* One call of a typed replacer (scr_regex_replace_fn's contract): string - * arguments are passed +1 (the closure ABI's owned parameters), the offset - * as a number, and the result is the closure's +1 string. */ -static ScrStr *scr_regex_call_replacer(ScrClosure *f, uint32_t arity, int ncap, - ScrStr *const *caps, double offset, ScrStr *subject) { - void *p[4] = { NULL, NULL, NULL, NULL }; - for (uint32_t i = 0; i < arity && i < 4; i++) { - if ((int)i < ncap) { - if (!caps[i]) scr_trap("scriptc: internal error: typed replacer capture is unset\n"); - p[i] = scr_str_retain(caps[i]); - } else if ((int)i == ncap + 1) { - p[i] = scr_str_retain(subject); - } - } +/* The closure signature scr_regex_replace_fn calls: `arity` pointer + * parameters, except a number at `number_at` (-1 when there is none). A + * string and a checked-dynamic value are both pointers, and so are both + * results. */ +#define SCR_REPLACER_CALL(...) return ((void *(*)(ScrClosure *, __VA_ARGS__))fn) +static inline __attribute__((always_inline)) void *scr_regex_call_replacer( + ScrClosure *f, uint32_t arity, int number_at, void *const *p, double offset) { void *fn = f->fn; - typedef ScrStr *R; - switch (arity) { - case 0: return ((R (*)(ScrClosure *))fn)(f); - case 1: return ((R (*)(ScrClosure *, void *))fn)(f, p[0]); - case 2: - if (ncap == 1) return ((R (*)(ScrClosure *, void *, double))fn)(f, p[0], offset); - return ((R (*)(ScrClosure *, void *, void *))fn)(f, p[0], p[1]); - case 3: - if (ncap == 1) return ((R (*)(ScrClosure *, void *, double, void *))fn)(f, p[0], offset, p[2]); - if (ncap == 2) return ((R (*)(ScrClosure *, void *, void *, double))fn)(f, p[0], p[1], offset); - return ((R (*)(ScrClosure *, void *, void *, void *))fn)(f, p[0], p[1], p[2]); - case 4: - if (ncap == 2) - return ((R (*)(ScrClosure *, void *, void *, double, void *))fn)(f, p[0], p[1], offset, p[3]); - if (ncap == 3) - return ((R (*)(ScrClosure *, void *, void *, void *, double))fn)(f, p[0], p[1], p[2], offset); - if (ncap >= 4) - return ((R (*)(ScrClosure *, void *, void *, void *, void *))fn)(f, p[0], p[1], p[2], p[3]); - break; + typedef void *P; + typedef double D; + switch (arity * 8 + (uint32_t)(number_at < 0 ? 7 : number_at)) { + case 0 * 8 + 7: return ((void *(*)(ScrClosure *))fn)(f); + case 1 * 8 + 7: SCR_REPLACER_CALL(P)(f, p[0]); + case 2 * 8 + 7: SCR_REPLACER_CALL(P, P)(f, p[0], p[1]); + case 2 * 8 + 1: SCR_REPLACER_CALL(P, D)(f, p[0], offset); + case 3 * 8 + 7: SCR_REPLACER_CALL(P, P, P)(f, p[0], p[1], p[2]); + case 3 * 8 + 1: SCR_REPLACER_CALL(P, D, P)(f, p[0], offset, p[2]); + case 3 * 8 + 2: SCR_REPLACER_CALL(P, P, D)(f, p[0], p[1], offset); + case 4 * 8 + 7: SCR_REPLACER_CALL(P, P, P, P)(f, p[0], p[1], p[2], p[3]); + case 4 * 8 + 1: SCR_REPLACER_CALL(P, D, P, P)(f, p[0], offset, p[2], p[3]); + case 4 * 8 + 2: SCR_REPLACER_CALL(P, P, D, P)(f, p[0], p[1], offset, p[3]); + case 4 * 8 + 3: SCR_REPLACER_CALL(P, P, P, D)(f, p[0], p[1], p[2], offset); + case 5 * 8 + 7: SCR_REPLACER_CALL(P, P, P, P, P)(f, p[0], p[1], p[2], p[3], p[4]); + case 5 * 8 + 1: SCR_REPLACER_CALL(P, D, P, P, P)(f, p[0], offset, p[2], p[3], p[4]); + case 5 * 8 + 2: SCR_REPLACER_CALL(P, P, D, P, P)(f, p[0], p[1], offset, p[3], p[4]); + case 5 * 8 + 3: SCR_REPLACER_CALL(P, P, P, D, P)(f, p[0], p[1], p[2], offset, p[4]); + case 5 * 8 + 4: SCR_REPLACER_CALL(P, P, P, P, D)(f, p[0], p[1], p[2], p[3], offset); + case 6 * 8 + 7: SCR_REPLACER_CALL(P, P, P, P, P, P)(f, p[0], p[1], p[2], p[3], p[4], p[5]); + case 6 * 8 + 1: SCR_REPLACER_CALL(P, D, P, P, P, P)(f, p[0], offset, p[2], p[3], p[4], p[5]); + case 6 * 8 + 2: SCR_REPLACER_CALL(P, P, D, P, P, P)(f, p[0], p[1], offset, p[3], p[4], p[5]); + case 6 * 8 + 3: SCR_REPLACER_CALL(P, P, P, D, P, P)(f, p[0], p[1], p[2], offset, p[4], p[5]); + case 6 * 8 + 4: SCR_REPLACER_CALL(P, P, P, P, D, P)(f, p[0], p[1], p[2], p[3], offset, p[5]); + case 6 * 8 + 5: SCR_REPLACER_CALL(P, P, P, P, P, D)(f, p[0], p[1], p[2], p[3], p[4], offset); default: break; } scr_trap("scriptc: internal error: typed replacer signature\n"); } +#undef SCR_REPLACER_CALL + +/* Typed replacer argument k: a capture (NULL for an unset optional one) + * or the subject; the offset is passed separately. The compiler admits a + * typed parameter only at those positions. Inlined with each constant k. */ +static inline __attribute__((always_inline)) void scr_regex_typed_arg( + void **p, uint32_t k, uint32_t kinds, int capture_count, ScrStr *const *caps, ScrStr *subject) { + if ((int)k < capture_count) { + if (!caps[k] && !(kinds & (SCR_REPLACER_OPTIONAL << k))) + scr_trap("scriptc: internal error: typed replacer capture is unset\n"); + p[k] = caps[k] ? scr_str_retain(caps[k]) : NULL; + } else if ((int)k == capture_count + 1) { + p[k] = scr_str_retain(subject); + } +} + +/* One match's replacer arguments when some parameter is checked-dynamic + * (scr_regex_replace_fn's `kinds`): typed ones +1, the rest new boxes. + * `boxed_subject` is the subject's box; `groups` the match's groups + * object, or NULL when no parameter reads it. */ +__attribute__((noinline)) static void scr_regex_replacer_args( + void **p, uint32_t arity, uint32_t kinds, int capture_count, ScrStr *const *caps, double offset, + ScrStr *subject, ScrDyn *boxed_subject, ScrDyn *groups) { + for (uint32_t k = 0; k < arity; k++) { + int at = (int)k; + if (kinds & (1u << k)) { + if (at < capture_count) + p[k] = caps[k] ? scr_dyn_new_str(caps[k]) : scr_dyn_retain(scr_dyn_undefined()); + else if (at == capture_count) + p[k] = scr_dyn_new_num(offset); + else if (at == capture_count + 1) + p[k] = scr_dyn_retain(boxed_subject); + else if (at == capture_count + 2 && groups) + p[k] = scr_dyn_retain(groups); + else + p[k] = scr_dyn_retain(scr_dyn_undefined()); + } else if (at < capture_count) { + if (!caps[k] && !(kinds & (SCR_REPLACER_OPTIONAL << k))) + scr_trap("scriptc: internal error: typed replacer capture is unset\n"); + p[k] = caps[k] ? scr_str_retain(caps[k]) : NULL; + } else if (at == capture_count + 1) { + p[k] = scr_str_retain(subject); + } else if (at != capture_count) { + scr_trap("scriptc: internal error: typed replacer signature\n"); + } + } +} /* scr_regex_replace_callback's algorithm for a typed replacer: the matches - * are collected first, keeping only the captures the replacer reads, then - * the replacer runs once per match in order. */ -ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t arity, bool all) { + * are collected first, keeping only the captures and groups objects the + * replacer reads, then the replacer runs once per match in order. Typed + * parameters receive their values +1 (the closure ABI's owned + * parameters) and an unset optional capture NULL; checked-dynamic ones + * receive new boxes. Inlined into one copy for kinds 0, where the boxed + * handling folds away, and one for the other kinds. */ +static inline __attribute__((always_inline)) ScrStr * +scr_regex_replace_fn_impl(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t arity, uint32_t kinds, + bool all) { uint8_t *bc = scr_regex_bc(re); int flags = lre_get_flags(bc); bool global = (flags & LRE_FLAG_GLOBAL) != 0; @@ -1580,15 +1632,28 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } + if (arity > SCR_REPLACER_MAX_ARITY) scr_trap("scriptc: internal error: typed replacer arity\n"); bool sticky = (flags & LRE_FLAG_STICKY) != 0; bool unicode = (flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0; int capture_count = lre_get_capture_count(bc); size_t keep = arity < (uint32_t)capture_count ? arity : (size_t)capture_count; + /* Node passes the groups object after the subject when the pattern has + * named groups; only a checked-dynamic parameter can read it, and no + * typed parameter follows the subject. */ + bool past_subject = (uint32_t)capture_count + 2 < arity; + if (past_subject && !(kinds & SCR_REPLACER_BOXED_PARAMS)) + scr_trap("scriptc: internal error: typed replacer signature\n"); + const char *groupnames = past_subject ? lre_get_groupnames(bc) : NULL; + bool groups_read = groupnames != NULL; + if (groups_read && !(kinds & (1u << (capture_count + 2)))) + scr_trap("scriptc: internal error: typed replacer groups parameter\n"); + int number_at = capture_count < (int)arity && !(kinds & (1u << capture_count)) ? capture_count : -1; ScrRegexInput input; scr_regex_input_init(&input, re, bc, s); int len = input.len; struct { int start, end; } *matches = NULL; ScrStr **caps = NULL; + ScrDyn **groups = NULL; size_t count = 0, capacity = 0; if (global) re->last_index = 0; int position = sticky ? scr_regex_start(re, len) : 0; @@ -1610,6 +1675,11 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar if (!grown) scr_regex_oom(); caps = grown; } + if (groups_read) { + grown = realloc(groups, next * sizeof(*groups)); + if (!grown) scr_regex_oom(); + groups = grown; + } capacity = next; } for (size_t group = 0; group < keep; group++) { @@ -1618,6 +1688,18 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar cs && ce ? scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce)) : NULL; } + if (groups_read) { + ScrDyn *object = scr_dyn_new_obj_null_proto(); + const char *name = groupnames; + for (int group = 1; group < capture_count; group++) { + size_t length = strlen(name); + if (length && (capture[group * 2] || !scr_dyn_obj_get(object, name, length))) + scr_dyn_obj_set(object, name, length, + scr_regex_capture_value(&input, capture[group * 2], capture[group * 2 + 1])); + name += length + LRE_GROUP_NAME_TRAILER_LEN; + } + groups[count] = object; + } matches[count].start = start; matches[count].end = end; count++; @@ -1627,18 +1709,43 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar ScrJsonBuf output; scr_jb_init(&output); int next = 0; + /* One box of the subject serves every match. */ + ScrDyn *subject = NULL; + if (kinds & SCR_REPLACER_BOXED_PARAMS) subject = scr_dyn_new_str(s); scr_dyn_this_push_dyn(NULL); for (size_t i = 0; i < count; i++) { - ScrStr *replacement = scr_regex_call_replacer(f, arity, capture_count, caps + i * keep, - (double)matches[i].start, s); - if (scr_exc_pending()) break; + void *p[SCR_REPLACER_MAX_ARITY] = { NULL }; + double offset = (double)matches[i].start; + ScrStr *const *match_caps = caps + i * keep; + if (kinds & SCR_REPLACER_BOXED_PARAMS) { + scr_regex_replacer_args(p, arity, kinds, capture_count, match_caps, offset, s, subject, + groups_read ? groups[i] : NULL); + } else { + for (uint32_t k = 0; k < SCR_REPLACER_MAX_ARITY; k++) + if (k < arity) scr_regex_typed_arg(p, k, kinds, capture_count, match_caps, s); + } + void *result = scr_regex_call_replacer(f, arity, number_at, p, offset); + ScrStr *replacement = (ScrStr *)result; + if (kinds & SCR_REPLACER_BOXED_RESULT) { + replacement = scr_exc_pending() ? NULL : scr_dyn_string_coerce_js((ScrDyn *)result); + scr_dyn_release((ScrDyn *)result); + } + if (scr_exc_pending()) { + scr_str_release(replacement); + break; + } scr_regex_put_span(&output, &input, next, matches[i].start); scr_jb_put_str(&output, replacement); scr_str_release(replacement); next = matches[i].end; } scr_dyn_this_pop(); + if (subject) scr_dyn_release(subject); for (size_t i = 0; i < count * keep; i++) scr_str_release(caps[i]); + if (groups_read) { + for (size_t i = 0; i < count; i++) scr_dyn_release(groups[i]); + free(groups); + } free(caps); free(matches); if (scr_exc_pending()) { @@ -1651,6 +1758,24 @@ ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t ar return scr_jb_finish(&output); } +__attribute__((noinline)) static ScrStr *scr_regex_replace_typed(ScrStr *s, ScrRegex *re, + ScrClosure *f, uint32_t arity, + bool all) { + return scr_regex_replace_fn_impl(s, re, f, arity, 0, all); +} + +__attribute__((noinline)) static ScrStr *scr_regex_replace_kinds(ScrStr *s, ScrRegex *re, + ScrClosure *f, uint32_t arity, + uint32_t kinds, bool all) { + return scr_regex_replace_fn_impl(s, re, f, arity, kinds, all); +} + +ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t arity, + uint32_t kinds, bool all) { + if (kinds == 0) return scr_regex_replace_typed(s, re, f, arity, all); + return scr_regex_replace_kinds(s, re, f, arity, kinds, all); +} + /* The pinned Node ANSI matcher derives from ansi-regex, copyright (c) * Sindre Sorhus , used under the MIT license: * Permission is hereby granted, free of charge, to any person obtaining a diff --git a/packages/runtime/src/scr_regex.h b/packages/runtime/src/scr_regex.h index 01c545bc5..cb61e49f3 100644 --- a/packages/runtime/src/scr_regex.h +++ b/packages/runtime/src/scr_regex.h @@ -80,16 +80,26 @@ ScrStr *scr_regex_flags(ScrRegex *re); /* +1 */ ScrStr *scr_regex_to_string(ScrRegex *re); /* +1 */ /* replace: first match without /g, every match with it. */ ScrStr *scr_regex_replace(ScrStr *s, ScrRegex *re, ScrStr *rep); -/* replace/replaceAll with a typed replacer closure of `arity` (at most 4) - * parameters returning a string. Parameter i receives what Node passes at - * position i: the match and captures (strings) below the capture count n, - * the match offset (a number) at n, the subject (a string) at n + 1. The - * compiler admits a closure only when every position it declares is one of - * those with the declared kind, and every capture it reads is set in every - * match (scr_regex_replace_callback handles the rest). Borrows all. */ +/* replace/replaceAll with a replacer closure of `arity` (at most + * SCR_REPLACER_MAX_ARITY) parameters. Parameter i receives what Node passes + * at position i: the match and captures below the capture count n, the + * match offset at n, the subject at n + 1, the groups object at n + 2 when + * the pattern has named groups, and undefined after that. Bit i of + * `kinds` makes parameter i a checked-dynamic value, and bit + * SCR_REPLACER_OPTIONAL << i a capture held as a string or NULL for + * undefined; otherwise it is a string, or a number at n. + * SCR_REPLACER_BOXED_RESULT makes the result a checked-dynamic value, + * converted with ToString. The compiler admits a closure only when every + * typed position it declares is one of those with the declared kind, and + * every capture a string parameter reads is set in every match + * (scr_regex_replace_callback handles the rest). Borrows all. */ +#define SCR_REPLACER_MAX_ARITY 6 +#define SCR_REPLACER_BOXED_PARAMS 0xffu +#define SCR_REPLACER_OPTIONAL (1u << 8) +#define SCR_REPLACER_BOXED_RESULT (1u << 31) struct ScrClosure; ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, struct ScrClosure *f, uint32_t arity, - bool all); + uint32_t kinds, bool all); /* replaceAll: throws Node's TypeError when /g is missing (may-throw). */ ScrStr *scr_regex_replace_all(ScrStr *s, ScrRegex *re, ScrStr *rep); /* split: captured substrings are interleaved with the split pieces; diff --git a/tests/corpus/8118-replacer-params.ts b/tests/corpus/8118-replacer-params.ts new file mode 100644 index 000000000..14579ec84 --- /dev/null +++ b/tests/corpus/8118-replacer-params.ts @@ -0,0 +1,43 @@ +// Inline replace callbacks whose parameters after the match are left +// untyped: captures every match sets, captures that can be unset (in an +// alternation, an optional or zero-repeat group, a lookahead), the offset, +// the subject and the groups object, reassigned parameters, named groups, +// patterns with classes, escapes and lookbehind, function expressions and +// replaceAll. +const lines = ["id7,tag12;tag3", "x9 y10", "", "naïve 😀 42", "a-b-c"]; +for (const line of lines) { + console.log(line.replace(/tag(\d+)/g, (_m, d) => String(Number(d) + 1))); + console.log(line.replace(/(\w)(\d+)/g, (m, letter, digits, offset, subject) => + `[${letter}:${digits.length}@${offset}/${subject.length}]`)); + console.log(line.replace(/(a)|(\d)/g, (m, a, digit) => `<${typeof a},${typeof digit}>`)); + console.log(line.replace(/(x)?(\d)/g, (m, x, d) => `${x === undefined ? "-" : x}${d}`)); + console.log(line.replace(/(?:(t)a)*g/g, (m, t, offset) => `{${t}|${offset}}`)); + console.log(line.replace(/(?=(\d))\d/g, (m, ahead, offset) => `${ahead}${offset}`)); + console.log(line.replace(/(?\d)(\d)?/g, (m, n, rest, offset, subject, groups) => + `${n}${rest ?? "_"}${offset}${typeof subject}${JSON.stringify(groups)}`)); + console.log(line.replace(/([\(\)\[\]]|[a-c])(-)/g, (m, c, dash) => c.toUpperCase() + dash.length)); + console.log(line.replace(/(?<=-)(\w)/g, (m, c, offset) => c + offset.toFixed(1))); + console.log(line.replaceAll(/(\d)/g, function (m, d, offset) { + return `${d * 2}:${offset + 1}`; + })); + console.log(line.replace(/(\d)/g, (m, d) => { + d = d.length > 0 ? 5 : 6; + return String(d + 1); + })); + console.log(line.replace(/(\d)\1?/g, (m, d, offset, subject) => `${d}${offset}${subject === line}`)); + console.log(line.replace(/(.)/u, (m, c, offset, subject, extra) => `${c.codePointAt(0)}${offset}${extra}`)); + console.log(line.replace(/(\d){0,2}x/g, (m, d) => `${d}`)); + console.log(line.replace(/(\w)(\w)/g, (m, a, b, offset) => + [a.toUpperCase(), b.toLowerCase(), a.trim(), a.charCodeAt(0), b.codePointAt(0), a.slice(0, 1), + b.substring(0), a.repeat(2), b.padStart(3, "."), a.padEnd(2, "!"), b.includes("1"), + a.startsWith("t"), b.endsWith("g"), a.indexOf("a"), a.length, parseInt(b, 36), + parseFloat(b), Number(b), String(offset), offset - 1, offset * 2, offset % 3, offset < 4, + a === b, a !== "x", a + offset, offset + 1, `${offset}`].join(","))); + console.log(line.replace(/(\d+)/g, (m, d) => (d))); + console.log(line.replace(/(\d+)/g, function (m, d) { return d + d; })); +} +let count = 0; +console.log("tag1 tag2".replace(/tag(\d)/g, (m, d) => { + count += Number(d); + return d.repeat(2); +}), count); diff --git a/tests/corpus/8345-replace-callback-untyped-params.ts b/tests/corpus/8345-replace-callback-untyped-params.ts new file mode 100644 index 000000000..31d3a038b --- /dev/null +++ b/tests/corpus/8345-replace-callback-untyped-params.ts @@ -0,0 +1,85 @@ +// String replace/replaceAll with inline callbacks whose parameters have no +// annotations, so the lib types every parameter after the match `any`. +// Each such parameter holds what Node passes at its position: the captures, +// the offset, the subject, the groups object and then undefined. Captures a +// match can leave unset hold undefined, and a result that is not a string +// converts with ToString. +const vars: Record = { name: "world", place: "earth" }; +const template = "Hello {name}, welcome to {place}! {missing} {name}."; +console.log(template.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`)); +console.log(template.replace(/\{(\w+)\}/, (_m, key) => key.toUpperCase())); +console.log("some_snake_case_name".replace(/_([a-z])/g, (_m, c) => c.toUpperCase())); +console.log("2024-01-15 and 1999-07-04".replace(/(\d{4})-(\d{2})-(\d{2})/g, (_m, y, mo, d) => `${d}/${mo}/${y}`)); +console.log("a1b22".replace(/\d+/g, (m, offset, subject) => `[${m}@${offset}/${subject.length}]`)); +console.log("x1y2".replace(/(\d)/g, (m, digit, offset, subject) => `${m}${digit}${offset}${subject}`)); +console.log("k=v".replace(/(\w)=(\w)/, function (m, k, v, at, all) { + return `${v}=${k}:${at}:${all}:${m}`; +})); + +console.log("k=v".replace(/(\w)=(\w)/, function (this: void, m, k, v, at) { + return `${typeof this}:${v}=${k}:${at}:${m}`; +})); + +// A parameter the body assigns keeps `any`. +console.log("a1".replace(/(\d)/, (_m, d) => { + d = Number(d) + 1; + return String(d * 2); +})); + +// Captures that a match can leave unset. +console.log("ab".replace(/(a)|(b)/g, (m, a, b) => `${m}:${a}:${b};`)); +console.log("b xb".replace(/(x)?b/g, (m, x, at) => `${m}:${x}:${at}`)); +console.log("ab".replace(/((a)|b)/g, (m, outer, inner) => `${outer}:${inner};`)); +console.log("ab".replace(/(?:(a)|b)/g, (m, a, at) => `${m}:${a}:${at};`)); +console.log("aab".replace(/(a){0,2}b/, (m, a, at) => `${m}:${a}:${at}`)); +console.log("ab".replace(/(?=(a))a|b/g, (m, look, at) => `${m}:${look}:${at};`)); +console.log("ab".replace(/(?!(x))a/g, (m, x, at) => `${m}:${x}:${at};`)); +console.log("ab".replace(/(a)*b/, (m, a, at) => `${m}:${a}:${at}`)); +console.log("a(b)".replace(/\((b)\)/, (m, b, at) => `${m}:${b}:${at}`)); +console.log("a|b".replace(/[|(](b)?/, (m, b, at) => `${m}:${b}:${at}`)); + +// Annotated `string | undefined` captures hold undefined when unset. +console.log("2024-01-15 1999-07".replace(/(\d{4})-(\d{2})(-(\d{2}))?/g, (_m, y: string, mo: string, _x: string | undefined, d: string | undefined) => `${d ?? "??"}/${mo}/${y}`)); +console.log("ab".replace(/(a)|(b)/g, (m: string, a: string | undefined, b: string | undefined, at: number) => `${m}:${a ?? "-"}:${b ?? "-"}:${at};`)); +console.log("abc".replace(/(a)(b)(c)/, (m: string, a: string, b: string, c: string, at: number, all: string) => `${c}${b}${a}${m}${at}${all}`)); +console.log("abcd".replace(/(a)(b)(c)(d)/, (m, a, b, c, d, at, all) => `${d}${c}${b}${a}${m}${at}${all}`)); + +// Named groups pass the groups object after the subject; later parameters +// are undefined. +console.log("2024-01".replace(/(?\d{4})-(?\d{2})/, (m, y, mo, at, all, groups) => `${groups.m}.${groups.y}:${y}${mo}:${at}:${all}`)); +console.log("ab".replace(/(?a)|(?b)/g, (m, a, b, at, all, groups) => `${m}:${JSON.stringify(groups)};`)); +console.log("ab".replace(/b/, (m, at, all, extra, more) => `${m}:${at}:${all}:${extra}:${more}`)); +console.log("ab".replace(/(a)(b)/, (m, a, b, at, all, extra, more) => `${m}${a}${b}${at}${all}${extra}${more}`)); + +// Results that are not strings convert with ToString. +const counts: Record = { a: 1, b: true, c: null, d: undefined, e: { toString: () => "E" } }; +console.log("a b c d e".replace(/\w/g, (m) => counts[m])); +console.log("abc".replace(/(\w)/g, (m, c) => counts[c])); + +// String search patterns pass (match, offset, subject). +console.log("one two one".replaceAll("one", (m, at, subject) => `${m.length}@${at}/${subject.length}`)); +console.log("one two one".replace("two", (m, at, subject, extra) => `${m}${at}${subject.length}${extra}`)); + +// A throw stops the replacement. +try { + "a1b2c3".replace(/(\d)/g, (_m, d) => { + if (d === "2") throw new Error("stop at " + d); + return "#"; + }); +} catch (error) { + console.log((error as Error).message); +} +try { + "a1".replace(/(\d)/g, (_m, d) => counts.e.missing.field + d); +} catch (error) { + console.log(error instanceof TypeError); +} +try { + "ab".replace(/(a)|b/g, (m, a) => a.toUpperCase()); +} catch (error) { + console.log(error instanceof TypeError, (error as Error).message); +} + +let total = 0; +for (let i = 0; i < 2000; i++) total += template.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? String(i)).length; +console.log(total); diff --git a/tests/corpus/8346-replace-callback-untyped-params.mjs b/tests/corpus/8346-replace-callback-untyped-params.mjs new file mode 100644 index 000000000..2552113f3 --- /dev/null +++ b/tests/corpus/8346-replace-callback-untyped-params.mjs @@ -0,0 +1,32 @@ +// JavaScript string replace/replaceAll with inline callbacks. Each +// parameter holds what Node passes at its position: the captures, the +// offset, the subject, the groups object and then undefined. Captures a +// match can leave unset hold undefined, and a result that is not a string +// converts with ToString. +const vars = { name: "world", place: "earth" }; +const template = "Hello {name}, welcome to {place}! {missing} {name}."; +console.log(template.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`)); +console.log("some_snake_case_name".replace(/_([a-z])/g, (_m, c) => c.toUpperCase())); +console.log("2024-01-15 and 1999-07-04".replace(/(\d{4})-(\d{2})-(\d{2})/g, (_m, y, mo, d) => `${d}/${mo}/${y}`)); +console.log("a1b22".replace(/\d+/g, (m, offset, subject) => `[${m}@${offset}/${subject.length}]`)); +console.log("k=v".replace(/(\w)=(\w)/, function (m, k, v, at, all) { + return `${this === undefined}:${v}=${k}:${at}:${all}:${m}`; +})); +console.log("ab".replace(/(a)|(b)/g, (m, a, b) => `${m}:${a}:${b};`)); +console.log("b xb".replace(/(x)?b/g, (m, x, at) => `${m}:${x}:${at}`)); +console.log("2024-01".replace(/(?\d{4})-(?\d{2})/, (m, y, mo, at, all, groups) => `${groups.m}.${groups.y}:${y}${mo}:${at}:${all}`)); +console.log("ab".replace(/b/, (m, at, all, extra, more) => `${m}:${at}:${all}:${extra}:${more}`)); +const counts = { a: 1, b: true, c: null, d: undefined, e: { toString: () => "E" } }; +console.log("a b c d e".replace(/\w/g, (m) => counts[m])); +console.log("1-2".replace(/(\d)/g, (_m, d) => Number(d) * 10)); +console.log("one two one".replaceAll("one", (m, at, subject) => `${m.length}@${at}/${subject.length}`)); +let seen = ""; +console.log("xyz".replace(/(y)/, (m, y, at, subject) => { + seen = typeof m + typeof y + typeof at + typeof subject; + return y + at; +}), seen); +try { + "ab".replace(/(a)|b/g, (m, a) => a.toUpperCase()); +} catch (error) { + console.log(error instanceof TypeError, error.message); +} diff --git a/tests/harness/corpus-costs/8118-replacer-params.ts.json b/tests/harness/corpus-costs/8118-replacer-params.ts.json new file mode 100644 index 000000000..9b0bcec50 --- /dev/null +++ b/tests/harness/corpus-costs/8118-replacer-params.ts.json @@ -0,0 +1 @@ +{ "plain": 3670, "san": 4410 } diff --git a/tests/harness/corpus-costs/8345-replace-callback-untyped-params.ts.json b/tests/harness/corpus-costs/8345-replace-callback-untyped-params.ts.json new file mode 100644 index 000000000..5d5bbcefe --- /dev/null +++ b/tests/harness/corpus-costs/8345-replace-callback-untyped-params.ts.json @@ -0,0 +1 @@ +{ "plain": 3630, "san": 4420 } diff --git a/tests/harness/corpus-costs/8346-replace-callback-untyped-params.mjs.json b/tests/harness/corpus-costs/8346-replace-callback-untyped-params.mjs.json new file mode 100644 index 000000000..cdddf6d76 --- /dev/null +++ b/tests/harness/corpus-costs/8346-replace-callback-untyped-params.mjs.json @@ -0,0 +1 @@ +{ "plain": 2880, "san": 5080 } From ceb29078dc763bb0ccad928bb563ee90963d1b8c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 06:31:51 -0500 Subject: [PATCH 206/273] Skip comments when renaming a range's debug ids --- .../compiler/src/backend/llvm/emit-journal.ts | 47 ++++++++++++++---- .../compiler/test/chunked-emission.test.ts | 49 +++++++++++++++++++ 2 files changed, 86 insertions(+), 10 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emit-journal.ts b/packages/compiler/src/backend/llvm/emit-journal.ts index d6fed03a6..b2b3fc4cc 100644 --- a/packages/compiler/src/backend/llvm/emit-journal.ts +++ b/packages/compiler/src/backend/llvm/emit-journal.ts @@ -175,22 +175,49 @@ export function renameChunk( }; } -/** Renames metadata references `!N` outside string literals. Debug builds - * attach no alias tags, so every such reference in a function's text is a - * debug metadata id. */ +/** Renames metadata references `!N` outside string literals and comments. + * Debug builds attach no alias tags, so every such reference in a + * function's text is a debug metadata id. A string literal or a comment + * (`;` outside a string literal) ends at the end of its line; a quote + * inside a string literal is escaped (`\22`). */ function renameDebugIds(text: string, ids: ReadonlyMap): string { const parts: string[] = []; let copied = 0; - // The next quote at or after the scan, and whether the scan is inside a - // string literal. - let quote = text.indexOf('"'); + // The line of the reference being looked at, and whether the reference + // is inside a string literal or a comment there. The next quote and + // semicolon positions only move forward, so each is found once. + let lineEnd = -1; let quoted = false; + let comment = false; + let quote = text.indexOf('"'); + let semicolon = text.indexOf(";"); for (let at = text.indexOf("!"); at >= 0; at = text.indexOf("!", at + 1)) { - while (quote >= 0 && quote < at) { - quoted = !quoted; - quote = text.indexOf('"', quote + 1); + if (at > lineEnd) { + lineEnd = text.indexOf("\n", at); + if (lineEnd < 0) lineEnd = text.length; + quoted = false; + comment = false; + // Quotes and semicolons of earlier lines end with them. + if ((quote >= 0 && quote < at) || (semicolon >= 0 && semicolon < at)) { + const lineStart = text.lastIndexOf("\n", at) + 1; + while (quote >= 0 && quote < lineStart) quote = text.indexOf('"', quote + 1); + while (semicolon >= 0 && semicolon < lineStart) + semicolon = text.indexOf(";", semicolon + 1); + } } - if (quoted) continue; + while (!comment) { + const nextQuote = quote >= 0 && quote < at ? quote : -1; + const nextSemicolon = semicolon >= 0 && semicolon < at ? semicolon : -1; + if (nextQuote < 0 && nextSemicolon < 0) break; + if (nextQuote >= 0 && (nextSemicolon < 0 || nextQuote < nextSemicolon)) { + quoted = !quoted; + quote = text.indexOf('"', quote + 1); + } else { + if (!quoted) comment = true; + semicolon = text.indexOf(";", semicolon + 1); + } + } + if (quoted || comment) continue; if (at > 0 && isNameChar(text.charCodeAt(at - 1))) continue; let end = at + 1; while (end < text.length && isDigit(text.charCodeAt(end))) end++; diff --git a/packages/compiler/test/chunked-emission.test.ts b/packages/compiler/test/chunked-emission.test.ts index 567a082ef..47e82cf9e 100644 --- a/packages/compiler/test/chunked-emission.test.ts +++ b/packages/compiler/test/chunked-emission.test.ts @@ -177,3 +177,52 @@ test("debug builds emitted in ranges merge into the text one emitter gives", asy await rm(dir, { recursive: true, force: true }); } }, 300_000); + +// A method name holding a double quote is written into a comment after the +// function's `!dbg` attachment; the rename of the range's debug ids reads +// comments and string literals as ending at the end of their line. +test("debug ids after a comment that holds a quote are renamed", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-chunked-debug-quote-")); + try { + const entry = join(dir, "main.ts"); + const source = [ + "class Greeter {", + " ['say \"hi'](n: number): number {", + " return n + 1;", + " }", + "}", + "const greeter = new Greeter();", + "function twice(x: number): number {", + " return x * 2;", + "}", + "function report(label: string, value: number): void {", + " console.log(label, value);", + "}", + 'report("method", greeter[\'say "hi\'](1));', + 'report("twice", twice(3));', + "", + ].join("\n"); + await writeFile(entry, source); + 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 options: LlvmTargetOptions = { debugSources: new Map([[entry, source]]) }; + const serial = text(emitLlvmModuleSource(deserializeModule(ir), options)); + expect(serial).toMatch(/!dbg ![0-9]+ \{ ;[^\n]*"/); + for (const functionChunks of [2, 3]) { + const chunked = text( + emitLlvmModuleSource(deserializeModule(ir), { ...options, functionChunks }), + ); + expect(chunked === serial, `${functionChunks} chunks`).toBe(true); + } + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 60_000); From 24ef53ac237dd58c79aa590cff61c1775b275a86 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 06:32:18 -0500 Subject: [PATCH 207/273] Have tsc-ts read its library files from disk when it can Every file tsc-ts reads goes through scriptc's file system callbacks, including the ~80 files of tsc-ts's own default library directory: one round trip from the checker to the compiler and back for each. Those files belong to the installed tsc-ts (the build caches key on its executable's identity), and no build serves them differently. When tsc-ts takes --default-libraries-from-disk, the native compiler passes it to the checker it starts and to the shared checker server, so tsc-ts reads those files itself; every other path keeps the callbacks. Released tsc-ts versions reject the flag, so checker-features.ts asks each tsc-ts executable once (tsc-ts --api --default-libraries-from-disk with an empty input exits with 0 only when it knows the flag) and keeps the answer in the native cache, keyed by the executable's identity. With a tsc-ts that does not know the flag, builds run as before. Edit builds of small programs through the shared checker server are about 18 ms faster, and 6-9 ms faster without it. --- .../compiler/native/ts7-process-child.test.c | 10 ++-- packages/compiler/native/ts7-process.c | 27 ++++++---- packages/compiler/native/ts7-process.ffi.json | 4 +- packages/compiler/native/ts7-process.h | 5 +- packages/compiler/native/ts7-process.test.c | 24 +++++---- .../src/frontend/ts7/checker-features.test.ts | 45 ++++++++++++++++ .../src/frontend/ts7/checker-features.ts | 53 +++++++++++++++++++ .../src/frontend/ts7/checker-server.ts | 7 ++- .../compiler/src/frontend/ts7/native-api.ts | 12 +++-- .../src/frontend/ts7/native-process.ts | 27 ++++++++-- packages/compiler/src/native/cli.ts | 6 +++ 11 files changed, 185 insertions(+), 35 deletions(-) create mode 100644 packages/compiler/src/frontend/ts7/checker-features.test.ts create mode 100644 packages/compiler/src/frontend/ts7/checker-features.ts diff --git a/packages/compiler/native/ts7-process-child.test.c b/packages/compiler/native/ts7-process-child.test.c index 386a0a7c3..4d1cbc081 100644 --- a/packages/compiler/native/ts7-process-child.test.c +++ b/packages/compiler/native/ts7-process-child.test.c @@ -64,8 +64,11 @@ static int child_main(int argc, char **argv) { #endif int listening = argc >= 9 && strcmp(argv[5], "--listen") == 0 && strcmp(argv[7], "--idle-exit") == 0; int fixed = listening ? 9 : 5; - if (argc < fixed || argc > fixed + 1 || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 || - strncmp(argv[4], "--callbacks=", 12) != 0 || (argc == fixed + 1 && strcmp(argv[fixed], "--timing") != 0)) + int at = fixed, timing = 0, libraries = 0; + if (at < argc && strcmp(argv[at], "--timing") == 0) { timing = 1; at++; } + if (at < argc && strcmp(argv[at], "--default-libraries-from-disk") == 0) { libraries = 1; at++; } + if (argc < fixed || at != argc || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 || + strncmp(argv[4], "--callbacks=", 12) != 0) return 71; const char *mode = argv[3]; #ifndef _WIN32 @@ -76,7 +79,8 @@ static int child_main(int argc, char **argv) { fputc('\n', stdout); fputs(argv[4], stdout); fputc('\n', stdout); - fputs(argc == 6 ? "timing\n" : "plain\n", stdout); + fputs(timing ? "timing\n" : "plain\n", stdout); + if (libraries) fputs("libraries from disk\n", stdout); return ferror(stdout) ? 72 : 0; } if (strcmp(mode, "exit") == 0) return 0; diff --git a/packages/compiler/native/ts7-process.c b/packages/compiler/native/ts7-process.c index 3fd38a423..8261c341b 100644 --- a/packages/compiler/native/ts7-process.c +++ b/packages/compiler/native/ts7-process.c @@ -161,7 +161,7 @@ static wchar_t *command_line(wchar_t **arguments, size_t count) { } static int start_process(Ts7Process *p, char **args, size_t argc) { - wchar_t *wide[6] = {0}; + wchar_t *wide[7] = {0}; wchar_t *line = NULL; HANDLE child_input = NULL, child_output = NULL, child_error = NULL; LPPROC_THREAD_ATTRIBUTE_LIST attributes = NULL; @@ -330,7 +330,7 @@ static void stop_process(Ts7Process *p) { uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size, const uint8_t *cwd, size_t cwd_size, const uint8_t *callbacks, size_t callbacks_size, - uint8_t timing) { + uint8_t timing, uint8_t libraries_from_disk) { if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; } char *exe = copy_argument(executable, executable_size, ""); if (exe == NULL) return 0; @@ -346,8 +346,12 @@ uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size, } exit_registered = 1; } - char *args[] = {exe, "--api", "--cwd", directory, callback_flag, timing ? "--timing" : NULL, NULL}; - int started = start_process(p, args, timing ? 6 : 5); + char *args[8] = {exe, "--api", "--cwd", directory, callback_flag}; + size_t argc = 5; + if (timing) args[argc++] = "--timing"; + if (libraries_from_disk) args[argc++] = "--default-libraries-from-disk"; + args[argc] = NULL; + int started = start_process(p, args, argc); free(exe); free(directory); free(callback_flag); if (!started) { free(p); return 0; } p->id = next_id++; @@ -361,9 +365,10 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size, const uint8_t *executable, size_t executable_size, const uint8_t *cwd, size_t cwd_size, const uint8_t *callbacks, size_t callbacks_size, - uint8_t timing, uint32_t idle_exit, uint32_t memory) { + uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit, + uint32_t memory) { (void)name; (void)name_size; (void)executable; (void)executable_size; (void)cwd; (void)cwd_size; - (void)callbacks; (void)callbacks_size; (void)timing; (void)idle_exit; (void)memory; + (void)callbacks; (void)callbacks_size; (void)timing; (void)libraries_from_disk; (void)idle_exit; (void)memory; error_text("a shared checker server needs Unix sockets"); return 0; } @@ -455,7 +460,8 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size, const uint8_t *executable, size_t executable_size, const uint8_t *cwd, size_t cwd_size, const uint8_t *callbacks, size_t callbacks_size, - uint8_t timing, uint32_t idle_exit, uint32_t memory) { + uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit, + uint32_t memory) { if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; } char *server = copy_argument(name, name_size, ""); if (server == NULL) return 0; @@ -491,8 +497,11 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size, (void)snprintf(limit, sizeof(limit), "TSC_TS_LISTEN_MEMORY=%lu", (unsigned long)memory); pid_t pid = -1; if (callback_flag != NULL) { - char *args[] = {exe, "--api", "--cwd", dir, callback_flag, "--listen", socket_path, "--idle-exit", idle, - timing ? "--timing" : NULL, NULL}; + char *args[12] = {exe, "--api", "--cwd", dir, callback_flag, "--listen", socket_path, "--idle-exit", idle}; + size_t argc = 9; + if (timing) args[argc++] = "--timing"; + if (libraries_from_disk) args[argc++] = "--default-libraries-from-disk"; + args[argc] = NULL; pid = start_server(args, limit); } free(exe); free(dir); free(callback_flag); diff --git a/packages/compiler/native/ts7-process.ffi.json b/packages/compiler/native/ts7-process.ffi.json index c503e3cae..963c81f15 100644 --- a/packages/compiler/native/ts7-process.ffi.json +++ b/packages/compiler/native/ts7-process.ffi.json @@ -1,8 +1,8 @@ { "ffi_format": 6, "functions": [ - { "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool"], "returns": "u32" }, - { "name": "ts7NativeAttach", "symbol": "scriptc_ts7_attach", "params": ["string", "string", "string", "string", "bool", "u32", "u32"], "returns": "u32" }, + { "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool", "bool"], "returns": "u32" }, + { "name": "ts7NativeAttach", "symbol": "scriptc_ts7_attach", "params": ["string", "string", "string", "string", "bool", "bool", "u32", "u32"], "returns": "u32" }, { "name": "ts7NativeRead", "symbol": "scriptc_ts7_read", "params": ["u32", "mutable-bytes", "u32", "u32"], "returns": "i32" }, { "name": "ts7NativeWrite", "symbol": "scriptc_ts7_write", "params": ["u32", "bytes", "u32", "u32"], "returns": "i32" }, { "name": "ts7NativePid", "symbol": "scriptc_ts7_pid", "params": ["u32"], "returns": "u32" }, diff --git a/packages/compiler/native/ts7-process.h b/packages/compiler/native/ts7-process.h index 4199e8795..d51f939a9 100644 --- a/packages/compiler/native/ts7-process.h +++ b/packages/compiler/native/ts7-process.h @@ -12,7 +12,7 @@ uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size, const uint8_t *cwd, size_t cwd_size, const uint8_t *callbacks, size_t callbacks_size, - uint8_t timing); + uint8_t timing, uint8_t libraries_from_disk); /* Connects to the shared checker server `name` (`--api --listen`) in this * user's private server directory, starting it in its own process group * with `--idle-exit idle_exit` and a limit of `memory` megabytes @@ -25,7 +25,8 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size, const uint8_t *executable, size_t executable_size, const uint8_t *cwd, size_t cwd_size, const uint8_t *callbacks, size_t callbacks_size, - uint8_t timing, uint32_t idle_exit, uint32_t memory); + uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit, + uint32_t memory); int32_t scriptc_ts7_read(uint32_t handle, uint8_t *bytes, size_t size, uint32_t offset, uint32_t length); int32_t scriptc_ts7_write(uint32_t handle, const uint8_t *bytes, size_t size, diff --git a/packages/compiler/native/ts7-process.test.c b/packages/compiler/native/ts7-process.test.c index dc0c67ced..b6d760abd 100644 --- a/packages/compiler/native/ts7-process.test.c +++ b/packages/compiler/native/ts7-process.test.c @@ -21,12 +21,14 @@ static const char *executable; static const char *callbacks = "readFile,fileExists"; -static uint32_t start(const char *mode, int timing) { +static uint32_t start_with(const char *mode, int timing, int libraries) { return scriptc_ts7_open((const uint8_t *)executable, strlen(executable), (const uint8_t *)mode, strlen(mode), - (const uint8_t *)callbacks, strlen(callbacks), (uint8_t)timing); + (const uint8_t *)callbacks, strlen(callbacks), (uint8_t)timing, (uint8_t)libraries); } +static uint32_t start(const char *mode, int timing) { return start_with(mode, timing, 0); } + static void fail_message(void) { uint8_t message[1024]; int32_t size = scriptc_ts7_error(message, sizeof(message)); @@ -83,11 +85,13 @@ static void argument_cases(void) { "", "a b", "double\"quote", "end\\", "many\\\\\"quotes\\\\", "a;&$()`'b", "h\xc3\xa9llo \xf0\x9f\x8c\x8d", }; for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) { - for (int timing = 0; timing <= 1; timing++) { + for (int flags = 0; flags < 4; flags++) { + int timing = flags & 1, libraries = flags >> 1; char mode[256], expected[512]; snprintf(mode, sizeof(mode), "argv:%s", values[i]); - snprintf(expected, sizeof(expected), "%s\n--callbacks=%s\n%s\n", values[i], callbacks, timing ? "timing" : "plain"); - uint32_t id = start(mode, timing); + snprintf(expected, sizeof(expected), "%s\n--callbacks=%s\n%s\n%s", values[i], callbacks, + timing ? "timing" : "plain", libraries ? "libraries from disk\n" : ""); + uint32_t id = start_with(mode, timing, libraries); assert(id != 0); uint8_t actual[512]; read_exact(id, actual, strlen(expected)); @@ -147,10 +151,10 @@ static void failure_cases(void) { executable = "/scriptc/does-not-exist/tsgo"; for (int i = 0; i < 30; i++) { assert(start("echo", 0) == 0); fail_message(); } executable = saved; - assert(scriptc_ts7_open(NULL, 0, bytes, 1, bytes, 1, 0) == 0); - assert(scriptc_ts7_open((const uint8_t *)"abc\0def", 7, bytes, 1, bytes, 1, 0) == 0); - assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), NULL, 0, bytes, 1, 0) == 0); - assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), bytes, 1, NULL, 0, 0) == 0); + assert(scriptc_ts7_open(NULL, 0, bytes, 1, bytes, 1, 0, 0) == 0); + assert(scriptc_ts7_open((const uint8_t *)"abc\0def", 7, bytes, 1, bytes, 1, 0, 0) == 0); + assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), NULL, 0, bytes, 1, 0, 0) == 0); + assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), bytes, 1, NULL, 0, 0, 0) == 0); for (int i = 0; i < 30; i++) { uint32_t id = start("exit", 0); assert(id != 0); @@ -177,7 +181,7 @@ static void failure_cases(void) { static uint32_t attach(const char *name, const char *mode, uint32_t memory) { return scriptc_ts7_attach((const uint8_t *)name, strlen(name), (const uint8_t *)executable, strlen(executable), (const uint8_t *)mode, strlen(mode), (const uint8_t *)callbacks, strlen(callbacks), - 0, 1, memory); + 0, 0, 1, memory); } static void expect_line(uint32_t id, const char *expected) { diff --git a/packages/compiler/src/frontend/ts7/checker-features.test.ts b/packages/compiler/src/frontend/ts7/checker-features.test.ts new file mode 100644 index 000000000..fcd3576cb --- /dev/null +++ b/packages/compiler/src/frontend/ts7/checker-features.test.ts @@ -0,0 +1,45 @@ +import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, afterEach, expect, test } from "vitest"; +import { checkerReadsLibrariesFromDisk, setCheckerFeatureStore } from "./checker-features.js"; + +const dir = mkdtempSync(join(tmpdir(), "scriptc-checker-features-")); +afterAll(() => rmSync(dir, { recursive: true, force: true })); +afterEach(() => setCheckerFeatureStore(null)); + +/** A stand-in checker that records its arguments and exits with `status`. */ +function checker(name: string, status: number): { path: string; log: string } { + const path = join(dir, name); + const log = path + ".log"; + writeFileSync(path, `#!/bin/sh\necho "$@" >> '${log}'\nexit ${status}\n`); + chmodSync(path, 0o755); + return { path, log }; +} + +test.skipIf(process.platform === "win32")( + "the checker is asked once whether it reads its library files from the disk, and the answer is stored", + async () => { + const { readFileSync, existsSync } = await import("node:fs"); + const entries = new Map(); + setCheckerFeatureStore({ + read: (key) => entries.get(key) ?? null, + write: (key, text) => void entries.set(key, text), + }); + const current = checker("current", 0); + const released = checker("released", 2); + expect(checkerReadsLibrariesFromDisk(current.path, {})).toBe(true); + expect(checkerReadsLibrariesFromDisk(released.path, {})).toBe(false); + expect(readFileSync(current.log, "utf8")).toBe("--api --default-libraries-from-disk\n"); + expect([...entries.values()].sort()).toEqual(["0", "1"]); + // Later asks use the answers: the checkers do not run again. + expect(checkerReadsLibrariesFromDisk(current.path, {})).toBe(true); + expect(checkerReadsLibrariesFromDisk(released.path, {})).toBe(false); + expect(readFileSync(current.log, "utf8").split("\n").length).toBe(2); + expect(readFileSync(released.log, "utf8").split("\n").length).toBe(2); + // tsgo is never asked. + const tsgo = checker("tsgo", 0); + expect(checkerReadsLibrariesFromDisk(tsgo.path, { SCRIPTC_CHECKER: "tsgo" })).toBe(false); + expect(existsSync(tsgo.log)).toBe(false); + }, +); diff --git a/packages/compiler/src/frontend/ts7/checker-features.ts b/packages/compiler/src/frontend/ts7/checker-features.ts new file mode 100644 index 000000000..b80a54994 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/checker-features.ts @@ -0,0 +1,53 @@ +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { checkerBackend } from "./checker-backend.js"; +import { checkerExecutableIdentity } from "./lib-diagnostics.js"; + +/** Text entries kept between builds (the native compiler's cache). */ +export interface CheckerFeatureStore { + read(key: string): string | null; + write(key: string, text: string): void; +} + +let featureStore: CheckerFeatureStore | null = null; +const answers = new Map(); + +/** Keeps the answers of checkerReadsLibrariesFromDisk between builds. */ +export function setCheckerFeatureStore(store: CheckerFeatureStore | null): void { + featureStore = store; +} + +/** + * Whether the tsc-ts at `executable` takes `--api --default-libraries-from-disk`, which has it read the files + * of its default library directory from the disk instead of through the file system callbacks. tsc-ts releases + * before it reject the flag, so it is asked once per executable identity: the stored answer, or else + * `tsc-ts --api --default-libraries-from-disk` with an empty input, which exits with 0 when the flag is known + * and with a usage error when it is not. tsgo is never asked. + */ +export function checkerReadsLibrariesFromDisk( + executable: string, + env: NodeJS.ProcessEnv = process.env, +): boolean { + if (checkerBackend(env) !== "tsc-ts") return false; + const key = createHash("sha256") + .update(JSON.stringify(["default-libraries-from-disk", checkerExecutableIdentity(executable)])) + .digest("hex"); + const known = answers.get(key); + if (known !== undefined) return known; + const stored = featureStore?.read(key) ?? null; + let answer: boolean; + if (stored === "1" || stored === "0") answer = stored === "1"; + else { + const probe = spawnSync(executable, ["--api", "--default-libraries-from-disk"], { + encoding: "utf8", + stdio: "pipe", + timeout: 10_000, + }); + answer = probe.error === undefined && probe.status === 0; + // A probe that could not run says nothing about the flag: ask again next time. + if (probe.error === undefined && probe.status !== null) + featureStore?.write(key, answer ? "1" : "0"); + } + answers.set(key, answer); + return answer; +} diff --git a/packages/compiler/src/frontend/ts7/checker-server.ts b/packages/compiler/src/frontend/ts7/checker-server.ts index 6d80e2599..94c80f8db 100644 --- a/packages/compiler/src/frontend/ts7/checker-server.ts +++ b/packages/compiler/src/frontend/ts7/checker-server.ts @@ -19,14 +19,16 @@ export const CHECKER_SERVER_MEMORY = 2048; * answers, and the server keeps the files it parsed for the next build. * Returns the server's name, a digest of everything a server's answers * depend on besides the files it reads through the build: the tsc-ts - * executable, the working directory, the file system callbacks, timing, and - * the `TSC_TS_*` environment. Returns null when the shared server is not used. + * executable, the working directory, the file system callbacks, timing, + * whether it reads its library files from the disk, and the `TSC_TS_*` + * environment. Returns null when the shared server is not used. */ export function checkerServerName( executable: string, cwd: string, timing: boolean, env: NodeJS.ProcessEnv = process.env, + librariesFromDisk = false, ): string | null { if (env["SCRIPTC_CHECKER_SERVER"] !== "1") return null; if (process.platform !== "linux" && process.platform !== "darwin") return null; @@ -42,6 +44,7 @@ export function checkerServerName( cwd, TS7_FILE_SYSTEM_CALLBACKS, timing, + librariesFromDisk, tscTsEnv, ]), ) diff --git a/packages/compiler/src/frontend/ts7/native-api.ts b/packages/compiler/src/frontend/ts7/native-api.ts index f8e10efc5..fa0690e69 100644 --- a/packages/compiler/src/frontend/ts7/native-api.ts +++ b/packages/compiler/src/frontend/ts7/native-api.ts @@ -1,4 +1,5 @@ import { Ts7Api } from "./api.js"; +import { checkerReadsLibrariesFromDisk } from "./checker-features.js"; import { checkerServerName } from "./checker-server.js"; import { attachNativeTs7Process, spawnNativeTs7Wire } from "./native-process.js"; import { Ts7RpcClient } from "./rpc-client.js"; @@ -15,12 +16,17 @@ export function createNativeTs7Api(options: { shared?: boolean; }): Ts7Api { const timing = options.collectTiming ?? false; + // The checker reads its own library files when it can: they are part of the + // installed tsc-ts, so no build serves them differently. + const libraries = checkerReadsLibrariesFromDisk(options.executable); const server = - options.shared === false ? null : checkerServerName(options.executable, options.cwd, timing); + options.shared === false + ? null + : checkerServerName(options.executable, options.cwd, timing, process.env, libraries); const shared = server === null ? null - : attachNativeTs7Process(server, options.executable, options.cwd, timing); + : attachNativeTs7Process(server, options.executable, options.cwd, timing, libraries); if (shared !== null) { const rpc = new Ts7RpcClient(new Ts7Wire(shared)); try { @@ -32,7 +38,7 @@ export function createNativeTs7Api(options: { } } return new Ts7Api( - new Ts7RpcClient(spawnNativeTs7Wire(options.executable, options.cwd, timing)), + new Ts7RpcClient(spawnNativeTs7Wire(options.executable, options.cwd, timing, libraries)), options.fs, timing, ); diff --git a/packages/compiler/src/frontend/ts7/native-process.ts b/packages/compiler/src/frontend/ts7/native-process.ts index 0582ae673..d59543f32 100644 --- a/packages/compiler/src/frontend/ts7/native-process.ts +++ b/packages/compiler/src/frontend/ts7/native-process.ts @@ -10,6 +10,7 @@ declare function ts7NativeOpen( cwd: string, callbacks: string, timing: boolean, + librariesFromDisk: boolean, ): number; declare function ts7NativeAttach( name: string, @@ -17,6 +18,7 @@ declare function ts7NativeAttach( cwd: string, callbacks: string, timing: boolean, + librariesFromDisk: boolean, idleExit: number, memory: number, ): number; @@ -63,12 +65,26 @@ export class NativeTs7Process implements Ts7WireIo { private handle: number; readonly pid: number; - /** With `handle`, a connection to a shared checker server (attachNativeTs7Process). */ - constructor(executable: string, cwd: string, collectTiming = false, handle = 0) { + /** With `handle`, a connection to a shared checker server (attachNativeTs7Process). With + * `librariesFromDisk`, the server reads its default library files from the disk + * (checkerReadsLibrariesFromDisk). */ + constructor( + executable: string, + cwd: string, + collectTiming = false, + handle = 0, + librariesFromDisk = false, + ) { this.handle = handle !== 0 ? handle - : ts7NativeOpen(executable, cwd, TS7_FILE_SYSTEM_CALLBACKS, collectTiming); + : ts7NativeOpen( + executable, + cwd, + TS7_FILE_SYSTEM_CALLBACKS, + collectTiming, + librariesFromDisk, + ); if (this.handle === 0) throw nativeError("startup"); this.pid = ts7NativePid(this.handle); } @@ -125,6 +141,7 @@ export function attachNativeTs7Process( executable: string, cwd: string, collectTiming = false, + librariesFromDisk = false, ): NativeTs7Process | null { const handle = ts7NativeAttach( name, @@ -132,6 +149,7 @@ export function attachNativeTs7Process( cwd, TS7_FILE_SYSTEM_CALLBACKS, collectTiming, + librariesFromDisk, CHECKER_SERVER_IDLE_EXIT, CHECKER_SERVER_MEMORY, ); @@ -142,6 +160,7 @@ export function spawnNativeTs7Wire( executable: string, cwd: string, collectTiming = false, + librariesFromDisk = false, ): Ts7Wire { - return new Ts7Wire(new NativeTs7Process(executable, cwd, collectTiming)); + return new Ts7Wire(new NativeTs7Process(executable, cwd, collectTiming, 0, librariesFromDisk)); } diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index b729b2557..b3de05846 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -21,6 +21,7 @@ import { checkerExecutableIdentity, setLibDiagnosticsStore, } from "../frontend/ts7/lib-diagnostics.js"; +import { setCheckerFeatureStore } from "../frontend/ts7/checker-features.js"; import { loadNativeToolchain } from "./toolchain.js"; import { buildSanitizedRuntime } from "./sanitizer.js"; import { runCompilerTask } from "./task.js"; @@ -67,6 +68,11 @@ async function main(): Promise { }, checkerExecutableIdentity(toolchain.ts7Executable), ); + setCheckerFeatureStore({ + read: (key) => + store.read("checker-features", key, NATIVE_STRING_BYTES)?.toString("utf8") ?? null, + write: (key, text) => store.write("checker-features", key, text), + }); } compiler = new NativeCompiler(toolchain, cache); return compiler; From d8f287486087df2931f33db768b615c42fe8535f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 06:42:25 -0500 Subject: [PATCH 208/273] Keep replace callback parameters checked-dynamic where parseInt or parseFloat expects another type parseFloat and the text of parseInt take a string, and the radix of parseInt takes a number. An unannotated offset passed as their text, or a capture passed as the radix, is no longer bound to its value's type; it stays checked-dynamic, so the call compiles as it did before the binding. --- .../lowering/containers/replacer-params.ts | 26 +++++++++++++------ .../compiler/test/replacer-params.test.ts | 7 ++++- .../8345-replace-callback-untyped-params.ts | 5 ++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/containers/replacer-params.ts b/packages/compiler/src/frontend/lowering/containers/replacer-params.ts index e84ca1461..3a3eff1cf 100644 --- a/packages/compiler/src/frontend/lowering/containers/replacer-params.ts +++ b/packages/compiler/src/frontend/lowering/containers/replacer-params.ts @@ -115,7 +115,10 @@ const STRING_MEMBERS: ReadonlySet = new Set([ /** Whether every read of `symbol` in `body` is one that means the same * for an `any` value and for a `kind` value, and lowers for both: a - * template span, a Number/String/parseInt/parseFloat argument, a listed + * template span, the argument of Number or String, the text argument of + * parseInt or parseFloat for a string and the radix of parseInt for a + * number (each a parameter of that type, which an `any` value converts + * to), a listed * string member, `+`, strict equality, arithmetic and comparisons of * numbers, a returned value, and a string key of an element access on a * string-indexed record or an `any` value. Anything else keeps the @@ -144,13 +147,20 @@ function readsFit( return shape?.indexValue !== undefined && !shape.tuple; } if (ts.isArrowFunction(parent)) return parent.body === node; - if (ts.isCallExpression(parent)) - return ( - parent.expression !== node && - ["Number", "String", "parseInt", "parseFloat"].some((global) => - lowerer.isStdlibGlobal(parent.expression, global), - ) - ); + if (ts.isCallExpression(parent)) { + if (parent.expression === node) return false; + const position = parent.arguments.nodes.indexOf(node as ts.Expression); + if ( + lowerer.isStdlibGlobal(parent.expression, "Number") || + lowerer.isStdlibGlobal(parent.expression, "String") + ) + return position === 0; + if (lowerer.isStdlibGlobal(parent.expression, "parseFloat")) + return position === 0 && kind === "string"; + if (lowerer.isStdlibGlobal(parent.expression, "parseInt")) + return position === 0 ? kind === "string" : position === 1 && kind === "f64"; + return false; + } if (ts.isPropertyAccessExpression(parent)) { const member = parent.name.text; if (kind !== "string" || parent.expression !== node || !STRING_MEMBERS.has(member)) diff --git a/packages/compiler/test/replacer-params.test.ts b/packages/compiler/test/replacer-params.test.ts index bb5549a86..a5d22bb82 100644 --- a/packages/compiler/test/replacer-params.test.ts +++ b/packages/compiler/test/replacer-params.test.ts @@ -69,6 +69,8 @@ console.log(text.replace(/(a)|(b)/g, (m, a, b) => \`\${m}\${a}\${b}\`)); console.log(text.replace(/(a)(b)?/g, (m: string, a: string, b: string | undefined, at: number) => m + a + (b ?? "") + at)); const half = (n: number): number => n / 2; console.log(text.replace(/b/g, (m, at) => String(half(at)))); +console.log(text.replace(/b/g, (m, at) => String(parseFloat(at) + parseInt(at)))); +console.log(text.replace(/(\\d)/g, (m, d) => String(parseInt("11", d)))); console.log(text.replace(/(a)(b)(c)/g, (m, a, b, c, at, all, extra) => m + a + b + c + at + all + extra)); `); expect(replacerCalls(llvm)).toEqual({ @@ -76,7 +78,10 @@ console.log(text.replace(/(a)(b)(c)/g, (m, a, b, c, at, all, extra) => m + a + b // two captures a match can leave unset (bits 1 and 2); // \`string | undefined\` at capture 2 (bit 8 + 2); an offset passed to a // number parameter keeps its checked-dynamic box (bit 1). - direct: ["2:0", "2:-2147483648", "3:6", "4:1024", "2:2"], + // parseFloat and parseInt take text, and parseInt's radix is a + // number: the offset passed as text and the capture passed as the + // radix stay checked-dynamic (bit 1), so both calls compile. + direct: ["2:0", "2:-2147483648", "3:6", "4:1024", "2:2", "2:2", "2:2"], // Seven parameters exceed the direct calls' limit. boxed: 1, }); diff --git a/tests/corpus/8345-replace-callback-untyped-params.ts b/tests/corpus/8345-replace-callback-untyped-params.ts index 31d3a038b..18ddb3389 100644 --- a/tests/corpus/8345-replace-callback-untyped-params.ts +++ b/tests/corpus/8345-replace-callback-untyped-params.ts @@ -51,6 +51,11 @@ console.log("ab".replace(/(?a)|(?b)/g, (m, a, b, at, all, groups) => `${m} console.log("ab".replace(/b/, (m, at, all, extra, more) => `${m}:${at}:${all}:${extra}:${more}`)); console.log("ab".replace(/(a)(b)/, (m, a, b, at, all, extra, more) => `${m}${a}${b}${at}${all}${extra}${more}`)); +// An offset passed to parseFloat converts with ToString, and captures +// passed to parseInt and parseFloat are their text. +console.log("a1b22".replace(/\d+/g, (m, at) => `<${parseFloat(at) + parseInt(m, at + 10)}>`)); +console.log("1.5 2e1".replace(/(\S+)/g, (_m, d) => String(parseFloat(d) + parseInt(d) + Number(d)))); + // Results that are not strings convert with ToString. const counts: Record = { a: 1, b: true, c: null, d: undefined, e: { toString: () => "E" } }; console.log("a b c d e".replace(/\w/g, (m) => counts[m])); From c2e6c962e84988d9e178b82eb80b9841f4cdbec9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 06:49:46 -0500 Subject: [PATCH 209/273] Build the sanitized three.js nodes fixture in its own CI job On the san 1/20 shard the sanitized nodes build shares the runner with the corpus's other two workers and the side queue. It took 944 s and 860 s on #2194's runs and passed 1200 s on main's 7fa88083, failing the test timeout. The Linux san corpus now skips that one case (SCRIPTC_THREE_SANITIZED_NODES_JOB), and a three_nodes_san job runs it alone with the corpus's LeakSanitizer settings. The required test check waits for the new job. Every other three.js case, the plain nodes build and the macOS suites are unchanged. --- .github/workflows/ci.yml | 33 ++++++++++++++++++++++++++ scripts/ci-test-plan.mjs | 10 ++++++++ tests/harness/ci-test-plan.test.ts | 37 ++++++++++++++++++++++++++++++ tests/harness/three.test.ts | 9 ++++++++ 4 files changed, 89 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e949339fc..2b690b100 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -346,6 +346,37 @@ jobs: - run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz" - run: pnpm test tests/harness/effect.test.ts tests/harness/effect4.test.ts + # The sanitized three.js nodes build alone: beside the san corpus's other + # workers it took 860 to more than 1200 seconds of its shard + # (SCRIPTC_THREE_SANITIZED_NODES_JOB in scripts/ci-test-plan.mjs). It keeps + # the corpus's LeakSanitizer settings. + three_nodes_san: + needs: [validation_scope, lanes, native_linux] + if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }} + name: test (Linux, san, three.js nodes) + runs-on: ubuntu-24.04 + timeout-minutes: 40 + env: + SCRIPTC_SAN: "1" + ASAN_OPTIONS: detect_leaks=1 + LSAN_OPTIONS: suppressions=${{ github.workspace }}/tests/harness/lsan.supp:print_suppressions=0 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 11.1.3 + - uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: pnpm + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@v4 + with: + name: native-linux-x64-gnu + path: ${{ runner.temp }} + - run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz" + - run: pnpm test tests/harness/three.test.ts -t "'nodes' runs statically through LLVM on 'native'" + # Build each instrumented seed once. Downstream contracts retain the same # compiler and source artifacts without repeating seed construction. bootstrap_seed: @@ -950,6 +981,7 @@ jobs: tests_macos, native_linux, effects, + three_nodes_san, bootstrap_seed, bootstrap, linux_host_clang, @@ -982,6 +1014,7 @@ jobs: lane tests_macos "${{ needs.tests_macos.result }}" "${{ needs.lanes.outputs.macos }}" lane native_linux "${{ needs.native_linux.result }}" "${{ needs.lanes.outputs.linux }}" lane effects "${{ needs.effects.result }}" "${{ needs.lanes.outputs.effects }}" + lane three_nodes_san "${{ needs.three_nodes_san.result }}" "${{ needs.lanes.outputs.linux }}" lane bootstrap_seed "${{ needs.bootstrap_seed.result }}" "${{ needs.lanes.outputs.bootstrap }}" lane bootstrap "${{ needs.bootstrap.result }}" "${{ needs.lanes.outputs.bootstrap }}" lane linux_host_clang "${{ needs.linux_host_clang.result }}" "${{ needs.lanes.outputs.linux }}" diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs index 184f04d1c..88a77e32e 100644 --- a/scripts/ci-test-plan.mjs +++ b/scripts/ci-test-plan.mjs @@ -134,6 +134,15 @@ export const leakSuppressions = fileURLToPath( new URL("../tests/harness/lsan.supp", import.meta.url), ); +// The sanitized three.js nodes build runs in its own Linux job +// (three_nodes_san in .github/workflows/ci.yml). Beside the corpus's other +// workers it took 860 to more than 1200 seconds, past its test timeout. +export const sanitizedNodesJobEnv = { SCRIPTC_THREE_SANITIZED_NODES_JOB: "1" }; + +function corpusThreeEnv(flavor, platform) { + return flavor === "san" && platform === "linux" ? sanitizedNodesJobEnv : {}; +} + function corpusLeakEnv(flavor, platform) { if (flavor !== "san" || platform !== "linux") return {}; return { @@ -246,6 +255,7 @@ export function ciTestPlan({ env: { ...env, ...corpusLeakEnv(flavor, platform), + ...corpusThreeEnv(flavor, platform), SCRIPTC_TEST_WORKERS: String(workers - sideWorkers), SCRIPTC_LLVM_TEST_MODE: "dev", }, diff --git a/tests/harness/ci-test-plan.test.ts b/tests/harness/ci-test-plan.test.ts index 29268c841..e67e5582b 100644 --- a/tests/harness/ci-test-plan.test.ts +++ b/tests/harness/ci-test-plan.test.ts @@ -12,6 +12,7 @@ import { isolatedFiles, leakSuppressions, memoryHeavyFiles, + sanitizedNodesJobEnv, sideOwners, testCosts, } from "../../scripts/ci-test-plan.mjs"; @@ -90,6 +91,42 @@ test("LeakSanitizer runs over the sanitized corpus on Linux only", () => { for (const task of plan("san", "linux").side) expect(task.env.ASAN_OPTIONS).toBeUndefined(); }); +test("the sanitized three.js nodes build leaves the Linux corpus for its own job", () => { + const plan = (flavor: "plain" | "san", platform: string) => + ciTestPlan({ shard: "1/20", flavor, workers: 4, platform }); + const [name, value] = Object.entries(sanitizedNodesJobEnv)[0]!; + expect(plan("san", "linux").corpus.env[name]).toBe(value); + for (const env of [plan("plain", "linux").corpus.env, plan("san", "darwin").corpus.env]) + expect(env[name]).toBeUndefined(); + const three = readFileSync(new URL("./three.test.ts", import.meta.url), "utf8"); + expect(three).toContain(`process.env["${name}"] === "${value}"`); + const { jobs } = load( + readFileSync(new URL("../../.github/workflows/ci.yml", import.meta.url), "utf8"), + ) as { + jobs: Record< + string, + { needs: string[]; env: Record; steps: { run?: string }[] } + >; + }; + const job = jobs["three_nodes_san"]!; + expect(job.needs).toContain("native_linux"); + expect(jobs["test"]!.needs).toContain("three_nodes_san"); + expect(job.env[name]).toBeUndefined(); + expect(job.env["SCRIPTC_SAN"]).toBe("1"); + // The same LeakSanitizer settings as the corpus it left. + const corpus = plan("san", "linux").corpus.env; + expect(job.env["ASAN_OPTIONS"]).toBe(corpus.ASAN_OPTIONS); + expect(job.env["LSAN_OPTIONS"]).toBe( + corpus.LSAN_OPTIONS!.replace( + leakSuppressions, + "${{ github.workspace }}/tests/harness/lsan.supp", + ), + ); + expect(job.steps.at(-1)!.run).toBe( + `pnpm test tests/harness/three.test.ts -t "'nodes' runs statically through LLVM on 'native'"`, + ); +}); + test("the side queue balances measured files across the twenty Linux shards", () => { for (const flavor of ["plain", "san"] as const) { const costs = testCosts[flavor] as Record; diff --git a/tests/harness/three.test.ts b/tests/harness/three.test.ts index 27fb86d1f..e55242149 100644 --- a/tests/harness/three.test.ts +++ b/tests/harness/three.test.ts @@ -68,6 +68,15 @@ test.for(cases)( async ({ fixture, target }, context) => { const entry = join(import.meta.dirname, `../fixtures/three/${fixture}.mjs`); if (target === "wasm32-wasi" && !hasZig) context.skip(); + // CI's Linux sanitized lane builds this one in its own job + // (scripts/ci-test-plan.mjs). + if ( + fixture === "nodes" && + target === "native" && + process.env["SCRIPTC_SAN"] === "1" && + process.env["SCRIPTC_THREE_SANITIZED_NODES_JOB"] === "1" + ) + context.skip(); const dir = await mkdtemp("/tmp/scriptc-three-"); const previousTarget = process.env["SCRIPTC_TARGET"]; const previousCc = process.env["SCRIPTC_CC"]; From c80945ec21ddbd208f7191b2547ad217d6b76987 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 06:50:19 -0500 Subject: [PATCH 210/273] Leave timing and cache placement variables out of the frontend cache key The native frontend cache key included every SCRIPTC_* variable, so turning SCRIPTC_TIMING on or off, or changing SCRIPTC_CACHE_DIR, SCRIPTC_CACHE_MAX_MB, SCRIPTC_EMIT_WORKERS or SCRIPTC_EMIT_WORKER_FUNCTIONS, reran the frontend on an unchanged program. The frontend's output does not depend on these variables, so the key now leaves them out. Every other SCRIPTC_* variable still keys the entry. --- packages/compiler/src/native/prepare.test.ts | 27 +++++++++++++++++++- packages/compiler/src/native/prepare.ts | 26 ++++++++++++++----- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/native/prepare.test.ts b/packages/compiler/src/native/prepare.test.ts index 5d0f9faae..07698820e 100644 --- a/packages/compiler/src/native/prepare.test.ts +++ b/packages/compiler/src/native/prepare.test.ts @@ -1,7 +1,7 @@ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { afterEach, expect, test } from "vitest"; +import { afterEach, expect, test, vi } from "vitest"; import { LINUX_ARM64_GNU_TARGET, nativeCodegenTarget, @@ -23,6 +23,7 @@ function text(input: ReturnType["prepare"]>) { const scratch: string[] = []; afterEach(() => { + vi.unstubAllEnvs(); for (const path of scratch.splice(0)) rmSync(path, { recursive: true, force: true }); }); @@ -197,6 +198,30 @@ test("structurally invalid cached features cause a rebuild even with a valid dig expect(f.loads()).toBe(2); }); +test("timing, cache placement and emission worker variables keep the frontend output", () => { + for (const name of [ + "SCRIPTC_CACHE_DIR", + "SCRIPTC_CACHE_MAX_MB", + "SCRIPTC_EMIT_WORKERS", + "SCRIPTC_EMIT_WORKER_FUNCTIONS", + "SCRIPTC_TIMING", + "SCRIPTC_TEST_FRONTEND_KEY", + ]) + vi.stubEnv(name, undefined); + const f = fixture(); + expect(f.prepare().ok).toBe(true); + vi.stubEnv("SCRIPTC_CACHE_DIR", f.cache.root); + vi.stubEnv("SCRIPTC_CACHE_MAX_MB", "8192"); + vi.stubEnv("SCRIPTC_EMIT_WORKERS", "1"); + vi.stubEnv("SCRIPTC_EMIT_WORKER_FUNCTIONS", "100"); + vi.stubEnv("SCRIPTC_TIMING", "0"); + expect(f.prepare().ok).toBe(true); + expect(f.loads()).toBe(1); + vi.stubEnv("SCRIPTC_TEST_FRONTEND_KEY", "1"); + expect(f.prepare().ok).toBe(true); + expect(f.loads()).toBe(2); +}); + test("serialized IR travels as bounded parts through the frontend cache", () => { const f = fixture(); const first = f.prepare({ emitIr: true }); diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index a10d16585..11b8f9dd1 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -211,6 +211,24 @@ function implementationIdentity(toolchain: NativeToolchain): string { ); } +/** SCRIPTC_* variables that report on a build, place its cache or set + * backend emission parallelism. The frontend's output does not depend on + * them, so they are left out of its cache key. */ +const FRONTEND_INDEPENDENT_VARIABLES = new Set([ + "SCRIPTC_CACHE_DIR", + "SCRIPTC_CACHE_MAX_MB", + "SCRIPTC_EMIT_WORKERS", + "SCRIPTC_EMIT_WORKER_FUNCTIONS", + "SCRIPTC_TIMING", +]); + +/** The SCRIPTC_* variables a frontend cache entry depends on, sorted by name. */ +function frontendEnvironment(): [string, string | undefined][] { + return Object.entries(process.env) + .filter(([name]) => name.startsWith("SCRIPTC_") && !FRONTEND_INDEPENDENT_VARIABLES.has(name)) + .sort(([a], [b]) => a.localeCompare(b)); +} + function validFeatures(value: unknown): value is NativeLinkFeatures { if (value === null || typeof value !== "object" || Array.isArray(value)) return false; const features = value as Record; @@ -363,9 +381,7 @@ export function prepareNativeExecutable( implementation: implementationIdentity(toolchain), target: toolchain.target.name, declarations: toolchain.declarationsRoot ?? null, - environment: Object.entries(process.env) - .filter(([name]) => name.startsWith("SCRIPTC_")) - .sort(([a], [b]) => a.localeCompare(b)), + environment: frontendEnvironment(), }), ); const cached = readFrontendCache(cache, key, exclusions); @@ -493,9 +509,7 @@ export function prepareNativeLibrary( implementation: implementationIdentity(toolchain), target: toolchain.target.name, declarations: toolchain.declarationsRoot ?? null, - environment: Object.entries(process.env) - .filter(([name]) => name.startsWith("SCRIPTC_")) - .sort(([a], [b]) => a.localeCompare(b)), + environment: frontendEnvironment(), }), ); const cached = readFrontendCache(cache, key, exclusions); From 5247d2dfdd6bc2d0274151aec0d3059a5191962b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 07:28:48 -0500 Subject: [PATCH 211/273] List bench/regex-replacers among the dynamic-tier workloads Its replace callbacks receive checked-dynamic values where the lib types their parameters any: captures a match can leave unset, and results the checker types any. --- scripts/zero-cost-compare.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index 7ae33d64a..df407bbb7 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -398,7 +398,10 @@ export function dynamicTierSymbols(names) { * later too, decodes JSON.parse trees. bench/throw-chains, also added * later, tests caught values with `instanceof`, as bench/validate-errors * does. bench/dynamic-methods and - * bench/dynamic-properties are untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and + * bench/dynamic-properties are untyped JavaScript. bench/regex-replacers, + * also added later, passes checked-dynamic values to replace callbacks + * whose parameters the lib types `any`: captures a match can leave unset + * and results the checker types `any`. 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. */ @@ -421,6 +424,7 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/line-reader", "bench/json-unions", "bench/throw-chains", + "bench/regex-replacers", ]); /** Names of the functions (text and weak symbols) in `nm` output. Mach-O From 63a99cc4d94be47f7282a6df4926e8041ac1ced1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 07:37:50 -0500 Subject: [PATCH 212/273] Convert a loop's string receiver to UTF-16 at once only when the loop can read enough of it A versioned loop over a non-ASCII string passes the range of indexes it reads: a counted loop's start and limit, or a while loop's counter and bound. The runtime converts the string at once when that range has at least one unit per 16 bytes of it, and otherwise counts the loop as one indexed read, so a loop over a line of a long source text no longer keeps a UTF-16 copy of all of it. The publish call is weighted as unlikely, since loops re-entered over one string skip it. --- .../src/backend/llvm/bytes-loop-versions.ts | 48 +++++++-- .../compiler/src/backend/llvm/string-reads.ts | 8 +- .../test/string-unit-loop-emission.test.ts | 101 ++++++++++++++++-- packages/runtime/src/scr_string.c | 22 ++-- packages/runtime/src/scr_string_methods.h | 6 +- packages/runtime/test/test_string.c | 28 ++++- 6 files changed, 182 insertions(+), 31 deletions(-) diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index de704ad42..ab941e658 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -1146,7 +1146,7 @@ export function emitBytesLoopVersionGuard( }; const lengths = receivers.map((receiver, index) => { if (index === unitsIndex) { - const read = emitUnitsLength(host, receiver); + const read = emitUnitsLength(host, receiver, () => emitUnitsSpan(host, version, index)); units.read = read; return read.length; } @@ -1359,15 +1359,49 @@ export function emitBytesLoopVersionGuard( return { units: taken.pointer, length }; } +/** The range of indexes a version's loop reads from its units receiver, as + * two doubles: a counted loop's start and limit, or a while loop's one + * counter and the receiver's bound. Both NaN when the version gives no such + * range. The runtime converts the receiver at once only when the range is + * long enough to pay for it (scr_str_scan_units_for). */ +function emitUnitsSpan( + host: LlvmEmitterContext, + version: BytesLoopVersion, + receiver: number, +): { from: string; to: string } { + // Locals and literals only: they read no string (a `length` of the + // receiver would measure it). Anything else leaves the range unbounded. + const number = (expr: IrExpr): string | null => { + if (expr.kind !== "varRef" && expr.kind !== "numLit") return null; + const value = host.emitExpr(expr); + return value.type.kind === "f64" ? value.name : null; + }; + const unbounded = { from: "0x7FF8000000000000", to: "0x7FF8000000000000" }; + const counted = version.counted; + let from: IrExpr, to: IrExpr; + if (counted) [from, to] = [counted.start, counted.limit]; + else { + const bounded = version.bounded.filter((check) => check.receiver === receiver); + if (version.counters.length !== 1 || bounded.length !== 1 || version.paired !== null) + return unbounded; + [from, to] = [version.counters[0]!.ref, bounded[0]!.bound]; + } + const start = number(from), + limit = number(to); + return start !== null && limit !== null ? { from: start, to: limit } : unbounded; +} + /** The length of a version's units receiver `s` in UTF-16 units: its byte * length when it is ASCII; otherwise, once it is the scan receiver, the * published unit count (0 when the runtime does not hold its units). When * `s` is not already the scan receiver, the runtime converts it to units - * and publishes it (scr_str_scan_units). Also the units pointer, whether - * the loop is to read it, and whether the published receiver is `s`. */ + * and publishes it if the loop's `span` makes that pay + * (scr_str_scan_units_for). Also the units pointer, whether the loop is to + * read it, and whether the published receiver is `s`. */ function emitUnitsLength( host: LlvmEmitterContext, s: string, + span: () => { from: string; to: string }, ): { length: string; pointer: string; taken: string; published: string } { const B = host.B; const sz = host.sizeType; @@ -1381,14 +1415,15 @@ function emitUnitsLength( B.startBlock(publish); // When `s` is already the scan receiver (an earlier loop over it published // it), its units stay published until the runtime clears the scan: skip - // the call. + // the call. Loops re-entered over one string take that path; the weights + // keep the call and its reach out of the loop's code. const held = B.tmp(), holds = B.tmp(); B.line(`${held} = load ptr, ptr @scr_str_scan`); B.line(`${holds} = icmp eq ptr ${held}, ${s}`); const call = B.newLabel("bytes.version.publish.units"), load = B.newLabel("bytes.version.publish.load"); - B.condBr(holds, load, call); + B.condBr(holds, load, call, `, !prof !{!"branch_weights", i32 1000, i32 1}`); B.startBlock(call); host.declare(STRING_SCAN_UNITS_DECL); const receiver = B.tmp(), @@ -1398,7 +1433,8 @@ function emitUnitsLength( countSlot = B.tmp(), count = B.tmp(), published = B.tmp(); - B.line(`call void @scr_str_scan_units(ptr ${s})`); + const { from: first, to: bound } = span(); + B.line(`call void @scr_str_scan_units_for(ptr ${s}, double ${first}, double ${bound})`); B.br(load); B.startBlock(load); B.line(`${receiver} = load ptr, ptr @scr_str_scan`); diff --git a/packages/compiler/src/backend/llvm/string-reads.ts b/packages/compiler/src/backend/llvm/string-reads.ts index 9fd2fa78c..5b7a26c6c 100644 --- a/packages/compiler/src/backend/llvm/string-reads.ts +++ b/packages/compiler/src/backend/llvm/string-reads.ts @@ -25,9 +25,11 @@ import { literalCapWord, literalCapWord32 } from "./string-key-hash.js"; export const STRING_UTF16_LEN_DECL = "declare double @scr_str_utf16_len(ptr)"; export const STRING_CHAR_CODE_AT_DECL = "declare double @scr_str_char_code_at(ptr, double)"; /** Converts a string to UTF-16 units and publishes it as the scan receiver - * (bytes-loop-versions.ts). Like the fallbacks below, it can evict an - * index entry, so it carries no memory restriction. */ -export const STRING_SCAN_UNITS_DECL = "declare void @scr_str_scan_units(ptr)"; + * when the loop about to read it can read enough of it (the other two + * operands are the range of indexes it reads; bytes-loop-versions.ts). + * Like the fallbacks below, it can evict an index entry, so it carries no + * memory restriction. */ +export const STRING_SCAN_UNITS_DECL = "declare void @scr_str_scan_units_for(ptr, double, double)"; /** The runtime fallbacks are cold: both common cases read inline. They * carry no * memory restriction: a fallback can evict an index entry, which resets diff --git a/packages/compiler/test/string-unit-loop-emission.test.ts b/packages/compiler/test/string-unit-loop-emission.test.ts index fa11af007..f522159dc 100644 --- a/packages/compiler/test/string-unit-loop-emission.test.ts +++ b/packages/compiler/test/string-unit-loop-emission.test.ts @@ -51,8 +51,11 @@ console.log(total, spaces("plain ascii text"), spaces("")); const spaces = body("spaces"); // A string that is not ASCII is converted to units and published as the - // scan receiver once, before the loop, and its copy loads units. - expect(spaces).toMatch(/call void @scr_str_scan_units\(ptr %\S+\)/); + // scan receiver once, before the loop, and its copy loads units. The + // loop runs to the length, so it gives the runtime no range (NaN). + expect(spaces).toMatch( + /call void @scr_str_scan_units_for\(ptr %\S+, double 0x7FF8000000000000, double 0x7FF8000000000000\)/, + ); // The copies follow the checked one: bytes, then units. const bytesAt = spaces.search(/^bytes\.version\.fast\d+:/m); const unitsAt = spaces.search(/^bytes\.version\.fast\.units\d+:/m); @@ -127,19 +130,27 @@ console.log(words(text), words(text.replace(/a/g, "ä")), words("plain ascii")); // the runtime to publish `s` only when they differ. const guards = [ ...words.matchAll( - /^bytes\.version\.publish\d+:[^\n]*\n\s+(%\S+) = load ptr, ptr @scr_str_scan\n\s+(%\S+) = icmp eq ptr \1, (%\S+)\n\s+br i1 \2, label %(bytes\.version\.publish\.load\d+), label %(bytes\.version\.publish\.units\d+)\n/gm, + /^bytes\.version\.publish\d+:[^\n]*\n\s+(%\S+) = load ptr, ptr @scr_str_scan\n\s+(%\S+) = icmp eq ptr \1, (%\S+)\n\s+br i1 \2, label %(bytes\.version\.publish\.load\d+), label %(bytes\.version\.publish\.units\d+)[^\n]*\n/gm, ), ]; expect(guards.length).toBeGreaterThanOrEqual(2); for (const [, , , s, load, convert] of guards) { - const block = words.slice(words.search(new RegExp(`^${convert}:`, "m"))); - expect(block).toMatch( - new RegExp( - `^${convert}:[^\\n]*\\n\\s+call void @scr_str_scan_units\\(ptr ${s}\\)\\n\\s+br label %${load}\\n`, - ), + // The block reads the loop's range and makes no other call. + const block = words + .slice(words.search(new RegExp(`^${convert}:`, "m"))) + .match(new RegExp(`^${convert}:[^\\n]*\\n([\\s\\S]*?)\\n\\s+br label %${load}\\n`))!; + expect(block).not.toBeNull(); + const lines = block[1]!.split("\n").map((line) => line.trim()); + expect(lines.at(-1)).toMatch( + new RegExp(`^call void @scr_str_scan_units_for\\(ptr ${s}, double \\S+, double \\S+\\)$`), + ); + expect(lines.slice(0, -1).filter((line) => /\bcall\b(?! double @llvm\.)/.test(line))).toEqual( + [], ); } - const calls = words.match(/call void @scr_str_scan_units\(ptr %\S+\)/g)!; + const calls = words.match( + /call void @scr_str_scan_units_for\(ptr %\S+, double \S+, double \S+\)/g, + )!; expect(calls.length).toBe(guards.length); const exe = join(dir, "main"); @@ -150,3 +161,75 @@ console.log(words(text), words(text.replace(/a/g, "ä")), words("plain ascii")); await rm(dir, { recursive: true, force: true }); } }); + +test("loops over part of a string give the runtime the range of indexes they read", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-unit-reach-")); + try { + const entry = join(dir, "main.ts"); + await writeFile( + entry, + ` +function trim(text: string, start: number, end: number): string { + while (start < end && text.charCodeAt(start) === 32) start++; + return text.substring(start, end); +} +function hasBreak(text: string, pos: number, end: number): boolean { + for (let i = pos; i < end; i++) if (text.charCodeAt(i) === 10) return true; + return false; +} +let text = ""; +for (let k = 0; k < 2000; k++) text += k % 7 === 0 ? " café\\n" : " naïve façade"; +let kept = 0, + breaks = 0; +for (let at = 0; at + 12 < text.length; at += 12) { + kept += trim(text, at, at + 12).length; + if (hasBreak(text, at, at + 12)) breaks++; +} +console.log(kept, breaks); +`, + ); + const outPath = join(dir, "main.ir.json"); + const lowered = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!lowered.ok) + throw new Error(lowered.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const body = (name: string): string => + ll.match( + new RegExp( + `define internal [^\\n]+ @sc_bf_${name}(?:\\.i32)?\\([^\\n]*\\) #0 \\{[\\s\\S]*?\\n\\}`, + ), + )![0]; + // The publish block passes the loop's range with the receiver: the + // counter and the bound (a while loop), or the start and the limit (a + // counted loop), here the function's parameters. + for (const name of ["trim", "hasBreak"]) { + const block = body(name).match( + /^bytes\.version\.publish\.units\d+:[^\n]*\n([\s\S]*?)\n\s+br label %bytes\.version\.publish\.load\d+\n/m, + )!; + expect(block, name).not.toBeNull(); + const [, lines] = block; + const call = lines!.match( + /call void @scr_str_scan_units_for\(ptr %\S+, double (%\S+), double (%\S+)\)/, + )!; + expect(call, name).not.toBeNull(); + expect(call[1]).not.toBe(call[2]); + } + + 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)); + let text = ""; + for (let k = 0; k < 2000; k++) text += k % 7 === 0 ? " café\n" : " naïve façade"; + let kept = 0, + breaks = 0; + for (let at = 0; at + 12 < text.length; at += 12) { + let start = at; + while (start < at + 12 && text.charCodeAt(start) === 32) start++; + kept += at + 12 - start; + if (text.slice(at, at + 12).includes("\n")) breaks++; + } + expect(execFileSync(exe, { encoding: "utf8" })).toBe(`${kept} ${breaks}\n`); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index dfd0d23f8..1e7802fbb 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -1889,14 +1889,24 @@ static bool scr_sidx_units(const ScrStr *s, ScrSidx *e) { return true; } -/* A loop about to read every unit of s (bytes-loop-versions.ts) converts it - * at once instead of after the threshold's reads, and publishes it as the - * scan's receiver. ASCII receivers and receivers past SCR_SIDX_UNITS_MAX - * publish nothing; the caller compares scr_str_scan.s with s. */ -void scr_str_scan_units(ScrStr *s) { +/* A loop about to read s (bytes-loop-versions.ts) between the indexes + * `from` and `to` (NaN: no bound) converts it at once instead of after the + * threshold's reads, and publishes it as the scan's receiver, when that + * range has at least one unit per 16 bytes of s (the threshold's ratio). + * A shorter loop, such as one over a line of a source text, counts as one + * read, so a short loop does not keep all of a long string's units. ASCII + * receivers and receivers past SCR_SIDX_UNITS_MAX publish nothing; the + * caller compares scr_str_scan.s with s. */ +void scr_str_scan_units_for(ScrStr *s, double from, double to) { if (s->ascii || s->len > SCR_SIDX_UNITS_MAX) return; ScrSidx *e = scr_sidx(s); - if (!e->units) e->units = scr_str_units(s, &e->nunits); + if (!e->units) { + if (fabs(to - from) * 16 < (double)s->len) { + (void)scr_sidx_units(s, e); + return; + } + e->units = scr_str_units(s, &e->nunits); + } if (scr_str_scan.s != s) { scr_str_scan.s = s; scr_str_scan.units = e->units; diff --git a/packages/runtime/src/scr_string_methods.h b/packages/runtime/src/scr_string_methods.h index 820b1c9e6..01ccdd8ae 100644 --- a/packages/runtime/src/scr_string_methods.h +++ b/packages/runtime/src/scr_string_methods.h @@ -43,9 +43,11 @@ extern SCR_TL ScrStrScan scr_str_scan; double scr_str_char_code_at(ScrStr *s, double i); /* Converts a non-ASCII s up to the units cap to UTF-16 at once and - * publishes it in scr_str_scan, for a loop about to read all of it. Leaves + * publishes it in scr_str_scan, for a loop about to read it between the + * indexes `from` and `to` (NaN: no bound), when that range has at least + * one unit per 16 bytes. A shorter loop counts as one indexed read. Leaves * scr_str_scan unchanged for any other s. */ -void scr_str_scan_units(ScrStr *s); +void scr_str_scan_units_for(ScrStr *s, double from, double to); /* indexOf(needle, fromIndex): UTF-16 index of the first occurrence at or * after fromIndex (clamped to [0, length]), or -1. Empty needle returns the diff --git a/packages/runtime/test/test_string.c b/packages/runtime/test/test_string.c index 3e14a5958..f41c2de50 100644 --- a/packages/runtime/test/test_string.c +++ b/packages/runtime/test/test_string.c @@ -523,27 +523,45 @@ static void short_string_asserts(void) { } /* A loop's publish converts a non-ASCII receiver before any read, while a - * lone read still walks bytes. ASCII receivers publish nothing. */ + * lone read still walks bytes. ASCII receivers publish nothing. A loop + * that can read fewer than one unit per 16 bytes counts as one read. */ static void scan_units_asserts(void) { ScrStr *s = scr_str_new("a\xC3\xA9\xF0\x9F\x98\x80" "b", 8); /* a é 😀 b */ static const double codes[] = {97, 233, 0xD83D, 0xDE00, 98}; if (scr_str_char_code_at(s, 0) != 97 || scr_str_scan.s == s) failed++; - scr_str_scan_units(s); + scr_str_scan_units_for(s, NAN, NAN); if (scr_str_scan.s != s || scr_str_scan.n != 5) failed++; for (size_t i = 0; i < 5; i++) if (scr_str_scan.n == 5 && (scr_str_scan.units[i] != codes[i] || scr_str_char_code_at(s, (double)i) != codes[i])) failed++; if (!isnan(scr_str_char_code_at(s, 5))) failed++; - scr_str_scan_units(s); /* republishing keeps the same units */ + scr_str_scan_units_for(s, NAN, NAN); /* republishing keeps the same units */ const uint16_t *units = scr_str_scan.units; - scr_str_scan_units(s); + scr_str_scan_units_for(s, 0, 1); if (scr_str_scan.s != s || scr_str_scan.units != units) failed++; ScrStr *ascii = scr_str_new("plain", 5); - scr_str_scan_units(ascii); + scr_str_scan_units_for(ascii, NAN, NAN); if (scr_str_scan.s != s) failed++; scr_str_release(ascii); scr_str_release(s); if (scr_str_scan.s != NULL) failed++; + /* 200 units in 400 bytes: a loop over 25 indexes converts at once. */ + char text[400]; + for (size_t i = 0; i < sizeof text; i += 2) memcpy(text + i, "\xC3\xA9", 2); + ScrStr *wide = scr_str_new(text, sizeof text); + scr_str_scan_units_for(wide, 175, 200); + if (scr_str_scan.s != wide || scr_str_scan.n != 200 || scr_str_scan.units[199] != 233) failed++; + scr_str_release(wide); + /* A loop over 24 does not; each such loop counts as one read, so the + * 32nd converts (32 reads, one per 12.5 bytes). */ + ScrStr *line = scr_str_new(text, sizeof text); + scr_str_scan_units_for(line, 24, 0); + if (scr_str_scan.s == line) failed++; + for (int i = 1; i < 31; i++) scr_str_scan_units_for(line, i, i + 1); + if (scr_str_scan.s == line) failed++; + scr_str_scan_units_for(line, 31, 32); + if (scr_str_scan.s != line || scr_str_scan.n != 200) failed++; + scr_str_release(line); total++; } #ifdef SCR_SIDX_TEST From 390b404251c2894582740f1373937eb9d7d659bc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 07:46:24 -0500 Subject: [PATCH 213/273] Keep a union loop binding in the debug-variables fixture The fixture's loop iterated `[6, 7] as (number | undefined)[]` so that its binding kept the union layout the LLDB check reads through `item->tag` and `item->slot.arm0`. Since (T | undefined)[] stores T elements, an array that never holds undefined gives a `number` binding, and the check failed with "item is not a pointer". The loop now iterates `[6, 7, undefined]`, whose binding needs the union, and the check also reads the undefined arm's tag. --- packages/compiler/test/debug-variables.test.ts | 3 +++ tests/corpus/3068-debug-variables/main.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/compiler/test/debug-variables.test.ts b/packages/compiler/test/debug-variables.test.ts index ea92cc1d3..946f0f5d1 100644 --- a/packages/compiler/test/debug-variables.test.ts +++ b/packages/compiler/test/debug-variables.test.ts @@ -63,6 +63,8 @@ test.runIf(process.platform === "darwin")( "continue", "frame variable item->tag item->slot.arm0", "continue", + "frame variable item->tag", + "continue", "frame variable hoisted", "continue", "frame variable moduleValue", @@ -91,6 +93,7 @@ test.runIf(process.platform === "darwin")( "item->tag = f64", "item->slot.arm0 = 6", "item->slot.arm0 = 7", + "item->tag = undefined", "hoisted = 13", ]) { expect(debug.stdout).toContain(value); diff --git a/tests/corpus/3068-debug-variables/main.ts b/tests/corpus/3068-debug-variables/main.ts index d9399b49d..2e7cef4d6 100644 --- a/tests/corpus/3068-debug-variables/main.ts +++ b/tests/corpus/3068-debug-variables/main.ts @@ -21,7 +21,7 @@ function inspect(input: number, enabled: boolean, fallback = 9): void { for (let i = 0; i < bytes.length; i++) { console.log("loop", i, bytes[i]); // debug: loop } - for (const item of [6, 7] as (number | undefined)[]) { + for (const item of [6, 7, undefined]) { console.log("item"); // debug: item console.log(item); } From 2823840134976580d129093f6f95cbf1a534f50f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 07:49:06 -0500 Subject: [PATCH 214/273] Miss non-object receivers early in the property probes and inline the entry check Since the probes began caching absent properties (#2186), an untyped property read that reaches the load or get probe costs more than before: - Receivers that are not ordinary objects, such as class instances, have no shape and always miss, but they set up the probe's frame first. The exported probes now return the miss for them before calling the probe body, which stays out of line. - Each matching entry was answered through an out-of-line call (scr_js_entry_answer). It is inlined into the probe again. dynamic-properties executed 3.1% more instructions than before #2186; it now executes 0.4% fewer. --- packages/runtime/src/scr_js_object.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/runtime/src/scr_js_object.c b/packages/runtime/src/scr_js_object.c index e97651557..e3d0b3a54 100644 --- a/packages/runtime/src/scr_js_object.c +++ b/packages/runtime/src/scr_js_object.c @@ -308,13 +308,10 @@ static bool scr_js_absent_hit(const ScrDyn *receiver, const ScrJsIcEntry *entry) walked->v.obj.shape == entry->holder_shape && walked->prototype == NULL; } -/* A load site's probe. With `absent`, a read of a key that the receiver and - * its chain of plain shaped objects (ending in the default prototype) lack - * answers undefined and caches that answer. Method reads leave absent - * methods to the slow path, which names the missing function. */ /* The answer of a matching entry for `receiver`, or NULL when the entry no * longer holds. */ -static ScrDyn *scr_js_entry_answer(ScrDyn *receiver, const ScrJsIcEntry *entry) { +static inline __attribute__((always_inline)) ScrDyn *scr_js_entry_answer(ScrDyn *receiver, + const ScrJsIcEntry *entry) { if (entry->target == &scr_js_absent) return scr_js_absent_hit(receiver, entry) ? scr_dyn_retain(scr_dyn_undefined()) : NULL; return scr_js_load_hit(receiver, entry); @@ -345,7 +342,8 @@ static void scr_js_load_fill(ScrJsIc *ic, uint32_t stale, ScrJsSharedLoad *share * its chain of plain shaped objects (ending in the default prototype) lack * answers undefined and caches that answer. Method reads leave absent * methods to the slow path, which names the missing function. */ -static ScrDyn *scr_js_probe_load(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic, bool absent) { +static __attribute__((noinline)) ScrDyn *scr_js_probe_load(ScrDyn *receiver, const ScrStr *key, + ScrJsIc *ic, bool absent) { const ScrJsShape *shape = scr_js_shape_of(receiver); if (!shape) return NULL; uint32_t stale = UINT32_MAX; @@ -397,11 +395,15 @@ static ScrDyn *scr_js_probe_load(ScrDyn *receiver, const ScrStr *key, ScrJsIc *i return scr_dyn_retain(found->value); } +/* Receivers other than ordinary objects have no shape: they miss before + * the probe sets up its frame. */ ScrDyn *scr_js_load_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic) { + if (receiver->kind != SCR_DYN_OBJ) return NULL; return scr_js_probe_load(receiver, key, ic, false); } ScrDyn *scr_js_get_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic) { + if (receiver->kind != SCR_DYN_OBJ) return NULL; return scr_js_probe_load(receiver, key, ic, true); } From b28af56ba87803d8ec352d709a9649803df3c9d1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 07:53:02 -0500 Subject: [PATCH 215/273] Skip the weak-key table when freeing objects that were never keys Once a program creates a weak collection, every object freed afterwards called into the weak-key table to remove entries keyed by it, although almost none are keys: zod's validation freed 16.6 million objects against 23 keys. A per-thread bit filter indexed by the object's address now marks every address that entered the table (cleared when the table empties), and the free paths test it inline, so an object whose bit is clear makes no call. --- .../corpus/weak-keys-freed-reused.mjs.json | 6 ++++ packages/runtime/src/scr_json.c | 19 +++++++++-- tests/corpus/weak-keys-freed-reused.mjs | 32 +++++++++++++++++++ .../weak-keys-freed-reused.mjs.json | 1 + 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/weak-keys-freed-reused.mjs.json create mode 100644 tests/corpus/weak-keys-freed-reused.mjs create mode 100644 tests/harness/corpus-costs/weak-keys-freed-reused.mjs.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/weak-keys-freed-reused.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/weak-keys-freed-reused.mjs.json new file mode 100644 index 000000000..ab4ca8bda --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/weak-keys-freed-reused.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/weak-keys-freed-reused.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 43b107ae3..cd9eeecde 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1165,8 +1165,20 @@ static bool scr_dyn_to_primitive_result_is_object(const ScrDyn *d); * are never the native records the kept-source table is keyed by. */ static SCR_TL bool scr_weak_observing; static void scr_weak_disposed(void *key); +/* One bit per key address, set when a key enters the weak-key table: a + * disposed object whose bit is clear was never a key, so its disposal makes + * no call. Bits are cleared when the table empties. */ +#define SCR_WEAK_KEY_FILTER_BITS ((size_t)1 << 16) +static SCR_TL uint64_t scr_weak_key_filter[SCR_WEAK_KEY_FILTER_BITS / 64]; +static inline size_t scr_weak_key_filter_bit(const void *key) { + return ((uintptr_t)key >> 4) & (SCR_WEAK_KEY_FILTER_BITS - 1); +} +static inline bool scr_weak_maybe_key(const void *key) { + size_t bit = scr_weak_key_filter_bit(key); + return scr_weak_observing && (scr_weak_key_filter[bit >> 6] & (UINT64_C(1) << (bit & 63))); +} static inline void scr_dyn_weak_dispose(ScrDyn *d) { - if (scr_weak_observing) scr_weak_disposed(d); + if (scr_weak_maybe_key(d)) scr_weak_disposed(d); } /* A disposed node's child: most getters, setters, prototypes and symbol @@ -3479,7 +3491,7 @@ static void scr_record_source_disposed(void *rec) { /* 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) { - if (scr_weak_observing) scr_weak_disposed(object); + if (scr_weak_maybe_key(object)) scr_weak_disposed(object); if (scr_record_source_count) scr_record_source_disposed(object); } @@ -17413,6 +17425,8 @@ static ScrWeakKeySlot *scr_weak_key_slot_put(void *key) { scr_weak_keys[j].key = key; scr_weak_keys[j].head = NULL; scr_weak_keys_used++; + size_t bit = scr_weak_key_filter_bit(key); + scr_weak_key_filter[bit >> 6] |= UINT64_C(1) << (bit & 63); } return &scr_weak_keys[j]; } @@ -17422,6 +17436,7 @@ static void scr_weak_key_slot_delete(ScrWeakKeySlot *slot) { if (--scr_weak_keys_used == 0) { free(scr_weak_keys); scr_weak_keys = NULL; + memset(scr_weak_key_filter, 0, sizeof scr_weak_key_filter); return; } for (size_t k = i;;) { diff --git a/tests/corpus/weak-keys-freed-reused.mjs b/tests/corpus/weak-keys-freed-reused.mjs new file mode 100644 index 000000000..b24823526 --- /dev/null +++ b/tests/corpus/weak-keys-freed-reused.mjs @@ -0,0 +1,32 @@ +// Weak collection keys that become unreachable leave the collection, and +// new objects allocated afterwards (which can reuse the freed storage) are +// not found in it: plain objects, arrays, class instances and functions, +// alongside many objects that were never keys. +class Point { + constructor(x) { + this.x = x; + } +} +const map = new WeakMap(); +const set = new WeakSet(); +const kept = []; +function fill(round) { + for (let i = 0; i < 200; i++) { + const key = i % 4 === 0 ? { i } : i % 4 === 1 ? [i] : i % 4 === 2 ? new Point(i) : () => i; + map.set(key, round * 1000 + i); + set.add(key); + if (i % 50 === 0) kept.push(key); + } +} +let stale = 0; +let found = 0; +for (let round = 0; round < 20; round++) { + fill(round); + // Objects that were never keys, freed and reallocated in between. + for (let i = 0; i < 500; i++) { + const other = i % 2 ? { other: i } : new Point(-i); + if (map.has(other) || set.has(other)) stale++; + } +} +for (const key of kept) if (map.has(key) && set.has(key)) found++; +console.log(stale, found, kept.length, map.get(kept[0]), map.get(kept[kept.length - 1])); diff --git a/tests/harness/corpus-costs/weak-keys-freed-reused.mjs.json b/tests/harness/corpus-costs/weak-keys-freed-reused.mjs.json new file mode 100644 index 000000000..738211b5e --- /dev/null +++ b/tests/harness/corpus-costs/weak-keys-freed-reused.mjs.json @@ -0,0 +1 @@ +{ "plain": 5167, "san": 10508 } From b1a68147966691e22246422c80ef03d6e0d533cf Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 08:06:24 -0500 Subject: [PATCH 216/273] Prove typed-array loop bounds in worker programs when a view has shared storage A typed-array loop version in a worker program tested its bounds and its views' ownership together: every view had to have no shared storage. A loop over views of a SharedArrayBuffer always failed that guard and ran the checked copy, testing every element's index. The guard now tests the bounds first and the ownership after. When the bounds hold and every view is unshared, the loop runs the owned copy as before. When a view is shared, it runs a new third copy whose accesses skip their bounds checks and keep the shared-or-local path, without alias scopes. Versions with a UTF-16 units copy, and programs without workers, are unchanged. Corpus 8350 runs such loops over shared, unshared and mixed views, to counts within and past the views and over subarrays, with a worker writing through shared views. The emission test checks the split guard and the shared copy's accesses. --- .../backend/llvm/bytes-loop-versions.test.ts | 10 ++- .../src/backend/llvm/bytes-loop-versions.ts | 38 +++++++- .../src/backend/llvm/emit-statements.ts | 20 ++++- .../8350-worker-shared-loop-bounds.ts.json | 6 ++ ...orker-shared-loop-version-emission.test.ts | 74 ++++++++++++++++ .../corpus/8350-worker-shared-loop-bounds.ts | 86 +++++++++++++++++++ 6 files changed, 227 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8350-worker-shared-loop-bounds.ts.json create mode 100644 packages/compiler/test/worker-shared-loop-version-emission.test.ts create mode 100644 tests/corpus/8350-worker-shared-loop-bounds.ts 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 096712c97..0fa8fea34 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -317,13 +317,19 @@ test("worker programs test each receiver's shared storage once in the guard", () 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]; + const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.fast\.shared\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. + // The checked copy keeps the shared-memory path for each access, and so + // does the copy for shared views whose bounds hold. 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"); + const shared = /^bytes\.version\.fast\.shared\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec( + body, + )![0]; + expect(shared).toContain("load atomic double"); + expect(shared).toContain("store atomic double"); }); test("index temps of a version's counter are integers in the fast copy", () => { diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index de704ad42..472934f72 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -20,6 +20,7 @@ import { } from "./counted-loops.js"; import { emitBytesData, emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js"; import { f64Lit } from "./common.js"; +import { InternalCompilerError } from "../../errors.js"; import { emitStringAsciiFlag, emitStringByteLength, @@ -85,8 +86,15 @@ export interface BytesLoopVersion { /** Root receiver bindings, read once before the loop; `string` marks a * string whose `charCodeAt` and `charAt` reads the version covers. */ receivers: { ref: VarRef; global: boolean; string: boolean }[]; - /** Receivers the guard requires to have no shared storage. */ + /** Receivers the guard requires to have no shared storage for the fast + * copy. */ ownedReceivers: number[]; + /** Worker programs: when the bounds hold but a receiver in + * `ownedReceivers` has shared storage, the loop runs a second fast copy + * instead of the checked one. It skips the bounds checks of `bounds` and + * keeps shared-safe accesses (emitBytesLoopVersionGuard's `sharedFast`). Set + * when the version proves bounds as well as ownership. */ + sharedCopy: boolean; /** The string reads among `bounds`. */ stringReads: ReadonlySet; /** The version's one string receiver, when the loop may also run over @@ -752,6 +760,7 @@ export function matchBytesLoopVersion( owned: ownedNodes, receivers: order.map((index) => receivers[index]!), ownedReceivers: ownedKept.map(([receiver]) => renumber.get(receiver)!), + sharedCopy: ownedKept.length > 0 && bounds.size > 0, stringReads, unitsReceiver, unitLengths: unitLengths ?? new Set(), @@ -1102,6 +1111,10 @@ function findWhileCounterAccesses( * storage, and to `slow` otherwise (including a counted loop whose * integer form is not established, or a module receiver not yet set). * + * With `sharedFast` (a version with `sharedCopy`), the ownership tests are + * separate: when everything else holds but a view has shared storage, the + * guard branches to `sharedFast` instead of `slow`. + * * With `unitsFast`, the version's units receiver may instead be a string * that is not ASCII: the guard has the runtime publish it as the scan * receiver (one read), tests the same accesses against its UTF-16 length, @@ -1115,6 +1128,7 @@ export function emitBytesLoopVersionGuard( fast: string, slow: string, unitsFast?: string, + sharedFast?: string, ): { units: string | null; length: string | null } | null { const B = host.B; const next = (cond: string): void => { @@ -1165,10 +1179,13 @@ export function emitBytesLoopVersionGuard( if (string && index !== unitsIndex) conditions.push(emitStringAsciiFlag(host, receivers[index]!)); }); + // Without `sharedFast`, shared storage sends the loop to the checked copy + // with the other failed tests; with it, only to the shared fast copy. + const ownership: 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); + (sharedFast !== undefined ? ownership : conditions).push(owned); } for (const check of version.fixed) { const ok = B.tmp(); @@ -1351,7 +1368,22 @@ export function emitBytesLoopVersionGuard( const length = version.unitsReceiver !== null ? lengths[version.unitsReceiver]! : null; const taken = units.read; const target = taken ? B.newLabel("bytes.version.units") : fast; - if (valid === "true") B.br(target); + if (sharedFast !== undefined && taken) + throw new InternalCompilerError("llvm emitter bug: a shared fast copy with a units copy"); + if (sharedFast !== undefined && ownership.length > 0) { + // The bounds hold: unshared views take the owned copy. + const split = B.newLabel("bytes.version.owned"); + let owned = ownership[0]!; + for (const cond of ownership.slice(1)) { + const all = B.tmp(); + B.line(`${all} = and i1 ${owned}, ${cond}`); + owned = all; + } + if (valid === "true") B.br(split); + else B.condBr(valid, split, slow); + B.startBlock(split); + B.condBr(owned, target, sharedFast); + } else if (valid === "true") B.br(target); else B.condBr(valid, target, slow); if (!taken) return { units: null, length }; B.startBlock(target); diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index cbdeb0558..dc4e5090c 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -2139,7 +2139,9 @@ function declareAliasScopes(emitter: LlEmitter, scopes: readonly string[]): void * 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). */ + * proves in bounds (or thread-owned). In worker programs a version that + * proves both also has a copy for views with shared storage, in bounds but + * not owned. */ function emitVersionedLoop( emitter: LlEmitter, s: IrStmt & { kind: "for" | "while" }, @@ -2151,7 +2153,11 @@ function emitVersionedLoop( done = B.newLabel("bytes.version.done"); const unitsFast = version.unitsReceiver !== null ? B.newLabel("bytes.version.fast.units") : undefined; - const guard = emitBytesLoopVersionGuard(emitter, version, fast, slow, unitsFast); + const sharedFast = + version.sharedCopy && unitsFast === undefined + ? B.newLabel("bytes.version.fast.shared") + : undefined; + const guard = emitBytesLoopVersionGuard(emitter, version, fast, slow, unitsFast, sharedFast); const units = guard?.units ?? null; const originalBounds = emitter.bytesBounds, originalOwned = emitter.bytesOwned, @@ -2190,6 +2196,16 @@ function emitVersionedLoop( ]); emitter.emitStmt(s); B.br(done); + if (sharedFast !== undefined) { + // The bounds hold and a view has shared storage: the accesses skip + // their bounds checks and stay shared-safe, without alias scopes + // (which worker programs give only thread-owned views). + B.startBlock(sharedFast); + emitter.bytesOwned = originalOwned; + emitter.bytesAliasScopes = originalScopes; + emitter.emitStmt(s); + B.br(done); + } if (unitsFast !== undefined && units !== null) { // The string reads take the receiver's UTF-16 units. B.startBlock(unitsFast); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8350-worker-shared-loop-bounds.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8350-worker-shared-loop-bounds.ts.json new file mode 100644 index 000000000..db4376635 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8350-worker-shared-loop-bounds.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8350-worker-shared-loop-bounds.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/worker-shared-loop-version-emission.test.ts b/packages/compiler/test/worker-shared-loop-version-emission.test.ts new file mode 100644 index 000000000..7eebcc2e4 --- /dev/null +++ b/packages/compiler/test/worker-shared-loop-version-emission.test.ts @@ -0,0 +1,74 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +async function emitted(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-worker-shared-loop-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const kernel = ` +function sumTo(view: Float64Array, n: number): number { + let total = 0; + for (let i = 0; i < n; i++) total += view[i]!; + return total; +} +`; + +function body(ll: string, name: string): string { + return new RegExp(`^define internal [^\\n]*@sc_bf_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec( + ll, + )![0]; +} + +/** The instructions of the blocks whose labels start with `prefix`. */ +function blocks(fn: string, prefix: string): string { + return fn + .split(/\n(?=[\w.$-]+:)/) + .filter((block) => block.startsWith(prefix)) + .join("\n"); +} + +test("a worker program's loop over views that may be shared keeps a bounds-proven copy", async () => { + const ll = await emitted(` +import { Worker, isMainThread } from "node:worker_threads"; +${kernel} +if (isMainThread) { + const view = new Float64Array(new SharedArrayBuffer(64)); + console.log(sumTo(view, 8)); + new Worker(new URL(import.meta.url)); +} +`); + const fn = body(ll, "sumTo"); + // The guard tests the bounds first, then whether the view is shared. + expect(fn).toMatch(/^bytes\.version\.owned\d*:/m); + expect(fn).toMatch(/^bytes\.version\.fast\.shared\d*:/m); + // The shared copy reads with no bounds test, and shared-safe: a view + // with shared storage loads atomically. + const shared = blocks(fn, "bytes.version.fast.shared"); + expect(shared).not.toMatch(/icmp (ult|uge|ugt) i64 %\w+, %\w+/); + expect(fn).toMatch(/load atomic double, ptr %\w+ monotonic/); +}); + +test("a program without workers keeps two copies of the loop", async () => { + const ll = await emitted(`${kernel} +console.log(sumTo(new Float64Array(8), 8)); +`); + const fn = body(ll, "sumTo"); + expect(fn).toMatch(/^bytes\.version\.fast\d*:/m); + expect(fn).not.toMatch(/bytes\.version\.fast\.shared/); + expect(fn).not.toMatch(/bytes\.version\.owned/); +}); diff --git a/tests/corpus/8350-worker-shared-loop-bounds.ts b/tests/corpus/8350-worker-shared-loop-bounds.ts new file mode 100644 index 000000000..e5fd1ea9e --- /dev/null +++ b/tests/corpus/8350-worker-shared-loop-bounds.ts @@ -0,0 +1,86 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +// Typed-array loops in a worker program that run to a count passed in, not +// to a view's length, over views of a SharedArrayBuffer, of a thread's own +// buffer, and both in one loop. Counts within the views, past their ends +// (reads give undefined, writes are dropped) and over subarrays at an +// offset. A worker's writes through shared views are visible to the parent. + +interface Job { + shared: SharedArrayBuffer; + n: number; +} + +function rowSums(u: Float64Array, v: Float64Array, n: number, rows: number): void { + for (let i = 0; i < rows; i++) { + let t = 0; + for (let j = 0; j < n; j++) t += u[j]! / (i + j + 1); + v[i] = t; + } +} + +function copyScaled(src: Int32Array, dst: Int32Array, n: number, k: number): number { + let sum = 0; + for (let i = 0; i < n; i++) { + dst[i] = (src[i]! * k) | 0; + sum += dst[i]!; + } + return sum; +} + +function sumTo(view: Float64Array, n: number): number { + let total = 0; + for (let i = 0; i < n; i++) total += view[i]!; + return total; +} + +function fixed(view: Float64Array, count: number): string { + const parts: string[] = []; + for (let i = 0; i < count; i++) parts.push(view[i]!.toFixed(6)); + return parts.join(" "); +} + +function run(label: string, u: Float64Array, v: Float64Array, ints: Int32Array, out: Int32Array, n: number): string { + rowSums(u, v, n, 4); + const scaled = copyScaled(ints, out, n, 3); + return `${label}: rows ${fixed(v, 4)}; scaled ${scaled}; tail ${sumTo(u, n + 3)}`; +} + +function views(buffer: ArrayBufferLike, n: number): { u: Float64Array; v: Float64Array; ints: Int32Array; out: Int32Array } { + const u = new Float64Array(buffer, 0, n); + const v = new Float64Array(buffer, 8 * n, n); + const ints = new Int32Array(buffer, 16 * n, n); + const out = new Int32Array(buffer, 20 * n, n); + for (let i = 0; i < n; i++) { + u[i] = 1 + i * 0.25; + ints[i] = i * 7 - 20; + } + return { u, v, ints, out }; +} + +if (isMainThread) { + const n = 12; + const shared = new SharedArrayBuffer(24 * n); + const own = views(new ArrayBuffer(24 * n), n); + console.log(run("own", own.u, own.v, own.ints, own.out, n)); + const sh = views(shared, n); + console.log(run("shared", sh.u, sh.v, sh.ints, sh.out, n)); + console.log(run("shared, count past the end", sh.u, sh.v, sh.ints, sh.out, n + 5)); + const mixed = new Float64Array(n); + rowSums(sh.u, mixed, n, 4); + console.log("mixed", fixed(mixed, 4)); + console.log("subarray", sumTo(sh.u.subarray(3), n - 3), sumTo(sh.u.subarray(3), n)); + const worker = new Worker(new URL(import.meta.url), { workerData: { shared, n } satisfies Job }); + worker.on("message", (line: string) => { + console.log(line); + console.log("parent sees", sh.out.subarray(0, 5).join(","), sh.v[0]!.toFixed(6)); + }); +} else { + const { shared, n } = workerData as Job; + const u = new Float64Array(shared, 0, n); + const v = new Float64Array(shared, 8 * n, n); + const ints = new Int32Array(shared, 16 * n, n); + const out = new Int32Array(shared, 20 * n, n); + for (let i = 0; i < n; i++) ints[i] = i * 5 + 1; + parentPort!.postMessage(run("worker", u, v, ints, out, n)); +} From c6679d43fbc63840755e9e2e5c0e3a3063537284 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 08:18:46 -0500 Subject: [PATCH 217/273] Add no empty chunk to a byte-mode Readable's buffer push() of an empty Buffer outside object mode put a zero-length entry into the read buffer, and the chunks pushed after it stalled behind it: a 'data' listener saw nothing more and a readline interface never closed. Node's readableAddChunk adds no empty chunk outside object mode; it clears the reading flag and may read more. The runtime now does the same, as it already did for an encoded push that decodes to nothing. --- .../corpus/7516-readable-empty-push.ts.json | 6 ++ packages/runtime/src/scr_stream.c | 8 ++- tests/corpus/7516-readable-empty-push.ts | 69 +++++++++++++++++++ .../7516-readable-empty-push.ts.json | 1 + 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7516-readable-empty-push.ts.json create mode 100644 tests/corpus/7516-readable-empty-push.ts create mode 100644 tests/harness/corpus-costs/7516-readable-empty-push.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7516-readable-empty-push.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7516-readable-empty-push.ts.json new file mode 100644 index 000000000..9388c4f9a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7516-readable-empty-push.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7516-readable-empty-push.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 406d91599..201d6e34b 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -954,8 +954,14 @@ static bool scr_stream_add_chunk(ScrStream *s, ScrBytes *chunk, bool front) { } entry = dec; /* owned */ } else { - entry = scr_bytes_retain(chunk); entry_len = chunk->len; + if (entry_len == 0) { + /* Node's readableAddChunk adds no empty chunk outside object mode: + * it clears reading and may read more */ + if (!front) scr_stream_maybe_read_more(s); + return !st->r.ended && (st->r.length < st->r.hwm || st->r.length == 0); + } + entry = scr_bytes_retain(chunk); } if (entry_len > 0 && st->r.flowing == 1 && st->r.length == 0 && !st->r.in_read_sync && !front && diff --git a/tests/corpus/7516-readable-empty-push.ts b/tests/corpus/7516-readable-empty-push.ts new file mode 100644 index 000000000..27d88e3ed --- /dev/null +++ b/tests/corpus/7516-readable-empty-push.ts @@ -0,0 +1,69 @@ +// readable.push() of an empty Buffer outside object mode adds nothing, as +// in Node: the chunks pushed after it still reach 'data' listeners, +// readline and read(), and push(null) still ends the stream. In object +// mode an empty Buffer is a value like any other. +import { createInterface } from "node:readline"; +import { Readable } from "node:stream"; + +function lines(chunks: string[]): Promise { + return new Promise((resolve) => { + const src = new Readable({ read() {} }); + const rl = createInterface({ input: src, crlfDelay: Infinity }); + rl.on("line", (line: string) => console.log("line", JSON.stringify(line))); + rl.on("close", () => { + console.log("readline closed"); + resolve(); + }); + for (const chunk of chunks) src.push(Buffer.from(chunk, "utf8")); + src.push(null); + }); +} + +function data(): Promise { + return new Promise((resolve) => { + const src = new Readable({ read() {} }); + src.on("data", (chunk: Buffer) => console.log("data", JSON.stringify(String(chunk)))); + src.on("end", () => { + console.log("data end"); + resolve(); + }); + console.log("push a", src.push(Buffer.from("a"))); + console.log("push empty", src.push(Buffer.alloc(0))); + console.log("push b", src.push(Buffer.from("b"))); + src.push(null); + }); +} + +function paused(): void { + const src = new Readable({ read() {} }); + src.push(Buffer.from("x")); + src.push(Buffer.alloc(0)); + src.push(Buffer.from("y")); + console.log("readableLength", src.readableLength); + console.log("read", JSON.stringify(String(src.read()))); +} + +function objectMode(): Promise { + return new Promise((resolve) => { + const src = new Readable({ objectMode: true, read() {} }); + src.on("data", (chunk: Buffer) => console.log("object chunk length", chunk.length)); + src.on("end", () => { + console.log("object end"); + resolve(); + }); + src.push(Buffer.alloc(0)); + src.push(Buffer.from("z")); + src.push(null); + }); +} + +async function main(): Promise { + await lines(["a\nb", "", "c\n"]); + await lines(["", "", "only\n"]); + await data(); + paused(); + await objectMode(); + console.log("done"); +} + +main(); diff --git a/tests/harness/corpus-costs/7516-readable-empty-push.ts.json b/tests/harness/corpus-costs/7516-readable-empty-push.ts.json new file mode 100644 index 000000000..1cfe56769 --- /dev/null +++ b/tests/harness/corpus-costs/7516-readable-empty-push.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 3200 } From 9b2b97033aae21cf659623d605442f8f988eaa85 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 08:24:12 -0500 Subject: [PATCH 218/273] Add object literal members through the store caches in untyped code Untyped functions built each member of an object literal with scr_dyn_key_set: a lookup of the key in the new object, a walk of its empty prototype chain, a second lookup before the append, and a hash of the key to share its bytes. The first cached read of the object then computed its shape from its keys, hashing each key again. A member whose key is a constant string other than __proto__ now owns a store cache site and calls scr_js_store_probe, which appends the member through the cached shape transition, takes the key bytes from the shape, and leaves the object shaped. Members the probe does not take (Error field names, members past the 64th, a site's first object of a shape) keep scr_dyn_key_set, as do computed keys and literals in module initializers. --- .../compiler/src/backend/llvm/expr-dynamic.ts | 8 ++- packages/compiler/src/backend/llvm/js-ic.ts | 23 +++++++ packages/compiler/src/ir/js-ic.test.ts | 17 +++++ packages/compiler/src/ir/js-ic.ts | 17 ++++- packages/compiler/test/js-ic-emission.test.ts | 31 +++++++++ .../corpus/object-literal-members.mjs.json | 6 ++ tests/corpus/object-literal-members.mjs | 67 +++++++++++++++++++ .../object-literal-members.mjs.json | 1 + tests/harness/library-mode.test.ts | 4 +- tests/library-mode/js-ic-sessions/work.mjs | 9 ++- 10 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-literal-members.mjs.json create mode 100644 tests/corpus/object-literal-members.mjs create mode 100644 tests/harness/corpus-costs/object-literal-members.mjs.json diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 849715ab0..55bbb828b 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -31,7 +31,8 @@ import { elemAccess, vAdapters } from "./shapes.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js"; import { emitUnionWiden } from "./expr-records.js"; -import { emitJsIcMethod, jsIcMethods } from "./js-ic.js"; +import { emitJsIcLiteralMember, emitJsIcMethod, jsIcMethods } from "./js-ic.js"; +import { jsIcLiteralKey } from "../../ir/js-ic.js"; import { DYN_BUFFER_METHODS } from "../../ir/runtime-features.js"; export function emitDynamicExpr( @@ -316,9 +317,14 @@ export function emitDynamicExpr( const obj = B.tmp(); B.line(`${obj} = call ptr @scr_dyn_new_obj()`); const out = host.own({ name: obj, type: e.type }); + const cached = jsIcMethods(host); for (const f of e.fields ?? []) { const k = emitBorrowedInput(host, f.key); const v = emitBorrowedInput(host, f.value); + if (cached && jsIcLiteralKey(f.key) !== null) { + emitJsIcLiteralMember(host, obj, k.name, v.name); + continue; + } B.line( `call void @${f.key.type.kind === "dyn" ? "scr_dyn_key_set_computed" : "scr_dyn_key_set"}(ptr ${obj}, ptr ${k.name}, ptr ${v.name})`, ); diff --git a/packages/compiler/src/backend/llvm/js-ic.ts b/packages/compiler/src/backend/llvm/js-ic.ts index b69d3199d..11b894a60 100644 --- a/packages/compiler/src/backend/llvm/js-ic.ts +++ b/packages/compiler/src/backend/llvm/js-ic.ts @@ -14,6 +14,8 @@ * keyed dense array element at an integral number key: inline; * otherwise scr_js_keyed_probe (string keys of ordinary objects) * method dynInvoke's method read: scr_js_load_probe + * literal an object literal's constant-key member: scr_js_store_probe, + * else scr_dyn_key_set * * A probe that cannot answer from plain data leaves everything untouched, * and the site calls its helper (or the generic runtime entry), which @@ -838,6 +840,27 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue { return host.own({ name: out, type: e.type }); } +/** One member of an object literal (ir/js-ic.ts jsIcLiteralKey) added to + * the fresh object `obj` through the site's store cache, else with + * scr_dyn_key_set. The key and value are borrowed. */ +export function emitJsIcLiteralMember(host: Host, obj: string, key: string, value: string): void { + const B = host.B; + const ic = host.jsIcSite(); + host.declare(`declare zeroext i1 @scr_js_store_probe(ptr, ptr, ptr, ptr)`); + const done = B.tmp(); + B.line( + `${done} = call zeroext i1 @scr_js_store_probe(ptr ${obj}, ptr ${key}, ptr ${value}, ptr ${ic})`, + ); + const slow = B.newLabel("icl.slow"); + const join = B.newLabel("icl.j"); + B.condBr(done, join, slow); + B.startBlock(slow); + B.line(`call void @scr_dyn_key_set(ptr ${obj}, ptr ${key}, ptr ${value})`); + host.emitPendingCheck(); + B.br(join); + B.startBlock(join); +} + /** dynInvoke's method read on an ordinary object through the site's * cache, else scr_dyn_prepare_method. Returns the owned method (or NULL, * which scr_dyn_invoke_prepared_id answers with the full dispatch). */ diff --git a/packages/compiler/src/ir/js-ic.test.ts b/packages/compiler/src/ir/js-ic.test.ts index f5391b83e..35de18085 100644 --- a/packages/compiler/src/ir/js-ic.test.ts +++ b/packages/compiler/src/ir/js-ic.test.ts @@ -311,6 +311,23 @@ test("a function holds a site when it calls a classified helper or dispatches a expect(hasJsIcSite({ ...caller, name: "%init.0" }, sites)).toBe(true); }); +test("a function holds a site when an object literal has a constant-key member", () => { + const sites = sitesOf(); + const literal = (key: IrExpr): IrFunction => + helper( + "f", + ["o"], + [ret({ kind: "dynObjLit", fields: [{ key, value: ref("o") }], type: DYN, loc })], + ); + expect(hasJsIcSite(literal(str("x")), sites)).toBe(true); + // `__proto__` sets the prototype, and a computed key has no constant + // name: both keep the generic member store. + expect(hasJsIcSite(literal(str("__proto__")), sites)).toBe(false); + expect(hasJsIcSite(literal(ref("k")), sites)).toBe(false); + // Module initializers build their literals once. + expect(hasJsIcSite({ ...literal(str("x")), name: "%init.0" }, sites)).toBe(false); +}); + test("bag helpers that exit early for values no class branch matches are still the receiver", () => { /** `if (!(v)) return `: the class tables' early exit. */ const exit = (test: "classCapsule" | "funcKind", value: IrExpr, negated = true): IrStmt => ({ diff --git a/packages/compiler/src/ir/js-ic.ts b/packages/compiler/src/ir/js-ic.ts index 48019636a..5a483c585 100644 --- a/packages/compiler/src/ir/js-ic.ts +++ b/packages/compiler/src/ir/js-ic.ts @@ -296,20 +296,33 @@ function unwrapFromDyn(e: IrExpr | null | undefined): IrExpr | null { return e; } -/** Whether a function holds a cache site: a call to a classified helper or - * a method dispatch. */ +/** Whether a function holds a cache site: a call to a classified helper, + * a method dispatch, or an object literal member with a constant key. */ export function hasJsIcSite(fn: IrFunction, sites: JsIcSites): boolean { const methods = jsIcMethodSites(fn.name); return !everyStmtList(fn.body, { expr: (e) => !( (methods && e.kind === "dynInvoke") || + (methods && + e.kind === "dynObjLit" && + (e.fields ?? []).some((f) => jsIcLiteralKey(f.key) !== null)) || (e.kind === "call" && sites.site(e.callee) !== null) ), stmt: () => true, }); } +/** The key of an object literal member that its site adds through the + * store cache: a constant string other than `__proto__`, or null. The + * literal starts as an empty object with the default prototype, so each + * such member appends one data property, and the site caches the shape + * transition and shares the key bytes the shape holds. Module + * initializers have no literal sites (jsIcMethodSites). */ +export function jsIcLiteralKey(key: IrExpr): string | null { + return key.kind === "strLit" && key.value !== "__proto__" ? key.value : null; +} + /** Whether method dispatch in a function goes through caches. Module * initializers (`%init.N`) run once, and the methods they call at top level * are overwhelmingly registrations on native handles (`worker.on(...)`, diff --git a/packages/compiler/test/js-ic-emission.test.ts b/packages/compiler/test/js-ic-emission.test.ts index 15e8e92bc..79342e99d 100644 --- a/packages/compiler/test/js-ic-emission.test.ts +++ b/packages/compiler/test/js-ic-emission.test.ts @@ -64,6 +64,37 @@ console.log(read(o), at(dyn([5, 6]), 1), call(o), o.n); } }); +test("object literal members with constant keys add through store caches", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-literal-")); + try { + const entry = join(dir, "main.js"); + const outPath = join(dir, "main.ir.json"); + await writeFile( + entry, + ` +function make(v, k) { return { a: v, b: 2, [k]: 3 }; } +var top = { t: 1 }; +console.log(JSON.stringify(make(1, "c")), top.t); +`, + ); + 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(moduleUsesDynTier(mod)).toBe(true); + const llvm = emitLlvmModule(mod); + const make = body(llvm, "make"); + // Each constant-key member tries its site's store cache first and + // falls back to the generic member store; the computed key keeps the + // generic store only. + expect(make.match(/@scr_js_store_probe\(/g)).toHaveLength(2); + expect(make.match(/@scr_dyn_key_set\(/g)).toHaveLength(2); + expect(make).toContain("@scr_dyn_key_set_computed("); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + test("property sites keep their caches in programs that declare classes and subclasses", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-class-")); try { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-literal-members.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-literal-members.mjs.json new file mode 100644 index 000000000..f1f9017ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-literal-members.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/object-literal-members.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/object-literal-members.mjs b/tests/corpus/object-literal-members.mjs new file mode 100644 index 000000000..4bd211435 --- /dev/null +++ b/tests/corpus/object-literal-members.mjs @@ -0,0 +1,67 @@ +// Object literals built in untyped functions, many times through the same +// sites: key order (integer-like keys first), members named +// like Object.prototype members and Error fields, __proto__ members, wide +// literals, literals that differ only in a later member, and objects +// changed after they are built. +function point(x, y) { + return { x, y, label: "p" + x }; +} +function ordered(i) { + return { b: i, 2: "two", a: i + 1, 1: "one", "01": "zero-one" }; +} +function named(i) { + return { constructor: i, toString: "s" + i, message: "m" + i, name: "n" + i, code: i, stack: "st" }; +} +function withProto(proto, i) { + if (proto === null) return { __proto__: null, own: i }; + const o = { own: i }; + Object.setPrototypeOf(o, proto); + return o; +} +function wide(i) { + return { + k0: i, k1: i, k2: i, k3: i, k4: i, k5: i, k6: i, k7: i, k8: i, k9: i, + k10: i, k11: i, k12: i, k13: i, k14: i, k15: i, k16: i, k17: i, k18: i, k19: i, + k20: i, k21: i, k22: i, k23: i, k24: i, k25: i, k26: i, k27: i, k28: i, k29: i, + k30: i, k31: i, k32: i, k33: i, k34: i, k35: i, k36: i, k37: i, k38: i, k39: i, + k40: i, k41: i, k42: i, k43: i, k44: i, k45: i, k46: i, k47: i, k48: i, k49: i, + k50: i, k51: i, k52: i, k53: i, k54: i, k55: i, k56: i, k57: i, k58: i, k59: i, + k60: i, k61: i, k62: i, k63: i, k64: i, k65: i, k66: i, k67: i, k68: i, k69: i, + }; +} +function variant(i) { + const head = { kind: "v", n: i }; + return i % 2 ? { ...head, odd: true } : { ...head, even: true, n: -i }; +} + +let sum = 0; +const points = []; +for (let i = 0; i < 2000; i++) { + const p = point(i, i * 2); + sum += p.x + p.y; + if (i % 500 === 0) points.push(p); +} +points[0].z = 9; +delete points[1].label; +console.log(sum, JSON.stringify(points), Object.keys(points[2]).join()); + +const o = [ordered(1), ordered(2)]; +console.log(Object.keys(o[1]).join(), JSON.stringify(o[0]), o[1]["01"], o[1][1]); + +const n = [named(1), named(2)]; +console.log(JSON.stringify(n[1]), typeof n[0].toString, n[1].constructor, n[1] instanceof Error); + +const proto = { greet() { return "hi " + this.own; } }; +const w = [withProto(proto, 1), withProto(proto, 2), withProto(null, 3)]; +console.log(w[1].greet(), Object.getPrototypeOf(w[0]) === proto, Object.getPrototypeOf(w[2]), Object.keys(w[1]).join()); + +let wideSum = 0; +for (let i = 0; i < 50; i++) { + const big = wide(i); + wideSum += big.k0 + big.k69; +} +console.log(wideSum, Object.keys(wide(1)).length, Object.keys(wide(2))[69]); + +const v = []; +for (let i = 0; i < 6; i++) v.push(variant(i)); +console.log(JSON.stringify(v)); diff --git a/tests/harness/corpus-costs/object-literal-members.mjs.json b/tests/harness/corpus-costs/object-literal-members.mjs.json new file mode 100644 index 000000000..882d60088 --- /dev/null +++ b/tests/harness/corpus-costs/object-literal-members.mjs.json @@ -0,0 +1 @@ +{ "plain": 3565, "san": 6266 } diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index 4c1b96b37..4a3d4c390 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -386,7 +386,7 @@ globals: undefined fenced const run = runProbe(probe); expect(run.signal).toBeNull(); expect(run.status).toBe(0); - expect(run.stdout).toBe("run: 20000\n".repeat(3)); + expect(run.stdout).toBe("run: 40000\n".repeat(3)); }); /* ── K5/K6/K7: the trap channel ──────────────────────────────────────── */ @@ -614,7 +614,7 @@ survived, sink_calls=1 const run = runProbe(probe); expect(run.signal).toBeNull(); expect(run.status).toBe(0); - expect(run.stdout).toBe("run: 20000\n".repeat(3)); + expect(run.stdout).toBe("run: 40000\n".repeat(3)); }); platformTest("K10: K5/K7 under ASan", async () => { diff --git a/tests/library-mode/js-ic-sessions/work.mjs b/tests/library-mode/js-ic-sessions/work.mjs index 5c1afcd18..e6c4a2fc3 100644 --- a/tests/library-mode/js-ic-sessions/work.mjs +++ b/tests/library-mode/js-ic-sessions/work.mjs @@ -1,6 +1,6 @@ // Dynamic-tier property caches across library sessions: objects built empty -// and filled through untyped stores, read back through untyped loads, and a -// class instance at the same kind of sites. Each session must start with +// and filled through untyped stores or built by object literals, read back +// through untyped loads, and a class instance at the same kind of sites. Each session must start with // empty caches, since the shapes their entries named are freed when the // previous session ends. class Box { @@ -13,6 +13,9 @@ function fill(o, n) { o.b = n + 1; return o; } +function pair(n) { + return { a: n, b: n + 1 }; +} function sum(o) { return o.a + o.b; } @@ -21,6 +24,6 @@ function unbox(b) { } export function runAll() { let total = 0; - for (let i = 0; i < 100; i++) total += sum(fill({}, i)); + for (let i = 0; i < 100; i++) total += sum(fill({}, i)) + sum(pair(i)); return total + unbox(new Box(total)); } From 0488bef1a9ad7959de22df9ab78108e1c499f657 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 09:00:40 -0500 Subject: [PATCH 219/273] Describe RSA-PSS AUTO verification against restricted keys as Node does RSA_PSS_SALTLEN_AUTO recovers the salt length from the signature and does not enforce the key's minimum salt length, as in Node (OpenSSL). The runtime's exemption now says so, and corpus 8178's header, which said the minimum was still enforced, describes what it checks. --- packages/runtime/src/scr_pki.c | 3 +++ tests/corpus/8178-rsa-pss-auto-min-salt.cjs | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index 9877ac6c9..e41dd79c3 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -3255,6 +3255,9 @@ static bool sp_pss_key_rules(SpSigKey *key, mbedtls_md_type_t md, size_t hash_le return false; } key->mgf1 = k->pss_mgf1; + /* RSA_PSS_SALTLEN_AUTO (-2) is exempt from the key's minimum, as in + * OpenSSL: verification recovers the salt length from the signature and + * accepts any length (corpus 8178). */ if (!key->salt_given) { key->salt = k->pss_salt; } else if (key->salt != -2 && (key->salt == -1 ? (int)hash_len : key->salt) < k->pss_salt) { diff --git a/tests/corpus/8178-rsa-pss-auto-min-salt.cjs b/tests/corpus/8178-rsa-pss-auto-min-salt.cjs index 97b9d1c05..78e288591 100644 --- a/tests/corpus/8178-rsa-pss-auto-min-salt.cjs +++ b/tests/corpus/8178-rsa-pss-auto-min-salt.cjs @@ -1,6 +1,8 @@ // Verifying with RSA_PSS_SALTLEN_AUTO against an RSA-PSS key whose -// parameters set a minimum salt length still enforces that minimum, as in -// Node: a valid signature with a shorter salt does not verify. The keys use +// parameters set a minimum salt length recovers the salt length from the +// signature and does not enforce that minimum, as in Node: a valid +// signature with a shorter salt verifies. The default salt length (the +// key's minimum) accepts only that length. The keys use // an MGF1 digest equal to the message digest and a different one. The // stricter public key is the generated one with its minimum salt length // raised from 16 to 32 in its DER. From 8987d39791a1dabba6136196f66c6a8561151178 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 09:02:49 -0500 Subject: [PATCH 220/273] Keep modules eager when computed keys or symbol descriptions could run code --- .../src/node-islands/effect-gate.test.ts | 46 ++++++++++ .../compiler/src/node-islands/effect-gate.ts | 86 +++++++++++++++++-- 2 files changed, 127 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/node-islands/effect-gate.test.ts b/packages/compiler/src/node-islands/effect-gate.test.ts index 99e737c94..3e3e16fbe 100644 --- a/packages/compiler/src/node-islands/effect-gate.test.ts +++ b/packages/compiler/src/node-islands/effect-gate.test.ts @@ -80,10 +80,56 @@ const v = C.x;`, `const C = class { static get x() { return 1; } }; const v = C.x;`, ], + [ + "a computed key that converts an object", + `module.exports = { [{ toString() { return require("fs").readFileSync("/etc/hostname", "utf8"); } }]: 1 };`, + ], + [ + "a computed key of a local object", + `const k = { toString() { return "k"; } };\nexports.o = { [k]: 1 };`, + ], + [ + "a computed method name that converts an object", + `exports.o = { [{ valueOf() { return 1; } }]() {} };`, + ], + [ + "a computed class member name that converts an object", + `class C { [{ toString() { return "m"; } }]() {} }`, + ], + ["a computed key of a let", `let k = "k";\nexports.o = { [k]: 1 };`], + [ + "a symbol description that converts an object", + `const s = Symbol({ toString() { return "d"; } });`, + ], + [ + "a symbol key that converts an object", + `const s = Symbol.for({ toString() { return "d"; } });`, + ], ])("refuses %s", (_what, text) => { expect(effectGate(pkg({ "index.js": text })).ok).toBe(false); }); + test("admits computed keys that are literals or symbols", () => { + const entry = pkg({ + "index.js": `const kState = Symbol("state"); +const kName = "name"; +class C { [kState] = 1; [Symbol.iterator]() {} static [kName] = 2; } +exports.C = C; +exports.o = { ["a"]: 1, [kState]: 2, [kName]: 3, [Symbol.for("x")]: 4, [1n]() {} }; +`, + }); + expect(effectGate(entry)).toEqual({ ok: true, files: [entry] }); + }); + + test("refuses a destructured computed key that converts an object", () => { + const entry = pkg({ + "lib.js": `exports.x = 1;`, + "index.js": `const { [{ toString() { return "x"; } }]: x } = require("./lib");\nexports.x = x;`, + }); + expect(effectGate(join(entry, "..", "lib.js")).ok).toBe(true); + expect(effectGate(entry).ok).toBe(false); + }); + test("refuses a read of a static member that a base class defines with a getter", () => { const entry = pkg({ "base.js": `class Base { static get x() { return require("fs").readFileSync("/etc/hostname"); } } diff --git a/packages/compiler/src/node-islands/effect-gate.ts b/packages/compiler/src/node-islands/effect-gate.ts index d63781d20..d6a1a7d2a 100644 --- a/packages/compiler/src/node-islands/effect-gate.ts +++ b/packages/compiler/src/node-islands/effect-gate.ts @@ -153,6 +153,25 @@ const PURE_GLOBALS = new Set([ const CJS_GLOBALS = new Set(["require", "module", "exports", "__filename", "__dirname"]); +/** Symbol's well-known symbol properties: reading one runs no code. */ +const WELL_KNOWN_SYMBOLS = new Set([ + "asyncDispose", + "asyncIterator", + "dispose", + "hasInstance", + "isConcatSpreadable", + "iterator", + "match", + "matchAll", + "replace", + "search", + "species", + "split", + "toPrimitive", + "toStringTag", + "unscopables", +]); + export type EffectGateResult = | { ok: true; files: string[] } | { ok: false; reason: string; file: string }; @@ -359,12 +378,14 @@ function checkFile(parser: typeof ts, file: string, load: (file: string) => void case SK.ObjectLiteralExpression: for (const p of (e as ts.ObjectLiteralExpression).properties) { if (parser.isPropertyAssignment(p)) { - if (parser.isComputedPropertyName(p.name)) pure(p.name.expression); + if (parser.isComputedPropertyName(p.name)) + primitive(p.name.expression, "a computed key"); pure(p.initializer); } else if (parser.isShorthandPropertyAssignment(p)) { pure(p.name); } else if (parser.isMethodDeclaration(p)) { - if (parser.isComputedPropertyName(p.name)) pure(p.name.expression); + if (parser.isComputedPropertyName(p.name)) + primitive(p.name.expression, "a computed key"); } else { refuse(p, "a spread or accessor in an object literal"); } @@ -437,13 +458,17 @@ function checkFile(parser: typeof ts, file: string, load: (file: string) => void !locals.has(callee.expression.text) ) { const name = `${callee.expression.text}.${callee.name.text}`; - if (name === "Symbol.for" || name === "Object.freeze" || name === "Object.create") { + if (name === "Symbol.for") { + for (const a of call.arguments) primitive(a, "a symbol key"); + return; + } + if (name === "Object.freeze" || name === "Object.create") { for (const a of call.arguments) pure(a); return; } } if (parser.isIdentifier(callee) && callee.text === "Symbol" && !locals.has("Symbol")) { - for (const a of call.arguments) pure(a); + for (const a of call.arguments) primitive(a, "a symbol description"); return; } return refuse(e, "a call"); @@ -474,6 +499,46 @@ function checkFile(parser: typeof ts, file: string, load: (file: string) => void (parser.isPrefixUnaryExpression(e) && e.operator === SK.MinusToken && isLiteral(e.operand)) || (parser.isParenthesizedExpression(e) && isLiteral(e.expression)); + /** Whether converting `e` to a property key or a string runs no code: a + * literal, a well-known symbol, a symbol made from such a value, or a + * top-level const that holds one. Converting an object calls its + * toString, valueOf or Symbol.toPrimitive, which can run anything. */ + const primitiveKey = (e: ts.Expression): boolean => { + if (isLiteral(e) || e.kind === SK.BigIntLiteral) return true; + if (parser.isParenthesizedExpression(e)) return primitiveKey(e.expression); + if (parser.isIdentifier(e)) return primitiveConsts.has(e.text); + if ( + parser.isPropertyAccessExpression(e) && + parser.isIdentifier(e.expression) && + e.expression.text === "Symbol" && + !locals.has("Symbol") + ) + return WELL_KNOWN_SYMBOLS.has(e.name.text); + if (parser.isCallExpression(e) && e.questionDotToken === undefined) { + const callee = e.expression; + const symbol = + (parser.isIdentifier(callee) && callee.text === "Symbol") || + (parser.isPropertyAccessExpression(callee) && + parser.isIdentifier(callee.expression) && + callee.expression.text === "Symbol" && + callee.name.text === "for"); + return symbol && !locals.has("Symbol") && e.arguments.every(primitiveKey); + } + return false; + }; + const primitive = (e: ts.Expression, what: string): void => { + if (!primitiveKey(e)) refuse(e, `${what} that is not a literal or a symbol`); + }; + // Top-level consts initialized with a primitive key, in source order. + const primitiveConsts = new Set(); + for (const stmt of sf.statements) { + if (!parser.isVariableStatement(stmt)) continue; + if ((stmt.declarationList.flags & parser.NodeFlags.Const) === 0) continue; + for (const d of stmt.declarationList.declarations) + if (parser.isIdentifier(d.name) && d.initializer !== undefined && primitiveKey(d.initializer)) + primitiveConsts.add(d.name.text); + } + function checkClass(c: ts.ClassLikeDeclaration): void { const decorated = parser.canHaveDecorators(c) && (parser.getDecorators(c)?.length ?? 0) > 0; if (decorated) refuse(c, "a decorated class"); @@ -482,7 +547,8 @@ function checkFile(parser: typeof ts, file: string, load: (file: string) => void if (parser.isClassStaticBlockDeclaration(m)) refuse(m, "a class static block"); if (parser.canHaveDecorators(m) && (parser.getDecorators(m)?.length ?? 0) > 0) refuse(m, "a decorated class member"); - if (m.name !== undefined && parser.isComputedPropertyName(m.name)) pure(m.name.expression); + if (m.name !== undefined && parser.isComputedPropertyName(m.name)) + primitive(m.name.expression, "a computed key"); const isStatic = parser.canHaveModifiers(m) && (parser.getModifiers(m) ?? []).some((x) => x.kind === SK.StaticKeyword); @@ -594,6 +660,16 @@ function checkFile(parser: typeof ts, file: string, load: (file: string) => void noDefaults(el.name)), ); if (!noDefaults(d.name)) refuse(d, "a destructuring default or rest element"); + const keys = (n: ts.BindingName): void => { + if (parser.isIdentifier(n)) return; + for (const el of n.elements) { + if (parser.isOmittedExpression(el)) continue; + if (el.propertyName !== undefined && parser.isComputedPropertyName(el.propertyName)) + primitive(el.propertyName.expression, "a computed key"); + keys(el.name); + } + }; + keys(d.name); } if (d.initializer !== undefined) pure(d.initializer); } From 21e4c3683febd387866bfd20d8cee2ea2ade4420 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 09:13:52 -0500 Subject: [PATCH 221/273] Keep the budget in typed-array length loops that assign their array A loop to a typed array's length reads the length again on every iteration, so a body that assigns the array's binding changes the limit for the next iteration. The strip-mined form took the length once per block of 64 iterations, and after the body assigned a shorter array it ran on to the block's end, reading past the new length. The loop to a typed array's length is now strip-mined only when the array is a local of the function that the body never assigns (stableReceiver on the match); others keep the budget and test the length on every iteration. New corpus program: native-worker-length-loop-reassign.ts. --- .../src/backend/llvm/emit-statements.ts | 5 +- packages/compiler/src/ir/integer-loops.ts | 18 +++++- .../test/counted-loop-emission.test.ts | 15 ++++- ...native-worker-length-loop-reassign.ts.json | 6 ++ .../native-worker-length-loop-reassign.ts | 58 +++++++++++++++++++ ...native-worker-length-loop-reassign.ts.json | 1 + 6 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-length-loop-reassign.ts.json create mode 100644 tests/corpus/native-worker-length-loop-reassign.ts create mode 100644 tests/harness/corpus-costs/native-worker-length-loop-reassign.ts.json diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 2efe2cf3a..d7fccb2d4 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -1216,14 +1216,15 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { // length, polls between blocks of WORKER_POLL_BLOCK iterations instead // of counting a budget in its body: the inner loop over a block then // has no exit besides its own bound, which LLVM can vectorize. A typed - // array's length is fixed, so each block can take it as its limit. + // array's length is fixed, so each block can take it as its limit when + // the loop does not assign the array's binding. const stripMined = capturedLets.length === 0 && integerSlot !== null && ((countedLoop !== null && countedLimit !== null && Math.abs(countedLoop.step) * WORKER_POLL_BLOCK <= 2 ** 52) || - (integerLoop !== null && integerArrayLoop === null)) && + (integerLoop !== null && integerArrayLoop === null && integerLoop.stableReceiver)) && emitter.workerLoopPolls(s) && loopAccessesTypedArrays(s); const workerBudget = stripMined ? null : emitter.workerLoopBudget(s); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index 7cc49b7a0..ea866a134 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -12,6 +12,9 @@ import { preservesArrayLength } from "./array-length-stability.js"; export interface IntegerBytesForLoop { localId: string; limitReceiver: IrExpr; + /** The receiver is a local of the function that the body never assigns + * (a typed array's length is then the same on every iteration). */ + stableReceiver: boolean; } export interface IntegerCountedForLoop { @@ -682,7 +685,12 @@ export function matchIntegerBytesForLoop( if (!isUnitIncrement(stmt.update, init.localId)) return null; if (writesLocal(stmt.body, init.localId)) return null; - return { localId: init.localId, limitReceiver: cond.right.receiver }; + return { + localId: init.localId, + limitReceiver: cond.right.receiver, + stableReceiver: + receiverLocal !== undefined && !writesLocal(stmt.body, cond.right.receiver.localId), + }; } /** Array lengths are uint32, so zero-based loops and nested `j = i + 1` @@ -729,5 +737,11 @@ export function matchIntegerArrayForLoop( writesLocal(stmt.body, local.id) ) return null; - return { localId: local.id, limitReceiver: cond.right.receiver }; + return { + localId: local.id, + limitReceiver: cond.right.receiver, + stableReceiver: + locals.has(cond.right.receiver.localId) && + !writesLocal(stmt.body, cond.right.receiver.localId), + }; } diff --git a/packages/compiler/test/counted-loop-emission.test.ts b/packages/compiler/test/counted-loop-emission.test.ts index 206a3c57e..f617c6ab4 100644 --- a/packages/compiler/test/counted-loop-emission.test.ts +++ b/packages/compiler/test/counted-loop-emission.test.ts @@ -58,6 +58,14 @@ test("worker programs poll counted loops over typed arrays between blocks of ite "export function codes(a: Uint8Array): void {", " for (let i = 0; i < a.length; ++i) a[i] = a[i] === 97 ? 0 : 3;", "}", + "export function shrink(a: Uint8Array): number {", + " let t = 0;", + " for (let i = 0; i < a.length; ++i) {", + " t += a[i]!;", + " if (i === 2) a = a.subarray(0, 3);", + " }", + " return t;", + "}", "export function mix(n: number): number {", " let t = 0;", " for (let i = 0; i < n; i++) t = (t * 31 + i) % 1000003;", @@ -68,7 +76,7 @@ test("worker programs poll counted loops over typed arrays between blocks of ite "scale(a, b, 8);", "const c = new Uint8Array(8).fill(97);", "codes(c);", - "console.log(b[7], mix(8), c[0]);", + "console.log(b[7], mix(8), c[0], shrink(new Uint8Array(8).fill(1)));", workers ? "if (!isMainThread) new Worker(new URL(import.meta.url));" : "", ].join("\n"); const emit = async (workers: boolean): Promise => { @@ -113,6 +121,11 @@ test("worker programs poll counted loops over typed arrays between blocks of ite expect(codes).toMatch(/@llvm\.uadd\.sat\.i64\(i64 %\w+, i64 64\)/); expect(codes).toMatch(/@llvm\.umin\.i64\(i64 %\w+, i64 %\w+\)/); expect(codes).toMatch(/^loop\.poll\d+:$/m); + // A loop that assigns its array reads the new length on the next + // iteration: it keeps the budget. + const shrink = bodies(workerModule, "shrink"); + expect(shrink).toContain("worker cancellation budget"); + expect(shrink).not.toMatch(/^loop\.poll\d+:$/m); const mix = bodies(workerModule, "mix"); expect(mix).toContain("worker cancellation budget"); expect(mix).not.toContain("loop.poll"); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-length-loop-reassign.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-length-loop-reassign.ts.json new file mode 100644 index 000000000..f5230bace --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-length-loop-reassign.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/native-worker-length-loop-reassign.ts" + ], + "diags": [] +} diff --git a/tests/corpus/native-worker-length-loop-reassign.ts b/tests/corpus/native-worker-length-loop-reassign.ts new file mode 100644 index 000000000..7ec152b16 --- /dev/null +++ b/tests/corpus/native-worker-length-loop-reassign.ts @@ -0,0 +1,58 @@ +// Loops to a typed array's length in a program that starts workers, where +// the body assigns the loop's array: the condition reads the new array's +// length on the next iteration, shorter or longer. +import { Worker, isMainThread, parentPort } from "node:worker_threads"; + +function shrink(values: Float64Array): string { + let total = 0; + let steps = 0; + for (let i = 0; i < values.length; i++) { + total += values[i]!; + steps++; + if (i === 100) values = values.subarray(0, 50); + } + return `shrink ${steps} ${total}`; +} + +function grow(values: Uint8Array, longer: Uint8Array): string { + let total = 0; + let steps = 0; + for (let i = 0; i < values.length; i++) { + total += values[i]!; + steps++; + if (i === 10) values = longer; + } + return `grow ${steps} ${total}`; +} + +function swap(a: Int32Array, b: Int32Array): string { + let total = 0; + let steps = 0; + for (let i = 0; i < a.length; i++) { + total += a[i]!; + steps++; + if (i % 70 === 69) { + const t = a; + a = b; + b = t; + } + } + return `swap ${steps} ${total}`; +} + +function run(): string[] { + const values = new Float64Array(300).map((_, i) => i * 0.5); + const short = new Uint8Array(20).fill(3); + const long = new Uint8Array(200).map((_, i) => i & 7); + const a = new Int32Array(100).map((_, i) => i); + const b = new Int32Array(160).map((_, i) => -i); + return [shrink(values), grow(short, long), swap(a, b)]; +} + +if (isMainThread) { + for (const line of run()) console.log(line); + const worker = new Worker(new URL(import.meta.url)); + worker.on("message", (lines: string[]) => { + for (const line of lines) console.log("worker", line); + }); +} else parentPort!.postMessage(run()); diff --git a/tests/harness/corpus-costs/native-worker-length-loop-reassign.ts.json b/tests/harness/corpus-costs/native-worker-length-loop-reassign.ts.json new file mode 100644 index 000000000..5ee17db16 --- /dev/null +++ b/tests/harness/corpus-costs/native-worker-length-loop-reassign.ts.json @@ -0,0 +1 @@ +{ "plain": 3158, "san": 20404 } From 64e350edb6fca488a26982b9abc0a3729ccbb5fd Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 09:25:45 -0500 Subject: [PATCH 222/273] Drop a socket's 'drain' listeners when it closes The close step drops a socket's listener lists so that a listener which captures its socket does not keep it alive; the 'drain' list was not among them. A once('drain') listener still pending when the peer disconnected, or an on('drain') listener, held its socket and the socket's buffers after 'close'. The close step now drops the 'drain' listeners with the others. --- ...19-net-socket-drain-listener-close.ts.json | 6 ++ packages/runtime/src/scr_net.c | 1 + .../7519-net-socket-drain-listener-close.ts | 62 +++++++++++++++++++ ...19-net-socket-drain-listener-close.ts.json | 1 + 4 files changed, 70 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7519-net-socket-drain-listener-close.ts.json create mode 100644 tests/corpus/7519-net-socket-drain-listener-close.ts create mode 100644 tests/harness/corpus-costs/7519-net-socket-drain-listener-close.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7519-net-socket-drain-listener-close.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7519-net-socket-drain-listener-close.ts.json new file mode 100644 index 000000000..a3c10da99 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7519-net-socket-drain-listener-close.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7519-net-socket-drain-listener-close.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 85c7b7db7..a940fb7f1 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -5648,6 +5648,7 @@ static bool scr_net_sock_settle(ScrNetSocket *sock) { 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->drain_ls); scr_net_ls_drop(&sock->finish_ls); scr_net_ls_drop(&sock->wcb_ls); scr_net_ls_drop(&sock->ready_ls); diff --git a/tests/corpus/7519-net-socket-drain-listener-close.ts b/tests/corpus/7519-net-socket-drain-listener-close.ts new file mode 100644 index 000000000..e1b51e7b4 --- /dev/null +++ b/tests/corpus/7519-net-socket-drain-listener-close.ts @@ -0,0 +1,62 @@ +// 'drain' listeners that capture their socket do not keep it after it +// closes: a once('drain') listener still pending when the peer disconnects +// before the buffer drains, and an on('drain') listener on a socket that +// drained and then closed. The sanitized lane's exit audit checks that +// both sockets are freed. +import { connect, createServer, Socket } from "node:net"; + +function disconnectBeforeDrain(): Promise { + return new Promise((resolve) => { + const server = createServer((socket: Socket) => { + socket.on("error", () => {}); + socket.once("drain", () => console.log("unexpected drain", socket.writableLength)); + console.log("server write", socket.write(Buffer.alloc(16 * 1024 * 1024, 97))); + socket.on("close", () => { + console.log("server socket closed, need drain", socket.writableNeedDrain); + server.close(() => resolve()); + }); + }); + server.listen(0, "127.0.0.1", () => { + const addr = server.address(); + const port = typeof addr === "object" && addr !== null ? addr.port : 0; + const client = connect(port, "127.0.0.1", () => { + client.pause(); + setTimeout(() => client.destroy(), 30); + }); + }); + }); +} + +function drainThenClose(): Promise { + return new Promise((resolve) => { + const server = createServer((socket: Socket) => { + socket.pause(); + setTimeout(() => socket.resume(), 30); + }); + server.listen(0, "127.0.0.1", () => { + const addr = server.address(); + const port = typeof addr === "object" && addr !== null ? addr.port : 0; + const client = connect(port, "127.0.0.1", () => { + let drains = 0; + client.on("drain", () => { + drains++; + console.log("drain", drains, client.writableLength); + client.end(); + }); + console.log("client write", client.write(Buffer.alloc(16 * 1024 * 1024, 98))); + }); + client.on("close", () => { + console.log("client closed"); + server.close(() => resolve()); + }); + }); + }); +} + +async function main(): Promise { + await disconnectBeforeDrain(); + await drainThenClose(); + console.log("done"); +} + +main(); diff --git a/tests/harness/corpus-costs/7519-net-socket-drain-listener-close.ts.json b/tests/harness/corpus-costs/7519-net-socket-drain-listener-close.ts.json new file mode 100644 index 000000000..78ee9421a --- /dev/null +++ b/tests/harness/corpus-costs/7519-net-socket-drain-listener-close.ts.json @@ -0,0 +1 @@ +{ "plain": 2300, "san": 3100 } From 4458830db19e8df95837a24950fe2f3bdaeab9b6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 09:28:49 -0500 Subject: [PATCH 223/273] Keep native private methods out of reach of Node islands --- .../frontend/lowering/lower-node-island.ts | 10 ++++++- .../main.ts.json | 6 ++++ packages/runtime/src/scr_node_island_export.c | 8 ++++- .../6838-node-island-private-methods/main.ts | 30 +++++++++++++++++++ .../node_modules/private-pkg/index.d.ts | 2 ++ .../node_modules/private-pkg/index.js | 22 ++++++++++++++ .../node_modules/private-pkg/package.json | 1 + .../package.json | 1 + .../main.ts.json | 1 + 9 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6838-node-island-private-methods/main.ts.json create mode 100644 tests/corpus/6838-node-island-private-methods/main.ts create mode 100644 tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/index.d.ts create mode 100644 tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/index.js create mode 100644 tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/package.json create mode 100644 tests/corpus/6838-node-island-private-methods/package.json create mode 100644 tests/harness/corpus-costs/6838-node-island-private-methods/main.ts.json diff --git a/packages/compiler/src/frontend/lowering/lower-node-island.ts b/packages/compiler/src/frontend/lowering/lower-node-island.ts index 655c77268..8a9e5a34d 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-island.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-island.ts @@ -642,6 +642,14 @@ function toIsland(lowerer: Lowerer, node: ts.Expression): IrExpr { } } +/** Whether an island reads the lowered method `name` by that name: a + * public, string-keyed prototype method. The constructor, #private methods, + * accessor halves (`get:x`, `set:x`) and symbol-keyed slots (`sym:iterator`) + * are not properties an island can name. */ +function islandMethodName(name: string): boolean { + return name !== "constructor" && !name.startsWith("#") && !/^(?:get|set|sym):/.test(name); +} + /** A class instance crossing into an island: its capsule answers island * reads of its fields, and its prototype methods come from a table the * runtime builds once per class (nodeIsland.classMethods): the unbound @@ -661,7 +669,7 @@ function withClassMethods( c = c.def.base ? lowerer.classes.get(c.def.base) : undefined ) { for (const m of c.def.methods ?? []) - if (!names.includes(m) && m !== "constructor") names.push(m); + if (!names.includes(m) && islandMethodName(m)) names.push(m); } if (names.length === 0) return value; const loc = value.loc; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6838-node-island-private-methods/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6838-node-island-private-methods/main.ts.json new file mode 100644 index 000000000..269c78808 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6838-node-island-private-methods/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6838-node-island-private-methods/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_node_island_export.c b/packages/runtime/src/scr_node_island_export.c index 6d17e8cb1..a24cb6428 100644 --- a/packages/runtime/src/scr_node_island_export.c +++ b/packages/runtime/src/scr_node_island_export.c @@ -373,11 +373,17 @@ ScrDyn *scr_node_island_class_methods(ScrDyn *value, ScrDyn *builder) { return scr_dyn_retain(value); } -/* A prototype method of a class capsule (+1), or NULL. */ +/* A prototype method of a class capsule (+1), or NULL. An own property the + * island wrote under the method's name (kept in the capsule's materialized + * view) hides the method, as an own property hides a prototype's in Node. */ static ScrDyn *ni_class_method(const ScrDyn *target, const ScrDyn *key) { if (key->kind != SCR_DYN_STR) return NULL; ScrDyn *table = ni_methods_of(target); if (table == NULL) return NULL; + const ScrDyn *view = target->v.typed_ref.materialized; + if (view != NULL && view->kind == SCR_DYN_OBJ && + scr_dyn_obj_get(view, key->v.str->data, key->v.str->len) != NULL) + return NULL; ScrDyn *m = scr_dyn_obj_get(table, key->v.str->data, key->v.str->len); return m != NULL ? scr_dyn_retain(m) : NULL; } diff --git a/tests/corpus/6838-node-island-private-methods/main.ts b/tests/corpus/6838-node-island-private-methods/main.ts new file mode 100644 index 000000000..c9d350f5f --- /dev/null +++ b/tests/corpus/6838-node-island-private-methods/main.ts @@ -0,0 +1,30 @@ +// @requires-libnode +// A native class instance passed into a Node island shows the island only +// its public members: its #private methods, and the names the compiler +// gives accessors and symbol-keyed methods, read as undefined, as in Node. +import { probe, shadow } from "private-pkg"; + +class Account { + #balance = 10; + owner: string; + constructor(owner: string) { + this.owner = owner; + } + #secret(): string { + return `secret of ${this.owner}: ${this.#balance}`; + } + get label(): string { + return `account ${this.owner}`; + } + describe(): string { + return `${this.owner} (${this.#secret().length})`; + } + *[Symbol.iterator](): Generator { + yield this.owner; + } +} + +const a = new Account("ada"); +console.log(probe(a)); +console.log(a.describe(), [...a].join(",")); +console.log(shadow(new Account("bob"))); diff --git a/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/index.d.ts b/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/index.d.ts new file mode 100644 index 000000000..950911e74 --- /dev/null +++ b/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/index.d.ts @@ -0,0 +1,2 @@ +export declare function probe(o: object): string; +export declare function shadow(o: object): string; diff --git a/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/index.js b/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/index.js new file mode 100644 index 000000000..e1f4cbf8c --- /dev/null +++ b/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/index.js @@ -0,0 +1,22 @@ +"use strict"; + +// Reads a native instance's #private method and internal member names by +// string: an island sees only its public members. +function probe(o) { + const out = [typeof o["#secret"], "#secret" in o]; + // A property with the private method's spelling, holding undefined. + o["#secret"] = undefined; + const m = o["#secret"]; + out.push(typeof m, "#secret" in o); + if (typeof m === "function") out.push(m.call(o)); + out.push(typeof o["get:label"], typeof o["sym:iterator"], typeof o.describe); + return out.join(" "); +} + +// An own property holding undefined hides the prototype method. +function shadow(o) { + o.describe = undefined; + return `${typeof o.describe} ${Object.keys(o).includes("describe")}`; +} + +module.exports = { probe, shadow }; diff --git a/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/package.json b/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/package.json new file mode 100644 index 000000000..780b3e0b6 --- /dev/null +++ b/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/package.json @@ -0,0 +1 @@ +{ "name": "private-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6838-node-island-private-methods/package.json b/tests/corpus/6838-node-island-private-methods/package.json new file mode 100644 index 000000000..e9e6cb579 --- /dev/null +++ b/tests/corpus/6838-node-island-private-methods/package.json @@ -0,0 +1 @@ +{ "name": "island-private-methods", "private": true, "type": "module", "scriptc": { "packages": { "private-pkg": "node" } } } diff --git a/tests/harness/corpus-costs/6838-node-island-private-methods/main.ts.json b/tests/harness/corpus-costs/6838-node-island-private-methods/main.ts.json new file mode 100644 index 000000000..d2c83f967 --- /dev/null +++ b/tests/harness/corpus-costs/6838-node-island-private-methods/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3300 } From 16a7ae69c534225a1ed914476f01a8a32348e334 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 09:32:47 -0500 Subject: [PATCH 224/273] Describe socket write state as Node's state.length in the signature table The comments on net.sockWrite and net.sockWritableLength still described the unsent bytes; the runtime counts each write not yet completely sent in full. --- packages/compiler/src/ir/builtin-signatures.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f5c2ea339..576386851 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1547,9 +1547,9 @@ function buildLibFnSignatures() { * true } and a callback taking an address string or list, then dials * the answers in order. MAY THROW (host validation, a lookup throw). */ "net.connectLookupDyn": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T }, - /** socket.write: Node's Writable answer — false when the bytes the write - * left unsent reach the 65536-byte high-water mark ('drain' follows), - * and on an ended or closed socket. */ + /** socket.write: Node's Writable answer — false when the writes not yet + * completely sent, each counted in full, reach the 65536-byte + * high-water mark ('drain' follows), and on an ended or closed socket. */ "net.sockWrite": { argTypes: [NETSOCKET_T, STRING], result: BOOL }, "net.sockWriteBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: BOOL }, "net.sockEnd": { argTypes: [NETSOCKET_T], result: VOID }, @@ -2148,7 +2148,8 @@ function buildLibFnSignatures() { /** socket.address(): { address, family, port } or {} as a dyn. */ "net.sockAddress": { argTypes: [NETSOCKET_T], result: DYN }, "net.sockBytesWritten": { argTypes: [NETSOCKET_T], result: F64 }, - /** socket.writableLength: the bytes written and not yet sent. */ + /** socket.writableLength: Node's state.length, the length of every + * write not yet completely sent. */ "net.sockWritableLength": { argTypes: [NETSOCKET_T], result: F64 }, /** socket.writableNeedDrain: a write answered false and 'drain' has not * fired, with the write half open. */ From c1c4daed82d7c52ab1016d40c317bf37fecab52b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 24 Oct 2026 22:36:05 -0700 Subject: [PATCH 225/273] Throw Node's TypeError when an asserted callee holds no function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `f!()`, `t[k]!()` and `o.cb!()` on a value that holds undefined threw scriptc's checked-extraction message ("undefined is not representable in the target union …"). The non-null assertion is erased in JavaScript, so Node throws the call's own " is not a function". The checked extraction behind an asserted callee now carries that message, with V8's spelling of the callee: parentheses and non-null assertions dropped and call results printed as `f(...)`. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/coercions/state.ts | 5 +++- .../src/frontend/lowering/coercions/unions.ts | 27 +++++++++++++++---- .../lowering/expressions/narrowing.ts | 9 ++++++- .../src/frontend/lowering/lower-exprs.ts | 22 +++++++++++++-- .../compiler/src/frontend/lowering/lowerer.ts | 4 +-- .../frontend/lowering/union-narrow.test.ts | 19 +++++++++++++ .../src/frontend/lowering/union-narrow.ts | 8 ++++-- .../8240-asserted-callee-errors.ts.json | 6 +++++ tests/corpus/8240-asserted-callee-errors.ts | 25 +++++++++++++++++ .../8240-asserted-callee-errors.ts.json | 1 + 10 files changed, 113 insertions(+), 13 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8240-asserted-callee-errors.ts.json create mode 100644 tests/corpus/8240-asserted-callee-errors.ts create mode 100644 tests/harness/corpus-costs/8240-asserted-callee-errors.ts.json diff --git a/packages/compiler/src/frontend/lowering/coercions/state.ts b/packages/compiler/src/frontend/lowering/coercions/state.ts index ad1d7c6ea..db9bb53ad 100644 --- a/packages/compiler/src/frontend/lowering/coercions/state.ts +++ b/packages/compiler/src/frontend/lowering/coercions/state.ts @@ -12,7 +12,10 @@ export class CoercionState { // Provenance for checked extraction; consumers must not infer it from names. readonly checkedNarrows = new Set(); // The extracted arm of each checked (non-defaulting) extraction helper. - readonly checkedNarrowArms = new Map(); + readonly checkedNarrowArms = new Map< + string, + { unionId: string; tag: number; target: IrType; message?: string } + >(); readonly retags = new Map(); readonly narrows = new Map(); } diff --git a/packages/compiler/src/frontend/lowering/coercions/unions.ts b/packages/compiler/src/frontend/lowering/coercions/unions.ts index 434e687a5..53c87e429 100644 --- a/packages/compiler/src/frontend/lowering/coercions/unions.ts +++ b/packages/compiler/src/frontend/lowering/coercions/unions.ts @@ -394,18 +394,21 @@ export function unionRetagHelper( * the catchable TypeError — divergence 38's lying-assertion stance (an * unchecked unionNarrow would misread the payload where JS lets the * impossible value flow on). Null when the target isn't a non-unit arm - * of the union — those uses keep their erasure/fences. */ + * of the union — those uses keep their erasure/fences. `message` replaces + * the failure message (the TypeError of the operation consuming the + * value, such as a call's "f is not a function"). */ export function narrowedArmHelper( lowerer: Lowerer, fromId: string, target: IrType, loc: SrcLoc, + message?: string, ): string | null { const from = lowerer.unions.get(fromId); if (!from || isUnitType(target)) return null; const tag = lowerer.armTag(fromId, target); if (tag < 0) return null; - const key = `${fromId}:${tag}`; + const key = message === undefined ? `${fromId}:${tag}` : `${fromId}:${tag}:${message}`; const existing = lowerer.coercions.narrows.get(key); if (existing) { lowerer.coercions.checkedNarrows.add(existing); @@ -414,9 +417,22 @@ export function narrowedArmHelper( 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)); + const fn = buildUnionNarrow( + name, + from, + target, + loc, + (type) => lowerer.fmt(type), + null, + message ?? null, + ); if (!fn) throw new InternalCompilerError("lowerer bug: invalid checked union extraction"); - lowerer.coercions.checkedNarrowArms.set(name, { unionId: fromId, tag, target: from.arms[tag]! }); + lowerer.coercions.checkedNarrowArms.set(name, { + unionId: fromId, + tag, + target: from.arms[tag]!, + ...(message === undefined ? {} : { message }), + }); lowerer.liftedFns.push(fn); return name; } @@ -458,7 +474,8 @@ export function checkedNarrowCall( let failure: IrExpr | null = null; for (let tag = from.arms.length - 1; tag >= 0; tag--) { if (tag === arm.tag) continue; - const message = unionNarrowFailureMessage(from.arms[tag]!, (t) => lowerer.fmt(t)); + const message = + arm.message ?? unionNarrowFailureMessage(from.arms[tag]!, (t) => lowerer.fmt(t)); const thrown = nodeThrowExpr(1, "", message, type, loc); failure = failure === null diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index 01cab9901..9f0559672 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -875,8 +875,15 @@ function checkedStaleArm( /** V8's spelling of a callee in "is not a function": a string-literal * element key reads as a property name (`o["a"]` is `o.a`), other keys - * keep their brackets. */ + * keep their brackets, parentheses and non-null assertions disappear, and + * a call result reads `f(...)`. */ export function v8CalleeText(node: ts.Node): string { + // `!` is type-only syntax: the running JavaScript has no trace of it. + if (ts.isNonNullExpression(node) || ts.isParenthesizedExpression(node)) + return v8CalleeText(node.expression); + // A call result prints with its arguments elided. + if (ts.isCallExpression(node) && !node.questionDotToken) + return `${v8CalleeText(node.expression)}(...)`; if (ts.isElementAccessExpression(node)) { const key = node.argumentExpression; const object = v8CalleeText(node.expression); diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 20d05f3b7..006975085 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -89,7 +89,7 @@ import { classSymbolKeyOf } from "./symbol-fields.js"; import { lowerTypeofExpr } from "./expressions/typeof.js"; import { lowerIdentifierExpr } from "./expressions/identifiers.js"; import { lowerFieldCompoundValue, lowerIncDec } from "./expressions/updates.js"; -import { lowerTernary } from "./expressions/narrowing.js"; +import { lowerTernary, v8CalleeText } from "./expressions/narrowing.js"; import { classFieldTarget, lowerPropertyAccessExpr, @@ -433,7 +433,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const operand = assertionOperand(lowerer, expr, inner, target ?? null, loc); if (operand) return operand; if (target && target.kind !== "union" && !typeEquals(target, inner.type)) { - const helper = lowerer.narrowedArmHelper(inner.type.unionId, target, loc); + const helper = lowerer.narrowedArmHelper( + inner.type.unionId, + target, + loc, + assertedCalleeMessage(expr, target), + ); if (helper) { return lowerer.checkedNarrowCall(helper, inner, target, loc); } @@ -1160,6 +1165,19 @@ const NUMERIC_COMPOUNDS = new Set([ ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, ]); +/** The TypeError Node raises when an asserted callee (`t[k]!(…)`, `o.f!()`) + * holds no function: `!` is erased, so the call itself throws " is + * not a function" in V8's spelling of the callee. */ +function assertedCalleeMessage(expr: ts.NonNullExpression, target: IrType): string | undefined { + if (target.kind !== "func") return undefined; + let child: ts.Expression = expr; + while (ts.isParenthesizedExpression(child.parent)) child = child.parent; + const call = child.parent; + if (!ts.isCallExpression(call) || call.expression !== child || call.questionDotToken) + return undefined; + return `${v8CalleeText(expr.expression)} is not a function`; +} + /** Whether the use of `t[i]!` tells undefined from NaN: a converting use * that takes the value itself (see convertingUseOf), or a destination that * holds a missing element (see assertedElementKeepsUndefined) other than a diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index e49fde6bb..56e39ff0c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -10525,8 +10525,8 @@ export class Lowerer { return unionRetagHelper(this, fromId, toId, loc, trappable); } - narrowedArmHelper(fromId: string, target: IrType, loc: SrcLoc): string | null { - return narrowedArmHelper(this, fromId, target, loc); + narrowedArmHelper(fromId: string, target: IrType, loc: SrcLoc, message?: string): string | null { + return narrowedArmHelper(this, fromId, target, loc, message); } checkedNarrowCall(helper: string, value: IrExpr, type: IrType, loc: SrcLoc): IrExpr { diff --git a/packages/compiler/src/frontend/lowering/union-narrow.test.ts b/packages/compiler/src/frontend/lowering/union-narrow.test.ts index ed60fac8d..5c2a81b5e 100644 --- a/packages/compiler/src/frontend/lowering/union-narrow.test.ts +++ b/packages/compiler/src/frontend/lowering/union-narrow.test.ts @@ -97,3 +97,22 @@ test("a deferred read requires a real undefined tag", () => { const value: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; expect(buildUnionNarrow("bad", from, BOOL, loc, () => "", value)).toBeNull(); }); + +test("a failure message replaces every other tag's TypeError", () => { + const fn = buildUnionNarrow( + "call", + union, + STRING, + loc, + (type) => type.kind, + null, + "f is not a function", + )!; + const thrown = branches(fn).flatMap((branch) => { + const statement = branch.then[0]!; + return statement.kind === "throw" ? [statement.value] : []; + }); + expect(thrown).toHaveLength(4); + for (const value of thrown) + expect(value).toMatchObject({ kind: "libCall", args: [{ value: "f is not a function" }] }); +}); diff --git a/packages/compiler/src/frontend/lowering/union-narrow.ts b/packages/compiler/src/frontend/lowering/union-narrow.ts index 3bb09cb26..a167631fc 100644 --- a/packages/compiler/src/frontend/lowering/union-narrow.ts +++ b/packages/compiler/src/frontend/lowering/union-narrow.ts @@ -20,7 +20,10 @@ export function unionNarrowFailureMessage( * * Deferred scalar fields can supply the established undefined default: * false for booleans or NaN for numbers. No other tag receives that default, - * and reference-bearing targets always use checked extraction. */ + * and reference-bearing targets always use checked extraction. + * + * `failureMessage` replaces the TypeError message of every other tag (the + * error Node raises for the operation consuming the value). */ export function buildUnionNarrow( name: string, from: IrUnionDef, @@ -28,6 +31,7 @@ export function buildUnionNarrow( loc: SrcLoc, formatType: (type: IrType) => string, undefinedDefault: IrExpr | null = null, + failureMessage: string | null = null, ): IrFunction | null { if (isUnitType(target) || target.kind === "void") return null; const targetTag = from.arms.findIndex((arm) => typeEquals(arm, target)); @@ -66,7 +70,7 @@ export function buildUnionNarrow( args: [ { kind: "strLit", - value: unionNarrowFailureMessage(arm, formatType), + value: failureMessage ?? unionNarrowFailureMessage(arm, formatType), type: STRING, loc, }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8240-asserted-callee-errors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8240-asserted-callee-errors.ts.json new file mode 100644 index 000000000..365bf7076 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8240-asserted-callee-errors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8240-asserted-callee-errors.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8240-asserted-callee-errors.ts b/tests/corpus/8240-asserted-callee-errors.ts new file mode 100644 index 000000000..91537ed70 --- /dev/null +++ b/tests/corpus/8240-asserted-callee-errors.ts @@ -0,0 +1,25 @@ +// A non-null assertion is erased in JavaScript, so calling an asserted callee +// that holds undefined throws the call's own TypeError, with V8's spelling of +// the callee: element keys, literal keys as names, parentheses and asserted +// links dropped, call results as f(...). +type F = (x: number) => number; +const table: Record = { inc: (x) => x + 1 }; +const obj: { cb?: F; inner?: { cb?: F } } = { inner: {} }; +const arr: (F | undefined)[] = [undefined]; +let maybe: F | undefined; +function get(): F | undefined { return undefined; } +const key = process.argv.length > 99 ? "inc" : "missing"; +const cases: [string, () => number][] = [ + ["computed", () => table[key]!(1)], + ["literal key", () => table["missing"]!(1)], + ["dotted", () => obj.cb!(1)], + ["nested", () => obj.inner!.cb!(1)], + ["element", () => arr[0]!(1)], + ["local", () => maybe!(1)], + ["parens", () => (table[key])!(1)], + ["call result", () => get()!(1)], + ["parens outside", () => (table[key]!)(1)], +]; +for (const [name, run] of cases) { + try { console.log(name, run()); } catch (e) { console.log(name, (e as Error).name + ": " + (e as Error).message); } +} diff --git a/tests/harness/corpus-costs/8240-asserted-callee-errors.ts.json b/tests/harness/corpus-costs/8240-asserted-callee-errors.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/8240-asserted-callee-errors.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } From ffc7af36c760b0660e7ba6c2fcb3bda1cd66e14d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 09:59:43 -0500 Subject: [PATCH 226/273] Read a typed array's length without an invariant header load `length` and `byteLength` loaded the typed array's header with `!invariant.load` (#2016). LLVM may move such a load down to its use, past the release that frees the view, so `const slots = this.slots; this.slots = next; return slots.length` returned a garbage number in release builds. These reads are now ordinary loads that keep the header's alias tag. Header loads that only bound or address an element access stay invariant: their values reach only that access, which runs while the view is alive, so typed-array loops still hoist the length and data pointer. Corpus typed-array-length-after-release covers a replaced field, a deleted map entry and a rebound global. --- .../compiler/src/backend/llvm/bytes-alias.ts | 18 ++++-- .../compiler/src/backend/llvm/expr-bytes.ts | 17 ++++-- .../test/bytes-header-load-emission.test.ts | 60 +++++++++++++++++++ .../typed-array-length-after-release.ts.json | 6 ++ .../typed-array-length-after-release.ts | 56 +++++++++++++++++ .../typed-array-length-after-release.ts.json | 1 + 6 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/test/bytes-header-load-emission.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-length-after-release.ts.json create mode 100644 tests/corpus/typed-array-length-after-release.ts create mode 100644 tests/harness/corpus-costs/typed-array-length-after-release.ts.json diff --git a/packages/compiler/src/backend/llvm/bytes-alias.ts b/packages/compiler/src/backend/llvm/bytes-alias.ts index b7a672754..2dff9f70b 100644 --- a/packages/compiler/src/backend/llvm/bytes-alias.ts +++ b/packages/compiler/src/backend/llvm/bytes-alias.ts @@ -9,15 +9,25 @@ import type { LlvmEmitterContext } from "./expr-context.js"; * receiver slots loop-invariant. Untagged accesses and calls still alias * every category. * - * Header loads are also `!invariant.load`: no store or call between the - * construction of a view and its release changes them. Without it, a data - * pointer load ahead of a bounds branch is sunk into the in-bounds block, - * and LICM keeps a conditionally executed load inside the loop. */ + * Header loads that only bound or address an access to the view are also + * `!invariant.load`: no store or call between the construction of a view + * and its release changes them, and their values reach only that access, + * which runs while the view is alive. Without it, a data pointer load + * ahead of a bounds branch is sunk into the in-bounds block, and LICM keeps + * a conditionally executed load inside the loop. */ export function bytesHeaderAlias(host: LlvmEmitterContext): string { const tag = host.memoryAliasAttachment("bytes:header"); return tag === "" ? "" : `${tag}, !invariant.load !{}`; } +/** A header load whose value the program reads (`length`, `byteLength`) + * keeps the alias tag only. The value can outlive the view, and LLVM may + * move an invariant load to its use, past the release that frees the + * view. */ +export function bytesHeaderValueAlias(host: LlvmEmitterContext): string { + return host.memoryAliasAttachment("bytes:header"); +} + /** Element storage's tag, and the alias scope of the access being emitted * in a loop version's fast copy (bytesAccessScope). */ export function bytesElementAlias(host: LlvmEmitterContext): string { diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index a50464cd6..6f0b0380d 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -8,7 +8,7 @@ import { alwaysExactInteger, exactInteger } from "./integer-values.js"; import { byteNumberAccess } from "../../ir/byte-numbers.js"; import { emitByteNumber } from "./byte-numbers.js"; import { integerOrNaNIndex, integerOrNaNRange } from "./wrapping-sums.js"; -import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js"; +import { bytesElementAlias, bytesHeaderAlias, bytesHeaderValueAlias } from "./bytes-alias.js"; const BYTES_NUM_KIND: Record = { u8: { kind: 0, le: false }, @@ -160,12 +160,19 @@ export function emitBytesIndex( return idx; } -/** The element count of a typed array, in the target's size type. */ -export function emitBytesHeaderLength(host: LlvmEmitterContext, receiver: string): string { +/** The element count of a typed array, in the target's size type. `value`: + * the program reads the count itself (bytesHeaderValueAlias), rather than + * only bounding an access. */ +export function emitBytesHeaderLength( + host: LlvmEmitterContext, + receiver: string, + value = false, +): string { const p = host.B.tmp(); const len = host.B.tmp(); + const alias = value ? bytesHeaderValueAlias(host) : bytesHeaderAlias(host); host.B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`); - host.B.line(`${len} = load ${host.sizeType}, ptr ${p}${bytesHeaderAlias(host)}`); + host.B.line(`${len} = load ${host.sizeType}, ptr ${p}${alias}`); return len; } @@ -184,7 +191,7 @@ export function emitBytesLength( bytes: boolean, ): LlValue { const B = host.B; - const len = emitBytesHeaderLength(host, receiver); + const len = emitBytesHeaderLength(host, receiver, true); const size = BYTES_ELEMENT_SIZE[elem]; const count = bytes && size !== 1 ? B.tmp() : len; if (count !== len) B.line(`${count} = shl ${host.sizeType} ${len}, ${Math.log2(size)}`); diff --git a/packages/compiler/test/bytes-header-load-emission.test.ts b/packages/compiler/test/bytes-header-load-emission.test.ts new file mode 100644 index 000000000..bc7cb9803 --- /dev/null +++ b/packages/compiler/test/bytes-header-load-emission.test.ts @@ -0,0 +1,60 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule, validateModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; +import { type IrModule } from "../src/ir/ir.js"; + +async function lower(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-bytes-header-")); + try { + const entry = join(dir, "main.ts"), + output = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n")); + const module = deserializeModule(await readFile(output, "utf8")); + expect(validateModule(module)).toEqual([]); + return module; + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function body(llvm: string, symbol: string): string { + const found = new RegExp(`^define internal [^\\n]*@${symbol}(?:\\.i32)?\\([^]*?^}`, "m").exec( + llvm, + ); + expect(found, symbol).not.toBeNull(); + return found![0]; +} + +const INVARIANT_HEADER = /load (?:i64|i32|ptr), ptr [^\n]*, !tbaa !\d+, !invariant\.load !\{\}$/m; + +test("a typed array's length read as a value is an ordinary load; access bounds stay invariant", async () => { + const module = await lower(` +class Table { + slots: Int32Array = new Int32Array(4); + swap(): number { + const slots = this.slots; + this.slots = new Int32Array(1); + return slots.length + slots.byteLength; + } +} +function sum(values: Float64Array, scale: number): number { + let total = 0; + for (let i = 0; i < values.length; i++) total += values[i]! * scale; + return total; +} +const table = new Table(); +console.log(table.swap(), sum(new Float64Array([1, 2]), 2)); +`); + const llvm = emitLlvmModule(module); + // The length outlives the view: the release must not be crossed. + const swap = body(llvm, "sc_bf__x25_Table_swap"); + expect(swap).toMatch(/load i64, ptr [^\n]*, !tbaa !\d+$/m); + expect(swap).not.toContain("!invariant.load"); + expect(body(llvm, "sc_bf_sum")).toMatch(INVARIANT_HEADER); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-length-after-release.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-length-after-release.ts.json new file mode 100644 index 000000000..ca6fcb551 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-length-after-release.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/typed-array-length-after-release.ts" + ], + "diags": [] +} diff --git a/tests/corpus/typed-array-length-after-release.ts b/tests/corpus/typed-array-length-after-release.ts new file mode 100644 index 000000000..81fad4c77 --- /dev/null +++ b/tests/corpus/typed-array-length-after-release.ts @@ -0,0 +1,56 @@ +// A typed array's length and byteLength read just before the view's last +// reference goes away: the values stay those of the old view after a field +// replaces it, a map entry is deleted, or a local is rebound. +function grow(slots: Int32Array): Int32Array { + const next = new Int32Array(slots.length * 2); + next.set(slots); + return next; +} + +class Table { + slots: Int32Array = new Int32Array(4); + count = 0; + add(value: number): number { + const slots = this.slots; + slots[this.count % slots.length] = value; + this.count++; + if (this.count === slots.length) this.slots = grow(slots); + return this.slots.length; + } + swap(): number { + const slots = this.slots; + this.slots = new Int32Array(1); + return slots.length; + } + swapBytes(): number { + const slots = this.slots; + const bytes = slots.byteLength; + this.slots = new Int32Array(2); + return bytes + slots.length; + } +} + +const table = new Table(); +let size = 0; +for (let i = 0; i < 9; i++) size = table.add(i); +console.log(size, table.swap(), table.slots.length, table.add(5)); +console.log(table.swapBytes(), table.slots.length); + +const views = new Map(); +views.set("a", new Float64Array(6)); +function take(key: string): number { + const view = views.get(key); + if (view === undefined) return -1; + const length = view.length; + views.delete(key); + return length + view.byteLength; +} +console.log(take("a"), take("a"), views.size); + +let current = new Uint8Array(3); +function rebind(): number { + const old = current; + current = new Uint8Array(old.length + 1); + return old.length * 10 + current.length; +} +console.log(rebind(), rebind(), current.length); diff --git a/tests/harness/corpus-costs/typed-array-length-after-release.ts.json b/tests/harness/corpus-costs/typed-array-length-after-release.ts.json new file mode 100644 index 000000000..b062243b7 --- /dev/null +++ b/tests/harness/corpus-costs/typed-array-length-after-release.ts.json @@ -0,0 +1 @@ +{ "plain": 2800, "san": 2500 } From 541b05ddea63bf8ddde6b4828769ba4e46886717 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 09:22:06 -0700 Subject: [PATCH 227/273] Map generic alias casts inside generic bodies with the instance's arguments A cast to a generic type alias spelled over the enclosing function's own type parameters (`return x as Catalog`) stays symbolic in the checker inside the body, so a mapped type with key remapping, a template key filter or a conditional member refused with SC2001 in every instance. The checker interns alias instantiations, so the body's `Catalog` is the same type the signature declares. Binding unification now records, per call-keyed instance, each declared generic alias reference together with the alias the call's resolved signature instantiates. When the symbolic form does not map, mapType maps that concrete instantiation instead. A body forwarding its own alias to another generic resolves through the enclosing instance. Types that already mapped are unaffected. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/generic-functions.ts | 28 +++++ .../compiler/src/frontend/lowering/lowerer.ts | 10 ++ packages/compiler/src/frontend/type-mapper.ts | 15 +++ .../generic-alias-cast-instances.ts.json | 6 + tests/corpus/generic-alias-cast-instances.ts | 103 ++++++++++++++++++ .../generic-alias-cast-instances.ts.json | 1 + 6 files changed, 163 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-alias-cast-instances.ts.json create mode 100644 tests/corpus/generic-alias-cast-instances.ts create mode 100644 tests/harness/corpus-costs/generic-alias-cast-instances.ts.json diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 0f9e41723..7d7124027 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -921,6 +921,27 @@ function bindDefaultTypeParams( }); } +/** Generic alias references of a call-keyed instance's declared signature + * (`Catalog`), each paired with the same alias as the call's resolved + * signature instantiates it (`Catalog<{ g1: … }, "acme">`). The checker + * keeps the alias symbolic inside the generic body and offers no way to + * instantiate it with the bound arguments, but alias instantiations are + * interned: the body's `x as Catalog` is the very type the signature + * declares, so the pair recorded during unification is that re-instantiation. + * Keyed by the instance's ts-level bindings map, which every scope that + * lowers the instance installs (typeParamTsBindings). */ +const boundAliasInstances = new WeakMap, Map>(); + +/** The concrete instantiation of a generic alias reference inside the + * instance whose ts-level bindings are `tsBindings` (boundAliasInstances), + * or null when its signature never instantiated that reference. */ +export function boundAliasInstance( + tsBindings: Map, + type: ts.Type, +): ts.Type | null { + return boundAliasInstances.get(tsBindings)?.get(type) ?? null; +} + /** The structural half of binding inference: unify the DECLARED signature * (whose types mention the type parameters) against a TARGET signature * with the substitution applied — a call's resolved signature, or the @@ -1040,6 +1061,13 @@ function unifySignatureBindings( dAliasArgs.length && iAliasArgs.length === dAliasArgs.length ) { + if (tsBindings && declared !== inst) { + let pairs = boundAliasInstances.get(tsBindings); + if (!pairs) boundAliasInstances.set(tsBindings, (pairs = new Map())); + // A body forwarding its own alias (`inner(g)` returning `Catalog` inside an instance) resolves through the enclosing one. + if (!pairs.has(declared)) pairs.set(declared, lowerer.boundAliasTsResolver(inst) ?? inst); + } dAliasArgs.forEach((da, i) => { const ia = iAliasArgs[i]; if (ia) unify(da, ia, depth + 1); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ee7477e43..52f3567fb 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -456,6 +456,7 @@ import { inferTypeParamBindings, lowerGenericInstance, genericLocalClassBinding, + boundAliasInstance, } from "./generic-functions.js"; import { bindingNeverReassigned } from "./binding-analysis.js"; import type { InstantiationPath } from "./instantiation-path.js"; @@ -3792,6 +3793,7 @@ export class Lowerer { classNamer: (decl) => this.classNamer(decl), resolveTypeParam: this.typeParamResolver, resolveTypeParamTs: this.typeParamTsResolver, + resolveBoundAliasTs: this.boundAliasTsResolver, isErasing: () => this.erasingDepth > 0, genericClassInstance: (decl, ref, mapArgument) => this.genericClassInstanceType(decl, ref, mapArgument), @@ -9550,6 +9552,14 @@ export class Lowerer { return (sym && this.typeParamTsBindings.get(sym)) ?? null; }; + /** The concrete instantiation of a generic alias reference (`Catalog`) the current call-keyed instance's signature declares + * (boundAliasInstance). Inert outside generic function instantiation. */ + readonly boundAliasTsResolver = (t: ts.Type): ts.Type | null => { + if (!this.typeParamTsBindings || t.getAliasTypeArguments().length === 0) return null; + return boundAliasInstance(this.typeParamTsBindings, t); + }; + irTypeOf(node: ts.Node): IrType { const t = this.typeOf(node); // JS inference can leave mutable fields, accessor parameters and diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 17f9d0d11..5fa361db2 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1266,6 +1266,11 @@ export interface TypeMapperCtx { * only where literal identity matters — indexed accesses (`T[K]`) inside * generic bodies whose K is bound to a literal key. */ resolveTypeParamTs?: TypeParamTsResolver; + /** A generic alias reference inside a generic body (`Catalog`) + * re-instantiated with the instance's bound type arguments: the concrete + * checker type its signature instantiates, or null. Consulted only when + * the symbolic form does not map. */ + resolveBoundAliasTs?: TypeParamTsResolver; /** GENERIC program classes (monomorphization by flow): the mapping of a * concrete instantiation reference (`Box`) — the Lowerer * registers/reuses the instantiation (`Box%0`) and answers its object @@ -1830,6 +1835,16 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (!topLevel || !ts.isUnreadableConstituentsPanic(e)) throw e; mapped = null; } + // A generic alias the body spells over its own type parameters (`x as + // Catalog`) stays symbolic in the checker; when that form does not + // map, the instance's concrete instantiation of the same alias does. + if (mapped === null && ctx.resolveBoundAliasTs) { + const concrete = ctx.resolveBoundAliasTs(type); + if (concrete !== null && concrete !== type) { + mapped = mapType(concrete, ctx); + if (mapped !== null) contextResolutions++; + } + } if ( mapped !== null && memoKey !== null && diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-alias-cast-instances.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-alias-cast-instances.ts.json new file mode 100644 index 000000000..a229ee862 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-alias-cast-instances.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/generic-alias-cast-instances.ts" + ], + "diags": [] +} diff --git a/tests/corpus/generic-alias-cast-instances.ts b/tests/corpus/generic-alias-cast-instances.ts new file mode 100644 index 000000000..11a098214 --- /dev/null +++ b/tests/corpus/generic-alias-cast-instances.ts @@ -0,0 +1,103 @@ +// Casts to a generic type alias spelled over the enclosing function's own +// type parameters (`x as Catalog`). The checker keeps such an alias +// symbolic inside the generic body; each instance maps it with its own bound +// type arguments. + +type Groups = Record>; + +type EntryKey = { [A in keyof G]: keyof G[A] }[keyof G] & string; +type KeyOfKind = Extract, `${K}:${string}`>; +type EntryId = K extends `${string}:${infer I}` ? I : never; + +type Catalog = { + [Key in KeyOfKind as EntryId]: { id: EntryId; label: string; owner: P }; +}; + +function flatten(groups: Groups, kind: string): Record { + return Object.fromEntries( + Object.values(groups) + .flatMap((entries) => Object.entries(entries)) + .filter(([key]) => key.startsWith(kind + ":")) + .map(([, entry]) => [(entry as { id: string }).id, entry]), + ); +} + +function toolCatalog( + _owner: P, + groups: G, +): Catalog { + return flatten(groups, "tool") as Catalog; +} + +function partCatalog( + _owner: P, + groups: G, +): Catalog { + return flatten(groups, "part") as Catalog; +} + +const workshop = { + bench: { + "tool:saw": { id: "saw", label: "Hand saw", owner: "acme" }, + "part:bolt": { id: "bolt", label: "Hex bolt", owner: "acme" }, + }, + shelf: { + "tool:drill": { id: "drill", label: "Cordless drill", owner: "acme" }, + }, +}; + +const tools = toolCatalog("acme", workshop); +console.log(tools.saw.label, tools.drill.id, tools.drill.owner); +console.log(Object.keys(tools).join(","), JSON.stringify(tools.saw)); + +const parts = partCatalog("acme", workshop); +console.log(parts.bolt.label, Object.keys(parts).length); + +// A second instantiation with different groups gets its own layout. +const garden = { + shed: { + "tool:rake": { id: "rake", label: "Leaf rake", owner: "green" }, + "tool:hoe": { id: "hoe", label: "Garden hoe", owner: "green" }, + }, +}; +const gardenTools = toolCatalog("green", garden); +console.log(gardenTools.rake.label, gardenTools.hoe.label, Object.keys(gardenTools).join(",")); + +// No entry of the kind: the alias resolves to the empty record. +const gardenParts = partCatalog("green", garden); +console.log(Object.keys(gardenParts).length, JSON.stringify(gardenParts)); + +// A homomorphic mapped alias over the bound type, cast inside the body. +type Boxed = { [K in keyof T]: { value: T[K] } }; + +function boxAll>(values: T): Boxed { + const out: Record = {}; + for (const key of Object.keys(values)) out[key] = { value: values[key] }; + return out as Boxed; +} + +const boxed = boxAll({ width: 3, height: 4 }); +console.log(boxed.width.value * boxed.height.value); + +// The alias nested in an array type of the signature. +function boxEach>(rows: T[]): Boxed[] { + return rows.map((row) => { + const out: Record = {}; + for (const key of Object.keys(row)) out[key] = { value: row[key] }; + return out as Boxed; + }); +} + +const rows = boxEach([ + { x: 1, y: 2 }, + { x: 5, y: 8 }, +]); +console.log(rows.map((r) => r.x.value + r.y.value).join(" ")); + +// A generic body forwarding its parameters to another generic cast. +function ownedTools(groups: G): Catalog { + return toolCatalog("shared", groups); +} + +const shared = ownedTools({ rack: { "tool:clamp": { id: "clamp", label: "C-clamp", owner: "shared" } } }); +console.log(shared.clamp.label, shared.clamp.owner); diff --git a/tests/harness/corpus-costs/generic-alias-cast-instances.ts.json b/tests/harness/corpus-costs/generic-alias-cast-instances.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/generic-alias-cast-instances.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } From 7a175c374f1ea2a241a6958858abd44b1b84e5ef Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 11:26:13 -0500 Subject: [PATCH 228/273] Prepare v0.4.5 release --- CHANGELOG.md | 38 ++++++++++++++++++- 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, 68 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 612df47b5..e8ab725f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,42 @@ All notable changes to scriptc will be documented in this file. +## 0.4.5 + +### Highlights + +- **Fixes:** + - Release builds read a typed array's length without a header load that could read a freed view. + - `net.Socket` `write()` returns Node's backpressure boolean and fires `'drain'`. + - Super calls in JavaScript method values call the declared base method. + - Calling a non-function through an asserted callee throws Node's TypeError. +- **Faster programs:** + - Exceptions propagate by unwinding from directly called functions on Linux, instead of status checks after every call. + - Typed-array loops prove their bounds and keep their views disjoint in more cases, including views over shared memory in worker programs; integer loops stay in integers across nested counted loops, pure helper calls, powers of two and byte reads. + - Lone array callbacks run as loops with their bodies inlined, and row sums run two rows per pass. + - Number-key maps look keys up on their integer view and write updates through the entry the read found; small number-key maps use a direct index. + - Strings: base64 decodes eight characters at a time; Buffers fill, swap byte order and decode `utf16le` a word at a time; readline splits input in one scan; string scans over non-ASCII strings use their UTF-16 units. + - Untyped code: property caches also cache absent properties, object literal members go through the store caches, calls on plain arrays skip the method ladder, and dynamic values dispose without key-cache walks. + - JSON parses into discriminated unions, and dynamic values convert into nested unions, in one pass. + - BigInt division settles quotients from the top limbs, and limb shifts are vectorized. +- **Faster builds:** + - Function bodies are emitted on up to eight workers, including in dev builds, and repeated work in LLVM module emission is cut. + - Linux executables link with lld when the driver finds it, and unchanged executables are reused without selecting the runtime pack. + - Dev builds reuse their frontend output after edits that keep token positions, and unchanged frontend inputs are checked by file identity before hashing. + - Lowering fetches only the syntax trees it reads, and the large compiler tables are built on first use. + - Release builds of the native compiler use a profile collected on the release commit. + - Type checking can use a tsc-ts server shared by later builds (opt-in). +- **Node.js parity:** + - Crypto: RSA key generation lengths, RSA-PSS keys, encrypted private keys, `X509Certificate` fields and methods, and TLS certificates and `tls.Server` contexts. + - zlib: Brotli compression and decompression. + - Dates are mutable objects with Node's setters and conversions; URL hosts and paths parse as the URL Standard does, including non-ASCII hosts with UTS #46. + - `Math`, `JSON` and `Reflect` are real objects in JavaScript; objects can inherit from JavaScript class prototypes; typed arrays held as untyped values support named properties and `Symbol.species`. + - Error messages: `JSON.parse` syntax errors use V8's messages and positions; `Object.getPrototypeOf` of an Error answers `Error.prototype`. + - `util.inspect` of dynamic classes, functions and `%o` arguments, and `Symbol.toStringTag` fields and getters, match Node. +- **npm packages:** more npm and CommonJS dependencies compile statically, including namespace-style packages with guarded declarations and `var` exports, and untyped Maps, Sets and arrays are typed from their uses. + + + ## 0.4.4 ### Highlights @@ -29,8 +65,6 @@ All notable changes to scriptc will be documented in this file. - Split CommonJS `require` bindings, `__`-prefixed exports, string-key destructuring of `any` values, and record key order for values JavaScript built. - **npm packages:** zod 4 and `@hono/zod-validator` values are projected at the declaration boundary. - - ## 0.4.3 ### Highlights diff --git a/packages/cli-darwin-arm64/package.json b/packages/cli-darwin-arm64/package.json index bac01cc44..1a8ee5d2c 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.4", + "version": "0.4.5", "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 6c2fb3123..0cb24b908 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.4", + "version": "0.4.5", "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 289d8d6bd..3a1c7488d 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.4", + "version": "0.4.5", "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 3a601ca33..e671e926b 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.4", + "version": "0.4.5", "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 8f6b1aa5e..2b56308fd 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.4", + "version": "0.4.5", "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 ff0d1d686..4ca4c2837 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.4", + "version": "0.4.5", "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 e94ba31df..d734c0807 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.4", + "version": "0.4.5", "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 26cf10f07..59f9db90a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "scriptc", - "version": "0.4.4", + "version": "0.4.5", "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 570c0a1a5..03af418a5 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/compiler", - "version": "0.4.4", + "version": "0.4.5", "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 58b3da7e2..5f5d8f7e0 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1,6 +1,6 @@ { "schemaVersion": 2, - "compilerVersion": "0.4.4", + "compilerVersion": "0.4.5", "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 583a2bc58..4636be674 100644 --- a/packages/experimental/package.json +++ b/packages/experimental/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/experimental", - "version": "0.4.4", + "version": "0.4.5", "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 76d5fdb72..d1ac54f3e 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.4", + "version": "0.4.5", "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 fbb755f4a..b96a90cae 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.4", + "version": "0.4.5", "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 81fdc7d5b..49796f522 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.4", + "version": "0.4.5", "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 8b3b2f185..3001949c6 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.4", + "version": "0.4.5", "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 79b7eba3f..11b375ba4 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.4", + "version": "0.4.5", "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 5bffa5bef..ffe1ca7ae 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.4", + "version": "0.4.5", "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 14c747a4a..77e6fe687 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.4", + "version": "0.4.5", "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 1dfa1b177..7aea1da1e 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.4", + "version": "0.4.5", "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 32463357f..ae068a00a 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.4", + "version": "0.4.5", "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 c0577b04c..7dbdaf6b9 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.4", + "version": "0.4.5", "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 9c2134280..8a868a606 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.4", + "version": "0.4.5", "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 6b7563a6a..011d41b39 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.4", + "version": "0.4.5", "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 f0ae56929..92a3192e7 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.4", + "version": "0.4.5", "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 79fa8e7dd..5e751d829 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.4", + "version": "0.4.5", "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 45af8d2b7..63c045385 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.4", + "version": "0.4.5", "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 fe9d11bcd..a8602b395 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.4", + "version": "0.4.5", "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 ac2f4a5e9..799863cf0 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.4", + "version": "0.4.5", "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 9a5731da2..421f1f71f 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.4", + "version": "0.4.5", "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 1d58b00e5..90140ab15 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime", - "version": "0.4.4", + "version": "0.4.5", "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 c29967cc6..6ebcf5b8b 100644 --- a/packages/threads/package.json +++ b/packages/threads/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/threads", - "version": "0.4.4", + "version": "0.4.5", "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 db88b4b2f..b2aa805be 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/types", - "version": "0.4.4", + "version": "0.4.5", "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 25a630a8ca359a4e0dfd4883e5f045000d89d562 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 09:37:46 -0700 Subject: [PATCH 229/273] Map intersections of equally represented constituents as that type An intersection that restates an optional member as required with a separately written element type (`Opts & { files: Array<{ path: string }> }` over `files?: Array<{ path: string }>`) gives the member the type `A[] & B[]`. The checker keeps both element literals, and no intersection rule mapped an intersection of arrays, so the record was refused (SC2009). When every constituent maps to one IR type that is not the dynamic value, the intersection now maps to it. The rule runs only after every other intersection rule declined, so intersections that already mapped keep their mapping. pi's NormalizedBuildSystemPromptOptions is such an intersection; it sits on the chain from main()'s options to the extension API and refused main(). Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/frontend/type-mapper.ts | 26 ++++++ .../intersection-restated-members.ts.json | 6 ++ tests/corpus/intersection-restated-members.ts | 82 +++++++++++++++++++ .../intersection-restated-members.ts.json | 1 + 4 files changed, 115 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/intersection-restated-members.ts.json create mode 100644 tests/corpus/intersection-restated-members.ts create mode 100644 tests/harness/corpus-costs/intersection-restated-members.ts.json diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 17f9d0d11..edb3cf144 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -4948,6 +4948,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const record = mapRecordType(widened, ctx); if (record) return record; } + // An intersection whose constituents all map to ONE representation is + // that representation. An intersection that restates an optional member + // as required (`Opts & { files: Array<{ path: string }> }` over + // `files?: Array<{ path: string }>`) gives the member the type `A[] & B[]` + // with two separately written but structurally equal element literals; + // the checker keeps both, while their arrays (tuples, maps, ...) intern + // to one IR type. Reached only after every other intersection rule + // declined, so intersections that already mapped keep their mapping. A + // dynamic result stays unmapped: two opaque types are not one type. + if (widened.isIntersectionType()) { + const same = mapEqualConstituentsIntersection(widened, ctx); + if (same) return same; + } // Unions. `boolean` is internally `true | false`, and narrowing produces // literal unions like `"a" | "b"` — parts that map to the SAME IR type // collapse (deduplicated by typeKey), so a single surviving arm is just @@ -6813,6 +6826,19 @@ export function withUnitArm( * assigned keys, which is also Node's answer for Object.assign(fn, obj) * (a function's own length/name are non-enumerable). Null whenever the * shape doesn't fit — the caller falls through to the plain-func mapping. */ +/** The IR type every constituent of an intersection maps to, or null when a + * constituent is unmappable or dynamic, or two constituents differ. */ +function mapEqualConstituentsIntersection(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { + let same: IrType | null = null; + for (const part of ts.constituentTypes(widened)) { + const mapped = mapType(part, ctx); + if (mapped === null || mapped.kind === "dyn") return null; + if (same === null) same = mapped; + else if (!typeEquals(same, mapped)) return null; + } + return same; +} + function mapHybridCallableIntersection(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { const { checker, shapes } = ctx; if (!widened.isIntersectionType()) return null; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/intersection-restated-members.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/intersection-restated-members.ts.json new file mode 100644 index 000000000..4560cb4ae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/intersection-restated-members.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/intersection-restated-members.ts" + ], + "diags": [] +} diff --git a/tests/corpus/intersection-restated-members.ts b/tests/corpus/intersection-restated-members.ts new file mode 100644 index 000000000..249b9ddfb --- /dev/null +++ b/tests/corpus/intersection-restated-members.ts @@ -0,0 +1,82 @@ +// An intersection that restates a member with a separately written but +// structurally equal type maps as that one type. Restating an optional member +// as required makes it required; the checker keeps both element literals +// (`A[] & B[]`), and their arrays, tuples, and maps share one representation. + +interface Options { + root: string; + files?: Array<{ path: string; size: number }>; +} +type Normalized = Options & { files: Array<{ path: string; size: number }> }; + +function normalize(options: Options): Normalized { + return { ...options, files: options.files ?? [] }; +} + +function totalSize(n: Normalized): number { + let total = 0; + for (const f of n.files) total += f.size; + return total; +} + +const plain = normalize({ root: "/a" }); +console.log(plain.root, plain.files.length, totalSize(plain)); +const full = normalize({ + root: "/b", + files: [ + { path: "x.txt", size: 3 }, + { path: "y.txt", size: 4 }, + ], +}); +full.files.push({ path: "z.txt", size: 5 }); +console.log(full.files.map((f) => f.path).join(","), totalSize(full)); +console.log(JSON.stringify(full)); + +let maybe: Normalized | undefined; +if (process.argv.length < 99) maybe = { root: "/", files: [{ path: "a", size: 1 }] }; +console.log(maybe?.root, maybe?.files[0]?.path); + +// Nested records whose inner arrays are restated. +interface Settings { + name: string; + meta?: { tags: Array<{ key: string; value: string }> }; +} +type Resolved = Settings & { meta: { tags: Array<{ key: string; value: string }> } }; +const resolved: Resolved = { name: "s", meta: { tags: [{ key: "k", value: "v" }] } }; +resolved.meta.tags.push({ key: "k2", value: "v2" }); +for (const t of resolved.meta.tags) console.log(`${t.key}=${t.value}`); + +// Tuples, nested arrays, and maps. +interface Layout { + pair?: [{ x: number }, string]; + grid?: Array>; + index?: Map; +} +type Filled = Layout & { + pair: [{ x: number }, string]; + grid: Array>; + index: Map; +}; +const filled: Filled = { + pair: [{ x: 7 }, "seven"], + grid: [[{ cell: 1 }, { cell: 2 }], [{ cell: 3 }]], + index: new Map([["home", { hits: 2 }]]), +}; +filled.index.set("about", { hits: 1 }); +console.log(filled.pair[0].x, filled.pair[1]); +console.log(filled.grid.map((row) => row.map((c) => c.cell).join("+")).join(" | ")); +for (const [k, v] of filled.index) console.log(k, v.hits); + +// Both sides required, and both sides optional (the member stays optional). +type Twice = { items: Array<{ n: number }> } & { items: Array<{ n: number }> }; +const twice: Twice = { items: [{ n: 1 }, { n: 2 }] }; +console.log(twice.items.reduce((s, i) => s + i.n, 0)); +type BothOptional = { extra?: Array<{ n: number }> } & { extra?: Array<{ n: number }> }; +const none: BothOptional = {}; +const some: BothOptional = { extra: [{ n: 9 }] }; +console.log(none.extra === undefined, some.extra?.[0]?.n); + +// A restated member read through a function value. +type Handler = (event: Normalized) => string; +const describe: Handler = (event) => `${event.root}:${event.files.length}`; +console.log(describe(full), describe(plain)); diff --git a/tests/harness/corpus-costs/intersection-restated-members.ts.json b/tests/harness/corpus-costs/intersection-restated-members.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/intersection-restated-members.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } From 30e6ca9160d33cb24933fc392e86c36358dfc681 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 10:07:51 -0700 Subject: [PATCH 230/273] Convert promise and function values into a union's arm of their kind A promise or function value flowing into a union whose only arm of that kind has a wider payload or result (`Promise` into `Promise | undefined`, `(s) => Promise` into an optional `(s) => Promise` member) was refused with SC2003: an arm value wrapped into a union only when the union held an identical arm. It now takes the arm-level conversion (the checked payload widening, the function adapter) and wraps the result. `p === u` against such a union compares both sides as untyped values, so a promise keeps its identity although converting it into the union's arm builds a new promise. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/expressions/binary.ts | 25 +++++++ .../compiler/src/frontend/lowering/lowerer.ts | 25 +++++++ ...nion-promise-function-arm-widening.ts.json | 6 ++ .../union-promise-function-arm-widening.ts | 68 +++++++++++++++++++ ...nion-promise-function-arm-widening.ts.json | 1 + 5 files changed, 125 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-promise-function-arm-widening.ts.json create mode 100644 tests/corpus/union-promise-function-arm-widening.ts create mode 100644 tests/harness/corpus-costs/union-promise-function-arm-widening.ts.json diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index c172fc662..01b48554e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -2388,6 +2388,31 @@ export function lowerStrictEquality( loc, }; } + // A plain promise against a union whose promise arm has a different + // payload (`chains.get(id) === tail` with a `Map>`): + // wrapping would convert the promise into that arm, and the converted + // promise is a new object. Promises are objects to ===, so compare both + // sides as untyped values, where a converted promise answers with its + // origin's identity (the plain promise-vs-promise rule below). + const plainPromise = + left.type.kind === "promise" ? left : right.type.kind === "promise" ? right : null; + if ( + !bothUnion && + plainPromise !== null && + lowerer.armTag(ut.unionId, plainPromise.type) < 0 && + unionArms.some((arm) => arm.kind === "promise") && + lowerer.dynConvertible(left.type) && + lowerer.dynConvertible(right.type) + ) { + return { + kind: "dynScalarEq", + left: lowerer.coerceToExpected(left, DYN), + right: lowerer.coerceToExpected(right, DYN), + ...(negated ? { negated: true as const } : {}), + type: BOOL, + loc, + }; + } if ((sameUnion || !bothUnion) && lowerer.eqComparableUnion(ut.unionId)) { aliasRefusal(ut); return { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ee7477e43..059fab488 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -10397,6 +10397,31 @@ export class Lowerer { } } } + // A promise or function value against a union whose ONLY arm of + // that kind differs from it (`Promise` into `Promise | + // undefined`, `() => Promise` into an optional + // `() => Promise` member): convert into + // that arm with the arm-level conversion (the checked payload + // widening, the function adapter), then wrap. Several arms of the + // kind stay ambiguous and fall through to the union fence. + if (expr.type.kind === "promise" || expr.type.kind === "func") { + const arms = this.unions.get(expected.unionId)?.arms ?? []; + const candidates = arms.filter((arm) => arm.kind === expr.type.kind); + const arm = candidates.length === 1 ? candidates[0]! : null; + if (arm) { + const converted = this.coerceToExpected(expr, arm); + if (converted !== expr && typeEquals(converted.type, arm)) { + return { + kind: "unionWrap", + unionId: expected.unionId, + tag: arms.indexOf(arm), + value: converted, + type: expected, + loc: expr.loc, + }; + } + } + } // A width-coercible value against a union: coerce into the SINGLE // width-liftable arm, then wrap like any arm value (widthLiftPlan's // liftWrap — several candidate arms are ambiguous and decline). diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-promise-function-arm-widening.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-promise-function-arm-widening.ts.json new file mode 100644 index 000000000..91c268992 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-promise-function-arm-widening.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/union-promise-function-arm-widening.ts" + ], + "diags": [] +} diff --git a/tests/corpus/union-promise-function-arm-widening.ts b/tests/corpus/union-promise-function-arm-widening.ts new file mode 100644 index 000000000..751af6f41 --- /dev/null +++ b/tests/corpus/union-promise-function-arm-widening.ts @@ -0,0 +1,68 @@ +// A promise or function value flowing into a union whose only arm of that +// kind has a wider payload or result: the value converts into that arm. +// Promises keep their identity under === and !== against the union. + +const chains = new Map>(); + +async function serialize(key: string, task: () => Promise): Promise { + const previous = chains.get(key) ?? Promise.resolve(); + const queued = (async () => { + await previous.catch(() => {}); + return task(); + })(); + const tail = queued.catch(() => {}); + chains.set(key, tail); + void tail.then(() => { + if (chains.get(key) === tail) chains.delete(key); + console.log(`settled ${key}: ${chains.get(key) !== tail}`); + }); + return queued; +} + +interface Store { + pending?: Promise; + lookup?: (name: string) => Promise; + scale?: (n: number) => number | string; +} + +async function findName(name: string): Promise { + return name.length > 3 ? name.toUpperCase() : null; +} + +function double(n: number): number { + return n * 2; +} + +async function failing(): Promise { + throw new Error("task failed"); +} + +async function main(): Promise { + const a = await serialize("alpha", async () => 1); + const b = await serialize("alpha", async () => "two"); + const c = await serialize("beta", async (): Promise => true); + console.log(a, b, c, chains.size); + try { + await serialize("alpha", failing); + } catch (e) { + console.log("caught", (e as Error).message); + } + for (let i = 0; i < 4; i++) await Promise.resolve(); + console.log("remaining", chains.size); + + const store: Store = { lookup: findName, scale: double }; + const work: Promise = Promise.resolve(41); + store.pending = work; + console.log(store.pending === work, store.pending !== work); + const other: Promise = Promise.resolve(41); + console.log(store.pending === other); + console.log(await store.pending); + + const lookup = store.lookup; + if (lookup) console.log(await lookup("abcd"), await lookup("ab")); + console.log(store.scale?.(21)); + const empty: Store = {}; + console.log(empty.pending === work, empty.lookup === undefined, empty.scale?.(1)); +} + +main(); diff --git a/tests/harness/corpus-costs/union-promise-function-arm-widening.ts.json b/tests/harness/corpus-costs/union-promise-function-arm-widening.ts.json new file mode 100644 index 000000000..5c3da345d --- /dev/null +++ b/tests/harness/corpus-costs/union-promise-function-arm-widening.ts.json @@ -0,0 +1 @@ +{ "plain": 14002, "san": 16102 } From 9cdbc83aee7e85428c44bff096663e16b94fd6b4 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 10:07:59 -0700 Subject: [PATCH 231/273] Prefer the union arm that keeps every field of a converted record A record flowing into a union where several record arms could hold it by a width conversion was refused with SC2003 as ambiguous, even when one arm keeps every field of the record and the others hold it only by dropping fields (a model record whose option union is narrower than the catalog's, beside a classifier arm with a subset of its fields). That arm is the value's own layout, widened: it now wins when it is the only one. Plans that already resolved (a single candidate, the absent-field preference, discriminant and presence routes) are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../lowering/coercions/structural-plans.ts | 19 +++++++ .../corpus/union-record-lossless-arm.ts.json | 6 ++ tests/corpus/union-record-lossless-arm.ts | 55 +++++++++++++++++++ .../union-record-lossless-arm.ts.json | 1 + 4 files changed, 81 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-lossless-arm.ts.json create mode 100644 tests/corpus/union-record-lossless-arm.ts create mode 100644 tests/harness/corpus-costs/union-record-lossless-arm.ts.json diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts index f42663a13..bff419d48 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts @@ -128,6 +128,25 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width const route = presenceRoute(lowerer, src.shapeId, selected[0]!, selected[1]!); if (route) return route; } + // A record whose fields some arm keeps EVERY one of, each lifting + // (`{ id; limit; opts?: A | B }` into `{ id; limit; opts?: A | B | C } | + // { id; opts?: C }`): that arm is the value's own layout, widened; the + // others hold it only by dropping fields. Prefer the unique lossless arm. + if (selected.length > 1 && src.kind === "record") { + const source = lowerer.shapes.get(src.shapeId); + const lossless = selected.filter(({ arm }) => { + const target = arm.kind === "record" ? lowerer.shapes.get(arm.shapeId) : undefined; + return ( + source !== undefined && + target !== undefined && + !source.tuple && + !target.tuple && + target.fields.length === source.fields.length && + source.fields.every((field) => target.fields.some((kept) => kept.name === field.name)) + ); + }); + if (lossless.length === 1) selected = lossless; + } if (selected.length !== 1) return null; return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm }; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-lossless-arm.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-lossless-arm.ts.json new file mode 100644 index 000000000..143cda7d0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-lossless-arm.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/union-record-lossless-arm.ts" + ], + "diags": [] +} diff --git a/tests/corpus/union-record-lossless-arm.ts b/tests/corpus/union-record-lossless-arm.ts new file mode 100644 index 000000000..b90d5486e --- /dev/null +++ b/tests/corpus/union-record-lossless-arm.ts @@ -0,0 +1,55 @@ +// A record flowing into a union where several record arms can hold it by a +// width conversion: the arm that keeps every one of its fields is the +// value's own layout, widened, and wins over arms that drop fields. + +interface FastOpts { + fast?: boolean; + strict?: boolean; +} +interface DepthOpts { + depth?: number; +} +interface StrictOpts { + strict?: boolean; +} + +interface Wide { + id: string; + window: number; + kind?: string; + limit: number; + opts?: FastOpts | DepthOpts | StrictOpts; +} + +interface Judge { + id: string; + window: number; + kind: string; + opts?: StrictOpts; +} + +interface Narrow { + id: string; + window: number; + kind?: string; + limit: number; + opts?: FastOpts | DepthOpts; +} + +function show(entry: Wide | Judge): string { + const limit = "limit" in entry ? ` limit=${entry.limit}` : ""; + return `${entry.kind ?? "wide"} ${entry.id} window=${entry.window}${limit} ${JSON.stringify(entry.opts ?? null)}`; +} + +const narrow: Narrow = { id: "n1", window: 4, limit: 2, opts: { depth: 3 } }; +console.log(show(narrow)); + +const entries: (Wide | Judge)[] = [narrow, { id: "j1", window: 1, kind: "judge", opts: { strict: true } }]; +const plain: Narrow = { id: "n2", window: 8, kind: "custom", limit: 5 }; +entries.push(plain); +for (const entry of entries) console.log(show(entry)); + +let latest: Wide | Judge | undefined; +for (const item of [narrow, plain]) latest = item; +console.log(latest === undefined ? "none" : show(latest)); + diff --git a/tests/harness/corpus-costs/union-record-lossless-arm.ts.json b/tests/harness/corpus-costs/union-record-lossless-arm.ts.json new file mode 100644 index 000000000..011a8fbbf --- /dev/null +++ b/tests/harness/corpus-costs/union-record-lossless-arm.ts.json @@ -0,0 +1 @@ +{ "plain": 11809, "san": 13580 } From c46f70e451ba33c75183764b7797eafea6b8c6c3 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 10:27:52 -0700 Subject: [PATCH 232/273] Fix wrong npm declarations locally with type patches A project can now patch a package's declarations in package.json: "scriptc": { "typePatches": { "@": { "declarations": { "": "" } } } }. A leading "|" widens the declared type, any other type replaces it. Selectors name declarations by path (Type.member, fn(), fn(param), list()[], Env[string], export=(), default, file.js:local()) and apply to the package's .d.ts, its JSDoc and its @types twin alike. Patches edit file text where every consumer reads it: the program host's source reads (both checker worlds) and the npm-static passes that read package files themselves. Only type nodes and JSDoc type tags change, and no edit adds or removes a line. Installed copies are selected by version; an @types twin by the runtime package's installed version. Patches that do not apply as written warn (SC8030-SC8033) and the warnings repeat on executable cache hits. Without the setting nothing changes. Co-Authored-By: Claude Opus 5.5 --- docs/content/docs/dependencies.mdx | 33 + packages/compiler/src/cli/command.ts | 23 +- .../compiler/src/diagnostics/diagnostic.ts | 23 +- packages/compiler/src/diagnostics/envelope.ts | 8 +- packages/compiler/src/diagnostics/render.ts | 17 +- .../frontend/npm-static-type-exports-host.ts | 7 +- packages/compiler/src/frontend/npm-static.ts | 9 +- packages/compiler/src/frontend/pipeline.ts | 12 +- packages/compiler/src/frontend/program.ts | 6 +- .../compiler/src/frontend/ts7/program-host.ts | 6 +- .../src/frontend/type-patches/layer.ts | 360 +++++++ .../src/frontend/type-patches/locate.test.ts | 201 ++++ .../src/frontend/type-patches/locate.ts | 936 ++++++++++++++++++ .../frontend/type-patches/selector.test.ts | 59 ++ .../src/frontend/type-patches/selector.ts | 133 +++ .../src/frontend/type-patches/semver.test.ts | 48 + .../src/frontend/type-patches/semver.ts | 229 +++++ packages/compiler/src/index.ts | 26 +- packages/compiler/src/project-config.test.ts | 77 ++ packages/compiler/src/project-config.ts | 164 ++- .../npm/cases/type-patch-dts/main.ts.json | 6 + .../cases/type-patch-types-twin/main.ts.json | 6 + packages/compiler/test/type-patches.test.ts | 124 +++ tests/fixtures/commander-calc/package.json | 10 + .../fixtures/npm/cases/type-patch-dts/main.ts | 15 + .../node_modules/profilekit/index.d.ts | 10 + .../node_modules/profilekit/index.js | 14 + .../node_modules/profilekit/package.json | 1 + .../npm/cases/type-patch-dts/package.json | 15 + .../npm/cases/type-patch-types-twin/main.ts | 11 + .../node_modules/@types/tallyjs/index.d.ts | 7 + .../node_modules/@types/tallyjs/package.json | 1 + .../node_modules/tallyjs/index.js | 11 + .../node_modules/tallyjs/package.json | 1 + .../cases/type-patch-types-twin/package.json | 15 + tests/fixtures/type-patches/jsdoc.ts | 11 + tests/fixtures/type-patches/ms.ts | 9 + .../type-patches/node_modules/ms/index.js | 162 +++ .../type-patches/node_modules/ms/license.md | 21 + .../type-patches/node_modules/ms/package.json | 38 + .../node_modules/termctx/index.js | 43 + .../node_modules/termctx/package.json | 1 + tests/fixtures/type-patches/package.json | 26 + tests/fixtures/type-patches/stale/main.ts | 4 + .../fixtures/type-patches/stale/package.json | 17 + tests/harness/type-patches.test.ts | 143 +++ 46 files changed, 3076 insertions(+), 23 deletions(-) create mode 100644 packages/compiler/src/frontend/type-patches/layer.ts create mode 100644 packages/compiler/src/frontend/type-patches/locate.test.ts create mode 100644 packages/compiler/src/frontend/type-patches/locate.ts create mode 100644 packages/compiler/src/frontend/type-patches/selector.test.ts create mode 100644 packages/compiler/src/frontend/type-patches/selector.ts create mode 100644 packages/compiler/src/frontend/type-patches/semver.test.ts create mode 100644 packages/compiler/src/frontend/type-patches/semver.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-dts/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-types-twin/main.ts.json create mode 100644 packages/compiler/test/type-patches.test.ts create mode 100644 tests/fixtures/npm/cases/type-patch-dts/main.ts create mode 100644 tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/index.d.ts create mode 100644 tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/index.js create mode 100644 tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/package.json create mode 100644 tests/fixtures/npm/cases/type-patch-dts/package.json create mode 100644 tests/fixtures/npm/cases/type-patch-types-twin/main.ts create mode 100644 tests/fixtures/npm/cases/type-patch-types-twin/node_modules/@types/tallyjs/index.d.ts create mode 100644 tests/fixtures/npm/cases/type-patch-types-twin/node_modules/@types/tallyjs/package.json create mode 100644 tests/fixtures/npm/cases/type-patch-types-twin/node_modules/tallyjs/index.js create mode 100644 tests/fixtures/npm/cases/type-patch-types-twin/node_modules/tallyjs/package.json create mode 100644 tests/fixtures/npm/cases/type-patch-types-twin/package.json create mode 100644 tests/fixtures/type-patches/jsdoc.ts create mode 100644 tests/fixtures/type-patches/ms.ts create mode 100644 tests/fixtures/type-patches/node_modules/ms/index.js create mode 100644 tests/fixtures/type-patches/node_modules/ms/license.md create mode 100644 tests/fixtures/type-patches/node_modules/ms/package.json create mode 100644 tests/fixtures/type-patches/node_modules/termctx/index.js create mode 100644 tests/fixtures/type-patches/node_modules/termctx/package.json create mode 100644 tests/fixtures/type-patches/package.json create mode 100644 tests/fixtures/type-patches/stale/main.ts create mode 100644 tests/fixtures/type-patches/stale/package.json create mode 100644 tests/harness/type-patches.test.ts diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index a9c5ae138..ec1eb0601 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -141,6 +141,39 @@ CommonJS packages compile with Node.js's module semantics. Each `require()` eval 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. +## Fix a package's types + +scriptc trusts declared types: a package's `.d.ts` files, its `@types/` package, and the JSDoc types in its JavaScript. A value that contradicts its declared type throws a `TypeError` instead of flowing on, so a package whose declarations are wrong can stop a program that runs under Node.js. A common case is a member declared `string` or `number` that is sometimes `undefined`. + +Fix such a declaration in your project with a type patch in `package.json`: + +```json:package.json +{ + "scriptc": { + "typePatches": { + "commander@^15.0.0": { + "declarations": { + "HelpContext.helpWidth": "| undefined" + } + } + } + } +} +``` + +Each key names a package and a semver range of the installed versions the patch describes. `declarations` maps a declaration to a type: + +
    +
    `"| undefined"`
    +
    A type that starts with `|` widens the declared type, here to `number | undefined`.
    +
    `"string | null"`
    +
    Any other type replaces the declared type.
    +
    + +A declaration is named by its path: `Options.name` for a member of a type, `parse()` for a function's return type, `parse(text)` for a parameter, `list()[]` for an array's elements, and `Env[string]` for an index signature. `export=()` names the return type of the function the module exports, `default` its default export, and a file prefix such as `index.js:parse()` names a declaration inside one of the package's files. One patch applies to the package's own declarations, its JSDoc, and its `@types/` package. The patched type then holds everywhere, so your code has to handle the `undefined` that a widened declaration allows. An optional `"note"` string can link the upstream issue. + +Patches change only declarations, never the package's code, and values are still checked against the patched types. A build warns when a patch doesn't apply as written: when no range covers the installed version, when a declaration path matches nothing, when the declaration already has the type, or when a path is ambiguous. Report wrong declarations to the package too, so the patch can be removed later. + ## Provenance sources (experimental) `--provenance-sources` compiles a package from the source at the commit its npm provenance attestation names, instead of from its published JavaScript. This lets packages written in TypeScript compile from their original TypeScript. Packages without a usable attestation compile from their installed files as usual. diff --git a/packages/compiler/src/cli/command.ts b/packages/compiler/src/cli/command.ts index 608b97b67..8b2e4c480 100644 --- a/packages/compiler/src/cli/command.ts +++ b/packages/compiler/src/cli/command.ts @@ -19,7 +19,12 @@ import { } from "../node-islands/policy.js"; import { TIER_MODES, type TierMode } from "../frontend/tier-analysis.js"; import { buildEnvelope } from "../diagnostics/envelope.js"; -import { renderConfigWarnings, renderWarnings } from "../diagnostics/render.js"; +import { + renderConfigWarnings, + renderTypePatchWarnings, + renderWarnings, +} from "../diagnostics/render.js"; +import { isTypePatchCode } from "../diagnostics/diagnostic.js"; import { isPerformanceCode, renderPerformanceWarnings } from "../diagnostics/dynamic-tier.js"; import { isNativeOptimization } from "../backend/optimization.js"; import { @@ -637,20 +642,30 @@ async function main(args: string[], host: CliHost): Promise { // keeps stderr for the program's own output. const color = process.stderr.isTTY ?? false; const config = warnings.filter((d) => d.code === "SC8004"); - const divergences = warnings.filter((d) => !isPerformanceCode(d.code) && d.code !== "SC8004"); + const patches = warnings.filter((d) => isTypePatchCode(d.code)); + const divergences = warnings.filter( + (d) => !isPerformanceCode(d.code) && d.code !== "SC8004" && !isTypePatchCode(d.code), + ); const performance = warnings.filter((d) => isPerformanceCode(d.code)); if (config.length > 0) process.stderr.write( renderConfigWarnings(config, result.sourceTexts ?? new Map(), { color }) + "\n", ); - if (divergences.length > 0) { + if (patches.length > 0) { if (config.length > 0) process.stderr.write("\n"); + process.stderr.write( + renderTypePatchWarnings(patches, result.sourceTexts ?? new Map(), { color }) + "\n", + ); + } + if (divergences.length > 0) { + if (config.length > 0 || patches.length > 0) process.stderr.write("\n"); process.stderr.write( renderWarnings(divergences, result.sourceTexts ?? new Map(), { color }) + "\n", ); } if (performance.length > 0) { - if (divergences.length > 0 || config.length > 0) process.stderr.write("\n"); + if (divergences.length > 0 || config.length > 0 || patches.length > 0) + process.stderr.write("\n"); process.stderr.write(renderPerformanceWarnings(performance) + "\n"); } } diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 75827376e..8010cbb17 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -71,7 +71,9 @@ * package required to be native (SC8003) or every island * refused (SC8002) while outside the native subset, and the * R2 object-model constructs in user JavaScript (SC8101–SC8105, - * behind --strict-user-js) + * behind --strict-user-js); type patches from + * `scriptc.typePatches` that did not apply as written + * (SC8030–SC8033, warnings) * SC9xxx internal compiler errors (still source-anchored) */ import type { SrcLoc } from "../ir/ir.js"; @@ -264,6 +266,25 @@ export function nodeIslandsOffModuleDiag( }; } +/** SC8030–SC8033 — a type patch (`scriptc.typePatches`) that did not + * apply as written: no range covers the installed copy (SC8030), its + * selector matches nothing (SC8031), the declaration already has the type + * (SC8032), or the selector is ambiguous or not patchable (SC8033). + * Warnings: the build goes on with the declarations it has. */ +export function typePatchDiag( + code: "SC8030" | "SC8031" | "SC8032" | "SC8033", + message: string, + hint: string, + loc: SrcLoc, +): ScrDiagnostic { + return { code, message, loc, hint }; +} + +/** True for the type patch warnings (SC8030–SC8033). */ +export function isTypePatchCode(code: string): boolean { + return /^SC803[0-3]$/.test(code); +} + /** SC8003 — a package marked "native" is outside the native subset. */ export function nativePackageDiag(pkg: string, why: string, loc: SrcLoc): ScrDiagnostic { return { diff --git a/packages/compiler/src/diagnostics/envelope.ts b/packages/compiler/src/diagnostics/envelope.ts index ad2e31507..bdb545ff3 100644 --- a/packages/compiler/src/diagnostics/envelope.ts +++ b/packages/compiler/src/diagnostics/envelope.ts @@ -15,7 +15,7 @@ * version; renaming or removing a field, or changing a field's meaning, * bumps it. */ import { readFileSync } from "node:fs"; -import type { ScrDiagnostic } from "./diagnostic.js"; +import { isTypePatchCode, type ScrDiagnostic } from "./diagnostic.js"; import { isPerformanceCode } from "./dynamic-tier.js"; import type { WasmModuleReport } from "../wasm/discover.js"; @@ -217,6 +217,7 @@ export function categoryOf(diag: ScrDiagnostic): DiagnosticCategory { if (code === "SC0002" || code === "SC0004") return "environment"; if (/^SC[35]\d{3}$/.test(code)) return "environment"; if (code === "SC8004" || code === "SC8005" || code === "SC8012") return "environment"; + if (isTypePatchCode(code)) return "environment"; if (/^SC9\d{3}$/.test(code)) return "internal"; return "unsupported"; } @@ -335,7 +336,10 @@ export function locateDiagnostic( code: diag.code, category: categoryOf(diag), severity: - isDivergenceCode(diag.code) || isPerformanceCode(diag.code) || diag.code === "SC8004" + isDivergenceCode(diag.code) || + isPerformanceCode(diag.code) || + diag.code === "SC8004" || + isTypePatchCode(diag.code) ? "warning" : "error", scope, diff --git a/packages/compiler/src/diagnostics/render.ts b/packages/compiler/src/diagnostics/render.ts index 16fe3f0bf..a370eb3b1 100644 --- a/packages/compiler/src/diagnostics/render.ts +++ b/packages/compiler/src/diagnostics/render.ts @@ -4,7 +4,7 @@ * longer than FRAME_MAX_WIDTH (minified sources) shows a window of that * width around the diagnostic's column, with "…" where text is cut. */ -import type { ScrDiagnostic } from "./diagnostic.js"; +import { isTypePatchCode, type ScrDiagnostic } from "./diagnostic.js"; export interface RenderOptions { color?: boolean; @@ -157,7 +157,20 @@ export function renderDiagnostics( * settings in the project's scriptc configuration (SC8004), which builds do * not use. */ export function isWarningCode(code: string): boolean { - return /^SC6\d{3}$/.test(code) || code === "SC8004"; + return /^SC6\d{3}$/.test(code) || code === "SC8004" || isTypePatchCode(code); +} + +/** A build's type patch warnings (SC8030–SC8033). */ +export function renderTypePatchWarnings( + diags: readonly ScrDiagnostic[], + sourceTextByFile: Map, + opts: RenderOptions = {}, +): string { + const n = diags.length; + return ( + renderDiagnostics([...diags], sourceTextByFile, opts) + + `\n\n${n} warning${n === 1 ? "" : "s"}: ${n === 1 ? "this type patch" : "these type patches"} did not apply as written; the build uses the package's own declarations there.` + ); } /** A build's SC8004 warnings: the unknown scriptc settings it ignored. */ 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 8a61ed62b..f8aaa20f9 100644 --- a/packages/compiler/src/frontend/npm-static-type-exports-host.ts +++ b/packages/compiler/src/frontend/npm-static-type-exports-host.ts @@ -2,7 +2,8 @@ * 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 { trackedFileExists } from "./input-tracker.js"; +import { packageSourceText } from "./type-patches/layer.js"; import { resolveBareModule } from "./resolve.js"; import { isJsSourceFileName } from "./tsc-codes.js"; import { NPM_STATIC_TYPES_SUFFIX } from "./npm-static.js"; @@ -78,7 +79,7 @@ export function npmStaticTypeExportPlan( const parse = (file: string): ts.SourceFile | null => { let sf = parsed.get(file); if (sf === undefined) { - const text = trackedReadFile(file); + const text = packageSourceText(file); sf = text === null ? null : services.parse(file, text, "ts"); parsed.set(file, sf); } @@ -212,7 +213,7 @@ export function npmStaticTypeExportPlan( continue; const runtime = resolveBareModule(entryPath, pkg, "js-only"); if (runtime === null || !isJsSourceFileName(runtime.typesFile)) continue; - const runtimeText = trackedReadFile(runtime.typesFile); + const runtimeText = packageSourceText(runtime.typesFile); if (runtimeText === null) continue; const { types, values } = typesOf(declared.typesFile, new Set()); const declaredByRuntime = runtimeTypeNames(services, runtime.typesFile, runtimeText); diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index f51c6bbe2..ed442be79 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -23,6 +23,7 @@ import { trackedRealpath, } from "./input-tracker.js"; import { npmBuiltinAliasOf } from "./builtin-modules.js"; +import { packageSourceText } from "./type-patches/layer.js"; let activePackages: ReadonlySet = new Set(); let declarationOverloads: ReadonlyMap = new Map(); @@ -82,7 +83,7 @@ export function setNpmStaticTypeExports( 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); + const source = text ?? packageSourceText(path); return source === null ? text : `${source}\n${typedefs}\n`; } @@ -467,7 +468,11 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow if (hit !== undefined) return hit ?? undefined; let rewritten: string | null = null; try { - let original = trackedReadFile(path); + // A project type patch (type-patches/layer.ts) edits the text + // as the package wrote it, before any rewrite reads its JSDoc + // (unrewritten files fall through to the host, which serves the + // patched text too). + let original = packageSourceText(path); // A bundle's CommonJS chunks move to their own modules first, so // every later pass (and this shadow, for each chunk) sees plain // CommonJS module text. diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index b5771dedd..585aca867 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -47,6 +47,7 @@ import { THREADS_MODULE } from "./threads-import.js"; import { erasedTypeOnlyImport } from "./type-only-imports.js"; import { EXPERIMENTAL_MODULE } from "./experimental-import.js"; import { readProjectConfig } from "../project-config.js"; +import { typePatchDiagnostics } from "./type-patches/layer.js"; import { packageDirOf } from "../node-islands/origin.js"; import { scanFiles, @@ -735,10 +736,14 @@ function loadFrontend( const projectConfig = readProjectConfig(entryPath); const configDiagnostics = projectConfig.diagnostics; const report = options.nodeIslands === true; + // Type patches that did not apply as written (SC8030–SC8033) warn; the + // build goes on with the packages' own declarations there. + const patchDiagnostics = typePatchDiagnostics()?.diagnostics ?? []; + const buildWarnings = [...(report ? [] : configDiagnostics), ...patchDiagnostics]; return { preflight: report && configDiagnostics.length > 0 ? [...configDiagnostics, ...preflight] : preflight, - ...(!report && configDiagnostics.length > 0 ? { warnings: configDiagnostics } : {}), + ...(buildWarnings.length > 0 ? { warnings: buildWarnings } : {}), entryText: () => finalLoad.entry.text, entryExports: () => entryFunctionExports(finalLoad.entry), // The contract scans the PROGRAM's source files, not the runtime @@ -773,9 +778,10 @@ function loadFrontend( ...finalLoad.program.getImplementationSourceFiles().filter((sf) => !sf.isDeclarationFile), ].map((sf) => [sf.fileName, sf.text]), ); - // SC8004 diagnostics point into the project's package.json. + // SC8004 and type patch diagnostics point into the project's + // package.json. if ( - configDiagnostics.length > 0 && + (configDiagnostics.length > 0 || patchDiagnostics.length > 0) && projectConfig.manifest !== null && projectConfig.manifestText !== null ) diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 00afcd696..b93e3f217 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -140,6 +140,7 @@ import { } from "./type-only-imports.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; import { planDeclProjection, type DeclProjection } from "./decl-projection.js"; +import { configureTypePatches, packageSourceText } from "./type-patches/layer.js"; import { reportShapes } from "./shape-inference/index.js"; import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js"; import { @@ -477,7 +478,7 @@ function visitNpmStaticDeclarationClosure( if (normalized !== packageRoot && !normalized.startsWith(packageRoot + "/")) return; if (seen.has(path)) return; seen.add(path); - const source = trackedReadFile(path); + const source = packageSourceText(path); if (source === null) return; onSource(path, source); for (const specifier of npmStaticDeclarationReexports(services.parse(path, source, "ts"))) { @@ -1068,6 +1069,9 @@ export function loadProgram( // JavaScript, even when named. A registry entry's `sourceVersions` // (chalk 6) compile from their own source. configurePackageShims(entryPath); + // The project's type patches (type-patches/layer.ts), read by every host + // this load creates; nothing at all without `scriptc.typePatches`. + configureTypePatches(entryPath, (path, text, kind) => services.parse(path, text, kind)); const npmStaticPackages = [...new Set(opts?.npmStatic ?? [])].filter( (name) => !npmStaticOmitsPackage(entryPath, name), ); diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index 84cbe2474..9e8f51bb3 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -46,6 +46,7 @@ import { trackedRealpath, } from "../input-tracker.js"; import { packageShimDeclarationText } from "../package-shims.js"; +import { typePatchedText } from "../type-patches/layer.js"; import { ambientDtsPath, fallbackDtsPath, overridesDtsPath } from "../dts-paths.js"; import { reusedLibDiagnostics } from "./lib-diagnostics.js"; import { ts7Diagnostic } from "./diagnostic.js"; @@ -160,7 +161,7 @@ export class Ts7Host { const virtualFiles = this.virtualFiles; const shadow = options?.fsShadow ?? null; // A virtual file, a shadow's replacement, a package shim's declaration - // entry or the file on disk; null when shadowed out of existence or + // entry, a type-patched package file or the file on disk; null when shadowed out of existence or // missing. this.readSource = (fileName) => { const virtual = virtualFiles.get(tsgoPath(fileName)); @@ -175,6 +176,9 @@ export class Ts7Host { // program the host creates. const shim = packageShimDeclarationText(fileName); if (shim !== undefined) return shim; + // A package file a project type patch edits (type-patches/layer.ts). + const patched = typePatchedText(fileName); + if (patched !== undefined) return stripSourceBom(patched); const source = trackedReadFile(fileName); return source === null ? null : stripSourceBom(source); }; diff --git a/packages/compiler/src/frontend/type-patches/layer.ts b/packages/compiler/src/frontend/type-patches/layer.ts new file mode 100644 index 000000000..48ef6ecf0 --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/layer.ts @@ -0,0 +1,360 @@ +/* Type patches: project-owned fixes for wrong npm declarations. + * + * `scriptc.typePatches` in the entry's package.json maps + * `"@"` to declaration patches (project-config.ts): + * + * "commander@^15.0.0": { "declarations": { "HelpContext.helpWidth": "| undefined" } } + * + * The patches are applied to file TEXT, in one place every consumer reads + * through: the program host's source reads (ts7/program-host.ts), which + * serve both checker worlds (the lowering world that types package + * JavaScript from its JSDoc, and the declaration world that types user + * code at the declaration boundary), and the npm-static passes that read + * package files themselves (`packageSourceText`). Downstream, the patched + * declaration simply IS the declaration: scriptc enforces it exactly as it + * enforced the original, so a patch can never make a check weaker than the + * declaration it replaces. + * + * A patch applies to an installed copy whose version satisfies its range; + * an `@types/` twin is matched by the runtime package's name and + * installed version. Without patches nothing here runs: `typePatchedText` + * answers undefined from one null test, so every program reads the same + * bytes as before (the zero-cost rule). + * + * See scriptc-perf/results/design-type-patches/DESIGN.md. */ + +import { dirname, relative } from "node:path"; +import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; +import { typePatchDiag } from "../../diagnostics/diagnostic.js"; +import { readProjectConfig, type TypePatchEntry } from "../../project-config.js"; +import { trackedReadFile } from "../input-tracker.js"; +import type { SourceFile } from "../ts7/syntax.js"; +import { applyPatchEdits, patchFile, type PatchMatch, type PatchRequest } from "./locate.js"; +import { parseSelector, parseTypeText, type Selector, type TypePatchOp } from "./selector.js"; +import { satisfies } from "./semver.js"; + +export type TypePatchParse = (path: string, text: string, kind: "ts" | "js") => SourceFile; + +interface Request extends PatchRequest { + entry: TypePatchEntry; + /** Offset of the selector in the manifest. */ + offset: number; +} + +interface CopyInfo { + /** The runtime package's name (for an `@types` twin, the package it + * types). */ + pkg: string; + version: string | null; + root: string; + /** Requests whose entry's range the version satisfies. */ + requests: Request[]; +} + +interface MatchRecord extends PatchMatch { + file: string; + pkg: string; + version: string | null; +} + +interface State { + manifest: string; + manifestText: string; + parse: TypePatchParse; + byPackage: Map; + copies: Map; + files: Map; + /** Package copies whose version no patch range covers. */ + unmatchedCopies: Map; + /** Requests applicable to a copy this load read files of. */ + touched: Set; + matches: MatchRecord[]; + problems: Map; + /** Patched spans per served file (patched-text coordinates). */ + spans: Map; +} + +let state: State | null = null; + +/** Registers the project's type patches for one program load (or clears + * them). Called once per load, like configurePackageShims. */ +export function configureTypePatches(entryPath: string, parse: TypePatchParse): void { + const config = readProjectConfig(entryPath); + if (config.typePatches.length === 0 || config.manifest === null) { + state = null; + return; + } + const byPackage = new Map(); + let id = 0; + for (const entry of config.typePatches) { + for (const decl of entry.declarations) { + const selector = parseSelector(decl.selector); + const op = parseTypeText(decl.type); + if ("error" in selector || "error" in op) continue; // reported as SC8004 + const list = byPackage.get(entry.pkg) ?? []; + list.push({ + id: id++, + selector: selector as Selector, + op: op as TypePatchOp, + entry, + offset: decl.offset, + }); + byPackage.set(entry.pkg, list); + } + } + state = { + manifest: config.manifest, + manifestText: config.manifestText ?? "", + parse, + byPackage, + copies: new Map(), + files: new Map(), + unmatchedCopies: new Map(), + touched: new Set(), + matches: [], + problems: new Map(), + spans: new Map(), + }; +} + +/** True while the current load has type patches. */ +export function typePatchesActive(): boolean { + return state !== null; +} + +const PATCHABLE = /\.(?:[cm]?js|d\.[cm]?ts|[cm]?ts)$/; + +/** `@types/foo__bar` types `@foo/bar`. */ +function unmangleTypesName(name: string): string { + const bare = name.slice("@types/".length); + const i = bare.indexOf("__"); + return i > 0 ? `@${bare.slice(0, i)}/${bare.slice(i + 2)}` : bare; +} + +function versionAt(dir: string): string | null { + const text = trackedReadFile(`${dir}/package.json`); + if (text === null) return null; + try { + const parsed = JSON.parse(text) as { version?: unknown }; + return typeof parsed.version === "string" ? parsed.version : null; + } catch { + return null; + } +} + +/** The installed package copy a file belongs to, with the requests that + * apply to it; null for files outside node_modules or of unpatched + * packages. */ +function copyOf(s: State, path: string): { copy: CopyInfo; rel: string } | null { + const marker = "/node_modules/"; + const at = path.lastIndexOf(marker); + if (at < 0) return null; + const after = path.slice(at + marker.length); + const parts = after.split("/"); + const nameParts = parts[0]!.startsWith("@") ? 2 : 1; + if (parts.length <= nameParts) return null; + const name = parts.slice(0, nameParts).join("/"); + const root = path.slice(0, at + marker.length) + name; + const rel = parts.slice(nameParts).join("/"); + let copy = s.copies.get(root); + if (copy === undefined) { + copy = null; + const pkg = name.startsWith("@types/") ? unmangleTypesName(name) : name; + const requests = s.byPackage.get(pkg); + if (requests !== undefined) { + // A twin describes the runtime package installed beside it. + const version = name.startsWith("@types/") + ? (versionAt(path.slice(0, at + marker.length) + pkg) ?? versionAt(root)) + : versionAt(root); + const applicable = requests.filter( + (r) => version !== null && satisfies(version, r.entry.range), + ); + // Later entries win on the same selector. + const bySelector = new Map(); + for (const r of applicable) bySelector.set(r.selector.text, r); + const chosen = [...bySelector.values()]; + if (chosen.length === 0) { + const entries = [...new Set(requests.map((r) => r.entry))]; + s.unmatchedCopies.set(root, { pkg, version, entries }); + } + copy = chosen.length === 0 ? null : { pkg, version, root, requests: chosen }; + } + s.copies.set(root, copy); + } + if (copy === null) return null; + for (const r of copy.requests) s.touched.add(r); + return { copy, rel }; +} + +/** Names a request's root can appear under in a file's text. */ +function rootWords(sel: Selector): string[] { + if (sel.root === "export=") return ["exports", "export"]; + if (sel.root === "default") return ["default"]; + return [sel.root]; +} + +/** The patched text of a package file, or undefined when no patch applies + * to it (callers then read the file as usual). */ +export function typePatchedText(fileName: string): string | undefined { + const s = state; + if (s === null) return undefined; + const path = fileName.split("\\").join("/"); + if (!PATCHABLE.test(path)) return undefined; + const cached = s.files.get(path); + if (cached !== undefined) return cached ?? undefined; + let result: string | null = null; + const located = copyOf(s, path); + if (located !== null) { + const text = trackedReadFile(path); + const requests = located.copy.requests.filter( + (r) => + (r.selector.file === null || r.selector.file === located.rel) && + rootWords(r.selector).some((w) => text?.includes(w)), + ); + if (text !== null && requests.length > 0) { + const kind = /\.[cm]?ts$/.test(path) ? "ts" : "js"; + let out: ReturnType | null = null; + try { + out = patchFile(located.rel, text, s.parse(path, text, kind), requests); + } catch { + out = null; // unparseable: served unpatched, like other npm-static passes + } + if (out !== null) { + const byId = new Map(requests.map((r) => [r.id, r])); + for (const m of out.matches) { + s.matches.push({ + ...m, + file: path, + pkg: located.copy.pkg, + version: located.copy.version, + }); + } + for (const p of out.problems) { + const r = byId.get(p.id); + const prior = r === undefined ? undefined : s.problems.get(r); + if (r !== undefined && (prior === undefined || (p.always === true && !prior.always))) + s.problems.set(r, { + message: `${p.message} (${located.rel})`, + always: p.always === true, + }); + } + if (out.edits.length > 0) { + result = applyPatchEdits(text, out.edits); + s.spans.set(path, patchedSpans(out.edits)); + } + } + } + } + s.files.set(path, result); + return result ?? undefined; +} + +/** Edit spans in the patched text's coordinates. */ +function patchedSpans( + edits: readonly { start: number; end: number; text: string }[], +): { start: number; end: number }[] { + const sorted = [...edits].sort((a, b) => a.start - b.start || a.end - b.end); + const out: { start: number; end: number }[] = []; + let shift = 0; + for (const e of sorted) { + const start = e.start + shift; + out.push({ start, end: start + e.text.length }); + shift += e.text.length - (e.end - e.start); + } + return out; +} + +/** Text a package file is read as by passes that read package files + * themselves (npm-static): the patched text, or the file. */ +export function packageSourceText(path: string): string | null { + return typePatchedText(path) ?? trackedReadFile(path); +} + +/** The spans a type patch edited in a served file (patched-text offsets), + * for passes that rewrite package JSDoc and must keep patched comments + * (the shape overlay). Empty when nothing was patched. */ +export function typePatchedSpans(fileName: string): readonly { start: number; end: number }[] { + return state?.spans.get(fileName.split("\\").join("/")) ?? []; +} + +/** Every applied match of the current load (for `scriptc types patch + * --list` and tests). */ +export function typePatchMatches(): readonly MatchRecord[] { + return state?.matches ?? []; +} + +function loc(s: State, offset: number): { file: string; start: number; end: number } { + return { file: s.manifest, start: offset, end: offset }; +} + +/** The current load's type patch warnings: SC8030 (no range covers an + * installed copy), SC8031 (a selector matched nothing), SC8032 (redundant: + * the declaration already has the type), SC8033 (ambiguous or not + * patchable as written). Only packages the program loads are reported. */ +export function typePatchDiagnostics(): { + diagnostics: ScrDiagnostic[]; + manifest: string; + manifestText: string; +} | null { + const s = state; + if (s === null) return null; + const out: ScrDiagnostic[] = []; + for (const [root, info] of s.unmatchedCopies) { + const shown = info.version ?? "an unknown version"; + for (const entry of info.entries) { + out.push( + typePatchDiag( + "SC8030", + `type patch '${entry.key}' does not apply to ${info.pkg} ${shown} at ${relativeToManifest(s, root)}; its declarations are used unpatched`, + info.version === null + ? "the installed package has no version" + : `check whether the declarations of ${info.pkg} ${info.version} still need the patch, then widen the range or remove it`, + loc(s, entry.offset), + ), + ); + } + } + for (const r of s.touched) { + const mine = s.matches.filter((m) => m.id === r.id); + // A selector that matched somewhere (the .d.ts) is not a problem where + // it cannot apply (undocumented JavaScript), unless a declaration it + // names was left unpatched. + const problem = s.problems.get(r); + if (problem !== undefined && (mine.length === 0 || problem.always)) { + out.push( + typePatchDiag( + "SC8033", + `type patch '${r.entry.key}': ${problem.message}`, + "name one declaration, or patch the declared type it refers to", + loc(s, r.offset), + ), + ); + continue; + } + if (mine.length === 0) { + out.push( + typePatchDiag( + "SC8031", + `type patch '${r.entry.key}': '${r.selector.text}' matches no declaration in the files of ${r.entry.pkg} this program loads`, + "check the spelling against the package's declarations and JSDoc; the package may have renamed it", + loc(s, r.offset), + ), + ); + continue; + } + if (mine.every((m) => m.redundant)) + out.push( + typePatchDiag( + "SC8032", + `type patch '${r.entry.key}': '${r.selector.text}' is already declared as ${mine[0]!.before}`, + "the package's declarations already say this; remove the patch", + loc(s, r.offset), + ), + ); + } + return { diagnostics: out, manifest: s.manifest, manifestText: s.manifestText }; +} + +function relativeToManifest(s: State, path: string): string { + return relative(dirname(s.manifest), path).split("\\").join("/"); +} diff --git a/packages/compiler/src/frontend/type-patches/locate.test.ts b/packages/compiler/src/frontend/type-patches/locate.test.ts new file mode 100644 index 000000000..d43695d9a --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/locate.test.ts @@ -0,0 +1,201 @@ +import { describe, expect, test } from "vitest"; +import { nodeFrontendServices } from "../services-node.js"; +import { applyPatchEdits, patchFile, type PatchRequest } from "./locate.js"; +import { parseSelector, parseTypeText, type Selector, type TypePatchOp } from "./selector.js"; + +function patch(file: string, text: string, patches: Record) { + const requests: PatchRequest[] = Object.entries(patches).map(([sel, type], id) => ({ + id, + selector: parseSelector(sel) as Selector, + op: parseTypeText(type) as TypePatchOp, + })); + const kind = /\.[cm]?ts$/.test(file) ? "ts" : "js"; + const sf = nodeFrontendServices().parse(`/pkg/${file}`, text, kind); + const result = patchFile(file, text, sf, requests); + const out = applyPatchEdits(text, result.edits); + expect(out.split("\n").length).toBe(text.split("\n").length); + return { ...result, text: out }; +} + +const COMMANDER_LIKE = `class Command { + /** + * @typedef HelpContext + * @type {object} + * @property {boolean} error + * @property {number} helpWidth + * @property {function} write - includes stripColor if needed + * + * @returns {HelpContext} + * @private + */ + _getOutputContext(contextOptions) { + let helpWidth; + return { error: false, write: () => {}, helpWidth }; + } +} +module.exports = { Command }; +`; + +const MS_LIKE = `/** + * @param {String|Number} val + * @param {Object} [options] + * @return {String|Number} + */ +module.exports = function (val, options) { + return parse(val); +}; + +/** + * @param {String} str + * @return {Number} + */ +function parse(str) { + if (str.length > 100) { + return; + } + return 1; +} +`; + +describe("JSDoc", () => { + test("a @typedef property widens", () => { + const r = patch("lib/command.js", COMMANDER_LIKE, { "HelpContext.helpWidth": "| undefined" }); + expect(r.text).toContain("@property {number | undefined} helpWidth"); + expect(r.text).toContain("@returns {HelpContext}"); + expect(r.matches).toMatchObject([ + { line: 6, before: "number", after: "number | undefined", redundant: false }, + ]); + }); + + test("returns of export= and of a file-local function widen", () => { + const r = patch("index.js", MS_LIKE, { + "export=()": "| undefined", + "index.js:parse()": "| undefined", + }); + expect(r.text).toContain("@return {String|Number | undefined}\n */\nmodule.exports"); + expect(r.text).toContain("@return {Number | undefined}\n */\nfunction parse"); + expect(r.problems).toEqual([]); + }); + + test("a file prefix restricts the root to that file", () => { + const r = patch("other.js", MS_LIKE, { "index.js:parse()": "| undefined" }); + expect(r.matches).toEqual([]); + expect(r.text).toBe(MS_LIKE); + }); + + test("parameters, optional parameters and redundancy", () => { + const r = patch("index.js", MS_LIKE, { + "export=(options)": "| undefined", + "parse(str)": "string | undefined", + }); + expect(r.matches[0]).toMatchObject({ redundant: true }); + expect(r.text).toContain("@param {string | undefined} str"); + }); + + test("a replacement on an undocumented declaration inserts a tag", () => { + const text = + "/** Adds. */\nfunction add(a, b) { return a + b; }\nfunction sub(a, b) { return a - b; }\n"; + const r = patch("index.js", text, { "add()": "number", "sub(a)": "number" }); + expect(r.text).toBe( + "/** Adds. @returns {number} */\nfunction add(a, b) { return a + b; }\n/** @param {number} a */ function sub(a, b) { return a - b; }\n", + ); + }); + + test("a widening needs a written type", () => { + const r = patch("index.js", "function f() { return 1; }\n", { "f()": "| undefined" }); + expect(r.problems[0]!.message).toContain("no written type to widen"); + }); + + test("members of exported objects, classes and constructor assignments", () => { + const text = `class Box { + constructor() { + /** @type {string} */ + this.label = ""; + } + /** @returns {number} */ + size() { return 1; } +} +module.exports = { + /** @returns {string} */ + name() { return ""; }, + Box, +}; +`; + const r = patch("index.js", text, { + "Box.label": "| undefined", + "Box.size()": "| null", + "export=.name()": "| undefined", + }); + expect(r.text).toContain("@type {string | undefined} */\n this.label"); + expect(r.text).toContain("@returns {number | null} */\n size()"); + expect(r.text).toContain("@returns {string | undefined} */\n name()"); + }); + + test("function types are parenthesized before widening", () => { + const r = patch("index.js", "/** @type {(a: string) => void} */\nvar cb = null;\n", { + cb: "| undefined", + }); + expect(r.text).toContain("@type {((a: string) => void) | undefined}"); + }); +}); + +describe("declaration files", () => { + const DTS = `export interface Options { + name: string; + retries?: number; + env: Env; +} +export interface Env { + [key: string]: string; +} +export declare function fetchUser(id: string): Promise<{ email: string; id: string }>; +export declare function load(): User; +interface User { name: string; tags: string[] } +declare function ms(value: string): number; +declare function ms(value: number): string; +declare namespace ms { const version: string; } +export = ms; +`; + + test("members, optional members, index signatures and navigation", () => { + const r = patch("index.d.ts", DTS, { + "Options.name": "| undefined", + "Options.retries": "| undefined", + "Env[string]": "| undefined", + "fetchUser().email": "string | null", + "load().tags[]": "| undefined", + "Options.env[string]": "| null", + }); + expect(r.text).toContain(" name: string | undefined;"); + expect(r.text).toContain("[key: string]: string | undefined | null;"); + expect(r.text).toContain("Promise<{ email: string | null; id: string }>"); + expect(r.text).toContain("tags: (string | undefined)[]"); + expect(r.matches.find((m) => m.id === 1)).toMatchObject({ redundant: true }); + }); + + test("export= widens every overload, and a replacement of several is refused", () => { + const r = patch("index.d.ts", DTS, { "export=()": "| undefined" }); + expect(r.text).toContain("declare function ms(value: string): number | undefined;"); + expect(r.text).toContain("declare function ms(value: number): string | undefined;"); + const refused = patch("index.d.ts", DTS, { "export=()": "number" }); + expect(refused.problems[0]!.message).toContain("matches 2 declarations"); + expect(refused.text).toBe(DTS); + }); + + test("namespace members and generic types", () => { + const r = patch("index.d.ts", DTS, { "ms.version": "| undefined" }); + expect(r.text).toContain("const version: string | undefined;"); + const generic = patch( + "index.d.ts", + "export declare function f(): Map;\n", + { "f().a": "| undefined" }, + ); + expect(generic.problems[0]!.message).toContain("cannot follow"); + }); + + test("multi-line types keep their line count when replaced", () => { + const text = "export interface A {\n b: {\n c: number;\n };\n}\n"; + const r = patch("index.d.ts", text, { "A.b": "string" }); + expect(r.text.startsWith("export interface A {\n b: string\n\n;")).toBe(true); + }); +}); diff --git a/packages/compiler/src/frontend/type-patches/locate.ts b/packages/compiler/src/frontend/type-patches/locate.ts new file mode 100644 index 000000000..fc85a1634 --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/locate.ts @@ -0,0 +1,936 @@ +/* Finds the declarations a type patch selector names in one package file + * and computes the text edits that apply the patch. + * + * Declaration files (and TypeScript sources) are matched on their syntax + * tree: the edit replaces or widens a written type node. JavaScript is + * matched on its JSDoc: the edit replaces or widens the `{...}` of a + * `@typedef`'s `@property`, a `@returns`, a `@param` or a `@type` tag. + * Executable code is never edited, and no edit adds or removes a line + * break, so every later pass and diagnostic keeps its line. */ + +import * as ts from "../ts7/syntax.js"; +import { tokenStartOfNode } from "../ts7/node-functions.js"; +import { + normalizeType, + splitUnion, + type Selector, + type SelectorSegment, + type TypePatchOp, +} from "./selector.js"; + +export interface PatchRequest { + /** Caller's id, echoed in matches and problems. */ + id: number; + selector: Selector; + op: TypePatchOp; +} + +export interface PatchMatch { + id: number; + /** 1-based line of the patched type. */ + line: number; + before: string; + after: string; + /** The declaration already has the patched type: nothing was edited. */ + redundant: boolean; +} + +export interface PatchProblem { + id: number; + message: string; + /** Reported even when the selector matched elsewhere (an ambiguous + * replacement left a declaration unpatched). */ + always?: true; +} + +export interface PatchEdit { + start: number; + end: number; + text: string; +} + +export interface FilePatchResult { + edits: PatchEdit[]; + matches: PatchMatch[]; + problems: PatchProblem[]; +} + +/** A place a type is (or can be) written. */ +interface Slot { + /** The written type's span, or null when there is none. */ + span: { start: number; end: number } | null; + /** The declaration is optional (`x?:`, `[x]`), so it already admits + * undefined. */ + optional: boolean; + /** Where a replacement goes when nothing is written: the inserted text + * is `prefix + type + suffix`. */ + insert: { at: number; prefix: string; suffix: string } | null; + /** A JSDoc slot inside a comment (one-line edits only). */ + jsdoc: boolean; + /** The type is an array element (`T[]`): a union must be wrapped. */ + wrap?: boolean; +} + +type Step = { slots: Slot[] } | { problem: string }; + +/** Type texts that need parentheses before `| T` is appended. */ +function needsParens(text: string): boolean { + return /=>|\?|\bextends\b|^\s*new\b|^\s*function\b|^\s*!/.test(text); +} + +function lineOf(text: string, pos: number): number { + let line = 1; + for (let i = 0; i < pos && i < text.length; i++) if (text.charCodeAt(i) === 10) line++; + return line; +} + +function editsFor( + text: string, + req: PatchRequest, + slot: Slot, +): { edits: PatchEdit[]; match: PatchMatch } | { problem: string } { + const op = req.op; + if (slot.span === null) { + if (op.kind === "widen" || slot.insert === null) + return { + problem: `'${req.selector.text}' has no written type to widen; give the whole type instead`, + }; + const ins = slot.insert; + return { + edits: [{ start: ins.at, end: ins.at, text: ins.prefix + op.text + ins.suffix }], + match: { + id: req.id, + line: lineOf(text, ins.at), + before: "(none)", + after: op.text, + redundant: false, + }, + }; + } + const { start, end } = slot.span; + const original = text.slice(start, end); + const line = lineOf(text, start); + if (op.kind === "widen") { + const present = new Set(splitUnion(original).map(normalizeType)); + if (slot.optional) present.add("undefined"); + const add = op.arms.filter((a) => !present.has(normalizeType(a))); + if (add.length === 0) + return { + edits: [], + match: { id: req.id, line, before: original, after: original, redundant: true }, + }; + // `(() => void) | undefined`; an array element wraps the whole union: + // `(string | undefined)[]`. + const inner = needsParens(original); + const outer = slot.wrap === true; + const suffix = ` | ${add.join(" | ")}`; + const open = (outer ? "(" : "") + (inner ? "(" : ""); + const close = (inner ? ")" : "") + suffix + (outer ? ")" : ""); + const edits: PatchEdit[] = + open === "" + ? [{ start: end, end, text: close }] + : [ + { start, end: start, text: open }, + { start: end, end, text: close }, + ]; + const after = `${open}${original}${close}`; + return { edits, match: { id: req.id, line, before: original, after, redundant: false } }; + } + if (normalizeType(original) === normalizeType(op.text)) + return { + edits: [], + match: { id: req.id, line, before: original, after: original, redundant: true }, + }; + const breaks = original.split("\n").length - 1; + if (breaks > 0 && slot.jsdoc) + return { + problem: `'${req.selector.text}' names a JSDoc type that spans lines; replace it in one line upstream or widen it`, + }; + const replacement = + slot.wrap === true && (splitUnion(op.text).length > 1 || needsParens(op.text)) + ? `(${op.text})` + : op.text; + return { + edits: [{ start, end, text: replacement + "\n".repeat(breaks) }], + match: { id: req.id, line, before: original, after: replacement, redundant: false }, + }; +} + +/** Applies a file's requests. `sf` is the file's syntax tree, parsed as + * TypeScript for declaration files and TypeScript sources and as + * JavaScript otherwise. */ +export function patchFile( + relPath: string, + text: string, + sf: ts.SourceFile, + requests: readonly PatchRequest[], +): FilePatchResult { + const js = !/\.(d\.)?[mc]?ts$/.test(relPath); + const resolver = js ? new JsResolver(text, sf) : new TsResolver(text, sf); + const result: FilePatchResult = { edits: [], matches: [], problems: [] }; + for (const req of requests) { + if (req.selector.file !== null && req.selector.file !== relPath) continue; + const step = resolver.resolve(req.selector); + if ("problem" in step) { + result.problems.push({ id: req.id, message: step.problem }); + continue; + } + if (step.slots.length === 0) continue; + if (req.op.kind === "replace" && step.slots.length > 1) { + result.problems.push({ + id: req.id, + message: `'${req.selector.text}' matches ${step.slots.length} declarations in ${relPath}; a replacement must name one (widen them, or replace the declaration file)`, + always: true, + }); + continue; + } + for (const slot of step.slots) { + const out = editsFor(text, req, slot); + if ("problem" in out) { + result.problems.push({ id: req.id, message: out.problem }); + continue; + } + result.edits.push(...out.edits); + result.matches.push(out.match); + } + } + return result; +} + +/** Applies edits (sorted by position; at one position, in request order). + * An edit overlapping an earlier replacement is dropped. */ +export function applyPatchEdits(text: string, edits: readonly PatchEdit[]): string { + const sorted = edits + .map((e, i) => ({ e, i })) + .sort((a, b) => a.e.start - b.e.start || a.e.end - b.e.end || a.i - b.i) + .map((x) => x.e); + let out = ""; + let at = 0; + for (const e of sorted) { + if (e.start < at) continue; + out += text.slice(at, e.start) + e.text; + at = e.end; + } + return out + text.slice(at); +} + +function nameText(name: ts.Node | undefined): string | null { + if (name === undefined) return null; + if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isPrivateIdentifier(name)) + return name.text; + if (ts.isNumericLiteral(name)) return name.text; + return null; +} + +function hasDefaultModifier(node: ts.Node): boolean { + return (ts.getModifiers(node) ?? []).some((m) => m.kind === ts.SyntaxKind.DefaultKeyword); +} + +/* ── declaration files ─────────────────────────────────────────────── */ + +type TsPos = { node: ts.Node; optional: boolean; element?: boolean }; + +class TsResolver { + /** Top-level declarations by name, ambient module bodies included. */ + private readonly roots = new Map(); + private exportEquals: string | null = null; + private exportDefault: ts.Node[] = []; + + constructor( + private readonly text: string, + sf: ts.SourceFile, + ) { + this.collect(sf.statements.nodes, true); + } + + private collect(statements: readonly ts.Statement[], top: boolean): void { + for (const s of statements) { + if (ts.isExportAssignment(s)) { + if (ts.isIdentifier(s.expression)) { + if (s.isExportEquals) this.exportEquals = s.expression.text; + else this.exportDefault.push(...this.rootsNamed(s.expression.text)); + } + continue; + } + if (ts.isModuleDeclaration(s) && ts.isStringLiteral(s.name)) { + // `declare module "pkg" { ... }`: its statements are the module's. + if (top && s.body !== undefined && ts.isModuleBlock(s.body)) + this.collect(s.body.statements.nodes, false); + continue; + } + if (ts.isVariableStatement(s)) { + for (const d of s.declarationList.declarations.nodes) this.add(nameText(d.name), d); + continue; + } + const named = + ts.isFunctionDeclaration(s) || + ts.isClassDeclaration(s) || + ts.isInterfaceDeclaration(s) || + ts.isTypeAliasDeclaration(s) || + ts.isModuleDeclaration(s) || + ts.isEnumDeclaration(s); + if (!named) continue; + this.add(nameText((s as ts.FunctionDeclaration).name), s); + if ((ts.isFunctionDeclaration(s) || ts.isClassDeclaration(s)) && hasDefaultModifier(s)) + this.exportDefault.push(s); + } + } + + private add(name: string | null, node: ts.Node): void { + if (name === null) return; + const list = this.roots.get(name); + if (list === undefined) this.roots.set(name, [node]); + else list.push(node); + } + + private rootsNamed(name: string): ts.Node[] { + return this.roots.get(name) ?? []; + } + + resolve(sel: Selector): Step { + let start: ts.Node[]; + if (sel.root === "export=") + start = this.exportEquals === null ? [] : this.rootsNamed(this.exportEquals); + else if (sel.root === "default") start = this.exportDefault; + else start = this.rootsNamed(sel.root); + let positions: TsPos[] = start.map((node) => ({ node, optional: false })); + // A position that cannot continue is a problem only when no other + // position of the same name matches (a function merged with a + // namespace, an export and its declaration). + let problem: string | null = null; + for (const seg of sel.segments) { + const next: TsPos[] = []; + for (const pos of positions) { + const step = this.step(pos, seg); + if (typeof step === "string") problem ??= step; + else next.push(...step); + } + positions = next; + if (positions.length === 0) + return problem === null ? { slots: [] } : { problem: `'${sel.text}' ${problem}` }; + } + const slots: Slot[] = []; + for (const pos of positions) { + const slot = this.slotOf(pos); + if (typeof slot === "string") problem ??= slot; + else slots.push(slot); + } + if (slots.length === 0 && problem !== null) return { problem: `'${sel.text}' ${problem}` }; + return { slots }; + } + + private typeSlot(type: ts.TypeNode | undefined, optional: boolean): Slot { + return { + span: type === undefined ? null : { start: tokenStartOfNode(type), end: type.end }, + optional, + insert: null, + jsdoc: false, + }; + } + + private slotOf(pos: TsPos): Slot | string { + const n = pos.node; + if (ts.isTypeNode(n)) return { ...this.typeSlot(n, pos.optional), wrap: pos.element === true }; + if (ts.isPropertySignature(n) || ts.isPropertyDeclaration(n)) + return this.typeSlot( + n.type, + pos.optional || n.postfixToken?.kind === ts.SyntaxKind.QuestionToken, + ); + if ( + ts.isVariableDeclaration(n) || + ts.isTypeAliasDeclaration(n) || + ts.isIndexSignatureDeclaration(n) + ) + return this.typeSlot(n.type, false); + if (ts.isParameter(n)) + return this.typeSlot(n.type, n.questionToken !== undefined || n.initializer !== undefined); + if (ts.isGetAccessor(n)) return this.typeSlot(n.type, false); + if (this.isFunctionLike(n)) + return "names a function; add '()' for its return type or '(name)' for a parameter"; + return "names a declaration without a type of its own; name one of its members"; + } + + private isFunctionLike(n: ts.Node): boolean { + return ( + ts.isFunctionDeclaration(n) || + ts.isMethodSignatureDeclaration(n) || + ts.isMethodDeclaration(n) || + ts.isCallSignatureDeclaration(n) || + ts.isFunctionTypeNode(n) + ); + } + + /** The type a position stands for when a segment navigates into it. */ + private typeOf(n: ts.Node): ts.TypeNode | undefined { + if ( + ts.isPropertySignature(n) || + ts.isPropertyDeclaration(n) || + ts.isVariableDeclaration(n) || + ts.isTypeAliasDeclaration(n) || + ts.isParameter(n) + ) + return n.type; + return undefined; + } + + /** Object-like members a type node holds, following parentheses, unions, + * intersections, `Promise` and references to declarations in this + * file. Null when it passes through a type it cannot follow. */ + private containers(type: ts.TypeNode, depth = 0): ts.Node[] | null { + if (depth > 8) return null; + if (ts.isTypeLiteralNode(type)) return [type]; + if (ts.isParenthesizedTypeNode(type)) return this.containers(type.type, depth + 1); + if (ts.isUnionTypeNode(type) || ts.isIntersectionTypeNode(type)) { + const out: ts.Node[] = []; + for (const arm of type.types.nodes) { + if (arm.kind === ts.SyntaxKind.LiteralType || this.isKeywordType(arm)) continue; + const c = this.containers(arm, depth + 1); + if (c === null) return null; + out.push(...c); + } + return out; + } + if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)) { + const name = type.typeName.text; + const args = type.typeArguments?.nodes ?? []; + if ((name === "Promise" || name === "PromiseLike") && args.length === 1) + return this.containers(args[0]!, depth + 1); + if (args.length > 0) return null; + const decls = this.rootsNamed(name); + if (decls.length === 0) return null; + const out: ts.Node[] = []; + for (const d of decls) { + if (ts.isTypeAliasDeclaration(d)) { + const c = this.containers(d.type, depth + 1); + if (c === null) return null; + out.push(...c); + } else out.push(d); + } + return out; + } + return null; + } + + private isKeywordType(n: ts.Node): boolean { + return ( + n.kind === ts.SyntaxKind.NullKeyword || + n.kind === ts.SyntaxKind.UndefinedKeyword || + n.kind === ts.SyntaxKind.StringKeyword || + n.kind === ts.SyntaxKind.NumberKeyword || + n.kind === ts.SyntaxKind.BooleanKeyword + ); + } + + private membersOf(n: ts.Node): readonly ts.Node[] | null { + if (ts.isInterfaceDeclaration(n) || ts.isTypeLiteralNode(n) || ts.isClassDeclaration(n)) + return n.members.nodes; + if (ts.isModuleDeclaration(n)) + return n.body !== undefined && ts.isModuleBlock(n.body) ? n.body.statements.nodes : []; + return null; + } + + /** Expands a position into the object-like nodes that hold members. */ + private holders(pos: TsPos): ts.Node[] | string { + const n = pos.node; + if (this.membersOf(n) !== null) return [n]; + if (ts.isTypeAliasDeclaration(n) || ts.isTypeNode(n) || this.typeOf(n) !== undefined) { + const type = ts.isTypeNode(n) ? n : this.typeOf(n)!; + const c = this.containers(type); + if (c === null) + return "passes through a type it cannot follow (a generic, mapped, conditional or imported type); name the declared type itself"; + return c; + } + if (ts.isFunctionDeclaration(n) || ts.isEnumDeclaration(n)) return []; + return "has a member segment after a declaration without members"; + } + + private signaturesOf(pos: TsPos): ts.Node[] | string { + const n = pos.node; + if (this.isFunctionLike(n)) return [n]; + if (ts.isInterfaceDeclaration(n) || ts.isTypeLiteralNode(n)) + return n.members.nodes.filter((m) => ts.isCallSignatureDeclaration(m)); + const type = ts.isTypeNode(n) ? n : this.typeOf(n); + if (type !== undefined) { + let t: ts.TypeNode = type; + while (ts.isParenthesizedTypeNode(t)) t = t.type; + if (ts.isFunctionTypeNode(t)) return [t]; + const c = this.containers(t); + if (c === null) + return "passes through a type it cannot follow; name the declared type itself"; + return c.flatMap((h) => + ts.isInterfaceDeclaration(h) || ts.isTypeLiteralNode(h) + ? h.members.nodes.filter((m) => ts.isCallSignatureDeclaration(m)) + : [], + ); + } + if (ts.isModuleDeclaration(n) || ts.isClassDeclaration(n)) return []; + return "has '()' after a declaration that is not a function"; + } + + private step(pos: TsPos, seg: SelectorSegment): TsPos[] | string { + switch (seg.kind) { + case "member": { + const holders = this.holders(pos); + if (typeof holders === "string") return holders; + const out: TsPos[] = []; + for (const h of holders) { + const members = this.membersOf(h) ?? []; + for (const m of members) { + if (ts.isVariableStatement(m)) { + for (const d of m.declarationList.declarations.nodes) + if (nameText(d.name) === seg.name) out.push({ node: d, optional: false }); + continue; + } + if (nameText((m as ts.PropertySignatureDeclaration).name) === seg.name) + out.push({ node: m, optional: false }); + } + } + return out; + } + case "return": + case "param": { + const sigs = this.signaturesOf(pos); + if (typeof sigs === "string") return sigs; + const out: TsPos[] = []; + for (const s of sigs) { + const sig = s as ts.FunctionDeclaration; + if (seg.kind === "return") { + if (sig.type !== undefined) out.push({ node: sig.type, optional: false }); + else return "has no written return type"; + } else { + for (const p of sig.parameters.nodes) + if (nameText(p.name) === seg.name) out.push({ node: p, optional: false }); + } + } + return out; + } + case "element": { + let t: ts.TypeNode | undefined = ts.isTypeNode(pos.node) ? pos.node : this.typeOf(pos.node); + while (t !== undefined && ts.isParenthesizedTypeNode(t)) t = t.type; + if (t !== undefined && ts.isArrayTypeNode(t)) + return [{ node: t.elementType, optional: false, element: true }]; + if ( + t !== undefined && + ts.isTypeReferenceNode(t) && + ts.isIdentifier(t.typeName) && + (t.typeName.text === "Array" || t.typeName.text === "ReadonlyArray") && + t.typeArguments?.nodes.length === 1 + ) + return [{ node: t.typeArguments.nodes[0]!, optional: false }]; + return "has '[]' after a type that is not an array"; + } + case "index": { + const holders = this.holders(pos); + if (typeof holders === "string") return holders; + const want = + seg.key === "string" ? ts.SyntaxKind.StringKeyword : ts.SyntaxKind.NumberKeyword; + const out: TsPos[] = []; + for (const h of holders) + for (const m of this.membersOf(h) ?? []) + if (ts.isIndexSignatureDeclaration(m) && m.parameters.nodes[0]?.type?.kind === want) + out.push({ node: m, optional: false }); + return out; + } + } + } +} + +/* ── JSDoc in JavaScript ───────────────────────────────────────────── */ + +interface JsDocTag { + tag: string; + /** Inner span of the `{...}` type, or null. */ + type: { start: number; end: number } | null; + /** Parameter or property name (brackets and defaults removed). */ + name: string | null; + optional: boolean; + /** Offset of the `@`. */ + at: number; +} + +interface JsDocComment { + start: number; + end: number; + tags: JsDocTag[]; +} + +function parseJsDoc(text: string, start: number, end: number): JsDocComment { + const tags: JsDocTag[] = []; + const re = /(^|[\s*])@([A-Za-z]+)/g; + re.lastIndex = start + 3; + const body = text.slice(0, end - 2); + for (let m = re.exec(body); m !== null; m = re.exec(body)) { + const at = m.index + m[1]!.length; + const tag = m[2]!; + let i = at + 1 + tag.length; + while (i < body.length && (body[i] === " " || body[i] === "\t")) i++; + let type: JsDocTag["type"] = null; + if (body[i] === "{") { + let depth = 0; + let j = i; + for (; j < body.length; j++) { + if (body[j] === "{") depth++; + else if (body[j] === "}") { + depth--; + if (depth === 0) break; + } + } + if (j >= body.length) continue; + type = { start: i + 1, end: j }; + // Trim the inner span to the type text. + while (type.start < type.end && /\s/.test(text[type.start]!)) type.start++; + while (type.end > type.start && /\s/.test(text[type.end - 1]!)) type.end--; + i = j + 1; + re.lastIndex = i; + } + let name: string | null = null; + let optional = false; + const rest = /^[ \t]*(\[)?[ \t]*([A-Za-z_$][\w$.]*)/.exec(body.slice(i, i + 200)); + if (rest !== null) { + name = rest[2]!; + optional = rest[1] !== undefined; + } + if (type !== null && text[type.end - 1] === "=") optional = true; + tags.push({ tag, type, name, optional, at }); + } + return { start, end, tags }; +} + +const RETURN_TAGS = new Set(["returns", "return"]); +const PROPERTY_TAGS = new Set(["property", "prop"]); + +/** A declaration in JavaScript a selector can stand on. */ +type JsPos = + | { kind: "decl"; node: ts.Node; owner: ts.Node; fn: ts.Node | null; members: ts.Node | null } + | { kind: "typedef"; comment: JsDocComment; tagIndex: number; prefix: string } + | { kind: "slot"; slot: Slot }; + +class JsResolver { + private readonly comments: JsDocComment[] = []; + private readonly roots = new Map(); + private readonly exportEquals: JsPos[] = []; + private readonly exportDefault: JsPos[] = []; + + constructor( + private readonly text: string, + sf: ts.SourceFile, + ) { + const re = /\/\*\*(?!\/)[\s\S]*?\*\//g; + for (let m = re.exec(text); m !== null; m = re.exec(text)) + this.comments.push(parseJsDoc(text, m.index, m.index + m[0].length)); + for (const c of this.comments) + c.tags.forEach((t, i) => { + if ((t.tag === "typedef" || t.tag === "callback") && t.name !== null) + this.add(t.name, { kind: "typedef", comment: c, tagIndex: i, prefix: "" }); + }); + for (const s of sf.statements.nodes) this.collect(s); + } + + private add(name: string, pos: JsPos): void { + const list = this.roots.get(name); + if (list === undefined) this.roots.set(name, [pos]); + else list.push(pos); + } + + /** The position of a declaration whose value is `value` (or none). */ + private declPos(node: ts.Node, owner: ts.Node, value: ts.Node | undefined): JsPos { + let fn: ts.Node | null = null; + let members: ts.Node | null = null; + if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) fn = node; + else if (ts.isClassDeclaration(node)) members = node; + if (value !== undefined) { + let v = value; + while (ts.isParenthesizedExpression(v)) v = v.expression; + if (ts.isFunctionExpression(v) || ts.isArrowFunction(v)) fn = v; + else if (ts.isClassExpression(v) || ts.isObjectLiteralExpression(v)) members = v; + } + return { kind: "decl", node, owner, fn, members }; + } + + private collect(s: ts.Statement): void { + if (ts.isFunctionDeclaration(s) || ts.isClassDeclaration(s)) { + const pos = this.declPos(s, s, undefined); + const name = nameText(s.name); + if (name !== null) this.add(name, pos); + if (hasDefaultModifier(s)) this.exportDefault.push(pos); + return; + } + if (ts.isVariableStatement(s)) { + const decls = s.declarationList.declarations.nodes; + for (const d of decls) { + const name = nameText(d.name); + if (name !== null) + this.add(name, this.declPos(d, decls.length === 1 ? s : d, d.initializer)); + } + return; + } + if (ts.isExportAssignment(s)) { + if (!s.isExportEquals) this.exportDefault.push(this.declPos(s, s, s.expression)); + return; + } + if (!ts.isExpressionStatement(s)) return; + const e = s.expression; + if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return; + const target = exportTarget(e.left); + if (target === null) return; + if (target === "export=") { + this.exportEquals.push(this.declPos(s, s, e.right)); + // `module.exports = name`: the named declaration is the export. + if (ts.isIdentifier(e.right)) { + const named = this.roots.get(e.right.text); + if (named !== undefined) this.exportEquals.push(...named); + } + } else if (ts.isIdentifier(e.right) && this.roots.has(e.right.text)) { + // `exports.name = name`: the named declaration is the export. + for (const pos of this.roots.get(e.right.text)!) + if (pos.kind === "decl") this.add(target, pos); + } else this.add(target, this.declPos(s, s, e.right)); + } + + /** The last JSDoc comment directly before a node's first token. */ + private docOf(owner: ts.Node): JsDocComment | null { + const start = tokenStartOfNode(owner); + for (let i = this.comments.length - 1; i >= 0; i--) { + const c = this.comments[i]!; + if (c.end > start) continue; + return /^\s*$/.test(this.text.slice(c.end, start)) ? c : null; + } + return null; + } + + /** Tags of a comment that belong to the declaration it documents, not + * to a `@typedef` or `@callback` block inside it. */ + private ownTags(c: JsDocComment): JsDocTag[] { + const out: JsDocTag[] = []; + let inBlock = false; + for (const t of c.tags) { + if (t.tag === "typedef" || t.tag === "callback") { + inBlock = true; + continue; + } + if (inBlock && (PROPERTY_TAGS.has(t.tag) || t.tag === "type")) continue; + if (inBlock && t.tag !== "param" && !RETURN_TAGS.has(t.tag)) inBlock = false; + if (inBlock) continue; + out.push(t); + } + return out; + } + + private tagSlot(tag: JsDocTag): Slot { + return { span: tag.type, optional: tag.optional, insert: null, jsdoc: true }; + } + + /** A slot for a tag of the declaration `owner` documents; when it has no + * such tag, a replacement inserts one. */ + private docSlot(owner: ts.Node, tagNames: ReadonlySet, name: string | null): Slot { + const doc = this.docOf(owner); + if (doc !== null) { + const tag = this.ownTags(doc).find( + (t) => tagNames.has(t.tag) && (name === null || t.name === name), + ); + if (tag !== undefined && tag.type !== null) return this.tagSlot(tag); + } + const tagName = [...tagNames][0]!; + const named = name === null ? "" : ` ${name}`; + if (doc !== null) { + const at = doc.end - 2; + const pad = /\s/.test(this.text[at - 1] ?? "") ? "" : " "; + return { + span: null, + optional: false, + insert: { at, prefix: `${pad}@${tagName} {`, suffix: `}${named} ` }, + jsdoc: true, + }; + } + return { + span: null, + optional: false, + insert: { at: tokenStartOfNode(owner), prefix: `/** @${tagName} {`, suffix: `}${named} */ ` }, + jsdoc: true, + }; + } + + resolve(sel: Selector): Step { + let positions: JsPos[]; + if (sel.root === "export=") positions = this.exportEquals; + else if (sel.root === "default") positions = this.exportDefault; + else positions = this.roots.get(sel.root) ?? []; + let problem: string | null = null; + for (const seg of sel.segments) { + const next: JsPos[] = []; + for (const pos of positions) { + const step = this.step(pos, seg); + if (typeof step === "string") problem ??= step; + else next.push(...step); + } + positions = next; + if (positions.length === 0) + return problem === null ? { slots: [] } : { problem: `'${sel.text}' ${problem}` }; + } + const slots: Slot[] = []; + for (const pos of positions) { + const slot = this.slotOf(pos); + if (typeof slot === "string") problem ??= slot; + else if (slot !== null) slots.push(slot); + } + if (slots.length === 0 && problem !== null) return { problem: `'${sel.text}' ${problem}` }; + return { slots }; + } + + private slotOf(pos: JsPos): Slot | string | null { + if (pos.kind === "slot") return pos.slot; + if (pos.kind === "typedef") { + const own = pos.comment.tags[pos.tagIndex]!; + if (pos.prefix === "") { + // A typedef's own type: only when it is not the `object` that + // its @property tags describe. + const typeTag = this.typedefTypeTag(pos); + if (typeTag === null || /^object$/i.test(this.text.slice(typeTag.start, typeTag.end))) + return "names a @typedef of an object; name one of its properties"; + return { span: typeTag, optional: false, insert: null, jsdoc: true }; + } + void own; + return null; + } + if (pos.fn !== null && pos.members === null) + return "names a function; add '()' for its return type or '(name)' for a parameter"; + if (pos.members !== null && !ts.isObjectLiteralExpression(pos.members)) + return "names a class; name one of its members"; + return this.docSlot(pos.owner, new Set(["type"]), null); + } + + private typedefTypeTag(pos: { + comment: JsDocComment; + tagIndex: number; + }): { start: number; end: number } | null { + const tags = pos.comment.tags; + const own = tags[pos.tagIndex]!; + if (own.type !== null) return own.type; + for (let i = pos.tagIndex + 1; i < tags.length; i++) { + const t = tags[i]!; + if (t.tag === "type") return t.type; + if (!PROPERTY_TAGS.has(t.tag)) break; + } + return null; + } + + private step(pos: JsPos, seg: SelectorSegment): JsPos[] | string { + // A JSDoc type is not navigated (a nested @property path continues + // from its typedef position instead). + if (pos.kind === "slot") return []; + if (pos.kind === "typedef") { + const tags = pos.comment.tags; + const own = tags[pos.tagIndex]!; + if (own.tag === "callback") { + if (seg.kind !== "return" && seg.kind !== "param") + return "has a member segment after a @callback"; + for (let i = pos.tagIndex + 1; i < tags.length; i++) { + const t = tags[i]!; + if (t.tag === "typedef" || t.tag === "callback") break; + if ( + seg.kind === "return" + ? RETURN_TAGS.has(t.tag) + : t.tag === "param" && t.name === seg.name + ) + return t.type === null ? [] : [{ kind: "slot", slot: this.tagSlot(t) }]; + } + return []; + } + if (seg.kind !== "member") return "can only name properties of a @typedef"; + const name = pos.prefix === "" ? seg.name : `${pos.prefix}.${seg.name}`; + for (let i = pos.tagIndex + 1; i < tags.length; i++) { + const t = tags[i]!; + if (t.tag === "typedef" || t.tag === "callback") break; + if (!PROPERTY_TAGS.has(t.tag) && t.tag !== "type") break; + if (PROPERTY_TAGS.has(t.tag) && t.name === name && t.type !== null) { + // A nested property path continues from here. + return [ + { kind: "slot", slot: this.tagSlot(t) }, + { ...pos, prefix: name }, + ]; + } + } + return []; + } + switch (seg.kind) { + case "return": + if (pos.fn === null) return "has '()' after a declaration that is not a function"; + return [{ kind: "slot", slot: this.docSlot(this.docOwner(pos), RETURN_TAGS, null) }]; + case "param": { + if (pos.fn === null) return "has '(name)' after a declaration that is not a function"; + const fn = pos.fn as ts.FunctionExpression; + if (!fn.parameters.nodes.some((p) => nameText(p.name) === seg.name)) return []; + return [ + { kind: "slot", slot: this.docSlot(this.docOwner(pos), new Set(["param"]), seg.name) }, + ]; + } + case "member": { + if (pos.members === null) return []; + return this.membersNamed(pos.members, seg.name); + } + default: + return "navigates into a JSDoc type; name the declared type itself"; + } + } + + /** The node whose leading comment documents a declaration. */ + private docOwner(pos: { owner: ts.Node }): ts.Node { + return pos.owner; + } + + private membersNamed(holder: ts.Node, name: string): JsPos[] { + const out: JsPos[] = []; + if (ts.isObjectLiteralExpression(holder)) { + for (const p of holder.properties.nodes) { + if (nameText((p as ts.PropertyAssignment).name) !== name) continue; + if (ts.isPropertyAssignment(p)) out.push(this.declPos(p, p, p.initializer)); + else if (ts.isMethodDeclaration(p)) out.push(this.declPos(p, p, undefined)); + // `{ parse }`: the declaration of that name. + else if (ts.isShorthandPropertyAssignment(p)) out.push(...(this.roots.get(name) ?? [])); + } + return out; + } + const cls = holder as ts.ClassDeclaration; + for (const m of cls.members.nodes) { + if (ts.isConstructorDeclaration(m) && m.body !== undefined) { + // `this.name = value` in the constructor, documented by its comment. + for (const s of m.body.statements.nodes) { + if (!ts.isExpressionStatement(s) || !ts.isBinaryExpression(s.expression)) continue; + const e = s.expression; + if ( + e.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(e.left) && + e.left.expression.kind === ts.SyntaxKind.ThisKeyword && + nameText(e.left.name) === name + ) + out.push(this.declPos(s, s, e.right)); + } + continue; + } + if (nameText((m as ts.MethodDeclaration).name) !== name) continue; + if (ts.isMethodDeclaration(m)) out.push(this.declPos(m, m, undefined)); + else if (ts.isPropertyDeclaration(m)) out.push(this.declPos(m, m, m.initializer)); + else if (ts.isGetAccessor(m)) + out.push({ kind: "decl", node: m, owner: m, fn: m, members: null }); + } + return out; + } +} + +/** `module.exports` → "export="; `exports.x` / `module.exports.x` → "x". */ +function exportTarget(left: ts.Expression): string | null { + if (!ts.isPropertyAccessExpression(left)) return null; + const obj = left.expression; + const prop = nameText(left.name); + if (prop === null) return null; + if (ts.isIdentifier(obj) && obj.text === "module" && prop === "exports") return "export="; + if (ts.isIdentifier(obj) && obj.text === "exports") return prop; + if ( + ts.isPropertyAccessExpression(obj) && + ts.isIdentifier(obj.expression) && + obj.expression.text === "module" && + nameText(obj.name) === "exports" + ) + return prop; + return null; +} diff --git a/packages/compiler/src/frontend/type-patches/selector.test.ts b/packages/compiler/src/frontend/type-patches/selector.test.ts new file mode 100644 index 000000000..716c994a8 --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/selector.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "vitest"; +import { parseSelector, parseTypeText, splitUnion } from "./selector.js"; + +describe("parseSelector", () => { + test("members, returns, parameters, elements and index signatures", () => { + expect(parseSelector("HelpContext.helpWidth")).toEqual({ + file: null, + root: "HelpContext", + segments: [{ kind: "member", name: "helpWidth" }], + text: "HelpContext.helpWidth", + }); + expect(parseSelector("index.js:parse()")).toMatchObject({ + file: "index.js", + root: "parse", + segments: [{ kind: "return" }], + }); + expect(parseSelector("export=()")).toMatchObject({ + root: "export=", + segments: [{ kind: "return" }], + }); + expect(parseSelector("fetchUser(id)")).toMatchObject({ + segments: [{ kind: "param", name: "id" }], + }); + expect(parseSelector("list()[].name")).toMatchObject({ + segments: [{ kind: "return" }, { kind: "element" }, { kind: "member", name: "name" }], + }); + expect(parseSelector("Env[string]")).toMatchObject({ + segments: [{ kind: "index", key: "string" }], + }); + }); + + test("malformed selectors say why", () => { + expect(parseSelector("")).toEqual({ error: "is empty" }); + expect(parseSelector("../x.js:f()")).toEqual({ error: "names a file outside the package" }); + expect(parseSelector("1abc")).toHaveProperty("error"); + expect(parseSelector("a.")).toHaveProperty("error"); + expect(parseSelector("a(1)")).toHaveProperty("error"); + }); +}); + +describe("parseTypeText", () => { + test("a leading bar widens, anything else replaces", () => { + expect(parseTypeText("| undefined")).toEqual({ kind: "widen", arms: ["undefined"] }); + expect(parseTypeText("| undefined | null")).toEqual({ + kind: "widen", + arms: ["undefined", "null"], + }); + expect(parseTypeText("string | null")).toEqual({ kind: "replace", text: "string | null" }); + expect(parseTypeText("a\nb")).toEqual({ error: "must be one line" }); + }); + + test("splitUnion keeps nested unions and arrows together", () => { + expect(splitUnion("(a | b) | (() => c | d) | { x: e | f }")).toEqual([ + "(a | b)", + "(() => c | d)", + "{ x: e | f }", + ]); + }); +}); diff --git a/packages/compiler/src/frontend/type-patches/selector.ts b/packages/compiler/src/frontend/type-patches/selector.ts new file mode 100644 index 000000000..8fdd8b67c --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/selector.ts @@ -0,0 +1,133 @@ +/* Type patch selectors: names of declarations in a package, written the + * same way for JSDoc in its JavaScript and for its declaration files. + * + * selector := [ file ":" ] root segment* + * root := identifier | "export=" | "default" + * segment := "." name member (interface, type alias, @typedef, + * class, namespace, object type) + * | "()" return type + * | "(" name ")" type of the named parameter + * | "[]" element type of an array type + * | "[string]" | "[number]" value type of an index signature + * + * Examples: `HelpContext.helpWidth`, `index.js:parse()`, `export=()`, + * `fetchUser().email`, `Env[string]`. */ + +export type SelectorSegment = + | { kind: "member"; name: string } + | { kind: "return" } + | { kind: "param"; name: string } + | { kind: "element" } + | { kind: "index"; key: "string" | "number" }; + +export interface Selector { + /** Package-relative file the root must be declared in, or null. */ + file: string | null; + /** A declared name, `export=` or `default`. */ + root: string; + segments: readonly SelectorSegment[]; + /** The selector as written. */ + text: string; +} + +const IDENT = /^[A-Za-z_$][\w$]*/; + +/** The parsed selector, or a message saying what is wrong with it. */ +export function parseSelector(text: string): Selector | { error: string } { + let rest = text.trim(); + if (rest === "") return { error: "is empty" }; + let file: string | null = null; + const colon = rest.indexOf(":"); + if (colon >= 0) { + file = rest.slice(0, colon).replace(/^\.\//, ""); + rest = rest.slice(colon + 1); + if (file === "" || file.startsWith("/") || file.split("/").includes("..")) + return { error: "names a file outside the package" }; + } + let root: string; + if (rest.startsWith("export=")) { + root = "export="; + rest = rest.slice("export=".length); + } else { + const m = IDENT.exec(rest); + if (m === null) return { error: "must start with a declared name, 'export=' or 'default'" }; + root = m[0]; + rest = rest.slice(root.length); + } + const segments: SelectorSegment[] = []; + while (rest !== "") { + if (rest.startsWith(".")) { + const m = IDENT.exec(rest.slice(1)); + if (m === null) return { error: `has no member name after '.' at '${rest}'` }; + segments.push({ kind: "member", name: m[0] }); + rest = rest.slice(1 + m[0].length); + } else if (rest.startsWith("()")) { + segments.push({ kind: "return" }); + rest = rest.slice(2); + } else if (rest.startsWith("(")) { + const m = /^\(\s*([A-Za-z_$][\w$]*)\s*\)/.exec(rest); + if (m === null) return { error: `has a malformed parameter at '${rest}'` }; + segments.push({ kind: "param", name: m[1]! }); + rest = rest.slice(m[0].length); + } else if (rest.startsWith("[]")) { + segments.push({ kind: "element" }); + rest = rest.slice(2); + } else if (rest.startsWith("[string]") || rest.startsWith("[number]")) { + segments.push({ kind: "index", key: rest.startsWith("[string]") ? "string" : "number" }); + rest = rest.slice("[string]".length); + } else { + return { error: `has an unexpected '${rest}'` }; + } + } + return { file, root, segments, text: text.trim() }; +} + +/** A widening (`| undefined`) or a replacement type. */ +export type TypePatchOp = + | { kind: "widen"; arms: readonly string[] } + | { kind: "replace"; text: string }; + +/** The operation a patch's type text stands for, or a message. */ +export function parseTypeText(text: string): TypePatchOp | { error: string } { + if (/[\r\n]/.test(text)) return { error: "must be one line" }; + const t = text.trim(); + if (t === "") return { error: "is empty" }; + if (t.startsWith("|")) { + const arms = splitUnion(t.slice(1)); + if (arms.some((a) => a === "")) return { error: "has an empty union member" }; + return { kind: "widen", arms }; + } + return { kind: "replace", text: t }; +} + +/** The top-level union members of a type text, trimmed. */ +export function splitUnion(text: string): string[] { + const out: string[] = []; + let depth = 0; + let quote: string | null = null; + let start = 0; + for (let i = 0; i < text.length; i++) { + const ch = text[i]!; + if (quote !== null) { + if (ch === "\\") i++; + else if (ch === quote) quote = null; + continue; + } + if (ch === '"' || ch === "'" || ch === "`") quote = ch; + else if (ch === "(" || ch === "[" || ch === "{" || ch === "<") depth++; + else if (ch === ")" || ch === "]" || ch === "}" || ch === ">") { + // `=>` is not a closing bracket. + if (!(ch === ">" && text[i - 1] === "=")) depth--; + } else if (ch === "|" && depth === 0) { + out.push(text.slice(start, i).trim()); + start = i + 1; + } + } + out.push(text.slice(start).trim()); + return out; +} + +/** Whitespace-insensitive text of a type for comparisons. */ +export function normalizeType(text: string): string { + return text.replace(/\s+/g, ""); +} diff --git a/packages/compiler/src/frontend/type-patches/semver.test.ts b/packages/compiler/src/frontend/type-patches/semver.test.ts new file mode 100644 index 000000000..83663e3a2 --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/semver.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "vitest"; +import { isValidRange, satisfies } from "./semver.js"; + +describe("satisfies", () => { + test.each([ + ["15.0.0", "^15.0.0", true], + ["15.4.2", "^15.0.0", true], + ["16.0.0", "^15.0.0", false], + ["0.2.5", "^0.2.3", true], + ["0.3.0", "^0.2.3", false], + ["0.0.3", "^0.0.3", true], + ["0.0.4", "^0.0.3", false], + ["1.2.9", "~1.2.3", true], + ["1.3.0", "~1.2.3", false], + ["1.9.0", "~1", true], + ["2.1.3", "2.1.x", true], + ["2.2.0", "2.1.x", false], + ["2.1.3", "2", true], + ["3.0.0", "2", false], + ["2.1.3", "*", true], + ["1.2.3", "1.2.3", true], + ["1.2.4", "=1.2.3", false], + ["1.5.0", ">=1.2.0 <2.0.0", true], + ["2.0.0", ">=1.2.0 <2.0.0", false], + ["2.0.0", ">= 1.2.0", true], + ["1.3.0", ">1.2", true], + ["1.2.9", ">1.2", false], + ["1.2.9", "<=1.2", true], + ["1.3.0", "<=1.2", false], + ["2.3.9", "1.2.3 - 2.3", true], + ["2.4.0", "1.2.3 - 2.3", false], + ["3.1.0", "^1.0.0 || ^3.0.0", true], + ["2.1.0", "^1.0.0 || ^3.0.0", false], + ["2.0.0-beta.1", "^1.0.0", false], + ["2.0.0-beta.1", "^2.0.0", false], + ["2.0.0-beta.2", "^2.0.0-beta.1", true], + ["2.0.0-alpha", "^2.0.0-beta.1", false], + ["2.1.0-beta.2", "^2.0.0-beta.1", false], + ])("%s satisfies %s: %s", (version, range, expected) => { + expect(satisfies(version, range)).toBe(expected); + }); + + test("malformed input never satisfies", () => { + expect(satisfies("not-a-version", "^1.0.0")).toBe(false); + expect(isValidRange("^banana")).toBe(false); + expect(isValidRange(">=1 <2 || 3.x")).toBe(true); + }); +}); diff --git a/packages/compiler/src/frontend/type-patches/semver.ts b/packages/compiler/src/frontend/type-patches/semver.ts new file mode 100644 index 000000000..86e9d1183 --- /dev/null +++ b/packages/compiler/src/frontend/type-patches/semver.ts @@ -0,0 +1,229 @@ +/* The npm semver range subset type patches key on (`commander@^15.0.0`): + * exact versions, x-ranges, `^`, `~`, comparators, hyphen ranges and `||`. + * A prerelease version satisfies a comparator set only when one of the + * set's comparators names a prerelease of the same major.minor.patch, as + * npm does. */ + +interface Version { + major: number; + minor: number; + patch: number; + pre: readonly (string | number)[]; +} + +interface Comparator { + op: "<" | "<=" | ">" | ">=" | "="; + version: Version; +} + +/** A partial version: missing or wildcard parts are null. */ +interface Partial { + major: number | null; + minor: number | null; + patch: number | null; + pre: readonly (string | number)[]; +} + +const VERSION = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/; + +function preParts(text: string | undefined): (string | number)[] { + if (text === undefined || text === "") return []; + return text.split(".").map((p) => (/^\d+$/.test(p) ? Number(p) : p)); +} + +/** A full version, or null. */ +export function parseVersion(text: string): Version | null { + const m = VERSION.exec(text.trim()); + if (m === null) return null; + return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), pre: preParts(m[4]) }; +} + +function parsePartial(text: string): Partial | null { + const m = + /^v?(\d+|[xX*])(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec( + text, + ); + if (m === null) return null; + const part = (s: string | undefined): number | null => + s === undefined || s === "x" || s === "X" || s === "*" ? null : Number(s); + const major = part(m[1]); + const minor = major === null ? null : part(m[2]); + const patch = minor === null ? null : part(m[3]); + return { major, minor, patch, pre: patch === null ? [] : preParts(m[4]) }; +} + +function v( + major: number, + minor: number, + patch: number, + pre: readonly (string | number)[] = [], +): Version { + return { major, minor, patch, pre }; +} + +/** `-0` marks the lowest prerelease, so `<2.0.0-0` excludes 2.0.0's + * prereleases as npm's desugaring does. */ +const LOWEST: readonly (string | number)[] = [0]; + +function comparePre(a: readonly (string | number)[], b: readonly (string | number)[]): number { + if (a.length === 0 && b.length === 0) return 0; + if (a.length === 0) return 1; + if (b.length === 0) return -1; + for (let i = 0; i < Math.max(a.length, b.length); i++) { + const x = a[i]; + const y = b[i]; + if (x === undefined) return -1; + if (y === undefined) return 1; + if (x === y) continue; + if (typeof x === "number" && typeof y === "number") return x < y ? -1 : 1; + if (typeof x === "number") return -1; + if (typeof y === "number") return 1; + return x < y ? -1 : 1; + } + return 0; +} + +export function compareVersions(a: Version, b: Version): number { + if (a.major !== b.major) return a.major < b.major ? -1 : 1; + if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1; + if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1; + return comparePre(a.pre, b.pre); +} + +/** The comparators a partial with an operator stands for. */ +function desugar(op: string, p: Partial): Comparator[] | null { + const { major, minor, patch } = p; + if (op === "^") { + if (major === null) return []; + const lo = v(major, minor ?? 0, patch ?? 0, p.pre); + let hi: Version; + if (major > 0 || minor === null) hi = v(major + 1, 0, 0, LOWEST); + else if (minor > 0 || patch === null) hi = v(0, minor + 1, 0, LOWEST); + else hi = v(0, 0, patch + 1, LOWEST); + return [ + { op: ">=", version: lo }, + { op: "<", version: hi }, + ]; + } + if (op === "~") { + if (major === null) return []; + const lo = v(major, minor ?? 0, patch ?? 0, p.pre); + const hi = minor === null ? v(major + 1, 0, 0, LOWEST) : v(major, minor + 1, 0, LOWEST); + return [ + { op: ">=", version: lo }, + { op: "<", version: hi }, + ]; + } + if (op === "" || op === "=") { + if (major === null) return []; + if (minor === null) + return [ + { op: ">=", version: v(major, 0, 0) }, + { op: "<", version: v(major + 1, 0, 0, LOWEST) }, + ]; + if (patch === null) + return [ + { op: ">=", version: v(major, minor, 0) }, + { op: "<", version: v(major, minor + 1, 0, LOWEST) }, + ]; + return [{ op: "=", version: v(major, minor, patch, p.pre) }]; + } + if (major === null) + return op === "<" || op === ">" ? [{ op: "<", version: v(0, 0, 0, LOWEST) }] : []; + if (op === ">") { + if (minor === null) return [{ op: ">=", version: v(major + 1, 0, 0) }]; + if (patch === null) return [{ op: ">=", version: v(major, minor + 1, 0) }]; + return [{ op: ">", version: v(major, minor, patch, p.pre) }]; + } + if (op === ">=") return [{ op: ">=", version: v(major, minor ?? 0, patch ?? 0, p.pre) }]; + if (op === "<") { + if (minor === null) return [{ op: "<", version: v(major, 0, 0, LOWEST) }]; + if (patch === null) return [{ op: "<", version: v(major, minor, 0, LOWEST) }]; + return [{ op: "<", version: v(major, minor, patch, p.pre) }]; + } + if (op === "<=") { + if (minor === null) return [{ op: "<", version: v(major + 1, 0, 0, LOWEST) }]; + if (patch === null) return [{ op: "<", version: v(major, minor + 1, 0, LOWEST) }]; + return [{ op: "<=", version: v(major, minor, patch, p.pre) }]; + } + return null; +} + +/** One `||` alternative: a list of comparators, or null when malformed. */ +function parseSet(text: string): Comparator[] | null { + const t = text.trim(); + const hyphen = /^(\S+)\s+-\s+(\S+)$/.exec(t); + if (hyphen !== null) { + const lo = parsePartial(hyphen[1]!); + const hi = parsePartial(hyphen[2]!); + if (lo === null || hi === null) return null; + return [...(desugar(">=", lo) ?? []), ...(desugar("<=", hi) ?? [])]; + } + if (t === "") return []; + const out: Comparator[] = []; + // `>= 1.2.3` may carry a space between the operator and the version. + const tokens = t.replace(/(<=|>=|<|>|=|\^|~)\s+/g, "$1").split(/\s+/); + for (const token of tokens) { + const m = /^(<=|>=|<|>|=|\^|~>?|)(.*)$/.exec(token); + if (m === null) return null; + const op = m[1] === "~>" ? "~" : m[1]!; + const p = parsePartial(m[2]!); + if (p === null) return null; + const cs = desugar(op, p); + if (cs === null) return null; + out.push(...cs); + } + return out; +} + +/** The parsed range, or null when it is not a semver range. */ +export function parseRange(range: string): Comparator[][] | null { + const sets: Comparator[][] = []; + for (const alt of range.split("||")) { + const set = parseSet(alt); + if (set === null) return null; + sets.push(set); + } + return sets; +} + +export function isValidRange(range: string): boolean { + return parseRange(range) !== null; +} + +function test(c: Comparator, version: Version): boolean { + const cmp = compareVersions(version, c.version); + switch (c.op) { + case "<": + return cmp < 0; + case "<=": + return cmp <= 0; + case ">": + return cmp > 0; + case ">=": + return cmp >= 0; + default: + return cmp === 0; + } +} + +/** True when `version` satisfies `range` (false for malformed input). */ +export function satisfies(version: string, range: string): boolean { + const ver = parseVersion(version); + const sets = parseRange(range); + if (ver === null || sets === null) return false; + return sets.some((set) => { + if (!set.every((c) => test(c, ver))) return false; + if (ver.pre.length === 0) return true; + // A prerelease only matches a set that names a prerelease of the same + // version tuple (the LOWEST markers of upper bounds do not count). + return set.some( + (c) => + c.version.pre.length > 0 && + c.version.pre !== LOWEST && + c.version.major === ver.major && + c.version.minor === ver.minor && + c.version.patch === ver.patch, + ); + }); +} diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 3e4871084..ffd56a1bf 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -52,7 +52,7 @@ import { InternalCompilerError } from "./errors.js"; 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 { existsSync, readFileSync } from "node:fs"; 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"; @@ -119,6 +119,7 @@ import { ffiNativeBuildDiag, iceDiag, nativeCodegenDiag, + isTypePatchCode, type ScrDiagnostic, } from "./diagnostics/diagnostic.js"; import { loadLibraryProfile, type LibraryProfile } from "./library/library-profile.js"; @@ -485,12 +486,31 @@ interface CompileWarningSink { sourceTexts: Map | null; } +/** The package.json texts type patch warnings point into, re-read for a + * cache hit's code frames (performance warnings need none). */ +function cachedWarningSources(warnings: readonly ScrDiagnostic[]): Map { + const texts = new Map(); + for (const w of warnings) { + if (!isTypePatchCode(w.code) || texts.has(w.loc.file)) continue; + try { + texts.set(w.loc.file, readFileSync(w.loc.file, "utf8")); + } catch { + // The frame is omitted; the message stands alone. + } + } + return texts; +} + /** 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)); + // Type patch warnings (SC8030–SC8033) describe the same unchanged inputs, + // so a hit repeats them too. + const performance = (sink?.warnings ?? []).filter( + (d) => isPerformanceCode(d.code) || isTypePatchCode(d.code), + ); return performance.length === 0 ? {} : { performance }; } @@ -1602,7 +1622,7 @@ async function compileTracked( // names is just as untyped as when it was built. if (warningSink !== undefined && earlyHit.performance !== undefined) { warningSink.warnings = earlyHit.performance; - warningSink.sourceTexts = new Map(); + warningSink.sourceTexts = cachedWarningSources(earlyHit.performance); } const executableCacheOptions = earlyCacheOptions; // Route/proof metadata is independently evictable. A full-compiler diff --git a/packages/compiler/src/project-config.test.ts b/packages/compiler/src/project-config.test.ts index 58ae914ca..04a5d2e0b 100644 --- a/packages/compiler/src/project-config.test.ts +++ b/packages/compiler/src/project-config.test.ts @@ -164,3 +164,80 @@ test("package setting keys named like Object.prototype members are unknown setti ]); expect(c.packages.has("debug")).toBe(false); }); + +test("typePatches: entries keyed by package and range", () => { + const c = config({ + typePatches: { + "commander@^15.0.0": { declarations: { "HelpContext.helpWidth": "| undefined" } }, + "@scope/lib@1.x || 2.x": { + declarations: { "Options.name": "string | null" }, + note: "https://example.invalid/issue/1", + }, + }, + }); + expect(c.diagnostics).toEqual([]); + expect( + c.typePatches.map(({ key, pkg, range, declarations }) => ({ + key, + pkg, + range, + declarations: declarations.map((d) => [d.selector, d.type]), + })), + ).toEqual([ + { + key: "commander@^15.0.0", + pkg: "commander", + range: "^15.0.0", + declarations: [["HelpContext.helpWidth", "| undefined"]], + }, + { + key: "@scope/lib@1.x || 2.x", + pkg: "@scope/lib", + range: "1.x || 2.x", + declarations: [["Options.name", "string | null"]], + }, + ]); + expect(c.typePatches[0]!.declarations[0]!.offset).toBeGreaterThan(0); +}); + +test("typePatches: malformed keys, ranges, selectors and types are SC8004", () => { + const c = config({ + typePatches: { + commander: { declarations: { "A.b": "| undefined" } }, + "ms@^banana": { declarations: {} }, + "@types/node@*": { declarations: {} }, + "typescript@5": { declarations: {} }, + "lib@^1.0.0": { + declarations: { + "1bad": "| undefined", + "A.b": 3, + "A.c": "string\nnumber", + "A.d": "| undefined", + }, + files: {}, + }, + "other@^1.0.0": [], + }, + }); + expect(c.diagnostics.every((d) => d.code === "SC8004")).toBe(true); + expect(c.diagnostics.map((d) => d.message)).toEqual([ + "package.json 'scriptc.typePatches.commander' must name a package and a version range", + `package.json 'scriptc.typePatches.ms@^banana' has the version range "^banana", which is not a semver range`, + "package.json 'scriptc.typePatches.@types/node@*' patches @types/node, whose declarations ship with scriptc or TypeScript", + "package.json 'scriptc.typePatches.typescript@5' patches typescript, whose declarations ship with scriptc or TypeScript", + "package.json 'scriptc.typePatches.lib@^1.0.0.files' is not a known type patch setting", + "package.json 'scriptc.typePatches.lib@^1.0.0.declarations.1bad' is not a selector: it must start with a declared name, 'export=' or 'default'", + "package.json 'scriptc.typePatches.lib@^1.0.0.declarations.A.b' must map to a type string, not 3", + "package.json 'scriptc.typePatches.lib@^1.0.0.declarations.A.c' has a type that must be one line", + "package.json 'scriptc.typePatches.other@^1.0.0' must be an object, not an array", + ]); + // The valid selector of an entry with an unknown key is dropped with it; + // entries with only bad selectors keep their valid ones. + expect(c.typePatches).toEqual([]); + const partial = config({ + typePatches: { + "lib@^1.0.0": { declarations: { "1bad": "| undefined", "A.d": "| undefined" } }, + }, + }); + expect(partial.typePatches[0]!.declarations.map((d) => d.selector)).toEqual(["A.d"]); +}); diff --git a/packages/compiler/src/project-config.ts b/packages/compiler/src/project-config.ts index 997e997c1..c43a6bd2c 100644 --- a/packages/compiler/src/project-config.ts +++ b/packages/compiler/src/project-config.ts @@ -13,7 +13,12 @@ * "proper-lockfile": { "run": "node", "evaluate": "lazy" } * }, * "nodeIslands": { "auto": true, "modules": ["src/plugins/*.ts"] }, - * "wasm": ["build/module.wasm"] + * "wasm": ["build/module.wasm"], + * "typePatches": { // frontend/type-patches/layer.ts + * "commander@^15.0.0": { + * "declarations": { "HelpContext.helpWidth": "| undefined" } + * } + * } * } * * Node islands are report-only in this version: the policy decides which @@ -23,6 +28,8 @@ import { dirname, join, resolve } from "node:path"; import { projectConfigDiag, type ScrDiagnostic } from "./diagnostics/diagnostic.js"; import { trackedReadFile } from "./frontend/input-tracker.js"; +import { parseSelector, parseTypeText } from "./frontend/type-patches/selector.js"; +import { isValidRange } from "./frontend/type-patches/semver.js"; /** How a package runs: compiled natively, in a Node island, or decided by * the detector. */ @@ -57,6 +64,25 @@ export interface NodeIslandsSetting { speculativeStart: boolean; } +/** One declaration patch: a selector and the type text it maps to. */ +export interface TypePatchDeclaration { + selector: string; + type: string; + /** Offset of the selector key in the manifest, for diagnostics. */ + offset: number; +} + +/** One `scriptc.typePatches["@"]` entry. */ +export interface TypePatchEntry { + /** The key as written. */ + key: string; + pkg: string; + range: string; + declarations: TypePatchDeclaration[]; + /** Offset of the key in the manifest. */ + offset: number; +} + export interface ProjectConfig { /** The package.json the configuration came from, or null. */ manifest: string | null; @@ -66,6 +92,8 @@ export interface ProjectConfig { nodeIslands: NodeIslandsSetting; /** `scriptc.wasm`: module paths, resolved against the manifest. */ wasm: string[]; + /** `scriptc.typePatches`, in manifest order. */ + typePatches: TypePatchEntry[]; /** SC8004 for every unknown key or value. */ diagnostics: ScrDiagnostic[]; } @@ -117,6 +145,7 @@ export function emptyProjectConfig(): ProjectConfig { packages: new Map(), nodeIslands: defaultNodeIslands(), wasm: [], + typePatches: [], diagnostics: [], }; } @@ -299,6 +328,9 @@ export function parseProjectConfig(manifest: string, text: string): ProjectConfi } } + const patches = root["typePatches"]; + if (patches !== undefined) readTypePatches(patches, config, problem, locate); + // `scriptc.wasm` keeps its lenient reading: string entries of an array. const wasm = root["wasm"]; if (Array.isArray(wasm)) { @@ -309,6 +341,136 @@ export function parseProjectConfig(manifest: string, text: string): ProjectConfi return config; } +const TYPE_PATCH_KEYS = ["declarations", "note"]; +/** Packages whose declarations ship with scriptc or TypeScript: a wrong + * one is a bug to report, not a project-local patch. */ +const UNPATCHABLE = /^(?:@types\/node|typescript|@typescript\/.*)$/; +const PACKAGE_NAME = /^(?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i; + +/** Reads and validates `scriptc.typePatches`. */ +function readTypePatches( + patches: unknown, + config: ProjectConfig, + problem: (path: string[], detail: string, hint: string) => void, + locate: JsonLocator, +): void { + if (!isRecord(patches)) { + problem( + ["typePatches"], + `must be an object mapping "@" to patches, not ${shown(patches)}`, + `write "typePatches": { "@^1.0.0": { "declarations": { "Type.member": "| undefined" } } }`, + ); + return; + } + for (const key of Object.keys(patches)) { + const at = key.lastIndexOf("@"); + const pkg = at > 0 ? key.slice(0, at) : key; + const range = at > 0 ? key.slice(at + 1).trim() : ""; + if (at <= 0 || range === "" || !PACKAGE_NAME.test(pkg)) { + problem( + ["typePatches", key], + "must name a package and a version range", + `write "${at > 0 ? pkg : key}@^1.0.0": the range says which installed versions the patch describes`, + ); + continue; + } + if (!isValidRange(range)) { + problem( + ["typePatches", key], + `has the version range "${range}", which is not a semver range`, + `use a range such as "^${/^\d/.test(range) ? range.replace(/[^\d.]/g, "") : "1.0.0"}" or "1.2.x"`, + ); + continue; + } + if (UNPATCHABLE.test(pkg)) { + problem( + ["typePatches", key], + `patches ${pkg}, whose declarations ship with scriptc or TypeScript`, + "report a wrong declaration there instead of patching it", + ); + continue; + } + const value = patches[key]; + if (!isRecord(value)) { + problem( + ["typePatches", key], + `must be an object, not ${shown(value)}`, + `write { "declarations": { "Type.member": "| undefined" } }`, + ); + continue; + } + const entry: TypePatchEntry = { + key, + pkg, + range, + declarations: [], + offset: locate.offsetOf(["scriptc", "typePatches", key]), + }; + let valid = true; + for (const k of Object.keys(value)) { + if (!TYPE_PATCH_KEYS.includes(k)) { + problem( + ["typePatches", key, k], + "is not a known type patch setting", + `the settings are ${TYPE_PATCH_KEYS.join(", ")}`, + ); + valid = false; + } + } + const note = value["note"]; + if (note !== undefined && typeof note !== "string") { + problem( + ["typePatches", key, "note"], + `must be a string, not ${shown(note)}`, + "write a short note, such as a link to the upstream issue", + ); + valid = false; + } + const decls = value["declarations"]; + if (decls !== undefined && !isRecord(decls)) { + problem( + ["typePatches", key, "declarations"], + `must be an object mapping selectors to types, not ${shown(decls)}`, + `write "declarations": { "Type.member": "| undefined" }`, + ); + valid = false; + } else if (decls !== undefined) { + for (const selector of Object.keys(decls)) { + const path = ["typePatches", key, "declarations", selector]; + const parsed = parseSelector(selector); + if ("error" in parsed) { + problem( + path, + `is not a selector: it ${parsed.error}`, + `name a declaration such as "Type.member", "fn()", "fn(param)", "file.js:local()" or "export=()"`, + ); + continue; + } + const type = decls[selector]; + if (typeof type !== "string") { + problem( + path, + `must map to a type string, not ${shown(type)}`, + `write "| undefined" to widen, or the whole type to replace it`, + ); + continue; + } + const op = parseTypeText(type); + if ("error" in op) { + problem( + path, + `has a type that ${op.error}`, + `write "| undefined" to widen, or the whole type on one line`, + ); + continue; + } + entry.declarations.push({ selector, type, offset: locate.offsetOf(["scriptc", ...path]) }); + } + } + if (valid) config.typePatches.push(entry); + } +} + let cachedManifest: string | null = null; let cachedText: string | null = null; let cachedConfig: ProjectConfig | null = null; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-dts/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-dts/main.ts.json new file mode 100644 index 000000000..c9a8e15fc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-dts/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/type-patch-dts/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-types-twin/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-types-twin/main.ts.json new file mode 100644 index 000000000..faa48f074 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-types-twin/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/type-patch-types-twin/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/type-patches.test.ts b/packages/compiler/test/type-patches.test.ts new file mode 100644 index 000000000..729a0da37 --- /dev/null +++ b/packages/compiler/test/type-patches.test.ts @@ -0,0 +1,124 @@ +/* The type patch layer (frontend/type-patches/layer.ts) against real + * packages: commander's HelpContext.helpWidth @typedef property and ms's + * @return tags, patched from the fixtures' package.json; installed-version + * selection; and the warnings for patches that do not apply as written. */ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { nodeFrontendServices } from "../src/frontend/services-node.js"; +import { + configureTypePatches, + typePatchDiagnostics, + typePatchedSpans, + typePatchedText, + typePatchMatches, +} from "../src/frontend/type-patches/layer.js"; + +const fixtures = join(import.meta.dirname, "../../../tests/fixtures"); +const parse = (path: string, text: string, kind: "ts" | "js") => + nodeFrontendServices().parse(path, text, kind); + +afterEach(() => configureTypePatches(join(tmpdir(), "none", "main.ts"), parse)); + +test("commander's HelpContext.helpWidth typedef property reads as number | undefined", () => { + const root = join(fixtures, "commander-calc"); + configureTypePatches(join(root, "calc.ts"), parse); + const command = join(root, "node_modules/commander/lib/command.js"); + const text = typePatchedText(command); + expect(text).toBeDefined(); + expect(text).toContain(" * @property {number | undefined} helpWidth\n"); + expect(text).toContain(" * @returns {HelpContext}\n"); + expect(typePatchMatches()).toMatchObject([ + { + file: command, + line: 2481, + before: "number", + after: "number | undefined", + pkg: "commander", + version: "15.0.0", + }, + ]); + const [span] = typePatchedSpans(command); + expect(text!.slice(span!.start, span!.end)).toBe(" | undefined"); + // Its declaration file has an unrelated HelpContext without helpWidth. + expect(typePatchedText(join(root, "node_modules/commander/typings/index.d.ts"))).toBeUndefined(); + // Other files of the package are served as they are. + expect(typePatchedText(join(root, "node_modules/commander/lib/help.js"))).toBeUndefined(); + expect(typePatchDiagnostics()?.diagnostics).toEqual([]); +}); + +test("ms's parse() and export return tags widen", () => { + const root = join(fixtures, "type-patches"); + configureTypePatches(join(root, "ms.ts"), parse); + const file = join(root, "node_modules/ms/index.js"); + const text = typePatchedText(file)!; + const original = readFileSync(file, "utf8"); + expect(text).toBe( + original + .replace(" * @return {String|Number}\n", " * @return {String|Number | undefined}\n") + .replace( + " * @return {Number}\n * @api private\n */\n\nfunction parse", + " * @return {Number | undefined}\n * @api private\n */\n\nfunction parse", + ), + ); + expect(text).not.toBe(original); + expect(typePatchDiagnostics()?.diagnostics.map((d) => d.code)).toEqual([]); +}); + +test("a project without type patches serves every file unchanged", () => { + configureTypePatches(join(fixtures, "npm-static", "ms-cli.ts"), parse); + expect(typePatchedText(join(fixtures, "npm-static/node_modules/ms/index.js"))).toBeUndefined(); + expect(typePatchDiagnostics()).toBeNull(); +}); + +test("patches that do not apply as written warn", () => { + const root = join(fixtures, "type-patches"); + configureTypePatches(join(root, "stale", "main.ts"), parse); + expect(typePatchedText(join(root, "node_modules/termctx/index.js"))).toBeUndefined(); + typePatchedText(join(root, "node_modules/ms/index.js")); + const diags = typePatchDiagnostics()!.diagnostics; + expect(diags.map((d) => [d.code, d.message])).toEqual([ + [ + "SC8030", + "type patch 'termctx@^2.0.0' does not apply to termctx 1.2.0 at ../node_modules/termctx; its declarations are used unpatched", + ], + [ + "SC8031", + "type patch 'ms@^2.1.0': 'index.js:format()' matches no declaration in the files of ms this program loads", + ], + ["SC8032", "type patch 'ms@^2.1.0': 'index.js:fmtShort(ms)' is already declared as Number"], + [ + "SC8033", + "type patch 'ms@^2.1.0': 'index.js:plural()' has no written type to widen; give the whole type instead (index.js)", + ], + ]); + expect( + diags.every((d) => d.loc.file === join(root, "stale", "package.json") && d.loc.start > 0), + ).toBe(true); +}); + +test("an @types twin is matched by the runtime package's installed version", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-type-patches-")); + try { + const twin = join(fixtures, "npm/cases/type-patch-types-twin"); + configureTypePatches(join(twin, "main.ts"), parse); + const text = typePatchedText(join(twin, "node_modules/@types/tallyjs/index.d.ts"))!; + expect(text).toContain(" max: number | undefined;\n mean: number | null;\n"); + // A range the installed runtime (0.4.2) does not satisfy leaves it alone, + // even though the twin's own version (0.4.0) would. + writeFileSync( + join(root, "package.json"), + JSON.stringify({ + scriptc: { + typePatches: { "tallyjs@0.4.0": { declarations: { "Totals.max": "| undefined" } } }, + }, + }), + ); + configureTypePatches(join(root, "main.ts"), parse); + expect(typePatchedText(join(twin, "node_modules/@types/tallyjs/index.d.ts"))).toBeUndefined(); + expect(typePatchDiagnostics()!.diagnostics.map((d) => d.code)).toEqual(["SC8030"]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/tests/fixtures/commander-calc/package.json b/tests/fixtures/commander-calc/package.json index f0ea76992..59f181b32 100644 --- a/tests/fixtures/commander-calc/package.json +++ b/tests/fixtures/commander-calc/package.json @@ -4,5 +4,15 @@ "type": "module", "dependencies": { "commander": "15.0.0" + }, + "scriptc": { + "typePatches": { + "commander@^15.0.0": { + "note": "off a TTY, _getOutputContext() returns helpWidth undefined although the @typedef says number", + "declarations": { + "HelpContext.helpWidth": "| undefined" + } + } + } } } diff --git a/tests/fixtures/npm/cases/type-patch-dts/main.ts b/tests/fixtures/npm/cases/type-patch-dts/main.ts new file mode 100644 index 000000000..1e442054b --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-dts/main.ts @@ -0,0 +1,15 @@ +// A package whose .d.ts says `email: string` for a value that is missing, +// patched to `string | undefined` (package.json scriptc.typePatches): the +// profiles cross the declaration boundary as native records, and user +// code handles the missing email the patched type now makes visible. +import { findProfile, loadProfiles } from "profilekit"; + +const profiles = loadProfiles(); +for (const p of profiles) console.log(p.id, p.name, p.email ?? "(no email)", "email" in p); +console.log(JSON.stringify(profiles)); +const emails = profiles.filter((p) => p.email !== undefined).map((p) => p.email!.toUpperCase()); +console.log(emails.join(",")); +const found = findProfile(3); +console.log(found === undefined ? "none" : found.name); +const missing = findProfile(9); +console.log(missing === undefined ? "none" : missing.name); diff --git a/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/index.d.ts b/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/index.d.ts new file mode 100644 index 000000000..445d78ea2 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/index.d.ts @@ -0,0 +1,10 @@ +// The declarations say every profile has an email and every lookup finds +// one; the JavaScript leaves email out for guests and returns undefined +// for unknown ids. +export type Profile = { + id: number; + name: string; + email: string; +}; +export declare function loadProfiles(): Profile[]; +export declare function findProfile(id: number): Profile; diff --git a/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/index.js b/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/index.js new file mode 100644 index 000000000..a20a082f6 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/index.js @@ -0,0 +1,14 @@ +"use strict"; +function row(id, name, email) { + const profile = { id: id, name: name }; + if (email !== null) profile.email = email; + return profile; +} +function loadProfiles() { + return [row(1, "ada", "ada@example.com"), row(2, "guest", null), row(3, "lin", "lin@example.com")]; +} +function findProfile(id) { + return loadProfiles().find((p) => p.id === id); +} +exports.loadProfiles = loadProfiles; +exports.findProfile = findProfile; diff --git a/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/package.json b/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/package.json new file mode 100644 index 000000000..9982f9eef --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-dts/node_modules/profilekit/package.json @@ -0,0 +1 @@ +{ "name": "profilekit", "version": "3.1.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/type-patch-dts/package.json b/tests/fixtures/npm/cases/type-patch-dts/package.json new file mode 100644 index 000000000..822491420 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-dts/package.json @@ -0,0 +1,15 @@ +{ + "name": "type-patch-dts-case", + "private": true, + "type": "module", + "scriptc": { + "typePatches": { + "profilekit@^3.0.0": { + "declarations": { + "Profile.email": "| undefined", + "findProfile()": "| undefined" + } + } + } + } +} diff --git a/tests/fixtures/npm/cases/type-patch-types-twin/main.ts b/tests/fixtures/npm/cases/type-patch-types-twin/main.ts new file mode 100644 index 000000000..702b51972 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-twin/main.ts @@ -0,0 +1,11 @@ +// A plain-JavaScript package typed by an @types twin that says `max: +// number` and `mean: number`; the project's type patches (keyed by the +// runtime package and its installed version) make them `number | +// undefined` and `number | null`. +import { totals } from "tallyjs"; + +for (const values of [[3, 9, 4], []]) { + const t = totals(values); + console.log(t.count, t.sum, t.max ?? "no max", t.mean === null ? "no mean" : t.mean.toFixed(2)); + console.log(JSON.stringify(t), Object.keys(t).join("|")); +} diff --git a/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/@types/tallyjs/index.d.ts b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/@types/tallyjs/index.d.ts new file mode 100644 index 000000000..4a7b81dcc --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/@types/tallyjs/index.d.ts @@ -0,0 +1,7 @@ +export type Totals = { + count: number; + sum: number; + max: number; + mean: number; +}; +export declare function totals(values: number[]): Totals; diff --git a/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/@types/tallyjs/package.json b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/@types/tallyjs/package.json new file mode 100644 index 000000000..fed3cc37a --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/@types/tallyjs/package.json @@ -0,0 +1 @@ +{ "name": "@types/tallyjs", "version": "0.4.0", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/tallyjs/index.js b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/tallyjs/index.js new file mode 100644 index 000000000..88d5ffc8f --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/tallyjs/index.js @@ -0,0 +1,11 @@ +"use strict"; +// Plain JavaScript; @types/tallyjs describes it, except that an empty +// list has no max (undefined) and no mean (null). +function totals(values) { + var sum = 0; + for (var i = 0; i < values.length; i++) sum += values[i]; + var max = values.length === 0 ? undefined : Math.max.apply(null, values); + var mean = values.length === 0 ? null : sum / values.length; + return { count: values.length, sum: sum, max: max, mean: mean }; +} +exports.totals = totals; diff --git a/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/tallyjs/package.json b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/tallyjs/package.json new file mode 100644 index 000000000..ad80ad430 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-twin/node_modules/tallyjs/package.json @@ -0,0 +1 @@ +{ "name": "tallyjs", "version": "0.4.2", "main": "index.js" } diff --git a/tests/fixtures/npm/cases/type-patch-types-twin/package.json b/tests/fixtures/npm/cases/type-patch-types-twin/package.json new file mode 100644 index 000000000..53ec04ede --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-twin/package.json @@ -0,0 +1,15 @@ +{ + "name": "type-patch-types-twin-case", + "private": true, + "type": "module", + "scriptc": { + "typePatches": { + "tallyjs@~0.4.0": { + "declarations": { + "Totals.max": "| undefined", + "Totals.mean": "number | null" + } + } + } + } +} diff --git a/tests/fixtures/type-patches/jsdoc.ts b/tests/fixtures/type-patches/jsdoc.ts new file mode 100644 index 000000000..52cd13638 --- /dev/null +++ b/tests/fixtures/type-patches/jsdoc.ts @@ -0,0 +1,11 @@ +// A JSDoc @typedef property and a @returns that say number for a value +// that is undefined off a TTY, fixed by the project's type patches. +import { Terminal } from "termctx"; + +for (const tty of [false, true]) { + const term = new Terminal(tty); + const context = term.outputContext(false); + console.log(context.error, context.width, term.columns()); + console.log(term.describe(context)); + console.log(term.describe(term.outputContext(true))); +} diff --git a/tests/fixtures/type-patches/ms.ts b/tests/fixtures/type-patches/ms.ts new file mode 100644 index 000000000..a70e221d6 --- /dev/null +++ b/tests/fixtures/type-patches/ms.ts @@ -0,0 +1,9 @@ +// ms's JSDoc says parse() returns a Number and ms() a String or Number; +// for text it cannot parse both return undefined. +import ms from "ms"; + +for (const text of ["2 hours", "not a duration", "1".repeat(101), "-1.5d"]) { + const value = ms(text); + console.log(value === undefined ? "invalid" : value); +} +console.log(ms(90_000), ms(90_000, { long: true })); diff --git a/tests/fixtures/type-patches/node_modules/ms/index.js b/tests/fixtures/type-patches/node_modules/ms/index.js new file mode 100644 index 000000000..ea734fb73 --- /dev/null +++ b/tests/fixtures/type-patches/node_modules/ms/index.js @@ -0,0 +1,162 @@ +/** + * Helpers. + */ + +var s = 1000; +var m = s * 60; +var h = m * 60; +var d = h * 24; +var w = d * 7; +var y = d * 365.25; + +/** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} [options] + * @throws {Error} throw an error if val is not a non-empty string or a number + * @return {String|Number} + * @api public + */ + +module.exports = function (val, options) { + options = options || {}; + var type = typeof val; + if (type === 'string' && val.length > 0) { + return parse(val); + } else if (type === 'number' && isFinite(val)) { + return options.long ? fmtLong(val) : fmtShort(val); + } + throw new Error( + 'val is not a non-empty string or a valid number. val=' + + JSON.stringify(val) + ); +}; + +/** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ + +function parse(str) { + str = String(str); + if (str.length > 100) { + return; + } + var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec( + str + ); + if (!match) { + return; + } + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'yrs': + case 'yr': + case 'y': + return n * y; + case 'weeks': + case 'week': + case 'w': + return n * w; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'hrs': + case 'hr': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'mins': + case 'min': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 'secs': + case 'sec': + case 's': + return n * s; + case 'milliseconds': + case 'millisecond': + case 'msecs': + case 'msec': + case 'ms': + return n; + default: + return undefined; + } +} + +/** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function fmtShort(ms) { + var msAbs = Math.abs(ms); + if (msAbs >= d) { + return Math.round(ms / d) + 'd'; + } + if (msAbs >= h) { + return Math.round(ms / h) + 'h'; + } + if (msAbs >= m) { + return Math.round(ms / m) + 'm'; + } + if (msAbs >= s) { + return Math.round(ms / s) + 's'; + } + return ms + 'ms'; +} + +/** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function fmtLong(ms) { + var msAbs = Math.abs(ms); + if (msAbs >= d) { + return plural(ms, msAbs, d, 'day'); + } + if (msAbs >= h) { + return plural(ms, msAbs, h, 'hour'); + } + if (msAbs >= m) { + return plural(ms, msAbs, m, 'minute'); + } + if (msAbs >= s) { + return plural(ms, msAbs, s, 'second'); + } + return ms + ' ms'; +} + +/** + * Pluralization helper. + */ + +function plural(ms, msAbs, n, name) { + var isPlural = msAbs >= n * 1.5; + return Math.round(ms / n) + ' ' + name + (isPlural ? 's' : ''); +} diff --git a/tests/fixtures/type-patches/node_modules/ms/license.md b/tests/fixtures/type-patches/node_modules/ms/license.md new file mode 100644 index 000000000..fa5d39b62 --- /dev/null +++ b/tests/fixtures/type-patches/node_modules/ms/license.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Vercel, Inc. + +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/type-patches/node_modules/ms/package.json b/tests/fixtures/type-patches/node_modules/ms/package.json new file mode 100644 index 000000000..b49dca98d --- /dev/null +++ b/tests/fixtures/type-patches/node_modules/ms/package.json @@ -0,0 +1,38 @@ +{ + "name": "ms", + "version": "2.1.3", + "description": "Tiny millisecond conversion utility", + "repository": "vercel/ms", + "main": "./index", + "files": [ + "index.js" + ], + "scripts": { + "precommit": "lint-staged", + "lint": "eslint lib/* bin/*", + "test": "mocha tests.js" + }, + "eslintConfig": { + "extends": "eslint:recommended", + "env": { + "node": true, + "es6": true + } + }, + "lint-staged": { + "*.js": [ + "npm run lint", + "the formatter --single-quote --write", + "git add" + ] + }, + "license": "MIT", + "devDependencies": { + "eslint": "4.18.2", + "expect.js": "0.3.1", + "husky": "0.14.3", + "lint-staged": "5.0.0", + "mocha": "4.0.1", + "the formatter": "2.0.5" + } +} diff --git a/tests/fixtures/type-patches/node_modules/termctx/index.js b/tests/fixtures/type-patches/node_modules/termctx/index.js new file mode 100644 index 000000000..c8b2b9d2d --- /dev/null +++ b/tests/fixtures/type-patches/node_modules/termctx/index.js @@ -0,0 +1,43 @@ +"use strict"; +// A terminal helper whose JSDoc says widths are numbers; off a TTY they +// are undefined (the shape of commander's HelpContext.helpWidth). + +class Terminal { + constructor(tty) { + this.tty = tty; + this.settings = { + columns: () => (this.tty ? 80 : undefined), + }; + } + + /** + * @returns {number} + */ + columns() { + return this.settings.columns(); + } + + /** + * @typedef OutputContext + * @type {object} + * @property {boolean} error + * @property {number} width + * + * @param {boolean} error + * @returns {OutputContext} + */ + outputContext(error) { + const width = this.settings.columns(); + return { error, width }; + } + + /** + * @param {OutputContext} context + * @returns {string} + */ + describe(context) { + return `width ${context.width}${context.error ? " (stderr)" : ""}`; + } +} + +module.exports = { Terminal }; diff --git a/tests/fixtures/type-patches/node_modules/termctx/package.json b/tests/fixtures/type-patches/node_modules/termctx/package.json new file mode 100644 index 000000000..1791f57af --- /dev/null +++ b/tests/fixtures/type-patches/node_modules/termctx/package.json @@ -0,0 +1 @@ +{ "name": "termctx", "version": "1.2.0", "main": "index.js" } diff --git a/tests/fixtures/type-patches/package.json b/tests/fixtures/type-patches/package.json new file mode 100644 index 000000000..f1b9f630e --- /dev/null +++ b/tests/fixtures/type-patches/package.json @@ -0,0 +1,26 @@ +{ + "name": "type-patches-fixture", + "private": true, + "type": "module", + "dependencies": { + "ms": "2.1.3", + "termctx": "1.2.0" + }, + "scriptc": { + "typePatches": { + "termctx@^1.0.0": { + "declarations": { + "OutputContext.width": "| undefined", + "Terminal.columns()": "| undefined" + } + }, + "ms@^2.1.0": { + "note": "parse() returns undefined for text it cannot parse", + "declarations": { + "index.js:parse()": "| undefined", + "export=()": "| undefined" + } + } + } + } +} diff --git a/tests/fixtures/type-patches/stale/main.ts b/tests/fixtures/type-patches/stale/main.ts new file mode 100644 index 000000000..05ae25057 --- /dev/null +++ b/tests/fixtures/type-patches/stale/main.ts @@ -0,0 +1,4 @@ +import ms from "ms"; +import { Terminal } from "termctx"; + +console.log(ms(1000), new Terminal(true).columns()); diff --git a/tests/fixtures/type-patches/stale/package.json b/tests/fixtures/type-patches/stale/package.json new file mode 100644 index 000000000..53669c8b5 --- /dev/null +++ b/tests/fixtures/type-patches/stale/package.json @@ -0,0 +1,17 @@ +{ + "name": "type-patches-stale", + "private": true, + "type": "module", + "scriptc": { + "typePatches": { + "termctx@^2.0.0": { "declarations": { "OutputContext.width": "| undefined" } }, + "ms@^2.1.0": { + "declarations": { + "index.js:format()": "| undefined", + "index.js:fmtShort(ms)": "| Number", + "index.js:plural()": "| undefined" + } + } + } + } +} diff --git a/tests/harness/type-patches.test.ts b/tests/harness/type-patches.test.ts new file mode 100644 index 000000000..ecd8ee923 --- /dev/null +++ b/tests/harness/type-patches.test.ts @@ -0,0 +1,143 @@ +/* Type patches (package.json scriptc.typePatches): project-owned fixes for + * npm declarations that do not describe what the package does. A JSDoc + * @typedef property and @returns tag that say number for an undefined + * value (the shape of commander's HelpContext.helpWidth), and ms's @return + * tags, compile natively and byte-match Node once patched; without the + * patches the same declarations throw where Node runs. Patches that do not + * apply as written warn (SC8030-SC8033). The `.d.ts` and `@types` cases + * run in npm.test.ts (type-patch-dts, type-patch-types-twin), and + * commander-calc carries the HelpContext.helpWidth patch. */ +import { execFile } from "node:child_process"; +import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +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 fixture = join(repoRoot, "tests/fixtures/type-patches"); +const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/type-patches"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +interface Run { + stdout: string; + stderr: string; + exitCode: number; +} + +async function run(cmd: string, args: string[]): Promise { + try { + const pending = execFileAsync(cmd, args, { encoding: "utf8" }); + pending.child.stdin?.end(); + const { stdout, stderr } = await pending; + return { stdout, stderr, exitCode: 0 }; + } catch (err) { + const e = err as { code?: unknown; stdout?: string; stderr?: string }; + if (typeof e.code !== "number") throw err; + return { stdout: e.stdout ?? "", stderr: e.stderr ?? "", exitCode: e.code }; + } +} + +async function build(entry: string, name: string, npmStatic: string[]) { + mkdirSync(outDir, { recursive: true }); + const result = await compile(entry, { outPath: join(outDir, name), outDir, sanitize, npmStatic }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return result; +} + +/** A copy of a fixture directory whose package.json has no scriptc key. */ +function unpatched(dir: string): string { + const copy = mkdtempSync(join(tmpdir(), "scriptc-unpatched-")); + cpSync(dir, copy, { recursive: true }); + const manifest = join(copy, "package.json"); + const parsed = JSON.parse(readFileSync(manifest, "utf8")) as Record; + delete parsed["scriptc"]; + writeFileSync(manifest, JSON.stringify(parsed, null, 2)); + return copy; +} + +describe("type patches", () => { + test.for([ + ["jsdoc.ts", ["termctx"]], + ["ms.ts", ["ms"]], + ] as const)( + "%s compiles with its patches and byte-matches Node", + { timeout: 600_000 }, + async ([file, pkgs]) => { + const entry = join(fixture, file); + const result = await build(entry, file.replace(".ts", ""), [...pkgs]); + expect(result.warnings ?? []).toEqual([]); + const [node, native] = await Promise.all([ + run(process.execPath, [entry]), + run(result.binaryPath, []), + ]); + expect(node.exitCode).toBe(0); + expect(native).toEqual(node); + }, + ); + + test( + "without the patches the lying JSDoc throws where Node runs", + { timeout: 600_000 }, + async () => { + const copy = unpatched(fixture); + try { + const result = await build(join(copy, "jsdoc.ts"), "jsdoc-unpatched", ["termctx"]); + const native = await run(result.binaryPath, []); + expect(native.exitCode).not.toBe(0); + expect(native.stderr).toContain("TypeError"); + } finally { + rmSync(copy, { recursive: true, force: true }); + } + }, + ); + + test( + "without the patch the lying .d.ts throws at the declaration boundary", + { timeout: 600_000 }, + async () => { + const copy = unpatched(join(repoRoot, "tests/fixtures/npm/cases/type-patch-dts")); + try { + mkdirSync(outDir, { recursive: true }); + const result = await compile(join(copy, "main.ts"), { + outPath: join(outDir, "dts-unpatched"), + outDir, + sanitize, + }); + // Unpatched, `p.email ?? ...` and `found === undefined` test values + // the declarations say cannot be missing; the build may refuse them + // or the boundary cast throws when the guest profile crosses. + if (result.ok) { + const native = await run(result.binaryPath, []); + expect(native.exitCode).not.toBe(0); + expect(native.stderr).toContain("TypeError: expected string at $, got undefined"); + } + } finally { + rmSync(copy, { recursive: true, force: true }); + } + }, + ); + + test( + "patches that do not apply as written warn and the build goes on", + { timeout: 600_000 }, + async () => { + const entry = join(fixture, "stale/main.ts"); + const result = await build(entry, "stale", ["ms", "termctx"]); + expect((result.warnings ?? []).map((w) => w.code)).toEqual([ + "SC8030", + "SC8031", + "SC8032", + "SC8033", + ]); + const [node, native] = await Promise.all([ + run(process.execPath, [entry]), + run(result.binaryPath, []), + ]); + expect(native).toEqual(node); + }, + ); +}); From b0c01dc34174fb0f4257f686b9466ba1dcdbbcba Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 12:48:07 -0500 Subject: [PATCH 233/273] Adapt the nursery trigger to where cyclic garbage dies A program's nursery trigger starts at 256 candidates and doubles, up to 4096, after each scheduled mature or full pass whose dead set held more mature and old objects than an eighth of what the nursery passes freed since the previous one; it halves after one that held less than a thirty-second of that. SCR_CYCLE_THRESHOLD and scr_cyc_set_nursery_threshold pin the trigger as before. The scheduled-age rule keeps counting collection points in the pinned or default trigger. --- packages/runtime/src/scr_cycle.c | 73 +++++++++++++++++++++++++++--- packages/runtime/src/scr_rc.h | 5 +- packages/runtime/test/test_cycle.c | 58 ++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 9 deletions(-) diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index d08f54a21..0d9df7106 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -603,7 +603,8 @@ static SCR_TL unsigned scr_cyc_backlog_shift[SCR_CYC_NGENS]; static SCR_TL bool scr_cyc_backlog_judged[SCR_CYC_NGENS]; #endif -/* Mature objects in the last full pass's dead set (scr_cyc_adapt_growth). */ +/* Mature objects in the last pass's dead set (scr_cyc_adapt_growth, + * scr_cyc_adapt_nursery). */ #ifndef SCR_WORKERS static SCR_TL size_t scr_cyc_mature_dead = 0; #endif @@ -639,23 +640,74 @@ static SCR_TL size_t scr_cyc_death_due_live = 0; /* The nursery trigger a program sets for itself * (scr_cyc_set_nursery_threshold), 0 for the default; SCR_CYCLE_THRESHOLD - * takes precedence over it. */ + * takes precedence over it. Either pins the trigger. */ static long scr_cyc_nursery_threshold_set = 0; static SCR_TL size_t scr_cyc_nursery_threshold_cached = 0; +#define SCR_CYC_NURSERY_ADAPTIVE SIZE_MAX -static size_t scr_cyc_nursery_threshold(void) { +/* An unpinned trigger follows where cyclic garbage dies. Garbage that + * outlives the nursery window survives its nursery pass and is promoted, + * and then only a mature or full pass finds it, after walking that level's + * live candidates. So after each scheduled mature or full pass whose dead + * set held more mature and old objects than 1/SCR_CYC_NURSERY_RATIO of what + * the nursery passes since the previous one freed, the window doubles, up + * to SCR_CYC_NURSERY_MAX_SHIFT doublings (4096 candidates); after one that + * held less than a quarter of that share, it halves, down to + * SCR_CYC_NURSERY_CANDIDATES. + * `nursery_freed`: what the nursery passes freed since the last scheduled + * mature or full pass. */ +#define SCR_CYC_NURSERY_RATIO 8 +#define SCR_CYC_NURSERY_MAX_SHIFT 4 +static SCR_TL unsigned char scr_cyc_nursery_shift = 0; +static SCR_TL size_t scr_cyc_nursery_freed = 0; + +static size_t scr_cyc_nursery_pinned(void) { if (scr_cyc_nursery_threshold_cached == 0) { const char *env = scr_getenv("SCR_CYCLE_THRESHOLD"); long v = env ? strtol(env, NULL, 10) : 0; if (v <= 0) v = scr_cyc_nursery_threshold_set; - scr_cyc_nursery_threshold_cached = v > 0 ? (size_t)v : SCR_CYC_NURSERY_CANDIDATES; + scr_cyc_nursery_threshold_cached = v > 0 ? (size_t)v : SCR_CYC_NURSERY_ADAPTIVE; } return scr_cyc_nursery_threshold_cached; } +/* The pinned trigger, or the default one. The scheduled-age rule counts + * collection points in it, so the adaptation does not lengthen how long an + * older backlog waits at them. */ +static size_t scr_cyc_nursery_base(void) { + size_t pinned = scr_cyc_nursery_pinned(); + return pinned != SCR_CYC_NURSERY_ADAPTIVE ? pinned : SCR_CYC_NURSERY_CANDIDATES; +} + +static size_t scr_cyc_nursery_threshold(void) { + size_t pinned = scr_cyc_nursery_pinned(); + if (pinned != SCR_CYC_NURSERY_ADAPTIVE) return pinned; + return (size_t)SCR_CYC_NURSERY_CANDIDATES << scr_cyc_nursery_shift; +} + +/* Adapts an unpinned trigger to the mature and old objects a scheduled + * mature or full pass reclaimed; returns whether the trigger changed. */ +static SCR_CYC_SCHEDULING bool scr_cyc_adapt_nursery(size_t older_freed) { + size_t nursery_freed = scr_cyc_nursery_freed; + scr_cyc_nursery_freed = 0; + if (scr_cyc_nursery_pinned() != SCR_CYC_NURSERY_ADAPTIVE) return false; + if (older_freed * SCR_CYC_NURSERY_RATIO > nursery_freed) { + if (scr_cyc_nursery_shift == SCR_CYC_NURSERY_MAX_SHIFT) return false; + scr_cyc_nursery_shift++; + return true; + } + if (older_freed * SCR_CYC_NURSERY_RATIO * 4 < nursery_freed && scr_cyc_nursery_shift > 0) { + scr_cyc_nursery_shift--; + return true; + } + return false; +} + void scr_cyc_set_nursery_threshold(long threshold) { scr_cyc_nursery_threshold_set = threshold; scr_cyc_nursery_threshold_cached = 0; + scr_cyc_nursery_shift = 0; + scr_cyc_nursery_freed = 0; } /* The number of buffered candidates (across every generation) at which the @@ -881,7 +933,14 @@ static SCR_CYC_SCHEDULING void scr_cyc_scheduled_pass(unsigned gen, bool by_grow size_t freed = scr_cyc_pass(gen); scr_cyc_keep_base = false; size_t old_reclaimed = scr_cyc_old_freed - old_freed_before + scr_cyc_mature_dead; - if (gen == SCR_CYC_NURSERY) return; + if (gen == SCR_CYC_NURSERY) { + scr_cyc_nursery_freed += freed; + return; + } + if (scr_cyc_adapt_nursery(old_reclaimed)) { + scr_cyc_trigger = scr_cyc_buffered() + scr_cyc_nursery_threshold(); + scr_cyc_window_update(); + } if (scr_cands.n != 0) { scr_cyc_backlog_judged[gen] = true; if (freed * SCR_CYC_GROWTH_DIV >= scr_cands.n) @@ -940,7 +999,7 @@ void scr_cyc_collect_scheduled(void) { if (!scr_cyc_older_backlog()) { scr_cyc_scheduled_mature_age = 0; } else if (++scr_cyc_scheduled_mature_age - >= scr_cyc_nursery_threshold()) { + >= scr_cyc_nursery_base()) { scr_cyc_scheduled_pass(SCR_CYC_OLD, false, false); return; } @@ -1981,7 +2040,7 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) { ScrCycHdr *h = scr_cyc_hdr(obj); const ScrCycType *type = scr_cyc_type(h); if (gen_limit < SCR_CYC_OLD) scr_cyc_freed(type, gen_limit); - else scr_cyc_mature_dead += scr_cyc_gen(h) == SCR_CYC_MATURE; + scr_cyc_mature_dead += scr_cyc_gen(h) == SCR_CYC_MATURE; #ifdef SCR_YIN if (scr_cyc_gen(h) == SCR_CYC_YOUNG) scr_yin_leave(h, obj); #endif diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index de9f21f44..171e420c2 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -703,8 +703,9 @@ void scr_cyc_set_growth_cap(long cap); /* Sets the nursery trigger SCR_CYCLE_THRESHOLD sets (scr_cycle.c): the * number of new candidates at which the release path runs a pass, for a * program that knows its own collection profile. The environment variable - * takes precedence; 0 restores the default. The trigger re-arms with it at - * the end of the next pass. Call it before the program allocates much. */ + * takes precedence; 0 restores the default, which starts at 256 and adapts + * to the program. The trigger re-arms with it at the end of the next pass. + * Call it before the program allocates much. */ void scr_cyc_set_nursery_threshold(long threshold); void scr_cyc_context_cleanup(void); diff --git a/packages/runtime/test/test_cycle.c b/packages/runtime/test/test_cycle.c index 0ab827e47..dc48abd26 100644 --- a/packages/runtime/test/test_cycle.c +++ b/packages/runtime/test/test_cycle.c @@ -575,6 +575,60 @@ static void check_program_nursery_threshold(void) { scr_collect_cycles(); } +/* How many dead rings the release path takes to run its next pass, from + * the end of the previous one: the nursery trigger in effect. */ +static size_t nursery_window(void) { + size_t before = freed, n = 0; + while (freed == before && n < 65536) { + release_live(make_ring(1)); + n++; + } + check(freed - before == n, "a nursery pass left dead rings behind"); + return n; +} + +/* A scheduled full pass that reclaims nothing: its only candidate is an + * older live leaf. */ +static void scheduled_pass_finding_nothing(Node *leaf) { + size_t before = freed; + read_live(leaf); + for (size_t i = 0; i < configured_nursery_threshold(); i++) + scr_cyc_collect_scheduled(); + check(freed == before, "a scheduled pass over a live leaf freed something"); +} + +/* Unpinned, the nursery trigger starts at 256 candidates. Each scheduled + * full pass that reclaims promoted rings doubles it, up to 4096; each one + * that reclaims nothing after the nursery passes freed rings halves it, + * down to 256. */ +static void check_adaptive_nursery_threshold(void) { + if (getenv("SCR_CYCLE_THRESHOLD") != NULL) return; /* it pins the trigger */ + scr_cyc_set_nursery_threshold(0); + scr_collect_cycles(); + check(nursery_window() == 256, "the unpinned nursery trigger did not start at 256"); + static const size_t widened[] = { 512, 1024, 2048, 4096, 4096 }; + for (size_t i = 0; i < sizeof(widened) / sizeof(widened[0]); i++) { + collect_dead_mature_rings(); + check(nursery_window() == widened[i], + "a full pass reclaiming promoted rings did not double the nursery trigger"); + } + + static const size_t narrowed[] = { 2048, 1024, 512, 256, 256 }; + Node *leaf = make_leaf(); + read_live(leaf); + scr_collect_cycles(); /* promote */ + for (size_t i = 0; i < sizeof(narrowed) / sizeof(narrowed[0]); i++) { + scheduled_pass_finding_nothing(leaf); + check(nursery_window() == narrowed[i], + "a full pass reclaiming nothing did not halve the nursery trigger"); + } + leaf->rc--; + scr_cyc_on_dead(leaf); + node_free(leaf); + scr_cyc_set_nursery_threshold(0); + scr_collect_cycles(); +} + /* The phase sweep (scr_collect_cycles_phase) is for a caller that knows a * phase's data has just died: it sweeps, and the growth schedule starts * over. After passes that find nothing have widened the old level's @@ -1506,6 +1560,9 @@ static void check_acyclic_objects(void) { } int main(void) { + /* The scheduling checks count candidates against a fixed nursery trigger; + * check_adaptive_nursery_threshold covers the unpinned one. */ + scr_cyc_set_nursery_threshold((long)configured_nursery_threshold()); check_inline_buffering(); check_old_garbage_reclaimed_while_growing(); check_older_rings_into_old(); @@ -1541,6 +1598,7 @@ int main(void) { check_acyclic_objects(); check_program_growth_cap(); check_program_nursery_threshold(); + check_adaptive_nursery_threshold(); printf("cycle collection checks passed: threshold=%zu growth_cap=%zu\n", configured_nursery_threshold(), configured_growth_cap()); return 0; From e24ed129334c694ca15d0c04b36291fef7fa9b22 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 12:48:21 -0500 Subject: [PATCH 234/273] Decode string storage within its length in Buffer encodings String storage can hold bytes that are not well-formed UTF-8: an HTTP header value keeps the raw bytes a client sent. Encoding such a string read past its end. - scr_bytes_next_cp decoded any high byte as the lead of a sequence and read up to three bytes after it without a bound. It now takes the storage length, decodes only complete, well-formed sequences, and returns any other byte as U+0000..U+00FF, advancing by one. That is each such byte's value in Node, which decodes header values as latin1. This covers latin1, ascii, binary and utf16le encoding, hex and base64 of non-ASCII text (which narrow through latin1), Buffer#write and TextEncoder#encodeInto. - base64 decoding read one byte past its input when the eight-character groups ended exactly at the end. The runtime's bytes unit test (built with ASan) encodes ill-formed storage at every offset within a word and random high bytes, and compares latin1, ascii and utf16le with a scalar reference. A corpus program sends such header bytes to a server and encodes them as Node does. --- .../corpus/8010-http-header-raw-bytes.ts.json | 6 + packages/runtime/src/scr_bytes.c | 54 ++++---- packages/runtime/test/test_bytes.c | 130 ++++++++++++++++++ tests/corpus/8010-http-header-raw-bytes.ts | 72 ++++++++++ .../8010-http-header-raw-bytes.ts.json | 1 + 5 files changed, 235 insertions(+), 28 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8010-http-header-raw-bytes.ts.json create mode 100644 tests/corpus/8010-http-header-raw-bytes.ts create mode 100644 tests/harness/corpus-costs/8010-http-header-raw-bytes.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8010-http-header-raw-bytes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8010-http-header-raw-bytes.ts.json new file mode 100644 index 000000000..78240d310 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8010-http-header-raw-bytes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8010-http-header-raw-bytes.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index df64b2ab2..5fb8a58d4 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -2240,29 +2240,26 @@ ScrStr *scr_text_decode_stream(ScrDyn *state, const ScrBytes *input, return result; } -/* Decode the next code point from an ScrStr's storage — ALWAYS valid - * UTF-8 (the string runtime never stores ill-formed sequences), so no - * validation re-runs here. */ -static uint32_t scr_bytes_next_cp(const uint8_t *in, size_t *ip) { - uint8_t b = in[*ip]; - if (b < 0x80) { - *ip += 1; - return b; - } - if ((b & 0xe0) == 0xc0) { - uint32_t cp = ((uint32_t)(b & 0x1f) << 6) | (in[*ip + 1] & 0x3f); - *ip += 2; - return cp; - } - if ((b & 0xf0) == 0xe0) { - uint32_t cp = ((uint32_t)(b & 0xf) << 12) | ((uint32_t)(in[*ip + 1] & 0x3f) << 6) | - (in[*ip + 2] & 0x3f); - *ip += 3; - return cp; - } - uint32_t cp = ((uint32_t)(b & 0x7) << 18) | ((uint32_t)(in[*ip + 1] & 0x3f) << 12) | - ((uint32_t)(in[*ip + 2] & 0x3f) << 6) | (in[*ip + 3] & 0x3f); - *ip += 4; +/* Decode the next code point from the n bytes of an ScrStr's storage. + * Storage is WTF-8, except where raw outside bytes become a string + * unchecked (HTTP header values). A byte that does not start a complete, + * well-formed sequence stands for U+0000..U+00FF, as each byte of such a + * header does in Node, and advances by one; nothing is read past n. */ +static uint32_t scr_bytes_next_cp(const uint8_t *in, size_t n, size_t *ip) { + size_t i = *ip; + uint8_t b = in[i]; + *ip = i + 1; + if (b < 0x80) return b; + size_t len = b >= 0xc2 && b <= 0xdf ? 2 : b >= 0xe0 && b <= 0xef ? 3 : b >= 0xf0 && b <= 0xf4 ? 4 : 0; + if (len == 0 || n - i < len) return b; + uint8_t lo = b == 0xe0 ? 0xa0 : b == 0xf0 ? 0x90 : 0x80; + uint8_t hi = b == 0xf4 ? 0x8f : 0xbf; + if (in[i + 1] < lo || in[i + 1] > hi) return b; + for (size_t k = 2; k < len; k++) + if ((in[i + k] & 0xc0) != 0x80) return b; + uint32_t cp = b & (0x7f >> len); + for (size_t k = 1; k < len; k++) cp = (cp << 6) | (in[i + k] & 0x3f); + *ip = i + len; return cp; } @@ -2287,7 +2284,7 @@ ScrDyn *scr_text_encode_into(const ScrDyn *source, const ScrDyn *destination) { read += ascii; if (written == limit) break; size_t end = written; - uint32_t cp = scr_bytes_next_cp((const uint8_t *)text->data, &end); + uint32_t cp = scr_bytes_next_cp((const uint8_t *)text->data, text->len, &end); if (end > dest->len) break; read += cp > 0xffff ? 2 : 1; written = end; @@ -2440,7 +2437,7 @@ ScrBytes *scr_buffer_transcode(const ScrDyn *source, const ScrDyn *from, const S } ScrStr *text = scr_str_from_utf8_lossy(input->data, len); for (size_t i = 0; i < text->len;) { - uint32_t cp = scr_bytes_next_cp((const uint8_t *)text->data, &i); + uint32_t cp = scr_bytes_next_cp((const uint8_t *)text->data, text->len, &i); written = scr_transcode_put(result->data, written, cp, to_encoding); } scr_str_release(text); @@ -2774,6 +2771,7 @@ static ScrBytes *scr_bytes_decode_b64(ScrBytesSink *sink, const uint8_t *in, siz o += 6; i += 8; } + if (i == n) break; /* the groups ended the input */ } /* Common complete groups need neither per-character branches nor state. */ if (have == 0 && n - i >= 4) { @@ -2824,7 +2822,7 @@ static ScrBytes *scr_bytes_encode_str(ScrBytesSink *sink, const ScrStr *s, const size_t o = s->ascii ? n : scr_bytes_ascii_prefix(in, n), i = o; memcpy(b->data, in, o); while (i < n) { - uint32_t cp = scr_bytes_next_cp(in, &i); + uint32_t cp = scr_bytes_next_cp(in, n, &i); if (cp > 0xffff) { cp -= 0x10000; b->data[o++] = (uint8_t)(0xd800 + (cp >> 10)); @@ -2841,7 +2839,7 @@ static ScrBytes *scr_bytes_encode_str(ScrBytesSink *sink, const ScrStr *s, const ScrBytes *b = scr_bytes_conversion_storage_in(sink, n * 2); size_t o = 0, i = 0; while (i < n) { - uint32_t cp = scr_bytes_next_cp(in, &i); + uint32_t cp = scr_bytes_next_cp(in, n, &i); if (cp > 0xffff) { cp -= 0x10000; uint32_t hi = 0xd800 + (cp >> 10), lo = 0xdc00 + (cp & 0x3ff); @@ -3745,7 +3743,7 @@ double scr_bytes_write_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc, continue; } } - uint32_t cp = scr_bytes_next_cp((const uint8_t *)s->data, &i); + uint32_t cp = scr_bytes_next_cp((const uint8_t *)s->data, s->len, &i); uint32_t first = cp, second = 0; if (cp > 0xffff) { cp -= 0x10000; diff --git a/packages/runtime/test/test_bytes.c b/packages/runtime/test/test_bytes.c index 8c6602948..1aea7a2d0 100644 --- a/packages/runtime/test/test_bytes.c +++ b/packages/runtime/test/test_bytes.c @@ -216,6 +216,135 @@ static void test_encodings(void) { scr_str_release(b64); } +/* The length of the well-formed WTF-8 sequence at in[i], or 0. */ +static size_t ref_seq_len(const uint8_t *in, size_t n, size_t i) { + uint8_t b = in[i]; + if (b < 0x80) return 1; + size_t len; + uint8_t lo = 0x80, hi = 0xbf; + if (b >= 0xc2 && b <= 0xdf) len = 2; + else if (b >= 0xe0 && b <= 0xef) len = 3; + else if (b >= 0xf0 && b <= 0xf4) len = 4; + else return 0; + if (b == 0xe0) lo = 0xa0; + if (b == 0xf0) lo = 0x90; + if (b == 0xf4) hi = 0x8f; + if (n - i < len || in[i + 1] < lo || in[i + 1] > hi) return 0; + for (size_t k = 2; k < len; k++) + if ((in[i + k] & 0xc0) != 0x80) return 0; + return len; +} + +/* The UTF-16 units of string storage, one at a time: a well-formed + * sequence gives its code point's units and any other byte stands for + * U+0000..U+00FF (a raw HTTP header byte). */ +static size_t ref_units(const uint8_t *in, size_t n, uint16_t *out) { + size_t k = 0; + for (size_t i = 0; i < n;) { + size_t len = ref_seq_len(in, n, i); + if (len == 0) { + out[k++] = in[i++]; + continue; + } + uint32_t cp = len == 1 ? in[i] : (uint32_t)(in[i] & (0x7f >> len)); + for (size_t j = 1; j < len; j++) cp = (cp << 6) | (in[i + j] & 0x3f); + i += len; + if (cp > 0xffff) { + out[k++] = (uint16_t)(0xd800 + ((cp - 0x10000) >> 10)); + out[k++] = (uint16_t)(0xdc00 + ((cp - 0x10000) & 0x3ff)); + } else { + out[k++] = (uint16_t)cp; + } + } + return k; +} + +/* Encodes storage that is not well-formed (raw header bytes become string + * storage unchecked) and compares latin1, ascii and utf16le with the + * reference; hex and base64 decode the latin1 bytes. ASan checks that no + * path reads or writes outside either buffer. */ +/* encs: latin1, ascii, utf16le, hex, base64. */ +static void check_raw_encodings(ScrStr *const *encs, const uint8_t *in, size_t n, const char *what) { + ScrStr *latin1 = encs[0], *ascii = encs[1], *utf16le = encs[2], *hex = encs[3], *b64 = encs[4]; + ScrStr *s = scr_str_new((const char *)in, n); + uint16_t units[64]; + size_t k = ref_units(in, n, units); + ScrStr *narrow[] = {latin1, ascii}; + for (size_t e = 0; e < 2; e++) { + ScrBytes *b = scr_bytes_from_str(s, narrow[e]); + bool ok = b->len == k; + for (size_t j = 0; ok && j < k; j++) ok = b->data[j] == (uint8_t)units[j]; + check(ok, what); + scr_bytes_release(b); + } + ScrBytes *wide = scr_bytes_from_str(s, utf16le); + bool ok = wide->len == 2 * k; + for (size_t j = 0; ok && j < k; j++) + ok = wide->data[2 * j] == (uint8_t)units[j] && wide->data[2 * j + 1] == units[j] >> 8; + check(ok, what); + scr_bytes_release(wide); + scr_bytes_release(scr_bytes_from_str(s, hex)); + scr_bytes_release(scr_bytes_from_str(s, b64)); + scr_str_release(s); +} + +static void test_raw_storage_encodings(void) { + /* Each pattern at every offset in an eight-byte word, alone and followed + * by ASCII, two-byte characters or more of the pattern. */ + static const struct { const char *bytes; size_t n; } patterns[] = { + {"\x80", 1}, {"\x80\x81\xbf\x90", 4}, {"\xc3", 1}, {"\xc3\xa9", 2}, + {"\xc3\xa9\xc3", 3}, {"\xc0\x80\xc1\xbf", 4}, {"\xe9\x41\xe9", 3}, + {"\xf0\x9f\x41", 3}, {"\xe2\x82", 2}, {"\xe2\x82\xac", 3}, + {"\xf0\x9f\x98\x80", 4}, {"\xed\xa0\x80", 3}, {"\xff\xfe\xf5", 3}, + {"\xdf\xbf\x80\x80", 4}, {"\xc2\x80\xc2", 3}, {"x\xe9y\x80\x80z\xdf", 7}, + }; + static const struct { const char *bytes; size_t n; } tails[] = { + {"", 0}, {"bcdefghij", 9}, {"\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9", 8}, {"\x80\xc3\xa9", 3}, + }; + ScrStr *encs[] = {S("latin1"), S("ascii"), S("utf16le"), S("hex"), S("base64")}; + uint8_t buf[64]; + char what[96]; + for (size_t p = 0; p < sizeof patterns / sizeof patterns[0]; p++) { + for (size_t offset = 0; offset < 17; offset++) { + for (size_t t = 0; t < sizeof tails / sizeof tails[0]; t++) { + for (size_t lead = 0; lead < 2; lead++) { + /* lead: the offset is two-byte characters instead of ASCII. */ + size_t n = 0; + for (size_t j = 0; j < offset; j++) { + if (lead && j + 1 < offset) { + buf[n++] = 0xc3; + buf[n++] = 0xa9; + j++; + } else { + buf[n++] = 'a'; + } + } + memcpy(buf + n, patterns[p].bytes, patterns[p].n); + n += patterns[p].n; + memcpy(buf + n, tails[t].bytes, tails[t].n); + n += tails[t].n; + snprintf(what, sizeof what, "raw storage encodings: pattern %zu offset %zu tail %zu lead %zu", + p, offset, t, lead); + check_raw_encodings(encs, buf, n, what); + } + } + } + } + /* Random bytes, mostly high, of every length up to 40. */ + uint32_t seed = 12345; + for (int round = 0; round < 4000; round++) { + size_t n = (size_t)round % 41; + for (size_t j = 0; j < n; j++) { + seed = seed * 1103515245u + 12345u; + uint8_t r = (uint8_t)(seed >> 16); + buf[j] = (seed >> 27) & 1 ? (uint8_t)(0x80 | r) : (uint8_t)(r & 0x7f); + } + snprintf(what, sizeof what, "raw storage encodings: random round %d", round); + check_raw_encodings(encs, buf, n, what); + } + for (size_t e = 0; e < sizeof encs / sizeof encs[0]; e++) scr_str_release(encs[e]); +} + static void test_concat_and_u32be(void) { ScrArr *list = scr_arr_new(SCR_ELEM_BYTES, 0); ScrBytes *a = scr_bytes_new(SCR_BYTES_U8, 2); @@ -445,6 +574,7 @@ int main(int argc, char **argv) { scr_bytes_test_bulk_storage(); scr_bytes_test_buffer_pool(); test_encodings(); + test_raw_storage_encodings(); test_concat_and_u32be(); test_from_arr_and_random(); test_zlib_roundtrip(); diff --git a/tests/corpus/8010-http-header-raw-bytes.ts b/tests/corpus/8010-http-header-raw-bytes.ts new file mode 100644 index 000000000..d8408f335 --- /dev/null +++ b/tests/corpus/8010-http-header-raw-bytes.ts @@ -0,0 +1,72 @@ +// Header values hold the raw bytes a client sent, one character per byte +// (Node decodes them as latin1). Bytes that are not well-formed UTF-8 (lone +// continuation bytes, runs of them, truncated leads, overlong leads and +// bytes no sequence uses) at every offset within an eight-byte word encode +// to the same bytes as in Node with latin1, ascii, binary, hex, base64 and +// utf16le. Strings built in the program take the same encodings with two-, +// three- and four-byte characters at every offset. +import { createServer } from "node:http"; +import { connect } from "node:net"; + +const patterns: number[][] = [ + [0x80], + [0x80, 0x81, 0xbf, 0x90], + [0xc3], + [0xc0, 0x80, 0xc1, 0xbf], + [0xe9, 0x41, 0xe9], + [0xf0, 0x9f, 0x41], + [0xe2, 0x82], + [0xff, 0xfe, 0xf5], + [0x78, 0xe9, 0x79, 0x80, 0x80, 0x7a, 0xdf], +]; +const encodings = ["latin1", "ascii", "binary", "hex", "base64", "utf16le"] as const; + +function hex(bytes: Buffer): string { + return bytes.toString("hex"); +} + +function describe(label: string, value: string): void { + console.log(label, encodings.map((encoding) => `${encoding}=${hex(Buffer.from(value, encoding))}`).join(" ")); +} + +const names: string[] = []; +const head: Buffer[] = [Buffer.from("GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n")]; +for (let p = 0; p < patterns.length; p++) { + for (let offset = 0; offset < 10; offset++) { + for (const tail of ["", "bcdefghij"]) { + const name = `x-${p}-${offset}-${tail.length}`; + names.push(name); + head.push(Buffer.from(`${name}: ${"a".repeat(offset)}`)); + head.push(Buffer.from(patterns[p]!)); + head.push(Buffer.from(`${tail}\r\n`)); + } + } +} +head.push(Buffer.from("\r\n")); + +const server = createServer((req, res) => { + for (const name of names) { + const value = req.headers[name]; + if (typeof value === "string") describe(name, value); + else console.log(name, "missing"); + } + res.end("ok"); +}); + +server.listen(0, "127.0.0.1", () => { + const address = server.address(); + const port = typeof address === "object" && address !== null ? address.port : 0; + const socket = connect({ port, host: "127.0.0.1" }, () => { + socket.write(Buffer.concat(head)); + }); + socket.on("data", () => {}); + socket.on("close", () => { + server.close(); + for (const text of ["é", "éü", "߀", "😀", "é😀ü", "߿\u0080"]) { + for (let offset = 0; offset < 17; offset++) { + const value = "a".repeat(offset) + text + "b".repeat(offset % 9) + text; + describe(`text ${offset} ${JSON.stringify(text)}`, value); + } + } + }); +}); diff --git a/tests/harness/corpus-costs/8010-http-header-raw-bytes.ts.json b/tests/harness/corpus-costs/8010-http-header-raw-bytes.ts.json new file mode 100644 index 000000000..532e3bd74 --- /dev/null +++ b/tests/harness/corpus-costs/8010-http-header-raw-bytes.ts.json @@ -0,0 +1 @@ +{ "plain": 3468, "san": 7463 } From a88fdacbe2ab53692a6e8ed9c76f6acba0847a7a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 13:00:18 -0500 Subject: [PATCH 235/273] Iterate strings over their bytes for-of, spread, Array.from, yield* and [Symbol.iterator]() over a string walked a UTF-16 index. Each step mapped the index to a byte offset, built the character, and measured its UTF-16 length to advance. They now walk a byte cursor over the WTF-8 data: - The cpAtByte intrinsic reads the character that starts at the cursor. ASCII bytes take the one-character table inline; other characters call scr_str_cp_at_byte. byteLength advances the cursor by the character's byte count. A lone surrogate is its own three-byte character, so it stays a one-unit character, as in Node. - Spread and Array.from of a string call scr_str_code_points, which counts the characters, allocates the array once and fills it. - The one-character strings of U+0080..U+07FF are made immortal on first use and shared, as the ASCII ones are static. The cpAt intrinsic, which no lowering emits any more, is removed. --- benchmarks/runtime/README.md | 1 + .../runtime/workloads/string-iteration.json | 6 + .../runtime/workloads/string-iteration.ts | 59 +++++++++ .../src/backend/llvm/expr-containers.ts | 36 ++++-- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/backend/llvm/string-lifetimes.test.ts | 2 +- .../src/backend/llvm/string-lifetimes.ts | 3 +- .../compiler/src/backend/llvm/string-reads.ts | 27 ++++ .../lowering/containers/array-construction.ts | 115 +----------------- .../lowering/containers/array-pipelines.ts | 3 +- .../lowering/containers/collection-methods.ts | 2 +- .../lowering/expressions/array-literals.ts | 2 +- .../frontend/lowering/generator-delegation.ts | 5 +- .../frontend/lowering/iterator-adapters.ts | 6 +- .../src/frontend/lowering/lower-stmts.ts | 32 ++--- .../compiler/src/ir/builtin-signatures.ts | 1 + packages/compiler/src/ir/integer-ranges.ts | 2 + .../compiler/src/ir/intrinsic-signatures.ts | 3 +- packages/compiler/src/ir/ir.ts | 11 +- .../test/string-iteration-emission.test.ts | 47 +++++++ .../8347-string-iteration-code-points.ts.json | 6 + packages/runtime/src/scr_string.c | 72 ++++++++++- packages/runtime/src/scr_string_methods.h | 9 ++ tests/corpus/1561-forof-strings.ts | 4 +- .../8347-string-iteration-code-points.ts | 96 +++++++++++++++ .../8347-string-iteration-code-points.ts.json | 1 + 26 files changed, 399 insertions(+), 153 deletions(-) create mode 100644 benchmarks/runtime/workloads/string-iteration.json create mode 100644 benchmarks/runtime/workloads/string-iteration.ts create mode 100644 packages/compiler/test/string-iteration-emission.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8347-string-iteration-code-points.ts.json create mode 100644 tests/corpus/8347-string-iteration-code-points.ts create mode 100644 tests/harness/corpus-costs/8347-string-iteration-code-points.ts.json diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index af3d3d239..0157503fd 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -32,6 +32,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` | | `regex-replacers` | cpu | regex `replace`/`replaceAll` with inline callbacks whose parameters have no annotations: dictionary placeholders, snake_case to camelCase, date reordering, optional captures, capitalization, and a string search that reads the offset | | `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 | +| `string-iteration` | cpu | code point iteration over text that mixes ASCII, Latin-1 letters, CJK and emoji: a for-of word tokenizer, spread reversal, and `Array.from` over words | | `template-render` | cpu | template literals, escaping, `map`/`join` | | `throw-chains` | cpu | exceptions caught 2, 5 and 20 recursive calls above the throw, through direct calls and through a callback, and a backtracking parser that throws on a failed alternative | | `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 | diff --git a/benchmarks/runtime/workloads/string-iteration.json b/benchmarks/runtime/workloads/string-iteration.json new file mode 100644 index 000000000..21456bd23 --- /dev/null +++ b/benchmarks/runtime/workloads/string-iteration.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/string-iteration.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/string-iteration.ts b/benchmarks/runtime/workloads/string-iteration.ts new file mode 100644 index 000000000..2abebd635 --- /dev/null +++ b/benchmarks/runtime/workloads/string-iteration.ts @@ -0,0 +1,59 @@ +// Code point iteration over mixed text: a word tokenizer over for-of, a +// character histogram, reversal through spread, and Array.from over +// words. The text mixes ASCII, Latin-1 letters, CJK and emoji. +let seed = 11; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} +const pieces = [ + "the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog", + "café", "naïve", "über", "señor", "straße", "déjà", "vu", + "日本", "語", "テキスト", "中文", "한국어", + "😀", "🚀", "👍", "🎉", +]; +const punctuation = [" ", " ", " ", ", ", ". ", "! ", "\n"]; +const scale = Number(process.argv[2] ?? "1"); +const lines: string[] = []; +for (let i = 0; i < Math.floor(4000 * scale); i++) { + let line = ""; + const words = 6 + Math.floor(random() * 10); + for (let w = 0; w < words; w++) { + line += pieces[Math.floor(random() * pieces.length)]!; + line += punctuation[Math.floor(random() * punctuation.length)]!; + } + lines.push(line); +} + +function isWordChar(c: string): boolean { + const code = c.codePointAt(0)!; + return (code >= 0x61 && code <= 0x7a) || (code >= 0x41 && code <= 0x5a) || code >= 0xc0; +} + +let tokens = 0; +let tokenUnits = 0; +let reversedLength = 0; +let fromLength = 0; +const histogram = new Map(); +for (let round = 0; round < 30; round++) { + for (const line of lines) { + let word = ""; + for (const c of line) { + if (isWordChar(c)) { + word += c; + continue; + } + if (word !== "") { + tokens++; + tokenUnits += word.length; + if (round === 0) histogram.set(word, (histogram.get(word) ?? 0) + 1); + fromLength += Array.from(word).length; + word = ""; + } + } + reversedLength += [...line].reverse().join("").length; + } +} +const top = [...histogram].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, 5); +console.log(top.map(([word, count]) => `${word}:${count}`).join(" ")); +console.log("tokens", tokens, "units", tokenUnits, "reversed", reversedLength, "from", fromLength); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 307481737..4360ae924 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -49,11 +49,13 @@ import { emitFusedMapSet, rememberMapEntry } from "./map-entry-fusion.js"; import { emitScratchStringSlice, emitStringSliceRead } from "./string-slices.js"; import { STRING_CHAR_AT_DECL, + STRING_CP_AT_BYTE_DECL, STRING_CHAR_AT_INT_MARKER, STRING_CHAR_CODE_AT_DECL, STRING_CHAR_CODE_AT_INT_MARKER, emitAsciiChar, emitAsciiCharCode, + emitStringByteLength, emitUnitCharCode, STRING_UTF16_LEN_DECL, } from "./string-reads.js"; @@ -564,16 +566,30 @@ export function emitStrIntrinsic( return call("scr_str_is_well_formed", "zeroext i1 (ptr)", `ptr ${r.name}`, "i1", false); case "toWellFormed": return call("scr_str_to_well_formed", "ptr (ptr)", `ptr ${r.name}`, "ptr", true); - case "cpAt": - // The code point AT an index as a one-code-point string (+1) — - // the string-for-of desugar's read. - return call( - "scr_str_cp_at", - "ptr (ptr, double)", - `ptr ${r.name}, double ${args[0]!.name}`, - "ptr", - true, - ); + case "byteLength": { + const t = B.tmp(); + // A byte count fits in a signed size, whose conversion is one instruction. + B.line(`${t} = sitofp ${host.sizeType} ${emitStringByteLength(host, r.name)} to double`); + return { name: t, type: e.type }; + } + case "cpAtByte": { + // sc_str_cp_at_byte (string-reads.ts) answers ASCII bytes from the + // charAt table inline. The cursor is a character start, an exact + // integer below the byte count. + host.declare(STRING_CHAR_AT_DECL); + host.declare(STRING_CP_AT_BYTE_DECL); + const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null; + let wide: string; + if (index) { + wide = widenInteger(host, index); + } else { + wide = B.tmp(); + B.line(`${wide} = fptosi double ${args[0]!.name} to i64`); + } + const t = B.tmp(); + B.line(`${t} = call ptr @sc_str_cp_at_byte(ptr ${r.name}, i64 ${wide})`); + return host.own({ name: t, type: e.type }); + } default: { const _exhaustive: never = method; void _exhaustive; diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 2548b75d5..7688746b8 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -104,6 +104,7 @@ export const LIB_FN_SYMS: Record = { "str.localeCase": "scr_str_locale_case", "str.replaceLiteral": "scr_str_replace_literal", "str.replaceChars": "scr_str_replace_chars", + "str.codePoints": "scr_str_code_points", "num.fromString": "scr_string_to_number", "math.round": "scr_math_round", "math.sin": "sin", diff --git a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts index 07ad31153..72fc57752 100644 --- a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts +++ b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts @@ -341,7 +341,7 @@ const methodCases: { { method: "normalize", args: [ref("argument")], result: STRING, target: "normalize" }, { method: "isWellFormed", args: [], result: BOOL, target: "is_well_formed" }, { method: "toWellFormed", args: [], result: STRING, target: "to_well_formed" }, - { method: "cpAt", args: [num(1)], result: STRING, target: "cp_at" }, + { method: "cpAtByte", args: [num(1)], result: STRING, target: "cp_at_byte", inline: true }, ]; test.each(methodCases)( diff --git a/packages/compiler/src/backend/llvm/string-lifetimes.ts b/packages/compiler/src/backend/llvm/string-lifetimes.ts index 6c3079cc1..5a4504813 100644 --- a/packages/compiler/src/backend/llvm/string-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/string-lifetimes.ts @@ -30,7 +30,8 @@ export function borrowsStringInputs(method: IrStrIntrinsicMethod): boolean { case "normalize": case "isWellFormed": case "toWellFormed": - case "cpAt": + case "byteLength": + case "cpAtByte": return true; default: return false; diff --git a/packages/compiler/src/backend/llvm/string-reads.ts b/packages/compiler/src/backend/llvm/string-reads.ts index 5b7a26c6c..1f77de2c2 100644 --- a/packages/compiler/src/backend/llvm/string-reads.ts +++ b/packages/compiler/src/backend/llvm/string-reads.ts @@ -43,6 +43,10 @@ export const STRING_CHAR_CODE_AT_INT_MARKER = "; uses @sc_str_char_code_at_int"; 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 code-point character starting at a byte offset (the string + * iterator's step). Requested together with STRING_CHAR_AT_DECL, which + * defines the ASCII table. */ +export const STRING_CP_AT_BYTE_DECL = "declare ptr @scr_str_cp_at_byte(ptr, i64)"; /** The proven-ASCII flag of string `s`, as an i1. */ export function emitStringAsciiFlag(host: LlvmEmitterContext, s: string): string { @@ -351,6 +355,29 @@ export const STRING_READ_HELPERS: readonly StringReadHelper[] = [ ]; }, }, + { + // The string iterator's step at byte `%b`, a character start below the + // byte count: an ASCII byte is the table entry for it; any other lead + // byte takes the runtime, which copies the character's bytes. + decl: STRING_CP_AT_BYTE_DECL, + define: (sz, attrs) => [ + `define internal ptr @sc_str_cp_at_byte(ptr %s, i64 %b) ${attrs} {`, + `entry:`, + ` %data = getelementptr inbounds %ScrStr, ptr %s, i32 1`, + ` %p = getelementptr inbounds i8, ptr %data, i64 %b`, + ` %byte = load i8, ptr %p`, + ` %ascii = icmp sge i8 %byte, 0`, + ` br i1 %ascii, label %read, label %slow`, + `read:`, + ` %code = zext i8 %byte to i64`, + ` %char = getelementptr inbounds [128 x { ${sz}, ${sz}, ${sz}, [2 x i8] }], ptr @sc_ascii_chars, i64 0, i64 %code`, + ` ret ptr %char`, + `slow:`, + ` %r = call ptr @scr_str_cp_at_byte(ptr %s, i64 %b) nounwind willreturn`, + ` ret ptr %r`, + `}`, + ], + }, { // An exact integer index, as for sc_str_char_code_at_int. Requested // together with STRING_CHAR_AT_DECL, which defines the table. diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 5029abc36..702837e40 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -1081,9 +1081,7 @@ export function lowerArrayFromCall( if ((args.length === 1 || args.length === 2) && !ts.isObjectLiteralExpression(args[0]!)) { const source = lowerer.lowerExpr(args[0]!); let src = - source.type.kind === "string" && args.length === 2 - ? strCharsCall(lowerer, source, loc) - : source; + source.type.kind === "string" && args.length === 2 ? strCharsCall(source, loc) : source; if (src.type.kind === "array") { const arrT = src.type; if (args.length === 1) { @@ -1171,7 +1169,7 @@ export function lowerArrayFromCall( } // String iteration advances by code point; the same helper serves // `[...s]` and both Array.from(s) forms. - if (src.type.kind === "string" && args.length === 1) return strCharsCall(lowerer, src, loc); + if (src.type.kind === "string" && args.length === 1) return strCharsCall(src, loc); if (src.type.kind === "dyn" && args.length === 1) { return lowerCheckedArrayFrom(lowerer, src, loc); } @@ -1318,112 +1316,9 @@ function buildArrayFromArrayFn( } /** `Array.from(s)` / `[...s]` on a STRING: the code-point split into a - * fresh string[], through one interned helper per module. */ -export function strCharsCall(lowerer: Lowerer, src: IrExpr, loc: SrcLoc): IrExpr { - const key = "strChars"; - let helper = lowerer.arrHofHelpers.get(key); - if (!helper) { - helper = "%str.chars"; - lowerer.arrHofHelpers.set(key, helper); - lowerer.liftedFns.push(buildStrCharsFn(helper, loc)); - } - return { kind: "call", callee: helper, args: [src], type: arrayOf(STRING), loc }; -} - -/** The code-point split, from existing IR nodes — the string for-of - * desugar's UTF-16 cursor as a function: - * - * out = []; - * i = 0; - * while (i < s.length) { ch = cpAt(s, i); i += ch.length; out.push(ch); } - * return out; - */ -function buildStrCharsFn(name: string, loc: SrcLoc): IrFunction { - const outT = arrayOf(STRING); - - const sLen = (recv: IrExpr): IrExpr => ({ - kind: "strIntrinsic", - method: "length", - receiver: recv, - args: [], - type: F64, - loc, - }); - const body: IrStmt[] = [ - { - kind: "varDecl", - localId: "out.0", - init: { kind: "arrayLit", elems: [], type: outT, loc }, - loc, - }, - { kind: "varDecl", localId: "i.0", init: { kind: "numLit", value: 0, type: F64, loc }, loc }, - { - kind: "while", - cond: { - kind: "bin", - op: "<", - left: varRef("i.0", F64, loc), - right: sLen(varRef("s.0", STRING, loc)), - type: BOOL, - loc, - }, - body: [ - { - kind: "varDecl", - localId: "ch.0", - init: { - kind: "strIntrinsic", - method: "cpAt", - receiver: varRef("s.0", STRING, loc), - args: [varRef("i.0", F64, loc)], - type: STRING, - loc, - }, - loc, - }, - { - kind: "assign", - localId: "i.0", - value: { - kind: "bin", - op: "+", - left: varRef("i.0", F64, loc), - right: sLen(varRef("ch.0", STRING, loc)), - type: F64, - loc, - }, - loc, - }, - { - kind: "exprStmt", - expr: { - kind: "arrIntrinsic", - method: "push", - receiver: varRef("out.0", outT, loc), - args: [varRef("ch.0", STRING, loc)], - type: F64, - loc, - }, - loc, - }, - ], - loc, - }, - { kind: "return", value: varRef("out.0", outT, loc), loc }, - ]; - return { - name, - params: [{ localId: "s.0", name: "s", type: STRING }], - returnType: outT, - locals: [ - { id: "s.0", name: "s", type: STRING, mutable: true }, - { id: "out.0", name: "out", type: outT, mutable: false }, - { id: "i.0", name: "i", type: F64, mutable: true }, - { id: "ch.0", name: "ch", type: STRING, mutable: false }, - ], - body, - loc, - }; + * fresh string[] (scr_str_code_points). */ +export function strCharsCall(src: IrExpr, loc: SrcLoc): IrExpr { + return { kind: "libCall", fn: "str.codePoints", args: [src], type: arrayOf(STRING), loc }; } /** The lowered `length` value of the one-property source literal, or null diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index 197af2a9e..9cc96d0a5 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -156,7 +156,8 @@ const PURE_STRING = new Set([ "toUpperCase", "isWellFormed", "toWellFormed", - "cpAt", + "byteLength", + "cpAtByte", ]); function primitive(t: IrType): boolean { diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index f18291a63..bfaca6f96 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -1135,7 +1135,7 @@ function setFromSeedValue( if (source.type.kind === "set" && typeEquals(source.type, setT)) { return { kind: "setIntrinsic", method: "clone", receiver: source, args: [], type: setT, loc }; } else if (source.type.kind === "string" && setT.elem.kind === "string") { - seed = strCharsCall(lowerer, source, loc); + seed = strCharsCall(source, loc); } else if (source.type.kind === "array" && typeEquals(source.type.elem, setT.elem)) { seed = source; } else if ( diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 903f0884d..77c16594f 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -504,7 +504,7 @@ export function lowerArrayLiteral( // interned helper as Array.from(s); the result rides the array // machinery below like any string[] source. if (src.type.kind === "string") { - src = strCharsCall(lowerer, src, locOf(el)); + src = strCharsCall(src, locOf(el)); } // `[...typedArray]`: represented typed arrays are dense numeric // iterables, so drain their elements into the fresh number[] that diff --git a/packages/compiler/src/frontend/lowering/generator-delegation.ts b/packages/compiler/src/frontend/lowering/generator-delegation.ts index a606c177e..81ecc8353 100644 --- a/packages/compiler/src/frontend/lowering/generator-delegation.ts +++ b/packages/compiler/src/frontend/lowering/generator-delegation.ts @@ -577,10 +577,11 @@ export function lowerBuiltinDelegation( loc, }); } else if (sourceT.kind === "string") { + // A byte cursor over the WTF-8 data, as for-of over a string walks. const ch = lowerer.declareHiddenLocal("%delegateElement", STRING); const length = (receiver: IrExpr): IrExpr => ({ kind: "strIntrinsic", - method: "length", + method: "byteLength", receiver, args: [], type: F64, @@ -595,7 +596,7 @@ export function lowerBuiltinDelegation( localId: ch.id, init: { kind: "strIntrinsic", - method: "cpAt", + method: "cpAtByte", receiver: src(), args: [at()], type: STRING, diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index 4909e7f45..78fcff84d 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -743,7 +743,7 @@ export function lowerStringIteratorValue( left: index, right: { kind: "strIntrinsic", - method: "length", + method: "byteLength", receiver: text, args: [], type: F64, @@ -758,7 +758,7 @@ export function lowerStringIteratorValue( localId: "point.0", init: { kind: "strIntrinsic", - method: "cpAt", + method: "cpAtByte", receiver: text, args: [index], type: STRING, @@ -775,7 +775,7 @@ export function lowerStringIteratorValue( left: index, right: { kind: "strIntrinsic", - method: "length", + method: "byteLength", receiver: point, args: [], type: F64, diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index d6d8c1cc7..9586727b3 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -2668,7 +2668,7 @@ export function lowerBindingPattern( lowerer.unsupported("SC1031", el, "rest elements over a union of array or tuple layouts"); const loc = locOf(el); let value = unionArrayValueRead(lowerer, srcRef(), numLit(i, loc), loc, (text) => - strCharsCall(lowerer, text, loc), + strCharsCall(text, loc), ); if (!value) lowerer.unsupported( @@ -2798,7 +2798,7 @@ export function lowerBindingPattern( } // STRING sources (`const [a, b] = str`): array destructuring walks // the STRING ITERATOR — code points, astral characters whole — - // which is Array.from(string)'s own machinery (%str.chars), so the + // which is Array.from(string)'s own machinery (str.codePoints), so the // source splits once into a hidden chars array and the pattern // lowers as an array pattern over string[]. Positions, holes, // defaults (with the bounds test), rest (the remaining code points @@ -2817,7 +2817,7 @@ export function lowerBindingPattern( out.push({ kind: "varDecl", localId: chars.id, - init: strCharsCall(lowerer, srcRef(), loc), + init: strCharsCall(srcRef(), loc), loc, }); lowerer.lowerBindingPattern( @@ -9826,7 +9826,7 @@ function lowerArrayAssignInto( let srcType = tmpRef().type; // A STRING source (`[a, b] = s`): the declaration path's rule in // assignment position — the string iterator's code-point split - // (%str.chars, astral characters whole) into a hidden chars array, + // (str.codePoints, astral characters whole) into a hidden chars array, // then the pattern proceeds as an array pattern over string[] // (defaults with the bounds test, rest packing the remaining code // points, elisions; positions past the last code point inherit the @@ -9839,7 +9839,7 @@ function lowerArrayAssignInto( out.push({ kind: "varDecl", localId: chars.id, - init: strCharsCall(lowerer, tmpRef(), loc), + init: strCharsCall(tmpRef(), loc), loc, }); tmpRef = () => ({ kind: "varRef", localId: chars.id, type: charsT, loc }); @@ -11383,16 +11383,18 @@ function lowerForOfMatchAll( /** `for (const ch of s)` over a STRING: JS's string iterator, which walks * code POINTS — each pass binds the whole character (a two-unit string * for astral chars, where charAt/slice would return one surrogate half). - * Desugars over the UTF-16 cursor machinery: a hidden unit - * index reads the character with the cpAt intrinsic and advances by ITS - * length before the body runs (so `continue` never skips the advance): + * Desugars over a byte cursor into the WTF-8 data: a hidden byte offset + * reads the character that starts there with the cpAtByte intrinsic and + * advances by ITS byte count before the body runs (so `continue` never + * skips the advance): * * { const %s = ; let %i = 0; - * while (%i < %s.length) { const ch = cpAt(%s, %i); %i += ch.length; } } + * while (%i < byteLength(%s)) { const ch = cpAtByte(%s, %i); %i += byteLength(ch); } } * - * Strings are immutable, so the length is stable and there is no + * Strings are immutable, so the byte count is stable and there is no * iterator state to unwind on throw; a lone surrogate is a one-unit - * character, so every step lands on a character start. */ + * character with its own three-byte encoding, so every step lands on a + * character start. */ /** for-of over a CLASS ITERABLE (classIteratorOf's shape) — the * iterator protocol as ordinary method calls: * @@ -12087,7 +12089,7 @@ function lowerForOfString( try { const s = lowerer.declareHiddenLocal("%strof", STRING); const i = lowerer.declareHiddenLocal("%iterof", F64); - i.mutable = true; // the unit index reassigns (hidden locals default const) + i.mutable = true; // the byte cursor reassigns (hidden locals default const) const sRef = (): IrExpr => ({ kind: "varRef", localId: s.id, type: STRING, loc }); const iRef = (): IrExpr => ({ kind: "varRef", localId: i.id, type: F64, loc }); // `for (var c of str)`: the character mechanics stay on a hidden @@ -12118,7 +12120,7 @@ function lowerForOfString( localId: ch.id, init: { kind: "strIntrinsic", - method: "cpAt", + method: "cpAtByte", receiver: sRef(), args: [iRef()], type: STRING, @@ -12145,7 +12147,7 @@ function lowerForOfString( left: iRef(), right: { kind: "strIntrinsic", - method: "length", + method: "byteLength", receiver: chRef, args: [], type: F64, @@ -12177,7 +12179,7 @@ function lowerForOfString( left: iRef(), right: { kind: "strIntrinsic", - method: "length", + method: "byteLength", receiver: sRef(), args: [], type: F64, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 576386851..bf92c8da3 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -848,6 +848,7 @@ function buildLibFnSignatures() { // (subject, characters, NUL-separated replacement texts): the fused // chain of global one-character replacements. "str.replaceChars": { argTypes: [STRING, STRING, STRING], result: STRING }, + "str.codePoints": { argTypes: [STRING], result: arrayOf(STRING) }, /* ToNumber(string) — ECMA-262 7.1.4.1 StringToNumber (scr_string.c): * trim the JS StrWhiteSpace set, empty/whitespace-only → +0, then the * whole span must be one StrNumericLiteral — signed decimal (Infinity diff --git a/packages/compiler/src/ir/integer-ranges.ts b/packages/compiler/src/ir/integer-ranges.ts index 1909040c4..51c0a929b 100644 --- a/packages/compiler/src/ir/integer-ranges.ts +++ b/packages/compiler/src/ir/integer-ranges.ts @@ -867,6 +867,8 @@ export function analyzeIntegerRangeFacts( ); if (e.type.kind === "f64") { if (e.method === "length") range = { min: 0, max: STRING_MAX_UNITS }; + // A string holds at most SCR_STR_MAX_CAP (0x7fffffff) bytes. + else if (e.method === "byteLength") range = { min: 0, max: STRING_MAX_UNITS }; // An empty needle is found at the clamped position, up to the length. else if (e.method === "indexOf") range = { min: -1, max: STRING_MAX_UNITS }; } diff --git a/packages/compiler/src/ir/intrinsic-signatures.ts b/packages/compiler/src/ir/intrinsic-signatures.ts index eda569883..3e26e77f2 100644 --- a/packages/compiler/src/ir/intrinsic-signatures.ts +++ b/packages/compiler/src/ir/intrinsic-signatures.ts @@ -31,7 +31,8 @@ export const STR_INTRINSIC_SIGS: Record< normalize: { argTypes: [STRING], minArgs: 1, result: STRING }, isWellFormed: { argTypes: [], minArgs: 0, result: BOOL }, toWellFormed: { argTypes: [], minArgs: 0, result: STRING }, - cpAt: { argTypes: [F64], minArgs: 1, result: STRING }, + byteLength: { argTypes: [], minArgs: 0, result: F64 }, + cpAtByte: { argTypes: [F64], minArgs: 1, result: STRING }, }; /** Per-method signature for regexIntrinsic. test/source/flags take the diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 19a5be71d..6118e4955 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -2447,10 +2447,13 @@ export type IrStrIntrinsicMethod = // surrogate, and the copy with each one replaced by U+FFFD. | "isWellFormed" | "toWellFormed" - // The string iterator's step: the full code-POINT character at a UTF-16 - // index (astral chars come back whole where charAt truncates) — for-of - // over strings desugars onto it, advancing by the result's length. - | "cpAt"; + // The string iterator over the WTF-8 bytes: the byte count of a string, + // and the code-point character that starts at a byte offset (a lone + // surrogate is a one-unit character). for-of, spread and Array.from of a + // string walk a byte cursor with them, advancing by the character's + // byte count; the cursor is always a character start below the count. + | "byteLength" + | "cpAtByte"; /** The typed-array/Buffer method surface (bytesIntrinsic). Receiver/arg * conventions (validated): `length`/`byteLength` are property reads → f64; diff --git a/packages/compiler/test/string-iteration-emission.test.ts b/packages/compiler/test/string-iteration-emission.test.ts new file mode 100644 index 000000000..7cda3da55 --- /dev/null +++ b/packages/compiler/test/string-iteration-emission.test.ts @@ -0,0 +1,47 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +const directories: string[] = []; +afterEach(async () => { + await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +async function emit(source: string): Promise { + const outDir = await mkdtemp(join(tmpdir(), "scriptc-string-iteration-")); + directories.push(outDir); + const entry = join(outDir, "main.ts"); + await writeFile(entry, source); + const outPath = join(outDir, "program.ll"); + const result = await compile(entry, { entry, outDir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result)).toBe(true); + return readFile(outPath, "utf8"); +} + +const calls = (llvm: string, symbol: string): string[] => + llvm.split("\n").filter((line) => line.includes("call ") && line.includes(`@${symbol}(`)); + +test("for-of over a string walks a byte cursor", async () => { + const llvm = await emit(` +function count(s: string): number { + let n = 0; + for (const c of s) n += c.length; + return n; +} +console.log(count(process.argv[1] ?? "")); +`); + expect(calls(llvm, "sc_str_cp_at_byte")).toHaveLength(1); + expect(calls(llvm, "scr_str_cp_at")).toHaveLength(0); + // The helper answers ASCII bytes from the one-character table. + expect(llvm).toContain("define internal ptr @sc_str_cp_at_byte(ptr %s, i64 %b)"); +}); + +test("spread and Array.from of a string build the array in one call", async () => { + const llvm = await emit(` +const s = process.argv[1] ?? ""; +console.log([...s].length, Array.from(s).length); +`); + expect(calls(llvm, "scr_str_code_points")).toHaveLength(2); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8347-string-iteration-code-points.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8347-string-iteration-code-points.ts.json new file mode 100644 index 000000000..1e83f7b6f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8347-string-iteration-code-points.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8347-string-iteration-code-points.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 1e7802fbb..cd17da0f8 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -3514,6 +3514,8 @@ ScrStr *scr_bool_to_scrstr(bool b) { * decode is a whole char; an i addressing the LOW half of an astral pair * (unreachable from the iterator, which only lands on char starts) answers * the containing char. Out of range → empty string. Borrows s; +1. */ +static ScrStr *scr_str_char_of(const char *bytes, size_t adv); + ScrStr *scr_str_cp_at(ScrStr *s, double i) { double idx = scr_to_integer_or_infinity(i); if (!(idx >= 0)) return scr_str_empty(); @@ -3528,7 +3530,75 @@ ScrStr *scr_str_cp_at(ScrStr *s, double i) { if (off >= s->len) return scr_str_empty(); /* out of range */ size_t adv; (void)scr_utf8_decode(s->data + off, &adv); - return scr_str_from_span(s->data + off, adv); + return scr_str_char_of(s->data + off, adv); +} + +/* The one-character strings of U+0080..U+07FF (two-byte sequences, lead + * bytes 0xC2..0xDF), made immortal on first use and shared by every + * thread, as the ASCII ones are static: the string iterator returns them + * instead of a new string per character. A string is immortal before its + * slot publishes it; a thread that loses the race to fill a slot keeps its + * own immortal copy, so at most one extra string per slot and thread. */ +static ScrStr *scr_two_byte_chars[0x800 - 0x80]; + +static ScrStr *scr_two_byte_char(const char *bytes) { + uint32_t cp = ((uint32_t)((unsigned char)bytes[0] & 0x1F) << 6) | ((unsigned char)bytes[1] & 0x3F); + ScrStr **slot = &scr_two_byte_chars[cp - 0x80]; + ScrStr *known = __atomic_load_n(slot, __ATOMIC_ACQUIRE); + if (known) return known; + ScrStr *made = scr_str_from_span(bytes, 2); + scr_str_hash_prime(made); + made->rc = SIZE_MAX; +#ifdef SCR_RC_AUDIT + scr_str_live_forget(); +#endif + scr_lsan_immortal(made); + ScrStr *expected = NULL; + if (__atomic_compare_exchange_n(slot, &expected, made, false, __ATOMIC_ACQ_REL, + __ATOMIC_ACQUIRE)) + return made; + return expected; +} + +/* The character whose encoding starts at bytes and is adv bytes long (+1, + * or immortal). */ +static ScrStr *scr_str_char_of(const char *bytes, size_t adv) { + unsigned char lead = (unsigned char)bytes[0]; + if (adv == 1 && lead < 0x80) return (ScrStr *)&scr_ascii1[lead]; + if (adv == 2) return scr_two_byte_char(bytes); + return scr_str_from_span(bytes, adv); +} + +ScrStr *scr_str_cp_at_byte(ScrStr *s, uint64_t b) { + if (b >= s->len) return scr_str_empty(); + size_t adv; + (void)scr_utf8_decode(s->data + b, &adv); + return scr_str_char_of(s->data + b, adv); +} + +ScrArr *scr_str_code_points(ScrStr *s) { + /* Every byte but a continuation byte starts one character; a lone + * surrogate's three bytes are one character. */ + size_t count = s->len; + if (!s->ascii) + for (size_t b = 0; b < s->len; b++) count -= ((unsigned char)s->data[b] & 0xC0) == 0x80; + ScrArr *out = scr_arr_new_inline(SCR_ELEM_STR, count, false); + size_t k = 0; + for (size_t b = 0; b < s->len;) { + unsigned char c = (unsigned char)s->data[b]; + if (c < 0x80) { + out->data[k++] = (uint64_t)(uintptr_t)&scr_ascii1[c]; + b++; + continue; + } + size_t adv; + (void)scr_utf8_decode(s->data + b, &adv); + out->data[k++] = (uint64_t)(uintptr_t)scr_str_char_of(s->data + b, adv); + b += adv; + } + out->len = (uint32_t)count; + out->filled = (uint32_t)count; + return out; } /* ── the URI component codecs (encodeURIComponent/decodeURIComponent) ── diff --git a/packages/runtime/src/scr_string_methods.h b/packages/runtime/src/scr_string_methods.h index 01ccdd8ae..2e69ee6ac 100644 --- a/packages/runtime/src/scr_string_methods.h +++ b/packages/runtime/src/scr_string_methods.h @@ -150,6 +150,15 @@ ScrStr *scr_str_char_at(ScrStr *s, double i); * (two units); the consuming loop advances by the result's length. * Out of range → empty string. Returns +1. */ ScrStr *scr_str_cp_at(ScrStr *s, double i); +/* The string iterator's step over the WTF-8 bytes: the code-point + * character whose encoding starts at byte offset b, a character start + * below s->len (a lone surrogate is its own three-byte character). The + * compiler answers ASCII bytes inline and calls this for the rest. + * Returns +1. */ +ScrStr *scr_str_cp_at_byte(ScrStr *s, uint64_t b); +/* [...s] and Array.from(s): the code-point characters of s in order, as a + * fresh string array of exactly that length. Returns +1. */ +struct ScrArr *scr_str_code_points(ScrStr *s); /* trimStart()/trimEnd(): the one-sided halves of trim. Returns +1. */ ScrStr *scr_str_trim_start(ScrStr *s); diff --git a/tests/corpus/1561-forof-strings.ts b/tests/corpus/1561-forof-strings.ts index 9d16efd9a..0ea09e755 100644 --- a/tests/corpus/1561-forof-strings.ts +++ b/tests/corpus/1561-forof-strings.ts @@ -1,7 +1,7 @@ // for-of over STRINGS: JS's string iterator walks code POINTS — each pass // binds the whole character, so astral chars arrive as one two-unit string -// (where charAt/slice would truncate the halves). Desugars over the UTF-16 -// cursor with the cpAt step intrinsic. +// (where charAt/slice would truncate the halves). Desugars over a byte +// cursor with the cpAtByte step intrinsic. for (const ch of "héllo 🎉x") console.log(ch, ch.length); // The empty string iterates zero times. diff --git a/tests/corpus/8347-string-iteration-code-points.ts b/tests/corpus/8347-string-iteration-code-points.ts new file mode 100644 index 000000000..3a1286140 --- /dev/null +++ b/tests/corpus/8347-string-iteration-code-points.ts @@ -0,0 +1,96 @@ +// The string iterator: for-of, spread, Array.from, yield* and +// [Symbol.iterator]() walk code points. Astral characters come back whole, +// a lone surrogate is a one-unit character, and halves joined by +// concatenation form one character, as in Node. +function unitCodes(c: string): string { + const codes: string[] = []; + for (let k = 0; k < c.length; k++) codes.push(c.charCodeAt(k).toString(16)); + return `${c.length}:${codes.join(".")}`; +} +const show = (chars: string[]): string => chars.map(unitCodes).join(" "); + +const samples = [ + "", + "abc", + "aé😀b", + "\u0000\u007f\u0080߿ࠀ￿", + "\u{10000}\u{10ffff}", + "é ñ", + "\ud800", + "\udc00", + "x\ud83d", + "\ude00x", + "\udc00\ud800", + "\ud83d" + "\ude00", + "a\ud83d" + "\ude00b\ud83d", + "日本語テキスト", +]; +for (const s of samples) { + const looped: string[] = []; + for (const c of s) looped.push(c); + console.log(JSON.stringify(s), show(looped), show([...s]), show(Array.from(s)), [...s].length, s.length); +} + +// Loop control, heads and bindings. +const text = "a😀b\ud800c"; +let out = ""; +outer: for (const c of text) { + if (c === "b") continue; + for (const d of "xy") { + if (d === "y" && c === "c") continue outer; + out += c + d; + } + if (c === "\ud800") out += "|"; +} +console.log(out); +for (var v of "hé") out += v; +console.log(out); +let last = ""; +for (last of "q😀") out += last.length; +console.log(out, last); +for (const [first] of ["😀z", "ab"].join("")) out += first; +console.log(out); +let source = "abc"; +for (const c of source) { + source += c; + out += source.length; +} +console.log(out, source); +let n = 0; +for (const c of "stop😀here") { + if (c === "😀") break; + n += c.length; +} +console.log(n); + +// Characters as values: lengths, Map keys and code points. +const counts = new Map(); +for (const c of "abracadabra😀😀é") counts.set(c, (counts.get(c) ?? 0) + 1); +console.log([...counts].map(([k, v]) => `${k}=${v}`).join(",")); +let points = 0; +for (const c of "😀é\ud800a") points += c.codePointAt(0)!; +console.log(points); + +// Generators and explicit iterators. +function* letters(s: string): Generator { + yield* s; +} +console.log(show([...letters("x😀\udc00y")])); +const it = "o😀"[Symbol.iterator](); +let step = it.next(); +while (!step.done) { + out += step.value.length; + step = it.next(); +} +console.log(out); + +// A long string built from mixed pieces. +let long = ""; +for (let i = 0; i < 2000; i++) long += i % 3 === 0 ? "a" : i % 3 === 1 ? "é" : "😀"; +let units = 0; +let chars = 0; +for (const c of long) { + units += c.length; + chars++; +} +console.log(units, chars, long.length, [...long].length); diff --git a/tests/harness/corpus-costs/8347-string-iteration-code-points.ts.json b/tests/harness/corpus-costs/8347-string-iteration-code-points.ts.json new file mode 100644 index 000000000..3e2ae0706 --- /dev/null +++ b/tests/harness/corpus-costs/8347-string-iteration-code-points.ts.json @@ -0,0 +1 @@ +{ "plain": 2770, "san": 3750 } From 8f425a8a1b14a3dc0f832c944674f73483502fab Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 13:01:23 -0500 Subject: [PATCH 236/273] Wait for mailbox events in ppoll on Linux A thread waiting for its mailbox slept on a condition variable. The kernel then places the woken thread without a hint, so with every CPU busy it can queue behind a running thread until that thread is preempted or blocks: a worker program's phase barrier (post to each worker, wait for every reply) lost about 1 ms per phase, and up to 5 ms, on 4 CPUs. On Linux the wait now sleeps in ppoll(2) on the mailbox's wake pipe, which every post already writes. A pipe write is a synchronous wakeup: the woken thread can run on the poster's CPU, which the poster frees when it waits in turn. Bytes left by signals whose events were already taken are drained under the lock first. Other platforms keep the condition variable. --- packages/runtime/src/scr_mailbox.c | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/scr_mailbox.c b/packages/runtime/src/scr_mailbox.c index b2af22774..6e5678378 100644 --- a/packages/runtime/src/scr_mailbox.c +++ b/packages/runtime/src/scr_mailbox.c @@ -21,6 +21,7 @@ #include #include #ifdef __linux__ +#include #include #endif #endif @@ -321,7 +322,32 @@ void scr_mailbox_wait(ScrMailbox *mailbox, double milliseconds) { if (!(milliseconds > 0)) return; if (scr_mailbox_spin(mailbox, milliseconds)) return; scr_mailbox_lock(mailbox); - if (!mailbox->head && !mailbox->closed) { + bool waiting = !mailbox->head && !mailbox->closed; +#ifdef __linux__ + /* Linux sleeps in ppoll(2) on the wake pipe rather than on the + * condition: a pipe write wakes its reader as a synchronous wakeup, + * which the scheduler can run on the writer's CPU when the writer is + * about to block. A worker that posts its result and then waits frees + * that CPU, so the thread it woke runs there at once instead of queueing + * behind another busy thread until the scheduler preempts it. Bytes left + * by signals whose events were already taken are drained first, under + * the lock: an event posted after the unlock writes a new byte. */ + if (waiting) scr_mailbox_drain(mailbox); + scr_mailbox_unlock(mailbox); + if (!waiting) return; + struct pollfd wake = {mailbox->read_fd, POLLIN, 0}; + if (isinf(milliseconds)) (void)ppoll(&wake, 1, NULL, NULL); + else { + /* The event loop rechecks its monotonic deadlines after every wake, + * including spurious ones. Bound conversion before casting time_t. */ + if (milliseconds > 86400000) milliseconds = 86400000; + struct timespec timeout = {(time_t)(milliseconds / 1000), + (long)ceil(fmod(milliseconds, 1000) * 1000000)}; + if (timeout.tv_nsec == 1000000000) { timeout.tv_sec++; timeout.tv_nsec = 0; } + (void)ppoll(&wake, 1, &timeout, NULL); + } +#else + if (waiting) { #ifdef _WIN32 DWORD timeout = milliseconds >= INFINITE ? INFINITE : (DWORD)ceil(milliseconds); (void)SleepConditionVariableSRW(&mailbox->condition, &mailbox->lock, timeout, 0); @@ -348,6 +374,7 @@ void scr_mailbox_wait(ScrMailbox *mailbox, double milliseconds) { #endif } scr_mailbox_unlock(mailbox); +#endif } int scr_mailbox_pollfd(const ScrMailbox *mailbox) { From ed50daa853aedeea1d644df779db93ada8479e80 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 11:01:57 -0700 Subject: [PATCH 237/273] Keep the whole union of an optional parameter that reaches a recursive placeholder An optional parameter typed by a union that is still being mapped gets a recursive placeholder: a union id whose arms arrive when the outer frame finishes. The function-type mapping then armed that placeholder with undefined, but the placeholder's arm list was still empty, so the result was a fresh union holding only `undefined`. The parameter lost its record arm, the same checker type mapped to two different layouts depending on where the mapping started, and later conversions failed (SC2001, SC2002, SC2003). This happens when an interface method takes an options object whose callbacks are typed by a subinterface of that same interface: `newSession(options?: { withSession?: (ctx: Replaced) => Promise })` with `interface Replaced extends CommandContext`. When the checker spelled the parameter with an undefined constituent, the placeholder already carries that arm, so the parameter keeps the placeholder. withUnitArm now refuses a pending placeholder instead of building the unit-only union. pi's ExtensionCommandContext is such an interface. It refused ExtensionAPI.registerCommand, and with it pi's main(). Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/type-mapper.test.ts | 14 +++ packages/compiler/src/frontend/type-mapper.ts | 24 +++- ...recursive-optional-callback-params.ts.json | 6 + .../recursive-optional-callback-params.ts | 107 ++++++++++++++++++ ...recursive-optional-callback-params.ts.json | 1 + 5 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-optional-callback-params.ts.json create mode 100644 tests/corpus/recursive-optional-callback-params.ts create mode 100644 tests/harness/corpus-costs/recursive-optional-callback-params.ts.json diff --git a/packages/compiler/src/frontend/type-mapper.test.ts b/packages/compiler/src/frontend/type-mapper.test.ts index 163ba7b9e..6e7e339d6 100644 --- a/packages/compiler/src/frontend/type-mapper.test.ts +++ b/packages/compiler/src/frontend/type-mapper.test.ts @@ -53,6 +53,20 @@ describe("union arm lookup", () => { }); }); +describe("recursive union placeholders", () => { + test("refuse a unit arm until their arms are known", () => { + const unions = new UnionRegistry(); + const recursive = {} as Type; + const union = unions.recursiveRef(recursive, () => "Recursive"); + const placeholder: IrType = { kind: "union", unionId: union }; + expect(unions.isPendingPlaceholder(union)).toBe(true); + expect(withUndefinedArm(placeholder, unions)).toBeNull(); + unions.finalizeRecursive(recursive, [{ kind: "record", shapeId: "r1" }, UNDEFINED_T]); + expect(unions.isPendingPlaceholder(union)).toBe(false); + expect(withUndefinedArm(placeholder, unions)).toEqual(placeholder); + }); +}); + describe("nullable collection union builders", () => { test.each([mapOf(STRING, F64), setOf(STRING)])( "optional %j fields and generator results share the same union", diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 17f9d0d11..63eed378d 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -717,6 +717,12 @@ export class UnionRegistry { return id !== undefined && this.pendingRec.has(id); } + /** True when `unionId` is a recursive placeholder whose arms are not + * known yet (its outer frame is still mapping, or failed). */ + isPendingPlaceholder(unionId: string): boolean { + return this.pendingRec.has(unionId); + } + /** Completes a recursive placeholder with its canonical arm list and * registers the structural key (first writer wins, like shapes). */ finalizeRecursive(t: ts.Type, arms: IrType[], discriminant?: IrUnionDiscriminant): string { @@ -5302,8 +5308,19 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions); // Belt and braces for non-strict type worlds: an optional param's // ABI slot is always the undefined-armed union (strictNullChecks - // already spells it that way; arm it here if the world didn't). - if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn") { + // already spells it that way; arm it here if the world didn't). A + // back-reference to a union still being mapped (`options?: { then?: + // (ctx: Sub) => void }` where Sub extends the interface declaring the + // method) is a recursive placeholder with no arms yet; when the checker + // spelled the parameter `X | undefined`, the placeholder receives that + // undefined arm when its frame finishes. + const pendingOptional = + pt?.kind === "union" && + ctx.unions.isPendingPlaceholder(pt.unionId) && + ts + .constituentTypes(checker.getTypeOfSymbol(p)) + .some((part) => (part.flags & ts.TypeFlags.Undefined) !== 0); + if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn" && !pendingOptional) { const armed = withUndefinedArm(pt, ctx.unions); if (!armed) return null; pt = armed; @@ -6782,6 +6799,9 @@ export function withUnitArm( if (t.kind === "union") { const def = unions.get(t.unionId); if (!def) return null; + // A recursive placeholder's arms arrive when its outer frame finishes; + // extending its (still empty) arm list would drop every arm but the unit. + if (unions.isPendingPlaceholder(t.unionId)) return null; if (def.arms.some((a) => a.kind === kind)) return t; const arms = [...def.arms, unit]; // Date and Promise arms beside other data arms have no union home. diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-optional-callback-params.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-optional-callback-params.ts.json new file mode 100644 index 000000000..abf783f85 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-optional-callback-params.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/recursive-optional-callback-params.ts" + ], + "diags": [] +} diff --git a/tests/corpus/recursive-optional-callback-params.ts b/tests/corpus/recursive-optional-callback-params.ts new file mode 100644 index 000000000..0c81235d6 --- /dev/null +++ b/tests/corpus/recursive-optional-callback-params.ts @@ -0,0 +1,107 @@ +// An optional parameter whose type reaches back to the interface that +// declares the method keeps its whole union. A command context's +// `newSession(options?)` takes callbacks typed by a subinterface of the +// context, so mapping the context passes through the options union while +// that union is still being mapped; the parameter still holds the options +// record, not only `undefined`. + +interface CommandContext { + cwd: string; + newSession(options?: { + parent?: string; + withSession?: (ctx: ReplacedContext) => Promise; + }): Promise<{ cancelled: boolean }>; + fork( + entryId: string, + options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedContext) => Promise }, + ): Promise<{ cancelled: boolean }>; +} + +interface ReplacedContext extends CommandContext { + depth: number; + send(content: string): Promise; +} + +interface Command { + description?: string; + handler: (args: string, ctx: CommandContext) => Promise; +} + +const log: string[] = []; +const commands = new Map(); + +function register(name: string, command: Command): void { + commands.set(name, command); +} + +register("new", { + description: "start a session", + handler: async (args, ctx) => { + const first = await ctx.newSession({ + parent: args, + withSession: async (next) => { + await next.send(`hello from ${next.cwd} at depth ${next.depth}`); + const nested = await next.newSession({ + withSession: async (inner) => { + await inner.send(`nested at depth ${inner.depth}`); + }, + }); + log.push(`nested cancelled=${nested.cancelled}`); + }, + }); + log.push(`first cancelled=${first.cancelled}`); + const bare = await ctx.newSession(); + log.push(`bare cancelled=${bare.cancelled}`); + }, +}); + +register("fork", { + handler: async (args, ctx) => { + const r = await ctx.fork(args, { + position: "at", + withSession: async (next) => next.send(`forked ${args} at depth ${next.depth}`), + }); + log.push(`fork cancelled=${r.cancelled}`); + const plain = await ctx.fork(args); + log.push(`plain fork cancelled=${plain.cancelled}`); + }, +}); + +function makeContext(depth: number): ReplacedContext { + return { + cwd: `/work/${depth}`, + depth, + newSession: async (options) => { + log.push(`newSession depth=${depth} parent=${options?.parent ?? "-"}`); + if (options?.withSession) { + await options.withSession(makeContext(depth + 1)); + return { cancelled: false }; + } + return { cancelled: true }; + }, + fork: async (entryId, options) => { + log.push(`fork ${entryId} position=${options?.position ?? "-"}`); + const callback = options?.withSession; + if (callback === undefined) return { cancelled: true }; + await callback(makeContext(depth + 1)); + return { cancelled: false }; + }, + send: async (content) => { + log.push(`send: ${content}`); + }, + }; +} + +async function run(): Promise { + const root: CommandContext = makeContext(0); + for (const [name, command] of commands) { + log.push(`run ${name} (${command.description ?? "no description"})`); + await command.handler(`entry-${name}`, root); + } + for (const line of log) console.log(line); +} + +run().catch((e: unknown) => { + console.log("failed", e instanceof Error ? e.message : String(e)); + process.exitCode = 1; +}); diff --git a/tests/harness/corpus-costs/recursive-optional-callback-params.ts.json b/tests/harness/corpus-costs/recursive-optional-callback-params.ts.json new file mode 100644 index 000000000..c33a4b32c --- /dev/null +++ b/tests/harness/corpus-costs/recursive-optional-callback-params.ts.json @@ -0,0 +1 @@ +{ "plain": 6000 } From ffbbf1590f2208d19a14ddb2bdf237aeb7c60c5e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 13:11:47 -0500 Subject: [PATCH 238/273] Send every dyn kind that can be a cycle candidate through one release path The inline dyn release sorted a surviving value into three groups by kind: arrays and objects tested the candidate-buffer word and then called scr_dyn_release_survivor, capsules and handles called it directly, and functions and proxies appended to the candidate buffer inline. All six kinds now take one path: a value already in a candidate buffer, or never one, returns inline, and scr_dyn_release_survivor, which now also covers functions and proxies, decides the rest. Behavior is unchanged, and each inlined release site keeps one kind test and one call. Effect's executable text is 10% smaller and it runs 5.8% faster; zod runs 3.8% faster, and bench:runtime's dynamic-properties 14% faster. --- .../compiler/src/backend/llvm/shapes.test.ts | 56 ++++++++++++------- packages/compiler/src/backend/llvm/shapes.ts | 39 +++++++------ packages/runtime/src/scr_dyn.h | 3 +- packages/runtime/src/scr_json.c | 10 +++- 4 files changed, 67 insertions(+), 41 deletions(-) diff --git a/packages/compiler/src/backend/llvm/shapes.test.ts b/packages/compiler/src/backend/llvm/shapes.test.ts index 61c1a074d..d9e5fa641 100644 --- a/packages/compiler/src/backend/llvm/shapes.test.ts +++ b/packages/compiler/src/backend/llvm/shapes.test.ts @@ -146,11 +146,19 @@ describe("LLVM runtime RC symbols", () => { // The dyn fast path buffers surviving values by kind, as scr_dyn_release does. const ir = emitInlineRcHelpers(host).join("\n"); expect(ir).toContain("call void @scr_dyn_release_survivor(ptr %o)"); - expect(ir).toMatch( - /switch i32 %kind, label %done \[ i32 4, label %container i32 5, label %container i32 8, label %root/, + // Every kind that can be a candidate takes one path; scalars stay inline. + expect(ir).toContain( + "switch i32 %kind, label %done [ i32 4, label %candidate i32 5, label %candidate i32 8, label %candidate i32 9, label %candidate i32 12, label %candidate i32 13, label %candidate ]", ); - // Arrays and objects not yet buffered ask the runtime, which skips intact documents. + // Values not yet buffered ask the runtime, which decides per value. expect(ir).toMatch(/br i1 %cqueued, label %done, label %survivor/); + // The runtime buffers dyn candidates, so the dyn helper has no inline append. + const dynHelper = ir.slice( + ir.indexOf("@sc_rc_release_dyn("), + ir.indexOf("\n}", ir.indexOf("@sc_rc_release_dyn(")), + ); + expect(dynHelper).not.toContain("sc_cyc_append"); + expect(dynHelper).not.toContain("scr_cyc_on_release"); }); test("sanitized builds call the fast-path helpers out of line", () => { @@ -208,36 +216,42 @@ describe("LLVM runtime RC symbols", () => { }); test("optimized builds buffer candidates through the module's append helper", () => { - const emit = (inlineRc: true | "release", extra: Partial) => { + const emit = (inlineRc: true | "release", extra: Partial, kind: "union" | "dyn") => { const { host } = declarationHost(64, inlineRc); const configured: ShapeHost = { ...host, inlineAlloc: true, ...extra }; - releaseSym(configured, { kind: "dyn" }); + releaseSym(configured, kind === "union" ? ({ kind, unionId: "u0" } as never) : { kind }); return { ir: emitInlineRcHelpers(configured).join("\n"), decls: inlineRcDecls(configured), }; }; + const { ir, decls } = emit(true, {}, "union"); + expect(ir).toContain("call preserve_mostcc void @sc_cyc_append(ptr %o)"); + expect(ir).toContain("define internal preserve_mostcc void @sc_cyc_append(ptr %o) noinline"); + // The young test reads the generation byte at obj-5; the append fills + // scr_cyc_buffers' nursery buffer and the header's position at obj-4. + expect(ir).toContain("getelementptr i8, ptr %o, i64 -5"); + expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 96"); + expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 104"); + expect(ir).toContain("load i64, ptr @scr_cyc_live"); + expect(ir).toContain("or i32 %i, -2147483648"); + expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)"); + expect(decls).toContain( + "@scr_cyc_buffers = external global { [3 x { ptr, i64, i64, i64 }], i64, i64 }", + ); + expect(decls).toContain("@scr_cyc_live = external global i64"); + // The dyn release buffers through scr_dyn_release_survivor, so a module + // whose only headered release is dyn's has no append helper. for (const mode of [true, "release"] as const) { - const { ir, decls } = emit(mode, {}); - expect(ir).toContain("call preserve_mostcc void @sc_cyc_append(ptr %o)"); - expect(ir).toContain("define internal preserve_mostcc void @sc_cyc_append(ptr %o) noinline"); - // The young test reads the generation byte at obj-5; the append fills - // scr_cyc_buffers' nursery buffer and the header's position at obj-4. - expect(ir).toContain("getelementptr i8, ptr %o, i64 -5"); - expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 96"); - expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 104"); - expect(ir).toContain("load i64, ptr @scr_cyc_live"); - expect(ir).toContain("or i32 %i, -2147483648"); - expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)"); - expect(decls).toContain( - "@scr_cyc_buffers = external global { [3 x { ptr, i64, i64, i64 }], i64, i64 }", - ); - expect(decls).toContain("@scr_cyc_live = external global i64"); + const dyn = emit(mode, {}, "dyn"); + expect(dyn.ir).not.toContain("sc_cyc_append"); + expect(dyn.ir).not.toContain("scr_cyc_on_release"); + expect(dyn.decls.join("\n")).not.toContain("scr_cyc_buffers"); } // Worker executables and targets without inline allocation call // scr_cyc_on_release, which tries the same append first. for (const extra of [{ threadLocalAlloc: true }, { inlineAlloc: false }]) { - const { ir, decls } = emit(true, extra); + const { ir, decls } = emit(true, extra, "union"); expect(ir).not.toContain("sc_cyc_append"); expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)"); expect(decls.join("\n")).not.toContain("scr_cyc_buffers"); diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index ab623f934..7498235b0 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -589,9 +589,10 @@ interface InlineRcFamily { * repeats its own checks, so the fast path never duplicates it. * * The dyn release buffers a surviving value by kind (scr_dyn_release): - * the fast path decides the always and never cases and calls - * scr_dyn_release_survivor for kinds whose candidacy is per value, - * including arrays and objects of intact documents (ScrDynRegion). + * the fast path skips scalars and values already in a candidate buffer or + * never candidates, and calls scr_dyn_release_survivor for the rest, which + * decides per value (intact documents, ScrDynRegion; capsules and handles + * without edges). * * The array release inlines the decrement of an array that stays alive * and is not headered (`a->cyc`): those are most array releases in @@ -698,9 +699,10 @@ function requestRcHelper(host: ShapeHost, family: string, op: RcOp): string { for (const decl of rcHelperDecls(family, op)) host.declare(decl); // Its body is written last (emitInlineRcHelpers), after the requests are // collected: a release that can buffer a candidate requests the append - // helper now. The jsval row (JS_RC_ROW) has no collector header; its - // cells release through dyn's helper, which makes its own request. - if (op === "release" && row.cycle === "always") requestCycleAppend(host); + // helper now. The dyn release buffers through scr_dyn_release_survivor + // instead. The jsval row (JS_RC_ROW) has no collector header; its cells + // release through dyn's helper. + if (op === "release" && row.cycle === "always" && family !== "dyn") requestCycleAppend(host); return `@${inlineRcName(family, op)}`; } @@ -820,8 +822,11 @@ function rcHelperDecls(family: string, op: RcOp): string[] { if (op === "retain" || row.release === null) return []; return [ `declare void @${row.release}(ptr)`, - ...(row.cycle === "always" ? [`declare void @scr_cyc_on_release(ptr)`] : []), - ...(family === "dyn" ? [`declare void @scr_dyn_release_survivor(ptr)`] : []), + ...(family === "dyn" + ? [`declare void @scr_dyn_release_survivor(ptr)`] + : row.cycle === "always" + ? [`declare void @scr_cyc_on_release(ptr)`] + : []), ]; } @@ -931,21 +936,21 @@ function releaseHelper(host: ShapeHost, family: string, row: InlineRcFamily): st // scr_dyn_release buffers a surviving value by kind: always for // functions and proxies, per value for arrays and objects (not // while their document is intact, ScrDynRegion), capsules and - // handles (scr_dyn_release_survivor), never for scalars. An - // array or object already in a candidate buffer stays inline. + // handles, never for scalars. Every kind that can be buffered + // takes one path, which keeps each inlined site small: a value + // already in a candidate buffer (or never one) stays inline, and + // scr_dyn_release_survivor decides the rest. ` %kp = getelementptr i8, ptr %o, ${S} ${S === "i64" ? 8 : 4} ; ScrDyn.kind`, ` %kind = load i32, ptr %kp`, - // SCR_DYN_ARR 4, OBJ 5, FUNC 8, PROXY 13; HANDLE 9, TYPED_REF 12. - ` switch i32 %kind, label %done [ i32 4, label %container i32 5, label %container i32 8, label %root i32 13, label %root i32 9, label %survivor i32 12, label %survivor ]`, - `container:`, + // SCR_DYN_ARR 4, OBJ 5, FUNC 8, HANDLE 9, TYPED_REF 12, PROXY 13. + ` switch i32 %kind, label %done [ i32 4, label %candidate i32 5, label %candidate i32 8, label %candidate i32 9, label %candidate i32 12, label %candidate i32 13, label %candidate ]`, + `candidate:`, ` %cbufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`, ` %cbuf = load i32, ptr %cbufp`, ` %cqueued = icmp slt i32 %cbuf, 0`, ` br i1 %cqueued, label %done, label %survivor`, `survivor:`, ` call void @scr_dyn_release_survivor(ptr %o)`, - ` br label %done`, - `root:`, ] : family === "box" ? [ @@ -964,7 +969,9 @@ function releaseHelper(host: ShapeHost, family: string, row: InlineRcFamily): st `root:`, ] : []), - ...(cycle === "always" ? cycleRootLines(host, "done") : [` br label %done`]), + ...(cycle === "always" && family !== "dyn" + ? cycleRootLines(host, "done") + : [` br label %done`]), `done:`, ` ret void`, `}`, diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 656f2b994..33cd63c05 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1941,7 +1941,8 @@ extern void scr_dyn_listener_fire_data(ScrClosure *cb, ScrBytes *chunk); extern void scr_dyn_listener_fire_err(ScrClosure *cb, ScrStr *msg); void *scr_dyn_retain_v(void *d); -/* After a compiled fast path decremented a surviving capsule or handle. */ +/* After a compiled fast path decremented a surviving value of a kind that + * can be a cycle candidate and is not one yet (scr_json.c). */ void scr_dyn_release_survivor(ScrDyn *d); void scr_dyn_release_v(void *d); void scr_dyn_trace_v(void *d, ScrTraceVisit visit, void *ctx); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index cd9eeecde..f2254cd44 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1317,9 +1317,9 @@ static void scr_dyn_gcfree(void *ptr) { scr_dyn_dispose(ptr, true); } static void scr_dyn_destroy(void *ptr) { scr_dyn_dispose(ptr, false); } /* The cycle bookkeeping of a value that survives a release. Compiled - * release fast paths decrement first and call this for the kinds whose - * candidacy depends on per-value state, matching scr_dyn_release: arrays - * and objects not yet buffered (skipped while their document is intact, + * release fast paths decrement first and call this for every kind that can + * be a candidate and is not one yet, matching scr_dyn_release: functions + * and proxies, arrays and objects (skipped while their document is intact, * ScrDynRegion), capsules and handles. */ void scr_dyn_release_survivor(ScrDyn *d) { if (d->kind == SCR_DYN_ARR || d->kind == SCR_DYN_OBJ) { @@ -1327,6 +1327,10 @@ void scr_dyn_release_survivor(ScrDyn *d) { if (!region || !region->intact) scr_cyc_released(d); return; } + if (d->kind == SCR_DYN_FUNC || d->kind == SCR_DYN_PROXY) { + scr_cyc_released(d); + return; + } if ((d->kind == SCR_DYN_TYPED_REF && (d->v.typed_ref.materialized || d->v.typed_ref.casts || d->v.typed_ref.traced || d->prototype || d->symbol_properties || d->symbol_keys)) || From fb17fc0715cfef4e8df42d3f4625e464ad127fcc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 13:15:28 -0500 Subject: [PATCH 239/273] Read fixed encrypted keys in corpus 8167's wrong-passphrase cases The cases that decrypt with a wrong passphrase used keys encrypted afresh on each run. A wrong key decrypts to random bytes, and about one run in two hundred those bytes pass the padding check, so OpenSSL fails at the ASN.1 step with a longer error than "bad decrypt" and the program's output changes. The section that reads encrypted keys now uses fixed encryptions of one P-256 key, so each case fails the same way on every run. --- tests/corpus/8167-encrypted-private-keys.cjs | 39 +++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/tests/corpus/8167-encrypted-private-keys.cjs b/tests/corpus/8167-encrypted-private-keys.cjs index 561a0aec3..7b274e500 100644 --- a/tests/corpus/8167-encrypted-private-keys.cjs +++ b/tests/corpus/8167-encrypted-private-keys.cjs @@ -51,11 +51,40 @@ tryIt("empty passphrase", () => { return same(c.createPrivateKey({ key: out, passphrase: "" }), rsa); }); -// Reading encrypted keys. -const p8pem = ec.privateKey.export({ type: "pkcs8", format: "pem", cipher: "aes-256-cbc", passphrase: "pw" }); -const p8der = ec.privateKey.export({ type: "pkcs8", format: "der", cipher: "aes-256-cbc", passphrase: "pw" }); -const sec1 = String(ec.privateKey.export({ type: "sec1", format: "pem", cipher: "aes-128-cbc", passphrase: "pw" })); -tryIt("public from encrypted", () => c.createPublicKey({ key: p8pem, passphrase: "pw" }).equals(ec.publicKey)); +// Reading encrypted keys. These are fixed encryptions of one P-256 key +// with the passphrase "pw": decrypting with a wrong passphrase gives +// random-looking bytes, which a fresh encryption would make fail at +// different steps from run to run. +const fixedKey = [ + "-----BEGIN PRIVATE KEY-----", + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgExcc5/xZ3vYL2Dpg", + "XnB7E8gQhcW1bkUcIGJWBGvrrGChRANCAAR0wxNUH1yo/U9nqFY4Gt5SVzPjvWq6", + "NzlStUC6Njk9BdR7SvBlNIGDAFlOfMwhfZbvvvMP+rLMR3RtBg3Ketxe", + "-----END PRIVATE KEY-----", +].join("\n") + "\n"; +const p8pem = [ + "-----BEGIN ENCRYPTED PRIVATE KEY-----", + "MIH0MF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBBkpqs2611cRjGDZTd4", + "FmNeAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQRhL9yxS4i0ngtke+", + "frhURASBkN0dk+GQNCGU5cFC+MMswOr9XwGaQnLbJo7VnhCje3//JaVJDjexrd69", + "YZliX7pKGNghfFf+eBAeOAkwbAK39T8KQPwTJVJ+Z5O485N2fl5dPlr3gaEA3+cm", + "LQgasmL7Zv9UOH2iwvWllFYL3bnwr7VsqTCW2xe53qTB+dWVurP0cCiZNGEnUzrf", + "4bvWNrW0nA==", + "-----END ENCRYPTED PRIVATE KEY-----", +].join("\n") + "\n"; +const p8der = Buffer.from("MIH0MF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBAOrVavYYEvy3KUHwK0hbQnAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQmEnqHOLegUirb49LD8oNhASBkGUwe3SZciScH/tWuoK5JWujckNHRKROwu81a5AGjIKxkvqCm+Qp9Gr4rPsb2vbv3bgfojbEKvSGvtuqB/X/jpN5UMZwWI3D9Ocd1+jEq1FqDb3rb3qxcoO9jmwuFFzNeYy4sW29XFuKTxu4aJHT1sEtMzIVlRLv53aXzpp9eQ9j47EOTWFqIpkt02TRFuazCQ==", "base64"); +const sec1 = [ + "-----BEGIN EC PRIVATE KEY-----", + "Proc-Type: 4,ENCRYPTED", + "DEK-Info: AES-128-CBC,E73A95A6783C45F914B397FE018521D2", + "", + "nsuhQFMPr+QFITcpmr2JWs+Zep4wxoCWMEk1jzyyXSJddfebIXOFg4xmtK8YJXIy", + "vqa6pvAIiak0iEZjVSn20+JAg2wqQ0sLaFPhBmhcZk+xuQoLD8CcYvROO5VtVELm", + "DkgOUcJ1oEfZJIRM6U2oDNFmj2NB/SnSQUf1niy7ba8=", + "-----END EC PRIVATE KEY-----", +].join("\n") + "\n"; +const fixedPublic = c.createPublicKey(fixedKey); +tryIt("public from encrypted", () => c.createPublicKey({ key: p8pem, passphrase: "pw" }).equals(fixedPublic)); tryIt("public from traditional", () => c.createPublicKey({ key: sec1, passphrase: "pw" }).type); tryIt("pkcs8 pem no passphrase", () => c.createPrivateKey(p8pem)); tryIt("pkcs8 der no passphrase", () => c.createPrivateKey({ key: p8der, format: "der", type: "pkcs8" })); From bd1bf39a765d7b8993f25862b40225388fc5e0f8 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 11:18:18 -0700 Subject: [PATCH 240/273] Close recursive record knots through erased generic method members A record or union that refers back to itself only closes its recursive knot when the frame did not resolve anything through context-sensitive hooks; otherwise the same checker type could answer differently per instantiation, so the knot is refused. Erasing a generic method member (`send(message: Message): string`) resolves the method's own type parameters to `unknown`, and that resolution counted as context-sensitive. Any recursive interface declaring a generic method was therefore refused: `interface Sub extends Base { send(m: Message): string }` reached from `Base { child?: Sub }`, or a list node with a generic method beside its `next` field. The member erasure now records its own type parameters, and resolving one of them no longer counts. Consulting an enclosing binding still does, so a generic method that mentions an outer type parameter keeps the fence. The erased member's mapping is not memoized under the member's type, since the same type maps to nothing outside the erasure. pi's ReplacedSessionContext (sendMessage) extends ExtensionCommandContext, whose methods take callbacks over ReplacedSessionContext. The fence refused ExtensionAPI.registerCommand and, through it, pi's main(). Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/frontend/type-mapper.ts | 22 ++++++- .../recursive-generic-method-members.ts.json | 6 ++ .../recursive-generic-method-members.ts | 58 +++++++++++++++++++ .../recursive-generic-method-members.ts.json | 1 + 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-generic-method-members.ts.json create mode 100644 tests/corpus/recursive-generic-method-members.ts create mode 100644 tests/harness/corpus-costs/recursive-generic-method-members.ts.json diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 17f9d0d11..99766d0ab 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1255,6 +1255,10 @@ export interface TypeMapperCtx { * (erasedGenericMemberType): the signature's own type parameters * resolve to `unknown` instead of refusing the mapping. */ erasingSignature?: ts.Signature; + /** Set with erasingSignature: the signature's own type parameters, and + * whether the erasure consulted an enclosing binding. An erasure that + * resolved only its own parameters answers the same in every context. */ + erasedMember?: { own: ReadonlySet; outerConsulted: boolean }; /** True while an erased generic body lowers (its type parameters are * bound to `unknown`). */ isErasing?: () => boolean; @@ -1835,6 +1839,7 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { memoKey !== null && topLevel && contextResolutions === sensitivityAtEntry && + ctx.erasingSignature === undefined && (ctx.canMemoizeType?.() ?? true) ) { ctx.typeMemo?.set(memoKey, mapped); @@ -1991,7 +1996,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { !ERASURE_KEPT_CONTAINERS.has(ts.symbolNameOf(type.getSymbol()) ?? "") && mentionsErasedTypeParam(type, ctx, 0) ) { - contextResolutions++; + if (!ctx.erasedMember || ctx.erasedMember.outerConsulted) contextResolutions++; return DYN; } // Readonly over a program class (Object.freeze's result type) is the @@ -2001,7 +2006,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (resolveTypeParam && type.flags & ts.TypeFlags.TypeParameter) { const bound = resolveTypeParam(type); if (bound) { - contextResolutions++; + if (!ctx.erasedMember?.own.has(type)) contextResolutions++; return bound; } } @@ -7309,10 +7314,21 @@ export function erasedGenericMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType const sig = sigs[0]!; const own = new Set(sig.getTypeParameters()); const outer = ctx.resolveTypeParam; + // Resolving only the member's own parameters does not make the erasure + // context-sensitive, so a recursive record holding a generic method + // (`interface Sub extends Base { send(m: Msg): void }` reached + // from Base) still closes its knot. Consulting an enclosing binding does. + const erasedMember = { own, outerConsulted: false }; const erased = mapType(t, { ...ctx, erasingSignature: sig, - resolveTypeParam: (param) => (own.has(param) ? DYN : (outer?.(param) ?? null)), + erasedMember, + resolveTypeParam: (param) => { + if (own.has(param)) return DYN; + if (outer === undefined) return null; + erasedMember.outerConsulted = true; + return outer(param); + }, }); return erased?.kind === "func" ? erased : undefined; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-generic-method-members.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-generic-method-members.ts.json new file mode 100644 index 000000000..6c3590ab8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-generic-method-members.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/recursive-generic-method-members.ts" + ], + "diags": [] +} diff --git a/tests/corpus/recursive-generic-method-members.ts b/tests/corpus/recursive-generic-method-members.ts new file mode 100644 index 000000000..1f4d8372d --- /dev/null +++ b/tests/corpus/recursive-generic-method-members.ts @@ -0,0 +1,58 @@ +// A recursive record that declares a generic method compiles. Erasing the +// method's own type parameters does not depend on any enclosing generic +// binding, so the record still closes its recursive knot: a parent +// interface whose child is a subinterface declaring a generic method, and a +// list node whose generic method sits beside a self-referencing field. + +interface Message { + content: string; + details?: T; +} + +interface TreeNode { + name: string; + child?: SendingNode; +} + +interface SendingNode extends TreeNode { + send(message: Message): string; +} + +interface ListNode { + value: number; + next: ListNode | null; + map(f: (value: number) => U): string; +} + +function describe(node: TreeNode): string { + const inner = node.child; + return inner ? `${node.name} > ${describe(inner)}` : node.name; +} + +function leaf(name: string): SendingNode { + return { name, send: () => `${name} got a message` }; +} + +const tree: SendingNode = { + name: "root", + child: { name: "middle", child: leaf("leaf"), send: () => "middle got a message" }, + send: () => "root got a message", +}; +console.log(describe(tree)); +console.log(tree.send({ content: "hello" })); +const middle = tree.child; +if (middle !== undefined) console.log(middle.send({ content: "hi", details: 42 })); + +function cons(value: number, next: ListNode | null): ListNode { + return { value, next, map: () => `node ${value}` }; +} + +let list: ListNode | null = cons(1, cons(2, cons(3, null))); +const values: number[] = []; +let labels = ""; +while (list !== null) { + values.push(list.value); + labels += `[${list.map((v) => v * 2)}]`; + list = list.next; +} +console.log(values.join(","), labels); diff --git a/tests/harness/corpus-costs/recursive-generic-method-members.ts.json b/tests/harness/corpus-costs/recursive-generic-method-members.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/recursive-generic-method-members.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } From ea599922a431abcc51210f7a07b2bc3a895721db Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 13:23:22 -0500 Subject: [PATCH 241/273] Iterate and read native arrays from untyped code without type-name scans Untyped code that loops over a native array (`for (const ch of checks)` in zod, over arrays the compiled code built) paid for the loop before its first element: - Every check of whether a typed capsule is a generator compared its type name against "async-generator<" with strncmp, and native array type names also start with 'a'. Iterator creation made several such checks. The test now looks at the second byte before comparing. - scr_dyn_iterator called the builtin values() through the general call path for a native array; it now creates the native iterator directly, as it already did for untyped arrays. - Read sites of `length` and `next` call scr_js_get_probe_native, which answers a native array's `length` and a native iterator's `next`, where the generic read built the array's checked view or looked the method up by name. Other read sites keep scr_js_get_probe, so their receivers that are not ordinary objects still miss without a further test. --- packages/compiler/src/backend/llvm/js-ic.ts | 8 +++-- packages/compiler/test/js-ic-emission.test.ts | 2 +- .../native-array-dynamic-reads/main.ts.json | 7 ++++ packages/runtime/src/scr_dyn.h | 3 ++ packages/runtime/src/scr_js.h | 4 +++ packages/runtime/src/scr_js_object.c | 5 +++ packages/runtime/src/scr_json.c | 31 +++++++++++++++--- .../corpus/native-array-dynamic-reads/main.ts | 32 +++++++++++++++++++ .../native-array-dynamic-reads/reads.js | 29 +++++++++++++++++ .../native-array-dynamic-reads/main.ts.json | 1 + 10 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-array-dynamic-reads/main.ts.json create mode 100644 tests/corpus/native-array-dynamic-reads/main.ts create mode 100644 tests/corpus/native-array-dynamic-reads/reads.js create mode 100644 tests/harness/corpus-costs/native-array-dynamic-reads/main.ts.json diff --git a/packages/compiler/src/backend/llvm/js-ic.ts b/packages/compiler/src/backend/llvm/js-ic.ts index 11b894a60..d51b838bb 100644 --- a/packages/compiler/src/backend/llvm/js-ic.ts +++ b/packages/compiler/src/backend/llvm/js-ic.ts @@ -406,9 +406,13 @@ function emitGet(host: Host, e: ExprOf<"call">, callee: IrFunction, key: string) inlineLoad(host, receiver.name, ic, INLINE_WAYS, INLINE_PROTO, (cell) => cell, hits, hit, probe); B.startBlock(probe); // The read's probe also answers keys the receiver's plain chain lacks. - host.declare(`declare ptr @scr_js_get_probe(ptr, ptr, ptr)`); + // Only `length` and `next` reads can be answered for a native array or + // iterator, so only those sites take the probe that tests them. + const probeFn = + key === "length" || key === "next" ? "scr_js_get_probe_native" : "scr_js_get_probe"; + host.declare(`declare ptr @${probeFn}(ptr, ptr, ptr)`); const probed = B.tmp(); - B.line(`${probed} = call ptr @scr_js_get_probe(ptr ${receiver.name}, ptr ${name}, ptr ${ic})`); + B.line(`${probed} = call ptr @${probeFn}(ptr ${receiver.name}, ptr ${name}, ptr ${ic})`); const found = B.tmp(); B.line(`${found} = icmp ne ptr ${probed}, null`); const probeEnd = B.currentLabel(); diff --git a/packages/compiler/test/js-ic-emission.test.ts b/packages/compiler/test/js-ic-emission.test.ts index 79342e99d..b350c743d 100644 --- a/packages/compiler/test/js-ic-emission.test.ts +++ b/packages/compiler/test/js-ic-emission.test.ts @@ -201,7 +201,7 @@ console.log(lib.total([1, 2, 3])); expect(fn).toContain("@scr_js_keyed_probe("); // The length read answers from the native array's length. expect(fn).toContain("icg.rest"); - expect(fn).toContain("@scr_js_get_probe("); + expect(fn).toContain("@scr_js_get_probe_native("); } finally { await rm(dir, { recursive: true, force: true }); } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-array-dynamic-reads/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-array-dynamic-reads/main.ts.json new file mode 100644 index 000000000..c3b3013ea --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-array-dynamic-reads/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/native-array-dynamic-reads/reads.js", + "/tests/corpus/native-array-dynamic-reads/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 656f2b994..bc20be862 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1955,6 +1955,9 @@ long scr_dyn_live_count(void); * scr_json.c, which programs with dynamic-tier code link. */ /* The builtin Object.prototype node, or NULL before its first use. */ const ScrDyn *scr_dyn_tier_object_prototype(void); +/* A read site's answer for a native array's length or a native iterator's + * next (scr_js_get_probe_native): +1, or NULL. */ +ScrDyn *scr_dyn_tier_unshaped_get(const ScrDyn *receiver, const ScrStr *key); /* Shared key bytes for `key` (+1, NUL-terminated), and their release. */ char *scr_dyn_tier_key_copy(const char *key, size_t key_len); void scr_dyn_tier_key_release(char *key, size_t key_len); diff --git a/packages/runtime/src/scr_js.h b/packages/runtime/src/scr_js.h index 3ff9becd3..5abab2053 100644 --- a/packages/runtime/src/scr_js.h +++ b/packages/runtime/src/scr_js.h @@ -235,6 +235,10 @@ ScrDyn *scr_js_load_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic); * a key that the receiver and its plain prototype chain lack, and caches * that answer. A method read uses scr_js_load_probe. */ ScrDyn *scr_js_get_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic); +/* scr_js_get_probe for a site whose key is `length` or `next`, which also + * answers a native array's length and a native iterator's next + * (scr_dyn_tier_unshaped_get). */ +ScrDyn *scr_js_get_probe_native(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic); /* `receiver.key = value` (value borrowed) when it replaces a writable own * data property or adds one the prototype chain lacks: true when done. */ bool scr_js_store_probe(ScrDyn *receiver, const ScrStr *key, ScrDyn *value, ScrJsIc *ic); diff --git a/packages/runtime/src/scr_js_object.c b/packages/runtime/src/scr_js_object.c index e3d0b3a54..4cee9e7e8 100644 --- a/packages/runtime/src/scr_js_object.c +++ b/packages/runtime/src/scr_js_object.c @@ -407,6 +407,11 @@ ScrDyn *scr_js_get_probe(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic) { return scr_js_probe_load(receiver, key, ic, true); } +ScrDyn *scr_js_get_probe_native(ScrDyn *receiver, const ScrStr *key, ScrJsIc *ic) { + if (receiver->kind != SCR_DYN_OBJ) return scr_dyn_tier_unshaped_get(receiver, key); + return scr_js_probe_load(receiver, key, ic, true); +} + /* ── stores ───────────────────────────────────────────────────────────── */ /* The generic store keeps a native Error's fields in sync with these keys diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index cd9eeecde..69339b0ca 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -96,9 +96,13 @@ static SCR_TL ScrDyn *scr_iterator_symbol; static SCR_TL ScrDyn *scr_async_iterator_symbol; static SCR_TL ScrDyn *scr_to_primitive_symbol; static bool scr_dyn_generator(const ScrDyn *value) { - return value && value->kind == SCR_DYN_TYPED_REF && - (value->v.typed_ref.type_key[0] == 'g' || value->v.typed_ref.type_key[0] == 'a') && - (!strncmp(value->v.typed_ref.type_key, "generator<", 10) || !strncmp(value->v.typed_ref.type_key, "async-generator<", 16)); + if (!value || value->kind != SCR_DYN_TYPED_REF) return false; + /* Array keys also start with 'a': the second byte sets them apart before + * any comparison. */ + const char *key = value->v.typed_ref.type_key; + size_t len = value->v.typed_ref.type_key_len; + if (key[0] == 'g') return len >= 10 && !memcmp(key, "generator<", 10); + return key[0] == 'a' && key[1] == 's' && len >= 16 && !memcmp(key, "async-generator<", 16); } static bool scr_dyn_generator_symbol(const ScrDyn *value, const ScrDyn *key) { const ScrDyn *symbol = value->v.typed_ref.type_key[0] == 'a' ? scr_async_iterator_symbol : scr_iterator_symbol; @@ -7494,6 +7498,23 @@ static ScrDyn *scr_native_iterator_new(ScrDyn *source) { return result; } +#ifdef SCR_JS_TIER +/* A read site's answer for a receiver without a JS shape whose property is + * plain data the runtime holds directly: a native array's length (the + * generic read would build its checked view first) and a native iterator's + * `next` (the builtin the generic read returns). +1, or NULL for the slow + * path. */ +ScrDyn *scr_dyn_tier_unshaped_get(const ScrDyn *receiver, const ScrStr *key) { + if (key->len == 6 && receiver->kind == SCR_DYN_TYPED_REF && !memcmp(key->data, "length", 6) && + scr_dyn_typed_ref_array_ops(receiver) && scr_dyn_ref_is_array(receiver)) + return scr_dyn_new_num((double)((ScrArr *)receiver->v.typed_ref.ptr)->len); + if (key->len == 4 && receiver->kind == SCR_DYN_HANDLE && + receiver->v.handle.tag == SCR_DYNH_ITERATOR && !memcmp(key->data, "next", 4)) + return scr_native_iterator_get(receiver->v.handle.ptr, key->data, key->len); + return NULL; +} +#endif + static ScrDyn *scr_native_collection_iterator(ScrDyn *source, unsigned selection) { ScrDyn *result = scr_native_iterator_new(source); ((ScrNativeIterator *)result->v.handle.ptr)->selection = selection; @@ -8384,7 +8405,9 @@ ScrDyn *scr_dyn_iterator(const ScrDyn *source, const ScrStr *spell) { } /* An array whose iterator method is the builtin values(): what calling * it with the array as `this` returns, without the call. */ - if (source->kind == SCR_DYN_ARR && method == scr_iterator_functions[1]) { + if (method == scr_iterator_functions[1] && + (source->kind == SCR_DYN_ARR || + (source->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(source)))) { scr_dyn_release(method); return scr_native_iterator_new((ScrDyn *)source); } diff --git a/tests/corpus/native-array-dynamic-reads/main.ts b/tests/corpus/native-array-dynamic-reads/main.ts new file mode 100644 index 000000000..0641114bf --- /dev/null +++ b/tests/corpus/native-array-dynamic-reads/main.ts @@ -0,0 +1,32 @@ +// Native arrays and iterators from typed code, read by untyped code: for-of +// over them, their length, an iterator's next read as a value and called, +// generators and async generators beside them, and an array whose +// iteration a Symbol.iterator use elsewhere in the program does not change. +import { asyncKinds, describe, lengths, sumAll, stepper } from "./reads.js"; + +interface Item { + sku: string; + qty: number; +} +const numbers: number[] = [1, 2, 3, 4]; +const items: Item[] = [ + { sku: "a", qty: 2 }, + { sku: "b", qty: 5 }, +]; +const empty: string[] = []; +function* counting(): Generator { + yield 10; + yield 20; +} +console.log(sumAll(numbers), sumAll(items.map((i) => i.qty)), sumAll(empty.map(Number))); +console.log(lengths([numbers, items, empty]).join()); +numbers.push(5); +console.log(lengths([numbers]).join(), sumAll(numbers)); +console.log(describe(items), describe(counting())); +console.log(stepper(numbers).join(), stepper(counting()).join()); +const iterable = { *[Symbol.iterator]() { yield "own"; } }; +console.log([...iterable].join(), sumAll(numbers)); +async function* later(): AsyncGenerator { + yield 1; +} +asyncKinds(later(), numbers).then((kinds) => console.log(kinds.join())); diff --git a/tests/corpus/native-array-dynamic-reads/reads.js b/tests/corpus/native-array-dynamic-reads/reads.js new file mode 100644 index 000000000..67e1a824a --- /dev/null +++ b/tests/corpus/native-array-dynamic-reads/reads.js @@ -0,0 +1,29 @@ +export function sumAll(values) { + let total = 0; + for (const value of values) total += value; + return total; +} +export function lengths(arrays) { + const out = []; + for (const array of arrays) out.push(array.length); + return out; +} +export function describe(values) { + const parts = []; + for (const value of values) parts.push(typeof value === "object" ? value.sku : value); + return parts.join("+"); +} +export function stepper(values) { + const iterator = values[Symbol.iterator](); + const next = iterator.next; + const out = []; + for (let step = next.call(iterator); !step.done; step = next.call(iterator)) out.push(step.value); + out.push(typeof iterator.next, iterator.next === next); + return out; +} +export async function asyncKinds(generator, array) { + const out = []; + for await (const value of generator) out.push(value); + for (const value of array) out.push(value); + return out; +} diff --git a/tests/harness/corpus-costs/native-array-dynamic-reads/main.ts.json b/tests/harness/corpus-costs/native-array-dynamic-reads/main.ts.json new file mode 100644 index 000000000..ac2ef5ae8 --- /dev/null +++ b/tests/harness/corpus-costs/native-array-dynamic-reads/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3567, "san": 6402 } From 15a183026f1874aeca4c9cadb6220224600d7a61 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 13:27:58 -0500 Subject: [PATCH 242/273] Allocate the shared two-byte characters from the system allocator The one-character strings of U+0080..U+07FF that string iteration shares between threads were allocated from the calling thread's memory. On Linux a worker that exits without sharing anything returns that memory, so the table kept strings that a later read found discarded. They now come from the system allocator, as other state that outlives a thread does. --- ...string-iteration-worker-characters.ts.json | 6 +++ packages/runtime/src/scr_string.c | 15 ++++--- ...8348-string-iteration-worker-characters.ts | 44 +++++++++++++++++++ ...string-iteration-worker-characters.ts.json | 1 + .../harness/string-iteration-workers.test.ts | 44 +++++++++++++++++++ 5 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8348-string-iteration-worker-characters.ts.json create mode 100644 tests/corpus/8348-string-iteration-worker-characters.ts create mode 100644 tests/harness/corpus-costs/8348-string-iteration-worker-characters.ts.json create mode 100644 tests/harness/string-iteration-workers.test.ts diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8348-string-iteration-worker-characters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8348-string-iteration-worker-characters.ts.json new file mode 100644 index 000000000..e767eaf65 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8348-string-iteration-worker-characters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8348-string-iteration-worker-characters.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index cd17da0f8..64a4772e5 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -3538,7 +3538,11 @@ ScrStr *scr_str_cp_at(ScrStr *s, double i) { * thread, as the ASCII ones are static: the string iterator returns them * instead of a new string per character. A string is immortal before its * slot publishes it; a thread that loses the race to fill a slot keeps its - * own immortal copy, so at most one extra string per slot and thread. */ + * own immortal copy, so at most one extra string per slot and thread. The + * strings come from the system allocator, as other state that outlives a + * thread does: a worker's own memory goes back when it exits without + * sharing (scr_alloc.c), and the table outlives the worker that filled a + * slot. */ static ScrStr *scr_two_byte_chars[0x800 - 0x80]; static ScrStr *scr_two_byte_char(const char *bytes) { @@ -3546,12 +3550,13 @@ static ScrStr *scr_two_byte_char(const char *bytes) { ScrStr **slot = &scr_two_byte_chars[cp - 0x80]; ScrStr *known = __atomic_load_n(slot, __ATOMIC_ACQUIRE); if (known) return known; - ScrStr *made = scr_str_from_span(bytes, 2); + ScrStr *made = calloc(1, sizeof(ScrStr) + 3); + if (!made) scr_oom(); + made->len = 2; + made->cap = 2; + memcpy(made->data, bytes, 2); scr_str_hash_prime(made); made->rc = SIZE_MAX; -#ifdef SCR_RC_AUDIT - scr_str_live_forget(); -#endif scr_lsan_immortal(made); ScrStr *expected = NULL; if (__atomic_compare_exchange_n(slot, &expected, made, false, __ATOMIC_ACQ_REL, diff --git a/tests/corpus/8348-string-iteration-worker-characters.ts b/tests/corpus/8348-string-iteration-worker-characters.ts new file mode 100644 index 000000000..52a38fb78 --- /dev/null +++ b/tests/corpus/8348-string-iteration-worker-characters.ts @@ -0,0 +1,44 @@ +import { Worker, isMainThread, parentPort } from "node:worker_threads"; + +// The one-character strings of U+0080..U+07FF that string iteration returns +// are shared by every thread. A worker that is the first to iterate them +// and exits without sharing anything else leaves them in use: the main +// thread iterates the same characters after the worker's memory goes back. + +const text = "aé ñ ß ü Ω ж א ع ߿"; + +function codes(s: string): string { + const out: string[] = []; + for (const c of s) out.push(`${c.length}:${c.charCodeAt(0).toString(16)}`); + return out.join(" "); +} + +function runWorker(): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker(new URL(import.meta.url)); + let count = -1; + worker.on("message", (value: number) => { + count = value; + }); + worker.on("error", reject); + worker.on("exit", () => resolve(count)); + }); +} + +async function main(): Promise { + console.log("worker", await runWorker()); + // The worker's memory goes back shortly after it exits. + await new Promise((resolve) => setTimeout(resolve, 300)); + console.log("main", codes(text)); + console.log("spread", [...text].join("|")); + console.log("from", Array.from("ñandú").map((c) => c.length).join(",")); +} + +if (isMainThread) { + void main(); +} else { + let count = 0; + for (const c of text) count += c.length; + count += [...text].length + Array.from("ñandú").length; + parentPort!.postMessage(count); +} diff --git a/tests/harness/corpus-costs/8348-string-iteration-worker-characters.ts.json b/tests/harness/corpus-costs/8348-string-iteration-worker-characters.ts.json new file mode 100644 index 000000000..4c96af583 --- /dev/null +++ b/tests/harness/corpus-costs/8348-string-iteration-worker-characters.ts.json @@ -0,0 +1 @@ +{ "plain": 3960, "san": 3900 } diff --git a/tests/harness/string-iteration-workers.test.ts b/tests/harness/string-iteration-workers.test.ts new file mode 100644 index 000000000..bc1feca95 --- /dev/null +++ b/tests/harness/string-iteration-workers.test.ts @@ -0,0 +1,44 @@ +/* The one-character strings that string iteration shares between threads + * outlive the worker that first made them. On Linux a worker that exits + * without sharing anything returns its memory; with SCR_SA_DISCARD=poison + * that memory becomes inaccessible instead, so a cached character + * allocated from the worker's memory would fault when the main thread + * reads it. The corpus program pins Node.js parity; this suite runs it + * with the discard poisoned and immediate. 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/8348-string-iteration-worker-characters.ts"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +test.skipIf(process.platform !== "linux" || sanitize)( + "characters a discarded worker cached stay readable", + { timeout: 240_000 }, + async () => { + const outDir = await mkdtemp(join(tmpdir(), "scriptc-string-iteration-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 } = await exec(result.binaryPath, [], { + env: { ...process.env, SCR_SA_DISCARD: "poison", SCR_SA_DISCARD_DELAY_MS: "0" }, + timeout: 120_000, + }); + expect(stdout).toContain("main 1:61 1:e9 1:20 1:f1"); + expect(stdout).toContain("spread a|é| |ñ"); + } finally { + await rm(outDir, { recursive: true, force: true }); + } + }, +); From dec6be6ebddb41f1996ba7e71264bbc224dc07af Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 13:35:04 -0500 Subject: [PATCH 243/273] Run global substitutions with a literal replacement through build-time replacers A global replace or replaceAll of a regex literal called the compiled matcher once per match: each call set up its frame and seek again, and the runtime then copied the gap and the replacement. A substitution of a literal inside the compiled subset with an ASCII replacement without $ patterns now gets a replacer built for that pattern and replacement. It appends each gap and the replacement to the output and continues the search after the match, as the runtime's loop does (one past an empty match), without returning. Gaps of up to 16 bytes are one 16-byte copy, and the search after a match is a 16-byte scan instead of a memchr call. scr_regex_replace_with runs the replacer on an ASCII subject; a subject outside ASCII keeps the runtime's loop. Plain-text patterns, which the runtime searches as strings, get no replacer. New corpus program: 8351-regex-literal-replacers.ts. --- .../compiler/src/backend/llvm/emit-module.ts | 18 ++ packages/compiler/src/backend/llvm/emitter.ts | 8 + .../src/backend/llvm/expr-primitives.ts | 23 +++ .../compiler/src/backend/llvm/expr-records.ts | 45 +++- .../src/backend/llvm/regex-matchers.test.ts | 44 ++++ .../src/backend/llvm/regex-matchers.ts | 194 ++++++++++++++++-- .../compiler/test/regex-replacers.test.ts | 42 ++++ .../8351-regex-literal-replacers.ts.json | 6 + packages/runtime/src/scr_regex.c | 45 ++++ packages/runtime/src/scr_regex.h | 16 ++ tests/corpus/8351-regex-literal-replacers.ts | 72 +++++++ .../8351-regex-literal-replacers.ts.json | 1 + 12 files changed, 491 insertions(+), 23 deletions(-) create mode 100644 packages/compiler/test/regex-replacers.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8351-regex-literal-replacers.ts.json create mode 100644 tests/corpus/8351-regex-literal-replacers.ts create mode 100644 tests/harness/corpus-costs/8351-regex-literal-replacers.ts.json diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index ee9089071..af00ebc2c 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -600,6 +600,23 @@ export function emitModuleParts(emitter: LlEmitter): string[] { regexMatchers.set(key, code); for (const decl of code.decls) emitter.declare(decl); } + // Replacers of global substitutions with a literal replacement; the call + // sites register only patterns inside the compiled subset. + const regexReplacers: RegexMatcherCode[] = []; + for (const rr of emitter.regexReplacers.values()) { + const code = compileRegexMatcher( + rr.sym, + rr.pattern, + rr.flags, + emitter.sizeType, + !emitter.wasi, + rr.replacement, + ); + if (code === null) + throw new InternalCompilerError(`llvm emitter bug: replacer ${rr.sym} did not compile`); + regexReplacers.push(code); + for (const decl of code.decls) emitter.declare(decl); + } const out: string[] = [ `; Generated by scriptc (LLVM backend) from ${emitter.mod.sourceFile}. Do not edit.`, @@ -882,6 +899,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { } if (emitter.regexInstances.size > 0) out.push(``); for (const code of regexMatchers.values()) out.push(...code.lines, ``); + for (const code of regexReplacers) 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, and the ScrArr header over it diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 1a2c8bef8..d15a8c017 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1303,6 +1303,12 @@ export class LlEmitter { string, { sym: string; src: string; fl: string; pattern: string; flags: string } >(); + /** Build-time replacers: one per (pattern, flags, replacement) of a + * global substitution with a literal replacement (regex-matchers.ts). */ + readonly regexReplacers = new Map< + string, + { sym: string; pattern: string; flags: string; replacement: string } + >(); /** Interned tagged-template strings objects: per-site key → { symbol, * interned cooked-literal refs }. One immortal ScrArr of string slots * per template SITE (the spec's per-occurrence identity — the C @@ -5439,6 +5445,8 @@ export class LlEmitter { 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 "regexReplacer": + return addNumberedSymbol(this.regexReplacers, key, value as never, "sc_rr_", names); case "templateStrings": return addNumberedSymbol( this.templateStringsInstances, diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index d3b72897e..eed138fe2 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -661,6 +661,29 @@ export function regexLiteralTemplate( return `@${re.sym}`; } +/** The build-time replacer of a global substitution of regex literal `e` + * with the literal `replacement` (regex-matchers.ts), registered on first + * use. */ +export function regexLiteralReplacer( + host: LlvmEmitterContext, + e: IrExpr & { kind: "regexLit" }, + replacement: string, +): string { + const key = `${e.flags}/${e.pattern}\0${replacement}`; + let rr = host.regexReplacers.get(key); + if (!rr) { + rr = { + sym: `sc_rr_${host.regexReplacers.size}`, + pattern: e.pattern, + flags: e.flags, + replacement, + }; + host.regexReplacers.set(key, rr); + } + host.journal?.entry("regexReplacer", key, rr); + return `@${rr.sym}`; +} + /** A string equality or comparison between `s.charAt(i)` and a literal of * exactly one UTF-16 code unit: the charAt call, the unit, and whether the * charAt is the left operand. */ diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts index 59ab625e5..51688eafa 100644 --- a/packages/compiler/src/backend/llvm/expr-records.ts +++ b/packages/compiler/src/backend/llvm/expr-records.ts @@ -6,7 +6,8 @@ import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext, LlValue } from "./expr-context.js"; import { f64Lit, llvmCommentText } from "./common.js"; import { borrowableInputs, emitBorrowedInputs, emitBorrowedInput } from "./borrowed-inputs.js"; -import { regexLiteralTemplate } from "./expr-primitives.js"; +import { regexLiteralReplacer, regexLiteralTemplate } from "./expr-primitives.js"; +import { canReplaceWith, regexLiteralCompiles } from "./regex-matchers.js"; import { isScalarUnionDef, loadInlineUnion, presentBits } from "./inline-unions.js"; import { preservesRegexInputs } from "./reference-effects.js"; @@ -160,6 +161,39 @@ function directRegexOperandMethod(method: string): boolean { } } +/** The build-time replacer of a substitution: a regex literal running on + * its template, with a literal replacement the replacer can write as is. + * Null when the substitution takes the runtime's loop. */ +function literalReplacer( + host: LlvmEmitterContext, + literal: IrExpr | null, + template: string | null, + replacement: IrExpr, +): string | null { + if (literal === null || template === null || literal.kind !== "regexLit") return null; + if (replacement.kind !== "strLit") return null; + if (!canReplaceWith(literal.pattern, literal.flags, replacement.value)) return null; + if (!regexLiteralCompiles(literal.pattern, literal.flags)) return null; + return regexLiteralReplacer(host, literal, replacement.value); +} + +/** scr_regex_replace_with: the runtime runs `replacer` on an ASCII subject + * and its own loop on any other. */ +function emitReplaceWith( + host: LlvmEmitterContext, + e: IrExpr, + r: LlValue, + args: LlValue[], + replacer: string, +): LlValue { + host.declare(`declare ptr @scr_regex_replace_with(ptr, ptr, ptr, ptr)`); + const t = host.B.tmp(); + host.B.line( + `${t} = call ptr @scr_regex_replace_with(ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${replacer})`, + ); + return host.own({ name: t, type: e.type }); +} + export function emitRegexIntrinsic( host: LlvmEmitterContext, e: IrExpr & { kind: "regexIntrinsic" }, @@ -234,11 +268,16 @@ export function emitRegexIntrinsic( "scr_regex_match_all_captures_into", `ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`, ); - case "replaceAll": + case "replaceAll": { + // A global literal (the only kind a replacer is built for) never takes + // replaceAll's non-global TypeError. + const replacer = literalReplacer(host, literal, template, e.args[1]!); + if (replacer !== null) return emitReplaceWith(host, e, r, args, replacer); return fallible( "scr_regex_replace_all", `ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`, ); + } case "split": host.declare(`declare ptr @scr_regex_split_limit(ptr, ptr, double)`); { @@ -323,6 +362,8 @@ export function emitRegexIntrinsic( return host.own({ name: t, type: e.type }); } case "replace": { + const replacer = literalReplacer(host, literal, template, e.args[1]!); + if (replacer !== null) return emitReplaceWith(host, e, r, args, replacer); host.declare(`declare ptr @scr_regex_replace(ptr, ptr, ptr)`); const t = B.tmp(); B.line( diff --git a/packages/compiler/src/backend/llvm/regex-matchers.test.ts b/packages/compiler/src/backend/llvm/regex-matchers.test.ts index d009de7c9..5f93f7295 100644 --- a/packages/compiler/src/backend/llvm/regex-matchers.test.ts +++ b/packages/compiler/src/backend/llvm/regex-matchers.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vitest"; import { + canReplaceWith, compileRegexMatcher, regexLiteralBailReason, regexLiteralCaptures, @@ -152,3 +153,46 @@ describe("regex literal matcher code", () => { expect(text).not.toContain("constant [128 x i8]"); }); }); + +describe("regex literal replacers", () => { + test("take global, non-sticky, non-plain-text patterns and ASCII replacements without $", () => { + expect(canReplaceWith("a[bc]", "g", "<2>")).toBe(true); + expect(canReplaceWith("a[bc]", "gi", "")).toBe(true); + expect(canReplaceWith("a[bc]", "", "x")).toBe(false); + expect(canReplaceWith("a[bc]", "gy", "x")).toBe(false); + expect(canReplaceWith("a[bc]", "g", "$1")).toBe(false); + expect(canReplaceWith("a[bc]", "g", "é")).toBe(false); + // Plain text is a string search at run time. + expect(canReplaceWith("&", "g", "&")).toBe(false); + expect(canReplaceWith("a\\.b", "g", "x")).toBe(false); + // Letters under i are not plain text. + expect(canReplaceWith("ab", "gi", "x")).toBe(true); + }); + + test("append the gap and the replacement and continue the search", () => { + const code = compileRegexMatcher("r", "<[^>]*>", "g", "i64", true, "|"); + const text = code!.lines.join("\n"); + expect(text).toContain('@r.rep = private unnamed_addr constant [1 x i8] c"|"'); + expect(text).toContain("call void @scr_re_replace_out_reserve(ptr %out, i64 %m.need)"); + expect(text).toContain("load <16 x i8>, ptr %m.src, align 1"); + expect(text).toMatch(/br i1 %m\.past, label %nomatch, label %scan/); + expect(text).not.toContain("m.ret:"); + expect(text).toContain("ret i32 %n.found"); + // The search after a match is a 16-byte scan, not a memchr call. + expect(text).not.toContain("@memchr"); + expect(code!.decls).toContain("declare void @scr_re_replace_out_reserve(ptr, i64)"); + // Without vectors the gap is a memcpy. + const scalar = compileRegexMatcher("r", "<[^>]*>", "g", "i64", false, "|")!.lines.join("\n"); + expect(scalar).not.toContain("<16 x i8>"); + expect(scalar).toContain( + "call void @llvm.memcpy.p0.p0.i64(ptr %m.dst, ptr %m.src, i64 %m.gapw, i1 false)", + ); + }); + + test("escape the replacement bytes and skip the copy of an empty one", () => { + const quoted = compileRegexMatcher("r", "a[bc]", "g", "i64", true, '"\\\n')!.lines.join("\n"); + expect(quoted).toContain('c"\\22\\5C\\0A"'); + const empty = compileRegexMatcher("r", "a[bc]", "g", "i64", true, "")!.lines.join("\n"); + expect(empty).not.toContain("@r.rep"); + }); +}); diff --git a/packages/compiler/src/backend/llvm/regex-matchers.ts b/packages/compiler/src/backend/llvm/regex-matchers.ts index d83d6bae5..0491a6446 100644 --- a/packages/compiler/src/backend/llvm/regex-matchers.ts +++ b/packages/compiler/src/backend/llvm/regex-matchers.ts @@ -24,6 +24,8 @@ * `len`, it returns the capture count (whole match included) so the runtime * can check it against the compiled bytecode. */ +import { InternalCompilerError } from "../../errors.js"; + const ASCII = 128; const INF = -1; /** Larger patterns keep the interpreter (binary size). */ @@ -1853,10 +1855,13 @@ export function compileRegexMatcher( flags: string, sizeType: string, vector: boolean, + replacement: string | null = null, ): RegexMatcherCode | null { const parsed = parseSubset(pattern, flags); if (parsed === null) return null; const sticky = flags.includes("y"); + if (replacement !== null && !canReplaceWith(pattern, flags, replacement)) + throw new InternalCompilerError(`llvm emitter bug: no replacer for /${pattern}/${flags}`); const { root, ncap } = parsed; const gen = new RxGen(sym, ncap, vector); const end: FirstInfo = { set: emptySet(), nullable: true, bol: false, uncond: true }; @@ -1873,6 +1878,12 @@ export function compileRegexMatcher( fn.push(` %stk = alloca ptr`); fn.push(` %sbuf = alloca [64 x i32]`); for (let r = 0; r < gen.regs; r++) fn.push(` %r${r} = alloca i32`); + if (replacement !== null) { + fn.push(` %next = alloca i32`); + fn.push(` %nfound = alloca i32`); + fn.push(` store i32 0, ptr %next`); + fn.push(` store i32 0, ptr %nfound`); + } fn.push(` store ptr %sbuf, ptr %stk`); fn.push(` store i32 64, ptr %scap`); fn.push(` store i32 %start, ptr %at`); @@ -1900,7 +1911,9 @@ export function compileRegexMatcher( } else { fn.push(` br label %seek`); } - if (firstCount === 1) { + // A replacer seeks again after every match, often a few bytes on: the + // 16-byte scan avoids a memchr call per match. + if (firstCount === 1 && replacement === null) { fn.push(`seek:`); const byte = intervals(first.set)[0]![0]!; fn.push(` %rest = sub i32 %len, %a`); @@ -1944,6 +1957,98 @@ export function compileRegexMatcher( fn.push(` br i1 %over, label %nomatch, label %scan`); } + if (replacement !== null) { + emitReplaceMatch(fn, sym, replacement, vector); + } else { + emitReturnMatch(fn, ncap); + } + + const result = replacement !== null ? "%n.found" : "0"; + fn.push(`nomatch:`); + if (replacement !== null) { + fn.push(` %n.next = load i32, ptr %next`); + fn.push(` %n.nextp = getelementptr inbounds i8, ptr %out, i64 16`); + fn.push(` %n.nextw = sext i32 %n.next to i64`); + fn.push(` store i64 %n.nextw, ptr %n.nextp`); + fn.push(` %n.found = load i32, ptr %nfound`); + } + fn.push(` %n.stk = load ptr, ptr %stk`); + fn.push(` %n.heap = icmp ne ptr %n.stk, %sbuf`); + fn.push(` br i1 %n.heap, label %n.free, label %n.ret`); + fn.push(`n.free:`); + fn.push(` call void @scr_regex_stack_free(ptr %n.stk)`); + fn.push(` br label %n.ret`); + fn.push(`n.ret:`); + fn.push(` ret i32 ${result}`); + + gen.emitTail(); + for (const l of gen.body) fn.push(l); + fn.push(`}`); + + const decls = REGEX_MATCHER_DECLS.slice(); + if (gen.body.some((l) => l.includes("@llvm.cttz.i16"))) + decls.push(`declare i16 @llvm.cttz.i16(i16, i1 immarg)`); + if (!sticky && !first.nullable && firstCount === 1 && replacement === null) + decls.push(`declare ptr @memchr(ptr, i32, ${sizeType})`); + if (replacement !== null) { + decls.push(`declare void @scr_re_replace_out_reserve(ptr, i64)`); + decls.push(`declare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1 immarg)`); + } + const globals = [...gen.globals]; + if (replacement !== null && replacement.length > 0) + globals.push( + `@${sym}.rep = private unnamed_addr constant [${replacement.length} x i8] c"${llvmBytes(replacement)}"`, + ); + return { lines: [...globals, ...fn], decls }; +} + +/** Whether a substitution can run through a build-time replacer: a global, + * non-sticky pattern that is not plain text (the runtime searches those as + * strings) and an ASCII replacement without `$` patterns. */ +export function canReplaceWith(pattern: string, flags: string, replacement: string): boolean { + if (!flags.includes("g") || flags.includes("y") || isPlainTextPattern(pattern, flags)) + return false; + for (let i = 0; i < replacement.length; i++) { + const c = replacement.charCodeAt(i); + if (c >= ASCII || c === 0x24) return false; + } + return true; +} + +/** scr_regex_plain_text's rule: a pattern of literal characters and escaped + * metacharacters (no letters under `i`) that the runtime searches as text. */ +function isPlainTextPattern(pattern: string, flags: string): boolean { + const fold = flags.includes("i"); + if (pattern.length === 0 || pattern.length > 256) return false; + for (let i = 0; i < pattern.length; i++) { + let c = pattern[i]!; + if (c === "\\") { + if (i + 1 >= pattern.length) return false; + c = pattern[++i]!; + if (!"^$\\.*+?()[]{}|/".includes(c)) return false; + } else { + const code = c.charCodeAt(0); + if (code < 0x20 || code >= 0x7f || "^$.*+?()[]{}|".includes(c)) return false; + } + if (fold && /[a-z]/i.test(c)) return false; + } + return true; +} + +function llvmBytes(text: string): string { + let out = ""; + for (let i = 0; i < text.length; i++) { + const c = text.charCodeAt(i); + out += + c >= 0x20 && c < 0x7f && c !== 0x22 && c !== 0x5c + ? text[i] + : `\\${c.toString(16).toUpperCase().padStart(2, "0")}`; + } + return out; +} + +/** The match block of a matcher: store the capture pointers, return 1. */ +function emitReturnMatch(fn: string[], ncap: number): void { fn.push(`match:`); fn.push(` %m.end = load i32, ptr %pos`); fn.push(` %m.start = load i32, ptr %at`); @@ -1969,25 +2074,72 @@ export function compileRegexMatcher( fn.push(` br label %m.ret`); fn.push(`m.ret:`); fn.push(` ret i32 1`); +} - fn.push(`nomatch:`); - fn.push(` %n.stk = load ptr, ptr %stk`); - fn.push(` %n.heap = icmp ne ptr %n.stk, %sbuf`); - fn.push(` br i1 %n.heap, label %n.free, label %n.ret`); - fn.push(`n.free:`); - fn.push(` call void @scr_regex_stack_free(ptr %n.stk)`); - fn.push(` br label %n.ret`); - fn.push(`n.ret:`); - fn.push(` ret i32 0`); - - gen.emitTail(); - for (const l of gen.body) fn.push(l); - fn.push(`}`); - - const decls = REGEX_MATCHER_DECLS.slice(); - if (gen.body.some((l) => l.includes("@llvm.cttz.i16"))) - decls.push(`declare i16 @llvm.cttz.i16(i16, i1 immarg)`); - if (!sticky && !first.nullable && firstCount === 1) - decls.push(`declare ptr @memchr(ptr, i32, ${sizeType})`); - return { lines: [...gen.globals, ...fn], decls }; +/** The match block of a replacer: append the gap since the previous match + * and the replacement to the output (ScrReReplaceOut: len, cap, next at + * 0, 8 and 16, then data), and continue the search at the match end, or one past it for an + * empty match, as the runtime's replace loop does. */ +function emitReplaceMatch(fn: string[], sym: string, replacement: string, vector: boolean): void { + const n = replacement.length; + fn.push(`match:`); + fn.push(` %m.end = load i32, ptr %pos`); + fn.push(` %m.start = load i32, ptr %at`); + fn.push(` %m.next = load i32, ptr %next`); + fn.push(` %m.gap = sub i32 %m.start, %m.next`); + fn.push(` %m.gapw = zext i32 %m.gap to i64`); + fn.push(` %m.lenp = getelementptr inbounds i8, ptr %out, i64 0`); + fn.push(` %m.capp = getelementptr inbounds i8, ptr %out, i64 8`); + fn.push(` %m.datap = getelementptr inbounds i8, ptr %out, i64 24`); + fn.push(` %m.len0 = load i64, ptr %m.lenp`); + fn.push(` %m.cap = load i64, ptr %m.capp`); + fn.push(` %m.room = sub i64 %m.cap, %m.len0`); + // 16 bytes of slack past the gap and the replacement for the 16-byte copy. + fn.push(` %m.need = add i64 %m.gapw, ${n + 32}`); + fn.push(` %m.fits = icmp uge i64 %m.room, %m.need`); + fn.push(` br i1 %m.fits, label %m.put, label %m.grow`); + fn.push(`m.grow:`); + fn.push(` call void @scr_re_replace_out_reserve(ptr %out, i64 %m.need)`); + fn.push(` br label %m.put`); + fn.push(`m.put:`); + fn.push(` %m.data = load ptr, ptr %m.datap`); + fn.push(` %m.len = load i64, ptr %m.lenp`); + fn.push(` %m.dst = getelementptr inbounds i8, ptr %m.data, i64 %m.len`); + fn.push(` %m.nextw = sext i32 %m.next to i64`); + fn.push(` %m.src = getelementptr inbounds i8, ptr %s, i64 %m.nextw`); + if (vector) { + // A gap of at most 16 bytes with 16 readable subject bytes is one + // unaligned 16-byte copy. + fn.push(` %m.small = icmp ule i32 %m.gap, 16`); + fn.push(` %m.rest = sub i32 %len, %m.next`); + fn.push(` %m.readable = icmp sge i32 %m.rest, 16`); + fn.push(` %m.fast = and i1 %m.small, %m.readable`); + fn.push(` br i1 %m.fast, label %m.copy16, label %m.copy`); + fn.push(`m.copy16:`); + fn.push(` %m.v = load <16 x i8>, ptr %m.src, align 1`); + fn.push(` store <16 x i8> %m.v, ptr %m.dst, align 1`); + fn.push(` br label %m.rep`); + } else { + fn.push(` br label %m.copy`); + } + fn.push(`m.copy:`); + fn.push(` call void @llvm.memcpy.p0.p0.i64(ptr %m.dst, ptr %m.src, i64 %m.gapw, i1 false)`); + fn.push(` br label %m.rep`); + fn.push(`m.rep:`); + fn.push(` %m.rdst = getelementptr inbounds i8, ptr %m.dst, i64 %m.gapw`); + if (n > 0) + fn.push(` call void @llvm.memcpy.p0.p0.i64(ptr %m.rdst, ptr @${sym}.rep, i64 ${n}, i1 false)`); + fn.push(` %m.len1 = add i64 %m.len, %m.gapw`); + fn.push(` %m.len2 = add i64 %m.len1, ${n}`); + fn.push(` store i64 %m.len2, ptr %m.lenp`); + fn.push(` store i32 %m.end, ptr %next`); + fn.push(` %m.found0 = load i32, ptr %nfound`); + fn.push(` %m.found1 = add i32 %m.found0, 1`); + fn.push(` store i32 %m.found1, ptr %nfound`); + fn.push(` %m.empty = icmp eq i32 %m.end, %m.start`); + fn.push(` %m.after = add i32 %m.end, 1`); + fn.push(` %m.at = select i1 %m.empty, i32 %m.after, i32 %m.end`); + fn.push(` store i32 %m.at, ptr %at`); + fn.push(` %m.past = icmp sgt i32 %m.at, %len`); + fn.push(` br i1 %m.past, label %nomatch, label %scan`); } diff --git a/packages/compiler/test/regex-replacers.test.ts b/packages/compiler/test/regex-replacers.test.ts new file mode 100644 index 000000000..746279eb4 --- /dev/null +++ b/packages/compiler/test/regex-replacers.test.ts @@ -0,0 +1,42 @@ +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-regex-replacers-")); + 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 }); + } +} + +test("global substitutions of regex literals with literal replacements run a replacer", async () => { + const llvm = await emitLlvm(` +const s = process.argv[2] ?? "ac"; +const rep = process.argv[3] ?? "-"; +console.log( + s.replace(/<[^>]*>/g, "|"), + s.replaceAll(/a[bc]/g, "<2>"), + s.replace(/<[^>]*>/g, "|"), + s.replace(/<[^>]*>/g, "$&$&"), + s.replace(/<[^>]*>/, "|"), + s.replace(/<[^>]*>/gy, "|"), + s.replace(/<[^>]*>/g, rep), +); +`); + const replacers = llvm.match(/^define internal i32 @sc_rr_\d+\(/gm) ?? []; + // One replacer per (pattern, flags, replacement): the repeated site shares it. + expect(replacers).toHaveLength(2); + expect(llvm.match(/call ptr @scr_regex_replace_with\(/g)).toHaveLength(3); + // $ patterns, non-global and sticky patterns, and computed replacements + // keep the runtime's loop. + expect(llvm.match(/call ptr @scr_regex_replace\(/g)!.length).toBeGreaterThanOrEqual(4); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8351-regex-literal-replacers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8351-regex-literal-replacers.ts.json new file mode 100644 index 000000000..be0149287 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8351-regex-literal-replacers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8351-regex-literal-replacers.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index ab323c288..2a98e92c6 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -1416,6 +1416,51 @@ ScrStr *scr_regex_replace(ScrStr *s, ScrRegex *re, ScrStr *rep) { return scr_replace_impl(s, re, rep); } +void scr_re_replace_out_reserve(ScrReReplaceOut *o, int64_t need) { + if (need < 0 || (uint64_t)need > SCR_STR_MAX_CAP - (uint64_t)o->len) scr_regex_oom(); + size_t cap = (size_t)o->cap * 2; + if (cap < (size_t)(o->len + need)) cap = (size_t)(o->len + need); + if (cap > SCR_STR_MAX_CAP) cap = SCR_STR_MAX_CAP; + o->str->len = (size_t)o->len; + o->str = scr_str_regrow(o->str, cap); + o->data = o->str->data; + o->cap = (int64_t)o->str->cap; +} + +/* A global substitution with a literal ASCII replacement through the + * build-time replacer of its (pattern, replacement) pair: on an ASCII + * subject the replacer writes each gap and the replacement into the output + * and continues the search after every match without returning. Every + * other subject, and a plain-text pattern (string search), takes + * scr_replace_impl. */ +ScrStr *scr_regex_replace_with(ScrStr *s, ScrRegex *re, ScrStr *rep, + ScrRegexMatcherFn replacer) { + (void)scr_regex_bc(re); + if (scr_regex_lit(re) || !scr_text_ascii(s)) return scr_replace_impl(s, re, rep); + if (s->len > (size_t)INT32_MAX - 1) scr_regex_oom(); + re->last_index = 0; + ScrReReplaceOut o; + o.str = scr_str_alloc_raw(0, s->len + 64); + o.data = o.str->data; + o.len = 0; + o.cap = (int64_t)o.str->cap; + o.next = 0; + int32_t n = replacer((const uint8_t *)s->data, (int32_t)s->len, 0, (uint8_t **)(void *)&o); + if (n == 0) { + o.str->len = 0; + scr_str_release(o.str); + return scr_str_retain(s); + } + size_t tail = s->len - (size_t)o.next; + if ((int64_t)(tail + 1) > o.cap - o.len) scr_re_replace_out_reserve(&o, (int64_t)tail + 1); + scr_copy_bytes(o.data + o.len, s->data + o.next, tail); + o.len += (int64_t)tail; + o.str->len = (size_t)o.len; + o.data[o.len] = '\0'; + scr_str_note_ascii(o.str); + return o.str; +} + typedef struct { ScrDyn *arguments; int start, end; diff --git a/packages/runtime/src/scr_regex.h b/packages/runtime/src/scr_regex.h index cb61e49f3..502a29266 100644 --- a/packages/runtime/src/scr_regex.h +++ b/packages/runtime/src/scr_regex.h @@ -80,6 +80,22 @@ ScrStr *scr_regex_flags(ScrRegex *re); /* +1 */ ScrStr *scr_regex_to_string(ScrRegex *re); /* +1 */ /* replace: first match without /g, every match with it. */ ScrStr *scr_regex_replace(ScrStr *s, ScrRegex *re, ScrStr *rep); +/* The output of a build-time replacer (regex-matchers.ts): the replacer + * appends each gap and the replacement at data[len] after reserving room, + * and leaves in `next` the subject index the unmatched tail starts at. */ +typedef struct { + int64_t len; + int64_t cap; + int64_t next; + char *data; /* at offset 24 on every target */ + ScrStr *str; +} ScrReReplaceOut; +/* Grows o->str so that `need` more bytes fit at o->data[o->len]. */ +void scr_re_replace_out_reserve(ScrReReplaceOut *o, int64_t need); +/* A global substitution with a literal ASCII replacement, through the + * replacer built for that pattern and replacement. Borrows all. */ +ScrStr *scr_regex_replace_with(ScrStr *s, ScrRegex *re, ScrStr *rep, + ScrRegexMatcherFn replacer); /* replace/replaceAll with a replacer closure of `arity` (at most * SCR_REPLACER_MAX_ARITY) parameters. Parameter i receives what Node passes * at position i: the match and captures below the capture count n, the diff --git a/tests/corpus/8351-regex-literal-replacers.ts b/tests/corpus/8351-regex-literal-replacers.ts new file mode 100644 index 000000000..12968d4d9 --- /dev/null +++ b/tests/corpus/8351-regex-literal-replacers.ts @@ -0,0 +1,72 @@ +// Global substitutions of regex literals with literal replacements: matches +// at every offset around 16-character blocks and at the end of the +// subject, empty matches, alternation order, case-insensitive and +// multiline flags, word boundaries, replacements that are empty, longer +// than the match or hold quotes and backslashes, outputs that grow far past +// the subject, subjects without a match, and subjects outside ASCII (which +// take the general loop). replace and replaceAll. +const substitutions: [string, (s: string) => string][] = [ + ["<[^>]*>", (s) => s.replace(/<[^>]*>/g, "|")], + ["a[NSt]|BY", (s) => s.replace(/a[NSt]|BY/g, "<2>")], + ["tHa[Nt]", (s) => s.replace(/tHa[Nt]/g, "<4>")], + ["\\|[^|][^|]*\\|", (s) => s.replace(/\|[^|][^|]*\|/g, "-")], + ["x*", (s) => s.replace(/x*/g, "-")], + ["\\b", (s) => s.replace(/\b/g, "^")], + ["^ab", (s) => s.replace(/^ab/gm, "Z")], + ["ab|abc|b", (s) => s.replace(/ab|abc|b/g, "#")], + ["AB (i)", (s) => s.replace(/AB/gi, "")], + ["(a)(b)?", (s) => s.replace(/(a)(b)?/g, "q")], + ["\\d+", (s) => s.replace(/\d+/g, '"\\\n')], + ["[^a-z]", (s) => s.replace(/[^a-z]/g, "")], + ["a", (s) => s.replace(/a/g, "<<<
    >>>")], + ["cat (replaceAll)", (s) => s.replaceAll(/cat/g, "dog")], + ["[aeiou]{2} (replaceAll)", (s) => s.replaceAll(/[aeiou]{2}/g, "~")], +]; + +function hash(text: string, h: number): number { + for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) >>> 0; + return h; +} + +const filler = "qwertyuiop asdfghjkl zxcvbnm"; +const needles = ["<2>", "", "aN", "BY", "tHat", "|ab|", "||", "xx", "ab", "abc", "AB", "123", "cat", "aeio", "a"]; +let checksum = 0; +let lines = 0; +for (let length = 0; length <= 70; 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 [name, sub] of substitutions) { + const out = sub(subject); + checksum = hash(out, checksum); + lines++; + if (length === 35 && (offset === 15 || offset === 16)) console.log(name, offset, JSON.stringify(out)); + } + } + } +} +console.log("lines", lines, "checksum", checksum); + +// Dense matches, matches that end the subject, and an output many times +// larger than the subject. +const dense = "<1><22><333>".repeat(40) + "tail"; +for (const [name, sub] of substitutions) console.log(name, JSON.stringify(sub(dense).slice(-40)), sub(dense).length); +const many = "a".repeat(5000); +console.log("grow", many.replace(/a/g, "<<<>>>").length, many.replace(/a/g, "<<<>>>").slice(4990)); +console.log("empty", JSON.stringify("".replace(/x*/g, "-")), JSON.stringify("".replace(/a/g, "b"))); +console.log("none", "zzzz".replace(/a[bc]/g, "!"), "zzzz".replaceAll(/a[bc]/g, "!")); + +// Subjects outside ASCII run the general loop with the same results. +for (const subject of ["héllo wörld ", "日本<語>", "a\u{1F600}b", "cat café cat"]) { + console.log(JSON.stringify(subject.replace(/<[^>]*>/g, "|")), JSON.stringify(subject.replaceAll(/cat/g, "dog")), JSON.stringify(subject.replace(/x*/g, "-"))); +} + +// A long DNA-like subject, as in regex-redux. +let dna = ""; +for (let i = 0; i < 4000; i++) dna += "acgt"[(i * 7 + (i >> 2)) % 4] + (i % 61 === 0 ? "tHaN" : i % 37 === 0 ? "aNBY" : ""); +const step1 = dna.replace(/tHa[Nt]/g, "<4>").replace(/a[NSt]|BY/g, "<2>"); +const step2 = step1.replace(/<[^>]*>/g, "|").replace(/\|[^|][^|]*\|/g, "-"); +console.log("dna", dna.length, step1.length, step2.length, hash(step2, 0)); diff --git a/tests/harness/corpus-costs/8351-regex-literal-replacers.ts.json b/tests/harness/corpus-costs/8351-regex-literal-replacers.ts.json new file mode 100644 index 000000000..56546d44c --- /dev/null +++ b/tests/harness/corpus-costs/8351-regex-literal-replacers.ts.json @@ -0,0 +1 @@ +{ "plain": 3074, "san": 44176 } From 734ced4f4ff5ef1ca55944a127269a3433a39719 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 13:49:34 -0500 Subject: [PATCH 244/273] Address review feedback --- .../src/frontend/type-patches/layer.ts | 23 ++++- .../src/frontend/type-patches/locate.ts | 44 ++++++++- packages/compiler/test/type-patches.test.ts | 97 ++++++++++++++++++- 3 files changed, 156 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/frontend/type-patches/layer.ts b/packages/compiler/src/frontend/type-patches/layer.ts index 48ef6ecf0..79b409110 100644 --- a/packages/compiler/src/frontend/type-patches/layer.ts +++ b/packages/compiler/src/frontend/type-patches/layer.ts @@ -59,6 +59,8 @@ interface MatchRecord extends PatchMatch { interface State { manifest: string; + /** The directory of the project's package.json. */ + projectDir: string; manifestText: string; parse: TypePatchParse; byPackage: Map; @@ -104,6 +106,7 @@ export function configureTypePatches(entryPath: string, parse: TypePatchParse): } state = { manifest: config.manifest, + projectDir: dirname(config.manifest), manifestText: config.manifestText ?? "", parse, byPackage, @@ -142,6 +145,22 @@ function versionAt(dir: string): string | null { } } +/** The version of the runtime package `pkg` an `@types` twin under + * `nodeModules` describes: the copy installed beside it, else the one the + * project resolves (pnpm keeps each package in its own directory, so a + * twin has no runtime package beside it), else null. */ +function twinRuntimeVersion(s: State, nodeModules: string, pkg: string): string | null { + const beside = versionAt(nodeModules + pkg); + if (beside !== null) return beside; + for (let dir = s.projectDir; ;) { + const version = versionAt(`${dir}/node_modules/${pkg}`); + if (version !== null) return version; + const parent = dirname(dir); + if (parent === dir) return null; + dir = parent; + } +} + /** The installed package copy a file belongs to, with the requests that * apply to it; null for files outside node_modules or of unpatched * packages. */ @@ -162,9 +181,9 @@ function copyOf(s: State, path: string): { copy: CopyInfo; rel: string } | null const pkg = name.startsWith("@types/") ? unmangleTypesName(name) : name; const requests = s.byPackage.get(pkg); if (requests !== undefined) { - // A twin describes the runtime package installed beside it. + // A twin is matched by the runtime package's version, never its own. const version = name.startsWith("@types/") - ? (versionAt(path.slice(0, at + marker.length) + pkg) ?? versionAt(root)) + ? twinRuntimeVersion(s, path.slice(0, at + marker.length), pkg) : versionAt(root); const applicable = requests.filter( (r) => version !== null && satisfies(version, r.entry.range), diff --git a/packages/compiler/src/frontend/type-patches/locate.ts b/packages/compiler/src/frontend/type-patches/locate.ts index fc85a1634..6c762f5c3 100644 --- a/packages/compiler/src/frontend/type-patches/locate.ts +++ b/packages/compiler/src/frontend/type-patches/locate.ts @@ -596,6 +596,26 @@ function parseJsDoc(text: string, start: number, end: number): JsDocComment { return { start, end, tags }; } +/** The spans of a file's string, template and regular expression literal + * tokens. */ +function literalSpans(sf: ts.SourceFile): { start: number; end: number }[] { + const out: { start: number; end: number }[] = []; + const kinds = new Set([ + ts.SyntaxKind.StringLiteral, + ts.SyntaxKind.NoSubstitutionTemplateLiteral, + ts.SyntaxKind.TemplateHead, + ts.SyntaxKind.TemplateMiddle, + ts.SyntaxKind.TemplateTail, + ts.SyntaxKind.RegularExpressionLiteral, + ]); + const visit = (n: ts.Node): void => { + if (kinds.has(n.kind)) out.push({ start: tokenStartOfNode(n), end: n.end }); + ts.forEachChild(n, visit); + }; + visit(sf); + return out.sort((a, b) => a.start - b.start); +} + const RETURN_TAGS = new Set(["returns", "return"]); const PROPERTY_TAGS = new Set(["property", "prop"]); @@ -615,15 +635,27 @@ class JsResolver { private readonly text: string, sf: ts.SourceFile, ) { + // JSDoc comments, not text inside string, template or regular + // expression literals that looks like one. + const literals = literalSpans(sf); const re = /\/\*\*(?!\/)[\s\S]*?\*\//g; - for (let m = re.exec(text); m !== null; m = re.exec(text)) + for (let m = re.exec(text); m !== null; m = re.exec(text)) { + const inside = literals.find((l) => l.start <= m!.index && m!.index < l.end); + if (inside !== undefined) { + re.lastIndex = inside.end; + continue; + } this.comments.push(parseJsDoc(text, m.index, m.index + m[0].length)); + } for (const c of this.comments) c.tags.forEach((t, i) => { if ((t.tag === "typedef" || t.tag === "callback") && t.name !== null) this.add(t.name, { kind: "typedef", comment: c, tagIndex: i, prefix: "" }); }); - for (const s of sf.statements.nodes) this.collect(s); + // Declarations first: `exports.name = name` may come before the + // function declaration it exports, which is hoisted. + for (const s of sf.statements.nodes) if (!ts.isExpressionStatement(s)) this.collect(s); + for (const s of sf.statements.nodes) if (ts.isExpressionStatement(s)) this.collect(s); } private add(name: string, pos: JsPos): void { @@ -681,9 +713,11 @@ class JsResolver { if (named !== undefined) this.exportEquals.push(...named); } } else if (ts.isIdentifier(e.right) && this.roots.has(e.right.text)) { - // `exports.name = name`: the named declaration is the export. - for (const pos of this.roots.get(e.right.text)!) - if (pos.kind === "decl") this.add(target, pos); + // `exports.name = local`: the named declaration is the export (and + // already is under its own name). + if (target !== e.right.text) + for (const pos of [...this.roots.get(e.right.text)!]) + if (pos.kind === "decl") this.add(target, pos); } else this.add(target, this.declPos(s, s, e.right)); } diff --git a/packages/compiler/test/type-patches.test.ts b/packages/compiler/test/type-patches.test.ts index 729a0da37..b4c0af9bc 100644 --- a/packages/compiler/test/type-patches.test.ts +++ b/packages/compiler/test/type-patches.test.ts @@ -2,7 +2,7 @@ * packages: commander's HelpContext.helpWidth @typedef property and ms's * @return tags, patched from the fixtures' package.json; installed-version * selection; and the warnings for patches that do not apply as written. */ -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, test } from "vitest"; @@ -122,3 +122,98 @@ test("an @types twin is matched by the runtime package's installed version", () rmSync(root, { recursive: true, force: true }); } }); + +test("an @types twin in pnpm's layout is matched by the runtime package the project resolves", () => { + // pnpm keeps each package in its own directory under node_modules/.pnpm + // and links the project's dependencies from node_modules: the twin has no + // runtime package beside it. + const root = mkdtempSync(join(tmpdir(), "scriptc-type-patches-pnpm-")); + try { + const store = join(root, "node_modules/.pnpm"); + const runtime = join(store, "tallyjs@0.5.0/node_modules/tallyjs"); + const types = join(store, "@types+tallyjs@0.4.0/node_modules/@types/tallyjs"); + mkdirSync(runtime, { recursive: true }); + mkdirSync(types, { recursive: true }); + writeFileSync(join(runtime, "package.json"), '{ "name": "tallyjs", "version": "0.5.0" }'); + writeFileSync(join(runtime, "index.js"), "exports.totals = () => ({});\n"); + writeFileSync(join(types, "package.json"), '{ "name": "@types/tallyjs", "version": "0.4.0" }'); + writeFileSync( + join(types, "index.d.ts"), + "export type Totals = {\n max: number;\n};\nexport declare function totals(): Totals;\n", + ); + mkdirSync(join(root, "node_modules/@types"), { recursive: true }); + symlinkSync(runtime, join(root, "node_modules/tallyjs")); + symlinkSync(types, join(root, "node_modules/@types/tallyjs")); + const patchFor = (range: string) => + writeFileSync( + join(root, "package.json"), + JSON.stringify({ + scriptc: { + typePatches: { + [`tallyjs@${range}`]: { declarations: { "Totals.max": "| undefined" } }, + }, + }, + }), + ); + // A range the runtime (0.5.0) satisfies applies, though the twin's own + // version (0.4.0) does not satisfy it. + patchFor("0.5.0"); + configureTypePatches(join(root, "main.ts"), parse); + expect(typePatchedText(join(types, "index.d.ts"))).toContain("max: number | undefined;"); + // A range only the twin's own version satisfies does not. + patchFor("0.4.0"); + configureTypePatches(join(root, "main.ts"), parse); + expect(typePatchedText(join(types, "index.d.ts"))).toBeUndefined(); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("an export before its hoisted function is patched, and string text that looks like JSDoc is not", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-type-patches-js-")); + try { + const pkg = join(root, "node_modules/hoistkit"); + mkdirSync(pkg, { recursive: true }); + writeFileSync(join(pkg, "package.json"), '{ "name": "hoistkit", "version": "1.0.0" }'); + const source = [ + '"use strict";', + "exports.measure = size;", + 'exports.doc = "/** @typedef {object} Opts\\n * @property {number} width */";', + "", + "/**", + " * @returns {number}", + " */", + "function size(s) {", + " return s.length || undefined;", + "}", + "", + ].join("\n"); + writeFileSync(join(pkg, "index.js"), source); + writeFileSync( + join(root, "package.json"), + JSON.stringify({ + scriptc: { + typePatches: { + "hoistkit@^1.0.0": { + declarations: { "measure()": "| undefined", "Opts.width": "| undefined" }, + }, + }, + }, + }), + ); + configureTypePatches(join(root, "main.ts"), parse); + // The export, under another name, comes before the function it exports: + // the function's @returns tag widens. + expect(typePatchedText(join(pkg, "index.js"))).toBe( + source.replace(" * @returns {number}\n", " * @returns {number | undefined}\n"), + ); + // The string's text is a value, not a typedef: the patch finds nothing. + expect( + typePatchDiagnostics()! + .diagnostics.map((d) => d.message) + .join("\n"), + ).toContain("Opts.width"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); From 177b2fde496334d47b025951cc6c1393866b2136 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 11:51:24 -0700 Subject: [PATCH 245/273] Cross package values at their declared type where the JS literal type lacks members An opted-in package's JavaScript types its own values. The lowering types a consumer's view of such a value from that JavaScript, and the declaration boundary (decl-projection.ts) casts to the declared type only where the JavaScript world answers `any`. A function that returns an object literal has a precise JavaScript type, so it never crossed, even where that literal type lacks members the declarations promise: - `{ [key: string]: string }` over `{ size: "12px" }`: the consumer read `settings["font"] ?? "x"` through the literal's fixed key set. The checker types the miss `any` (a JavaScript literal), but the keyed read took the declared fields' common type, so the miss trapped with "record has no key" and aborted the process. - an expando `out.max = ...` on a non-empty literal, which the JavaScript checker leaves out of the literal's type: reading `stats(xs).max` was refused with SC1090. A package value whose JavaScript type is an object literal that lacks a declared property or string index signature now crosses at its declared type, so missing keys read undefined through the index signature and expando members are read from the object. An index signature whose values do not project (a package interface) keeps the value untyped (`as any`), so its reads are the JavaScript reads. Only programs compiled with --npm-static whose consumers read such values are rewritten. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/decl-projection.ts | 62 +++++++++++++++++-- .../npm-static/declared-members-cli.ts | 21 +++++++ .../node_modules/declared-members/index.d.ts | 7 +++ .../node_modules/declared-members/index.js | 34 ++++++++++ .../declared-members/package.json | 6 ++ tests/harness/npm-static.test.ts | 12 ++++ 6 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/npm-static/declared-members-cli.ts create mode 100644 tests/fixtures/npm-static/node_modules/declared-members/index.d.ts create mode 100644 tests/fixtures/npm-static/node_modules/declared-members/index.js create mode 100644 tests/fixtures/npm-static/node_modules/declared-members/package.json diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts index 02a300996..a999559c2 100644 --- a/packages/compiler/src/frontend/decl-projection.ts +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -519,6 +519,41 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | return undefined; } }; + const stringIndex = (checker: ts.TypeChecker, type: ts.Type): boolean => { + try { + return checker + .getIndexInfosOfType(type) + .some((info) => (info.keyType.flags & ts.TypeFlags.String) !== 0); + } catch { + return false; + } + }; + /** Whether the JavaScript world types a package value as an object + * literal that lacks a member its declaration promises: a property (an + * expando `out.max = ...` on a non-empty literal, which the JavaScript + * checker does not add to the literal's type) or a string index + * signature (`Record` over a literal). Lowering the value + * at the narrower literal type would drop the member or fix the key set, + * so it crosses at its declared type instead. */ + const missesDeclaredMembers = (jsType: ts.Type, declType: ts.Type): boolean => { + if (!(jsType.flags & ts.TypeFlags.Object) || !(declType.flags & ts.TypeFlags.Object)) + return false; + const literal = jsType.getSymbol(); + if (literal === undefined || (literal.flags & ts.SymbolFlags.ObjectLiteral) === 0) return false; + try { + if (declChecker.isArrayType(declType) || declChecker.isTupleType(declType)) return false; + if (stringIndex(declChecker, declType) && !stringIndex(jsChecker, jsType)) return true; + return declChecker + .getPropertiesOfType(declType) + .some( + (prop) => + !String(prop.escapedName).startsWith("__@") && + jsChecker.getPropertyOfType(jsType, String(prop.escapedName)) === undefined, + ); + } catch { + return false; + } + }; const namesPackageSymbol = (node: ts.Node): boolean => { let found = false; const visit = (n: ts.Node): void => { @@ -540,7 +575,7 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const fileEdits: Edit[] = []; /** Candidate expressions (by js node) with their declared type, named * only for the casts that survive suppression. */ - const exprCandidates = new Map(); + const exprCandidates = new Map(); const exprOrder: ts.Node[] = []; /** Reads whose projectable receiver the JavaScript world types `any`: * kept only when nothing types the receiver. */ @@ -610,7 +645,14 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | return; } const jsType = typeAt(jsChecker, jsNode); - if (!isAnyish(jsType)) return; + if (jsType === null) return; + // A typed JavaScript value crosses only where its object type lacks a + // member the declaration promises (missesDeclaredMembers). + const typedJs = !isAnyish(jsType); + if (typedJs) { + const declType = typeAt(declChecker, declNode); + if (declType === null || !missesDeclaredMembers(jsType, declType)) return; + } // Package-produced? const d = declNode as ts.Expression; let produced = false; @@ -686,7 +728,18 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | // no contextual type } const declType = typeAt(declChecker, d); - if (declType === null || !printer.projectable(declType)) return; + if (declType === null) return; + if (!printer.projectable(declType)) { + // An index signature over values that do not project (package + // interfaces): the literal's fixed key set would still answer for + // keys it lacks, so the value stays untyped and every read is the + // JavaScript read. + if (typedJs && stringIndex(declChecker, declType)) { + exprCandidates.set(jsNode, "any"); + exprOrder.push(jsNode); + } + return; + } exprCandidates.set(jsNode, declType); exprOrder.push(jsNode); }; @@ -890,7 +943,8 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | ) continue; } - const alias = printer.project(exprCandidates.get(e)!); + const candidate = exprCandidates.get(e)!; + const alias = candidate === "any" ? "any" : printer.project(candidate); if (alias === null) continue; const start = ts.tokenStartOfNode(e); const stmt = e.parent; diff --git a/tests/fixtures/npm-static/declared-members-cli.ts b/tests/fixtures/npm-static/declared-members-cli.ts new file mode 100644 index 000000000..107ffc54d --- /dev/null +++ b/tests/fixtures/npm-static/declared-members-cli.ts @@ -0,0 +1,21 @@ +// A consumer reads members a package's declarations promise but its +// JavaScript adds in ways the inferred literal type misses: absent keys of +// index signatures read undefined, and expando properties read their values, +// as in Node. +import { counts, fonts, settings, stats } from "declared-members"; + +const key = process.argv.length > 10 ? "size" : "missing"; + +const s = settings(); +console.log(s["font"] ?? "default font", s["size"] ?? "default size", s.family); +console.log(s["font"] === undefined, typeof s["weight"], s.weight ?? "dot miss"); +console.log(s[key] ?? "dynamic miss", s[key], Object.keys(s)); + +const c = counts(); +console.log(c["chars"] ?? -1, c["lines"], c[key] === undefined); + +const st = stats([4, 9, 2]); +console.log(st.max, st.sum, st.count, st); + +const f = fonts(); +console.log(f["title"]?.name ?? "no title", f[key] === undefined, f["body"]?.name, f.code.name); diff --git a/tests/fixtures/npm-static/node_modules/declared-members/index.d.ts b/tests/fixtures/npm-static/node_modules/declared-members/index.d.ts new file mode 100644 index 000000000..8061fd5cb --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/declared-members/index.d.ts @@ -0,0 +1,7 @@ +export declare function settings(): { [key: string]: string }; +export declare function counts(): Record; +export declare function stats(xs: number[]): { count: number; sum: number; max: number }; +export interface Font { + name: string; +} +export declare function fonts(): Record; diff --git a/tests/fixtures/npm-static/node_modules/declared-members/index.js b/tests/fixtures/npm-static/node_modules/declared-members/index.js new file mode 100644 index 000000000..381bd4a46 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/declared-members/index.js @@ -0,0 +1,34 @@ +"use strict"; +// Each function returns an object literal whose JavaScript-inferred type +// lacks members that index.d.ts promises. + +// An index signature over a literal built empty, then assigned. +function settings() { + const out = {}; + out.size = "12px"; + out.family = "serif"; + return out; +} + +// An index signature over a literal with fixed keys. +function counts() { + return { lines: 3, words: 12 }; +} + +// An expando on a non-empty literal: the checker's literal type has no `max`. +function stats(xs) { + const out = { count: xs.length, sum: 0 }; + for (const x of xs) out.sum += x; + out.max = Math.max(...xs); + return out; +} + +// An index signature whose values are a declared interface. +function fonts() { + return { body: { name: "serif" }, code: { name: "mono" } }; +} + +exports.settings = settings; +exports.counts = counts; +exports.stats = stats; +exports.fonts = fonts; diff --git a/tests/fixtures/npm-static/node_modules/declared-members/package.json b/tests/fixtures/npm-static/node_modules/declared-members/package.json new file mode 100644 index 000000000..4a42ebdf2 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/declared-members/package.json @@ -0,0 +1,6 @@ +{ + "name": "declared-members", + "version": "1.0.0", + "main": "index.js", + "types": "index.d.ts" +} diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index f3a6225c4..076733739 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -1207,6 +1207,18 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { expect(nativeRes.exitCode).toBe(nodeRes.exitCode); }, 180_000); + test("package values cross at the members their declarations promise", async () => { + const entry = join(pilotRoot, "declared-members-cli.ts"); + const binary = await buildStatic(entry, ["declared-members"]); + const [nodeRes, nativeRes] = await Promise.all([ + runBinary("node", [entry]), + runBinary(binary, []), + ]); + expect(nativeRes.stdout).toEqual(nodeRes.stdout); + expect(comparableStderr(nativeRes.stderr)).toEqual(nodeRes.stderr); + expect(nativeRes.exitCode).toBe(nodeRes.exitCode); + }, 180_000); + test("the published Effect Function subpath initializes and runs statically", async () => { const entry = join(pilotRoot, "effect-function-cli.ts"); const { coverage } = analyze(entry, { npmStatic: ["effect"] }); From a4bc01bafbbf5f0b9b2c8c346b1c3bae3cac6d20 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 11:52:19 -0700 Subject: [PATCH 246/273] Count shorthand property reads of an initializer-less JavaScript let An initializer-less JavaScript `let` whose writes all agree gets a scalar slot when every read already has that flow type (inferredEvolvingLetType). The read scan resolved each identifier with getSymbolAtLocation, which answers the property symbol for a shorthand property name, so the `width` in `return { width }` was not seen as a read. A function that assigns the let on one path only (`let width; if (tty) width = 80; return { width };`) then stored it in a number slot: the path that never assigns returned 80 or 0 instead of undefined. The capture scan missed a closure that reads the let through a shorthand the same way. Both scans now resolve a shorthand name to the binding it reads, so such a let keeps its checked-dynamic slot and reads undefined until assigned, as in Node. TypeScript lets are unaffected (the inference applies to JavaScript only). Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-stmts.ts | 14 ++++--- .../evolving-let-shorthand-reads.js.json | 6 +++ tests/corpus/evolving-let-shorthand-reads.js | 41 +++++++++++++++++++ .../evolving-let-shorthand-reads.js.json | 1 + 4 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/evolving-let-shorthand-reads.js.json create mode 100644 tests/corpus/evolving-let-shorthand-reads.js create mode 100644 tests/harness/corpus-costs/evolving-let-shorthand-reads.js.json diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index d6d8c1cc7..0563f6165 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -5422,6 +5422,12 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration) const scope = decl.parent?.parent?.parent; if (symbol === undefined || (!ts.isBlock(scope) && !ts.isSourceFile(scope))) return null; + // A shorthand property's name (`{ width }`) resolves to the property + // symbol, but it reads the binding: the checker's shorthand-value symbol. + const refersToBinding = (node: ts.Identifier): boolean => + (ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node + ? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent) + : lowerer.checker.getSymbolAtLocation(node)) === symbol; const isPlainAssignmentTarget = (node: ts.Identifier): boolean => { let target: ts.Expression = node; while (ts.isParenthesizedExpression(target.parent) && target.parent.expression === target) { @@ -5437,11 +5443,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration) const captures = (fn: ts.Node): boolean => { let captured = false; ts.walkPreorder(fn, (node) => { - if ( - node !== fn && - ts.isIdentifier(node) && - lowerer.checker.getSymbolAtLocation(node) === symbol - ) { + if (node !== fn && ts.isIdentifier(node) && refersToBinding(node)) { captured = true; return "stop"; } @@ -5493,7 +5495,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration) if ( ts.isIdentifier(node) && node !== decl.name && - lowerer.checker.getSymbolAtLocation(node) === symbol && + refersToBinding(node) && !isPlainAssignmentTarget(node) ) { reads.push(node); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/evolving-let-shorthand-reads.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/evolving-let-shorthand-reads.js.json new file mode 100644 index 000000000..26d07a9f9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/evolving-let-shorthand-reads.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/evolving-let-shorthand-reads.js" + ], + "diags": [] +} diff --git a/tests/corpus/evolving-let-shorthand-reads.js b/tests/corpus/evolving-let-shorthand-reads.js new file mode 100644 index 000000000..201171bcc --- /dev/null +++ b/tests/corpus/evolving-let-shorthand-reads.js @@ -0,0 +1,41 @@ +// An initializer-less JavaScript `let` read through a shorthand property +// (`{ width }`) still reads undefined on the paths that never assign it, as +// npm packages do when they build an options or size object. +function size(tty) { + let width; + if (tty) width = 80; + return { width }; +} + +/** + * @param {boolean} tty + * @returns {{ width: number | undefined }} + */ +function documented(tty) { + /** @type {number | undefined} */ + let width; + if (tty) width = 80; + return { width }; +} + +function captured(tty) { + let width; + const read = () => ({ width }); + const before = read().width; + if (tty) width = 80; + return [before, read().width]; +} + +function nested(flags) { + let columns; + let rows; + if (flags.columns) columns = 120; + if (flags.rows) rows = 40; + return { size: { columns, rows } }; +} + +console.log(size(true).width, size(false).width); +console.log(documented(true).width, documented(false).width); +console.log(JSON.stringify(size(false)), "width" in size(false)); +console.log(captured(true), captured(false)); +console.log(nested({ columns: true }), nested({ rows: true }), nested({})); diff --git a/tests/harness/corpus-costs/evolving-let-shorthand-reads.js.json b/tests/harness/corpus-costs/evolving-let-shorthand-reads.js.json new file mode 100644 index 000000000..f93ba161d --- /dev/null +++ b/tests/harness/corpus-costs/evolving-let-shorthand-reads.js.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 2800 } From 317d724a7b439cf8056eee530bf9cf9458227b16 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 10:46:52 -0700 Subject: [PATCH 247/273] Keep bound alias instantiations on the lowering pass The compiler is compiled by scriptc and keeps caches on their owners instead of weak collections (compiler-weak-collections.test.ts). Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/generic-functions.ts | 25 ++----------------- .../compiler/src/frontend/lowering/lowerer.ts | 15 ++++++++--- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 7d7124027..a2eda839a 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -921,27 +921,6 @@ function bindDefaultTypeParams( }); } -/** Generic alias references of a call-keyed instance's declared signature - * (`Catalog`), each paired with the same alias as the call's resolved - * signature instantiates it (`Catalog<{ g1: … }, "acme">`). The checker - * keeps the alias symbolic inside the generic body and offers no way to - * instantiate it with the bound arguments, but alias instantiations are - * interned: the body's `x as Catalog` is the very type the signature - * declares, so the pair recorded during unification is that re-instantiation. - * Keyed by the instance's ts-level bindings map, which every scope that - * lowers the instance installs (typeParamTsBindings). */ -const boundAliasInstances = new WeakMap, Map>(); - -/** The concrete instantiation of a generic alias reference inside the - * instance whose ts-level bindings are `tsBindings` (boundAliasInstances), - * or null when its signature never instantiated that reference. */ -export function boundAliasInstance( - tsBindings: Map, - type: ts.Type, -): ts.Type | null { - return boundAliasInstances.get(tsBindings)?.get(type) ?? null; -} - /** The structural half of binding inference: unify the DECLARED signature * (whose types mention the type parameters) against a TARGET signature * with the substitution applied — a call's resolved signature, or the @@ -1062,8 +1041,8 @@ function unifySignatureBindings( iAliasArgs.length === dAliasArgs.length ) { if (tsBindings && declared !== inst) { - let pairs = boundAliasInstances.get(tsBindings); - if (!pairs) boundAliasInstances.set(tsBindings, (pairs = new Map())); + let pairs = lowerer.boundAliasInstances.get(tsBindings); + if (!pairs) lowerer.boundAliasInstances.set(tsBindings, (pairs = new Map())); // A body forwarding its own alias (`inner(g)` returning `Catalog` inside an instance) resolves through the enclosing one. if (!pairs.has(declared)) pairs.set(declared, lowerer.boundAliasTsResolver(inst) ?? inst); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 52f3567fb..216851c9c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -456,7 +456,6 @@ import { inferTypeParamBindings, lowerGenericInstance, genericLocalClassBinding, - boundAliasInstance, } from "./generic-functions.js"; import { bindingNeverReassigned } from "./binding-analysis.js"; import type { InstantiationPath } from "./instantiation-path.js"; @@ -2234,6 +2233,16 @@ export class Lowerer { * the body needs — indexed accesses (`T[K]` needs K's literal key) and * keyed record reads (`o[k]` where k's type is a literal-bound K). */ typeParamTsBindings: Map | null = null; + /** Generic alias references of a call-keyed instance's declared signature + * (`Catalog`), each paired with the same alias as the call's + * resolved signature instantiates it (`Catalog<{ g1: … }, "acme">`), + * keyed by the instance's ts-level bindings (typeParamTsBindings). The + * checker keeps the alias symbolic inside the generic body and offers no + * way to instantiate it with the bound arguments, but alias + * instantiations are interned: the body's `x as Catalog` is the + * very type the signature declares, so the pair recorded during binding + * unification is that re-instantiation. */ + readonly boundAliasInstances = new Map, Map>(); /** Non-null while an IMPLICIT-ANY instance body lowers (npm-static JS; * see generic-functions.ts): bound param symbol → * the call site's checker type, consulted by typeOf for identifier @@ -9554,10 +9563,10 @@ export class Lowerer { /** The concrete instantiation of a generic alias reference (`Catalog`) the current call-keyed instance's signature declares - * (boundAliasInstance). Inert outside generic function instantiation. */ + * (boundAliasInstances). Inert outside generic function instantiation. */ readonly boundAliasTsResolver = (t: ts.Type): ts.Type | null => { if (!this.typeParamTsBindings || t.getAliasTypeArguments().length === 0) return null; - return boundAliasInstance(this.typeParamTsBindings, t); + return this.boundAliasInstances.get(this.typeParamTsBindings)?.get(t) ?? null; }; irTypeOf(node: ts.Node): IrType { From 6000485a17155e71dba5a33c482a347309aadbba Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 14:04:10 -0500 Subject: [PATCH 248/273] Report either decryption error of a wrong passphrase in corpus 8183 With a fresh key and IV each run, Node's OpenSSL now and then accepts the padding of a wrong-passphrase decryption and rejects the decrypted structure instead: ERR_OSSL_UNSUPPORTED rather than ERR_OSSL_BAD_DECRYPT, in about 1 run in 100. The program now prints both as one decryption error. --- tests/corpus/8183-key-passphrase-encoding.cjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/corpus/8183-key-passphrase-encoding.cjs b/tests/corpus/8183-key-passphrase-encoding.cjs index e52fa6c1c..ca817f386 100644 --- a/tests/corpus/8183-key-passphrase-encoding.cjs +++ b/tests/corpus/8183-key-passphrase-encoding.cjs @@ -9,12 +9,17 @@ const hex = Buffer.from(pass).toString("hex"); const pem = privateKey.export({ format: "pem", type: "pkcs8", cipher: "aes-256-cbc", passphrase: hex, encoding: "hex" }); const der = privateKey.export({ format: "der", type: "pkcs8", cipher: "aes-128-cbc", passphrase: pass }); +// A wrong passphrase usually fails the decryption's padding check +// (ERR_OSSL_BAD_DECRYPT); with a fresh key and IV each run, Node's +// OpenSSL now and then accepts the padding and rejects the decrypted +// structure instead (ERR_OSSL_UNSUPPORTED). Both are reported as one. function attempt(label, options) { try { const key = c.createPrivateKey(options); console.log(label, key.asymmetricKeyType, key.equals(privateKey)); } catch (e) { - console.log(label, "throws", e.code); + const wrong = e.code === "ERR_OSSL_BAD_DECRYPT" || e.code === "ERR_OSSL_UNSUPPORTED"; + console.log(label, "throws", wrong ? "a decryption error" : e.code); } } From a2be7d8bc03ca9be9a3e1f941bbc4d759e449bd3 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 12:05:14 -0700 Subject: [PATCH 249/273] Answer Object.prototype's methods on typed records Records inherit toString, toLocaleString, valueOf, hasOwnProperty, isPrototypeOf and propertyIsEnumerable. Calls of these methods by name run an own callable entry when the record's type allows one and the record holds it, and otherwise the built-in with the record as receiver; toLocaleString, valueOf and isPrototypeOf on records were refused, and toString ignored an own entry. Keyed reads of records of functions answer the built-in as a native function value when the key misses: bound to the record when called at once, with an undefined `this` otherwise. The backend fuses the read with its fallback, so hits cost nothing extra. Records of other value types keep reading undefined for these names; the limitations page says so. Co-Authored-By: Claude Opus 5.5 --- .../record-identity-and-property-order.mdx | 7 + .../compiler/src/backend/llvm/expr-control.ts | 22 +- .../compiler/src/backend/llvm/expr-records.ts | 40 +- .../lowering/expressions/element-access.ts | 13 +- .../lowering/expressions/property-access.ts | 7 + .../src/frontend/lowering/lower-calls.ts | 4 + .../src/frontend/lowering/record-inherited.ts | 815 ++++++++++++++++++ ...8360-record-prototype-method-calls.ts.json | 6 + ...61-record-function-table-inherited.ts.json | 6 + .../8360-record-prototype-method-calls.ts | 56 ++ .../8361-record-function-table-inherited.ts | 40 + ...8360-record-prototype-method-calls.ts.json | 1 + ...61-record-function-table-inherited.ts.json | 1 + 13 files changed, 1014 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/record-inherited.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8360-record-prototype-method-calls.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8361-record-function-table-inherited.ts.json create mode 100644 tests/corpus/8360-record-prototype-method-calls.ts create mode 100644 tests/corpus/8361-record-function-table-inherited.ts create mode 100644 tests/harness/corpus-costs/8360-record-prototype-method-calls.ts.json create mode 100644 tests/harness/corpus-costs/8361-record-function-table-inherited.ts.json diff --git a/docs/content/limitations/record-identity-and-property-order.mdx b/docs/content/limitations/record-identity-and-property-order.mdx index dd180cb7b..fc0b2be92 100644 --- a/docs/content/limitations/record-identity-and-property-order.mdx +++ b/docs/content/limitations/record-identity-and-property-order.mdx @@ -25,3 +25,10 @@ Index-signature records list declared fields before the other keys. Optional fields and properties added through a record's `unknown` view can be deleted. A required field has no absent state, so deleting it through the view throws `TypeError: Cannot delete property 'x' of #` and leaves the record unchanged, where Node.js removes the property. + +Records inherit `toString`, `toLocaleString`, `valueOf`, `hasOwnProperty`, `isPrototypeOf` and `propertyIsEnumerable` from `Object.prototype`. +A call of one of these methods by name runs the built-in with the record as its receiver, unless the record holds an own function under that name. +A keyed read such as `handlers[name]` of a record whose values are functions answers the built-in when the record lacks the key, so truthiness, `typeof` and calls match Node.js. +A keyed read of a record whose values are not functions, such as `counts["toString"]` on a `Record`, answers `undefined` where Node.js answers the inherited function, so `counts[word] ?? 0` counts such a word as new. +Calling a built-in read from a record of functions throws a `TypeError` when its answer does not fit the record's function type, for example `valueOf` read from a `Record number>`. +Other inherited names, such as `constructor`, read `undefined` from typed records. diff --git a/packages/compiler/src/backend/llvm/expr-control.ts b/packages/compiler/src/backend/llvm/expr-control.ts index 2f7c31813..5ffa54eb5 100644 --- a/packages/compiler/src/backend/llvm/expr-control.ts +++ b/packages/compiler/src/backend/llvm/expr-control.ts @@ -2,11 +2,12 @@ import { emitBorrowedInput } from "./borrowed-inputs.js"; /* Focused LLVM expression emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; import { undefinedArmTag } from "../../ir/analysis.js"; -import { isRefCounted, isUnitType, typeEquals, type IrExpr } from "../../ir/ir.js"; +import { isRefCounted, isUnitType, typeEquals, type IrExpr, type IrType } from "../../ir/ir.js"; import { DYN_KIND } from "./dyn.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js"; import { f64Lit } from "./common.js"; +import { emitRecordKeyGetOr } from "./expr-records.js"; import { NULLABLE_NULL, SHAPE_WORD_INDEX, shapeWordHeader } from "./nullable-unions.js"; import { emitUnionTag } from "./union-repr.js"; @@ -397,6 +398,13 @@ export function emitControlExpr( throw new InternalCompilerError("llvm emitter bug: nullish union lacks unit arms"); const fused = emitNullishOptionalChain(host, e); if (fused) return fused; + if ( + e.left.kind === "recordKeyGet" && + e.left.missNaN !== true && + typeEquals(e.type, e.left.type) && + storesNoUnit(host, e.left) + ) + return emitRecordKeyGetOr(host, e.left, e.right); if ( !typeEquals(e.type, e.left.type) && (e.left.kind === "varRef" || @@ -700,3 +708,15 @@ function emitUnionTagSet(host: LlvmEmitterContext, set: TagSet): string { B.line(`${t} = phi i1 [ true, %${yes} ], [ false, %${no} ]`); return t; } + +/** Every value a keyed read can hit is present (no stored undefined or + * null), so only a missing key yields the read's unit arm. */ +function storesNoUnit(host: LlvmEmitterContext, e: ExprOf<"recordKeyGet">): boolean { + const shape = host.recordShape(e.shapeId); + const holdsUnit = (t: IrType): boolean => + isUnitType(t) || + t.kind === "dyn" || + (t.kind === "union" && (host.unionsById.get(t.unionId)?.arms ?? []).some(isUnitType)); + if (shape.indexValue !== undefined && holdsUnit(shape.indexValue)) return false; + return e.overflowOnly === true || !shape.fields.some((f) => holdsUnit(f.type)); +} diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts index 59ab625e5..86f20fd45 100644 --- a/packages/compiler/src/backend/llvm/expr-records.ts +++ b/packages/compiler/src/backend/llvm/expr-records.ts @@ -338,6 +338,39 @@ export function emitRegexIntrinsic( } } +/** `r[k] ?? d` in its pass-through shape: a missing key evaluates `d` + * where the read would store undefined, so a hit costs nothing extra. */ +export function emitRecordKeyGetOr( + host: LlvmEmitterContext, + e: IrExpr & { kind: "recordKeyGet" }, + fallback: IrExpr, +): LlValue { + const B = host.B; + const obj = host.emitExpr(e.obj); + const k = host.emitExpr(e.key); + const ty = host.llType(e.type); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca ${ty}`); + const join = B.newLabel("rkg.j"); + keyedRecordReadInto( + host, + slot, + join, + obj.name, + k.name, + e.shapeId, + e.type, + e.overflowOnly === true, + e.loc, + false, + () => host.emitBranchInto(slot, fallback), + ); + B.startBlock(join); + const t = B.tmp(); + B.line(`${t} = load ${ty}, ptr ${slot}`); + return host.own({ name: t, type: e.type }); +} + export function emitRecordKeyGet( host: LlvmEmitterContext, e: IrExpr & { kind: "recordKeyGet" }, @@ -485,6 +518,7 @@ export function keyedRecordReadInto( overflowOnly: boolean, loc?: SrcLoc, missNaN = false, + missInto: (() => void) | null = null, ): void { const B = host.B; const shape = host.recordShape(shapeId); @@ -687,8 +721,10 @@ export function keyedRecordReadInto( } } if (def !== null && resultType.kind === "union" && undefTag >= 0) { - // The miss path: the result union's undefined arm. - B.line(`store ptr ${host.unitInstanceRef(resultType.unionId, undefTag)}, ptr ${slot}`); + // The miss path: the result union's undefined arm, or the caller's + // own value for a miss (missInto stores it into the slot). + if (missInto !== null) missInto(); + else B.line(`store ptr ${host.unitInstanceRef(resultType.unionId, undefTag)}, ptr ${slot}`); B.br(join); return; } diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index a2f2ebdb6..565d0f843 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -69,6 +69,7 @@ import { InternalCompilerError } from "../../../errors.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../../ir/build.js"; import { npmStaticPackageOfPath } from "../../npm-static.js"; import { isSafeToRepeat } from "./evaluation-safety.js"; +import { withInheritedMethods } from "../record-inherited.js"; import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js"; import { isClassCallback } from "../class-callbacks.js"; import { isKeyedFieldAccess } from "./keyed-field-targets.js"; @@ -1444,7 +1445,17 @@ export function lowerRecordKeyRead( loc, }; if (nanCoded) return read; - return includeUndefined || optionalRead ? read : lowerer.maybeNarrow(read, expr); + const answered = withInheritedMethods(lowerer, read, litKey, isCalledAccess(expr)); + return includeUndefined || optionalRead ? answered : lowerer.maybeNarrow(answered, expr); +} + +/** The access is the callee of a call (`o[k](…)`, `o[k]?.(…)`, `o[k]!(…)`), + * which passes the object as `this`. */ +function isCalledAccess(node: ts.Expression): boolean { + let child: ts.Expression = node; + while (ts.isParenthesizedExpression(child.parent) || ts.isNonNullExpression(child.parent)) + child = child.parent; + return ts.isCallExpression(child.parent) && child.parent.expression === child; } /** ToPropertyKey for represented record keys. Missing array-derived string diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 35bb16cd3..f213b6cbf 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -115,6 +115,7 @@ import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../. import { typedArrayLacksMember } from "../builtins/typed-array-members.js"; import { TEMPORAL_GLOBAL } from "../../builtin-modules.js"; import { tryLowerExpression } from "./try-lower-expression.js"; +import { lowerRecordInheritedMember } from "../record-inherited.js"; import { DATE_METHOD_HINT } from "../builtins/dates.js"; import { cjsClassExprWholeExportOf, @@ -3188,6 +3189,12 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi loc: locOf(expr), }; } + // Object.prototype's methods read as values: an own entry of the + // member's function type, else the built-in. + if (actual?.type.kind === "record") { + const inherited = lowerRecordInheritedMember(lowerer, actual, expr); + if (inherited) return inherited; + } // A member the checker declares on the receiver, stored in a pure // index-signature record (an object literal that spread one, see // dictSpreadLiteralShape): the key is always present, so the keyed diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index c8f02562e..aeb1993d4 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -280,6 +280,7 @@ import { } from "./generic-functions.js"; import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generation.js"; +import { lowerRecordInheritedCall } from "./record-inherited.js"; const ZLIB_STREAM_METHODS: ReadonlySet = new Set(["flush", "params", "close", "reset"]); @@ -1643,6 +1644,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // Union receivers whose every arm has a text — the ngrok // `(chunk: Buffer | string) => chunk.toString()` idiom. lowerUnionToStringCall(lowerer, expr, expr.expression) ?? + // Object.prototype's methods called by name on records: an own + // callable entry, else the built-in with the record as receiver. + lowerRecordInheritedCall(lowerer, expr, expr.expression) ?? // Object.prototype.toString's default answer on records and // override-free program classes — "[object Object]", folded. lowerDefaultToStringCall(lowerer, expr, expr.expression) ?? diff --git a/packages/compiler/src/frontend/lowering/record-inherited.ts b/packages/compiler/src/frontend/lowering/record-inherited.ts new file mode 100644 index 000000000..2c01ccd0a --- /dev/null +++ b/packages/compiler/src/frontend/lowering/record-inherited.ts @@ -0,0 +1,815 @@ +/* Object.prototype's methods on typed records. + * + * A record is an ordinary object, so `r.toString()`, `r.hasOwnProperty(k)` + * and the other Object.prototype methods answer from its prototype unless + * the record holds an own entry under that name. Method calls with a + * constant name call an own entry when the record's type lets one be + * callable and otherwise run the built-in natively with the record as its + * receiver. Keyed reads of records whose values are functions or checked + * values answer the built-in as a native function value on the miss path, + * so truthiness, typeof and calls behave as in Node; hits are untouched. + * Records of other value types keep reading undefined for these names + * (a documented limitation). No dynamic-tier unit is involved: every + * built-in is a small lifted function over the record. */ +import * as ts from "../ts7/adapter.js"; +import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; +import { + BOOL, + DYN, + STRING, + UNDEFINED_T, + isUnitType, + typeEquals, + typeKey, + type IrExpr, + type IrLocal, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../ir/ir.js"; +import type { Lowerer } from "./lowerer.js"; +import { locOf } from "../program.js"; +import { keyedHelperName } from "./stable-names.js"; +import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { nullProtoRecordShape, recordNullProtoTest } from "./null-prototype.js"; +import { recordHasOwnHelper } from "./lower-calls.js"; +import { OTHER_ARM_FAILURE_MESSAGE } from "./coercions/unions.js"; + +/** Object.prototype's methods a record answers from its prototype. */ +export const RECORD_INHERITED_METHODS: ReadonlySet = new Set([ + "toString", + "toLocaleString", + "valueOf", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", +]); + +type FuncType = IrType & { kind: "func" }; + +/** Declares a hidden local in the function being built: the lowerer's + * current function, or a lifted helper's own local list. */ +type DeclareLocal = (name: string, type: IrType) => string; + +function liftedLocals(): { declare: DeclareLocal; locals: IrLocal[] } { + const locals: IrLocal[] = []; + return { + locals, + declare: (name, type) => { + const id = `${name}.${locals.length}`; + locals.push({ id, name, type, mutable: false }); + return id; + }, + }; +} + +/** The form a read of `type` gives a built-in method value: the union's one + * function arm (beside an undefined arm), a checked value, or null when the + * type cannot hold a function. */ +function methodValueForm(lowerer: Lowerer, type: IrType): FuncType | "dyn" | null { + if (type.kind === "dyn") return "dyn"; + if (type.kind !== "union") return null; + const arms = lowerer.unions.get(type.unionId)?.arms ?? []; + if (!arms.some((arm) => arm.kind === "undefinedT")) return null; + const funcs = arms.filter((arm): arm is FuncType => arm.kind === "func"); + if (funcs.length !== 1 || funcs[0]!.restAbi !== undefined) return null; + return funcs[0]!; +} + +/** A value of `type` may be a function that a call can enter. */ +function callableType(lowerer: Lowerer, type: IrType): boolean { + if (type.kind === "func" || type.kind === "dyn") return true; + if (type.kind !== "union") return false; + return (lowerer.unions.get(type.unionId)?.arms ?? []).some( + (arm) => arm.kind === "func" || arm.kind === "dyn", + ); +} + +/** A keyed record read (`r[k]`, `r["toString"]`) whose miss answers the + * inherited Object.prototype method for those names. Hits keep the + * ordinary read; only the miss path tests the key. `bound`: the read is + * the callee of a call (`r[k](…)`, `r[k]?.(…)`), whose `this` is the + * record; any other read yields the method as a plain value, whose calls + * see an undefined `this` as in Node. */ +export function withInheritedMethods( + lowerer: Lowerer, + read: IrExpr & { kind: "recordKeyGet" }, + litKey: string | null, + bound: boolean, +): IrExpr { + if (litKey !== null && !RECORD_INHERITED_METHODS.has(litKey)) return read; + if (read.missNaN === true) return read; + const form = methodValueForm(lowerer, read.type); + const shape = lowerer.shapes.get(read.shapeId); + if (form === null || !shape || shape.tuple) return read; + const loc = read.loc; + const stmts: IrStmt[] = []; + const stable = (value: IrExpr, name: string): IrExpr => { + if (isSafeToRepeat(value) || value.kind === "strLit") return value; + const local = lowerer.declareHiddenLocal(name, value.type); + stmts.push({ kind: "varDecl", localId: local.id, init: value, loc }); + return varRef(local.id, value.type, loc); + }; + const obj = stable(read.obj, "%inheritedRecv"); + const key = stable(read.key, "%inheritedKey"); + const helper = inheritedReadHelper(lowerer, read.shapeId, read.type, form, bound, loc); + const fallback: IrExpr = { kind: "call", callee: helper, args: [obj, key], type: read.type, loc }; + if ( + form !== "dyn" && + read.type.kind === "union" && + !(lowerer.unions.get(read.type.unionId)?.arms ?? []).some((arm) => arm.kind === "nullT") + ) { + // `r[k] ?? inherited(r, k)`: the hit is the read's own value, untouched. + const coalesced: IrExpr = { + kind: "nullish", + left: { ...read, obj, key }, + right: fallback, + type: read.type, + loc, + }; + return stmts.length === 0 + ? coalesced + : { kind: "seqExpr", stmts, result: coalesced, type: read.type, loc }; + } + const value = lowerer.declareHiddenLocal("%inheritedRead", read.type); + stmts.push({ kind: "varDecl", localId: value.id, init: { ...read, obj, key }, loc }); + const found = varRef(value.id, read.type, loc); + let missing: IrExpr; + if (form === "dyn") { + missing = { kind: "dynTest", test: "undefined", value: found, type: BOOL, loc } as IrExpr; + } else { + const unionId = (read.type as IrType & { kind: "union" }).unionId; + const tag = (lowerer.unions.get(unionId)?.arms ?? []).findIndex( + (arm) => arm.kind === "undefinedT", + ); + missing = { kind: "unionIsTag", unionId, tag, negated: false, value: found, type: BOOL, loc }; + } + return { + kind: "seqExpr", + stmts, + result: { + kind: "ternary", + cond: missing, + then: fallback, + else_: found, + type: read.type, + loc, + }, + type: read.type, + loc, + }; +} + +/** `%rec.inherited.(o, k)`: the inherited method `k` names as a value + * of `readType` (bound to `o`), or undefined. */ +function inheritedReadHelper( + lowerer: Lowerer, + shapeId: string, + readType: IrType, + form: FuncType | "dyn", + bound: boolean, + loc: SrcLoc, +): string { + const key = `rec.inherited:${bound ? "bound" : "value"}:${shapeId}:${typeKey(readType)}`; + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, "%rec.inherited", key); + lowerer.arrHofHelpers.set(key, name); + const recT: IrType = { kind: "record", shapeId }; + const o = varRef("o.0", recT, loc); + const k = varRef("k.0", STRING, loc); + const fnType: FuncType = + form === "dyn" ? ({ kind: "func", params: [DYN], ret: DYN } as FuncType) : form; + const undefinedValue: IrExpr = + form === "dyn" + ? dynUndefinedExpr(loc) + : lowerer.coerceToExpected( + { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + readType, + ); + const named = (method: string): IrExpr => ({ + kind: "strEq", + negated: false, + left: k, + right: { kind: "strLit", value: method, type: STRING, loc }, + type: BOOL, + loc, + }); + const methods = [...RECORD_INHERITED_METHODS]; + let anyName: IrExpr = named(methods[0]!); + for (const method of methods.slice(1)) + anyName = { kind: "logical", op: "||", left: anyName, right: named(method), type: BOOL, loc }; + const body: IrStmt[] = [ + // Most misses name no inherited method: one comparison chain, nothing else. + { + kind: "if", + cond: { kind: "unary", op: "!", operand: anyName, type: BOOL, loc }, + then: [{ kind: "return", value: { ...undefinedValue }, loc }], + else_: null, + loc, + }, + // An own key holding undefined shadows the prototype like any own key. + { + kind: "if", + cond: { + kind: "call", + callee: recordHasOwnHelper(lowerer, shapeId, loc), + args: [o, k], + type: BOOL, + loc, + }, + then: [{ kind: "return", value: { ...undefinedValue }, loc }], + else_: null, + loc, + }, + ]; + if (nullProtoRecordShape(lowerer, recT)) + body.push({ + kind: "if", + cond: recordNullProtoTest(o, shapeId, loc), + then: [{ kind: "return", value: { ...undefinedValue }, loc }], + else_: null, + loc, + }); + for (const method of methods) { + const value: IrExpr = bound + ? { + kind: "call", + callee: boundMethodFactory(lowerer, shapeId, method, fnType, loc), + args: [o], + type: fnType, + loc, + } + : { + kind: "closure", + fnName: unboundMethodImpl(lowerer, method, fnType, loc), + captures: [], + type: fnType, + loc, + }; + body.push({ + kind: "if", + cond: named(method), + then: [ + { + kind: "return", + value: lowerer.coerceToExpected(value, form === "dyn" ? DYN : readType), + loc, + }, + ], + else_: null, + loc, + }); + } + body.push({ kind: "return", value: { ...undefinedValue }, loc }); + lowerer.liftedFns.push({ + name, + params: [ + { localId: "o.0", name: "o", type: recT }, + { localId: "k.0", name: "k", type: STRING }, + ], + returnType: readType, + locals: [ + { id: "o.0", name: "o", type: recT, mutable: false }, + { id: "k.0", name: "k", type: STRING, mutable: false }, + ], + body, + loc, + }); + return name; +} + +/** `%rec.bind.(o)`: the built-in `method` bound to `o` as a closure. */ +function boundMethodFactory( + lowerer: Lowerer, + shapeId: string, + method: string, + fnType: FuncType, + loc: SrcLoc, +): string { + const key = `rec.bind:${shapeId}:${method}:${typeKey(fnType)}`; + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, "%rec.bind", key); + lowerer.arrHofHelpers.set(key, name); + const recT: IrType = { kind: "record", shapeId }; + lowerer.liftedFns.push({ + name, + params: [{ localId: "o.0", name: "o", type: recT }], + returnType: fnType, + locals: [{ id: "o.0", name: "o", type: recT, mutable: false, boxed: true }], + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: inheritedMethodImpl(lowerer, shapeId, method, fnType, loc), + captures: ["o.0"], + type: fnType, + loc, + }, + loc, + }, + ], + loc, + }); + return name; +} + +/** The lifted body of one built-in bound to its record, with the read's + * function signature: the built-in's answer converted to the signature's + * return type, or the checked-conversion TypeError where it cannot be. */ +function inheritedMethodImpl( + lowerer: Lowerer, + shapeId: string, + method: string, + fnType: FuncType, + loc: SrcLoc, +): string { + const key = `rec.inheritedImpl:${shapeId}:${method}:${typeKey(fnType)}`; + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, `%rec.${method}`, key); + lowerer.arrHofHelpers.set(key, name); + const recT: IrType = { kind: "record", shapeId }; + const o = varRef("o.0", recT, loc); + const params = fnType.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type })); + const args = params.map((p) => varRef(p.localId, p.type, loc)); + const body: IrStmt[] = []; + const hidden = liftedLocals(); + const answer = builtinAnswer(lowerer, hidden.declare, o, shapeId, method, args, fnType.ret, loc); + body.push(...returnAs(lowerer, answer, fnType.ret, loc)); + lowerer.liftedFns.push({ + name, + params, + returnType: fnType.ret, + captures: [{ localId: "o.0", name: "o", type: recT }], + locals: [ + { id: "o.0", name: "o", type: recT, mutable: false, boxed: true }, + ...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), + ...hidden.locals, + ], + body, + loc, + }); + return name; +} + +/** One built-in as a plain function value (no receiver): its answer for an + * undefined `this`, converted like inheritedMethodImpl's. */ +function unboundMethodImpl( + lowerer: Lowerer, + method: string, + fnType: FuncType, + loc: SrcLoc, +): string { + const key = `rec.unboundImpl:${method}:${typeKey(fnType)}`; + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, `%obj.${method}`, key); + lowerer.arrHofHelpers.set(key, name); + const params = fnType.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type })); + const arg: IrExpr | undefined = params[0] && varRef(params[0].localId, params[0].type, loc); + const coerce = "Cannot convert undefined or null to object"; + let answer: IrExpr; + switch (method) { + case "toString": + answer = { kind: "strLit", value: "[object Undefined]", type: STRING, loc }; + break; + case "toLocaleString": + answer = nodeThrowExpr( + 1, + "", + "Object.prototype.toLocaleString called on null or undefined", + STRING, + loc, + ); + break; + case "isPrototypeOf": { + // A primitive argument answers false before `this` converts. + const no: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; + const kind = arg?.type.kind; + if ( + arg === undefined || + isUnitType(arg.type) || + kind === "string" || + kind === "f64" || + kind === "bool" || + kind === "bigint" + ) + answer = no; + else if (kind === "dyn") + answer = { + kind: "ternary", + cond: { + kind: "logical", + op: "||", + left: { kind: "dynTest", test: "object", value: arg, type: BOOL, loc }, + right: { kind: "dynTest", test: "function", value: arg, type: BOOL, loc }, + type: BOOL, + loc, + } as IrExpr, + then: nodeThrowExpr(1, "", coerce, BOOL, loc), + else_: no, + type: BOOL, + loc, + }; + else answer = nodeThrowExpr(1, "", coerce, BOOL, loc); + break; + } + default: + answer = nodeThrowExpr(1, "", coerce, method === "valueOf" ? DYN : BOOL, loc); + } + const body: IrStmt[] = + answer.kind === "libCall" && answer.fn === "error.nodeThrow" + ? [ + { + kind: "return", + value: nodeThrowExpr( + 1, + "", + (answer.args[2] as IrExpr & { kind: "strLit" }).value, + fnType.ret, + loc, + ), + loc, + }, + ] + : returnAs(lowerer, answer, fnType.ret, loc); + lowerer.liftedFns.push({ + name, + params, + returnType: fnType.ret, + locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), + body, + loc, + }); + return name; +} + +/** The built-in's answer with `o` as its receiver. `want` is the type the + * caller needs (valueOf answers the record itself). */ +function builtinAnswer( + lowerer: Lowerer, + declare: DeclareLocal, + o: IrExpr, + shapeId: string, + method: string, + args: readonly IrExpr[], + want: IrType, + loc: SrcLoc, +): IrExpr { + switch (method) { + case "toString": + return { kind: "strLit", value: "[object Object]", type: STRING, loc }; + case "toLocaleString": + // Object.prototype.toLocaleString calls this.toString(). + return callOwnOrBuiltin( + lowerer, + declare, + o, + shapeId, + "toString", + [], + STRING, + "toString", + loc, + ); + case "valueOf": + return want.kind === "dyn" ? lowerer.coerceToExpected(o, DYN) : o; + case "isPrototypeOf": + // No object is created with a typed record as its prototype. + return { kind: "boolLit", value: false, type: BOOL, loc }; + default: { + const key = propertyKeyOf(lowerer, args[0], loc); + return { + kind: "call", + callee: recordHasOwnHelper(lowerer, shapeId, loc), + args: [o, key], + type: BOOL, + loc, + }; + } + } +} + +/** ToPropertyKey of a built-in's key argument (absent: "undefined"). */ +function propertyKeyOf(lowerer: Lowerer, arg: IrExpr | undefined, loc: SrcLoc): IrExpr { + if (arg === undefined || isUnitType(arg.type)) + return { + kind: "strLit", + value: arg?.type.kind === "nullT" ? "null" : "undefined", + type: STRING, + loc, + }; + if (arg.type.kind === "string") return arg; + if (arg.type.kind === "f64" || arg.type.kind === "bool") + return { kind: "toString", operand: arg, type: STRING, loc }; + return { + kind: "libCall", + fn: "dyn.toStringCoerce", + args: [lowerer.coerceToExpected(arg, DYN)], + type: STRING, + loc, + }; +} + +/** `value` returned as `ret`: as is, wrapped into a union arm or a checked + * value, discarded for void, or the checked-conversion TypeError. */ +function returnAs(lowerer: Lowerer, value: IrExpr, ret: IrType, loc: SrcLoc): IrStmt[] { + const converted = convertTo(lowerer, value, ret); + if (converted !== null) return [{ kind: "return", value: converted, loc }]; + if (ret.kind === "void") + return [ + { kind: "exprStmt", expr: value, loc }, + { kind: "return", value: null, loc }, + ]; + return [ + { kind: "exprStmt", expr: value, loc }, + { kind: "return", value: nodeThrowExpr(1, "", OTHER_ARM_FAILURE_MESSAGE, ret, loc), loc }, + ]; +} + +function convertTo(lowerer: Lowerer, value: IrExpr, want: IrType): IrExpr | null { + if (typeEquals(value.type, want)) return value; + if (want.kind === "dyn" && lowerer.dynConvertible(value.type)) + return lowerer.coerceToExpected(value, DYN); + if (want.kind === "union" && lowerer.armTag(want.unionId, value.type) >= 0) + return lowerer.coerceToExpected(value, want); + return null; +} + +/** `o.method(...args)` on a record: an own callable entry when the record + * holds one, else the built-in. `calleeText` names the callee for Node's + * "is not a function". */ +function callOwnOrBuiltin( + lowerer: Lowerer, + declare: DeclareLocal, + o: IrExpr, + shapeId: string, + method: string, + args: readonly IrExpr[], + want: IrType, + calleeText: string, + loc: SrcLoc, +): IrExpr { + const shape = lowerer.shapes.get(shapeId)!; + const field = shape.fields.find((f) => f.name === method); + const ownType = field ? field.type : shape.indexValue; + let builtin = builtinAnswer(lowerer, declare, o, shapeId, method, args, want, loc); + const recT: IrType = { kind: "record", shapeId }; + if (nullProtoRecordShape(lowerer, recT)) + builtin = { + kind: "ternary", + cond: recordNullProtoTest(o, shapeId, loc), + then: nodeThrowExpr(1, "", `${calleeText} is not a function`, builtin.type, loc), + else_: builtin, + type: builtin.type, + loc, + }; + const builtinAsWant = convertTo(lowerer, builtin, want) ?? builtin; + if (ownType === undefined || !callableType(lowerer, ownType)) return builtinAsWant; + const own: IrExpr = field + ? { kind: "recordGet", obj: o, shapeId, field: method, type: field.type, loc } + : { + kind: "recordKeyGet", + obj: o, + shapeId, + key: { kind: "strLit", value: method, type: STRING, loc }, + overflowOnly: true, + type: lowerer.withUndefinedArmOf(shape.indexValue!) ?? shape.indexValue!, + loc, + }; + const called = callOwn(lowerer, declare, own, args, builtinAsWant.type, calleeText, loc); + return { + kind: "ternary", + cond: { + kind: "call", + callee: recordHasOwnHelper(lowerer, shapeId, loc), + args: [o, { kind: "strLit", value: method, type: STRING, loc }], + type: BOOL, + loc, + }, + then: called, + else_: builtinAsWant, + type: builtinAsWant.type, + loc, + }; +} + +/** Call the own entry `own` (present, of any stored type) with `args`. A + * non-function throws Node's "is not a function"; a function whose + * parameters or result cannot carry the call throws the checked-conversion + * TypeError (the program's types claimed otherwise). */ +function callOwn( + lowerer: Lowerer, + declare: DeclareLocal, + own: IrExpr, + args: readonly IrExpr[], + want: IrType, + calleeText: string, + loc: SrcLoc, +): IrExpr { + const notFunction = nodeThrowExpr(1, "", `${calleeText} is not a function`, want, loc); + const lie = nodeThrowExpr(1, "", OTHER_ARM_FAILURE_MESSAGE, want, loc); + const localId = declare("%ownMethod", own.type); + const value = varRef(localId, own.type, loc); + const enter = (fn: IrExpr): IrExpr => { + if (fn.type.kind === "dyn") { + const result: IrExpr = { + kind: "dynCall", + callee: fn, + calleeName: calleeText, + args: args.map((a) => lowerer.coerceToExpected(a, DYN)), + type: DYN, + loc, + } as IrExpr; + return convertTo(lowerer, result, want) ?? sequence(result, lie, loc); + } + const sig = fn.type as FuncType; + if (sig.restAbi !== undefined) return lie; + const passed: IrExpr[] = []; + for (let i = 0; i < sig.params.length; i++) { + const param = sig.params[i]!; + const arg = args[i]; + const given = + arg === undefined + ? convertTo( + lowerer, + { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + param, + ) + : convertTo(lowerer, arg, param); + if (given === null) return lie; + passed.push(given); + } + const result: IrExpr = { kind: "callValue", callee: fn, args: passed, type: sig.ret, loc }; + if (sig.ret.kind === "void") return sequence(result, lie, loc); + return convertTo(lowerer, result, want) ?? sequence(result, lie, loc); + }; + let call: IrExpr; + if (own.type.kind === "func" || own.type.kind === "dyn") call = enter(value); + else if (own.type.kind === "union") { + const unionId = own.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms ?? []; + call = notFunction; + for (let tag = arms.length - 1; tag >= 0; tag--) { + const arm = arms[tag]!; + if (arm.kind !== "func" && arm.kind !== "dyn") continue; + call = { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value, type: BOOL, loc }, + then: enter({ kind: "unionNarrow", unionId, tag, value, type: arm, loc }), + else_: call, + type: want, + loc, + }; + } + } else call = notFunction; + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId, init: own, loc }], + result: call, + type: want, + loc, + }; +} + +function sequence(first: IrExpr, result: IrExpr, loc: SrcLoc): IrExpr { + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: first, loc }], + result, + type: result.type, + loc, + }; +} + +/** `r.toString()`, `r.hasOwnProperty(k)` and the other Object.prototype + * methods called by name on a typed record (the checker resolved the + * member to Object.prototype's). Null for other calls. */ +export function lowerRecordInheritedCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + const method = access.name.text; + if ( + call.questionDotToken || + access.questionDotToken || + !RECORD_INHERITED_METHODS.has(method) || + call.arguments.nodes.some(ts.isSpreadElement) || + !lowerer.isStdlibMember(access) + ) + return null; + const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + if (recvT?.kind !== "record") return null; + const shape = lowerer.shapes.get(recvT.shapeId); + if (!shape || shape.tuple) return null; + const want = + method === "valueOf" + ? (lowerer.mapTypeOf(lowerer.typeOf(call)) ?? recvT) + : method === "toString" || method === "toLocaleString" + ? STRING + : BOOL; + // Without a possible own function, toString and the own-key tests keep + // their existing lowerings (with the special record views they know). + if ( + (method === "hasOwnProperty" || method === "propertyIsEnumerable" || method === "toString") && + !shape.fields.some((f) => f.name === method && callableType(lowerer, f.type)) && + !(shape.indexValue && callableType(lowerer, shape.indexValue)) + ) + return null; + const loc = locOf(call); + const stmts: IrStmt[] = []; + const receiver = lowerer.lowerExpr(access.expression); + if (receiver.type.kind !== "record") return null; + const recvLocal = lowerer.declareHiddenLocal("%protoRecv", receiver.type); + stmts.push({ kind: "varDecl", localId: recvLocal.id, init: receiver, loc }); + const o = varRef(recvLocal.id, receiver.type, loc); + const args: IrExpr[] = []; + for (const node of call.arguments.nodes) { + const arg = lowerer.lowerExpr(node); + const local = lowerer.declareHiddenLocal("%protoArg", arg.type); + stmts.push({ kind: "varDecl", localId: local.id, init: arg, loc }); + args.push(varRef(local.id, arg.type, loc)); + } + const result = callOwnOrBuiltin( + lowerer, + (name, type) => lowerer.declareHiddenLocal(name, type).id, + o, + receiver.type.shapeId, + method, + args, + want, + ts.getTextOfNode(access), + loc, + ); + return { kind: "seqExpr", stmts, result, type: result.type, loc }; +} + +/** `r.toString` and the other Object.prototype methods read by name (not + * called) on an index-signature record: an own entry of the member's + * function type when the record holds one, else the built-in as a plain + * function value. Null when the checker's member type is not a function. */ +export function lowerRecordInheritedMember( + lowerer: Lowerer, + receiver: IrExpr, + expr: ts.PropertyAccessExpression, +): IrExpr | null { + const method = expr.name.text; + if (!RECORD_INHERITED_METHODS.has(method) || !lowerer.isStdlibMember(expr)) return null; + if (receiver.type.kind !== "record") return null; + const want = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (want?.kind !== "func" || want.restAbi !== undefined) return null; + const shapeId = receiver.type.shapeId; + const shape = lowerer.shapes.get(shapeId); + if (!shape || shape.tuple) return null; + const loc = locOf(expr); + const builtin: IrExpr = { + kind: "closure", + fnName: unboundMethodImpl(lowerer, method, want, loc), + captures: [], + type: want, + loc, + }; + const recvLocal = lowerer.declareHiddenLocal("%protoRecv", receiver.type); + const o = varRef(recvLocal.id, receiver.type, loc); + const stmts: IrStmt[] = [{ kind: "varDecl", localId: recvLocal.id, init: receiver, loc }]; + let result: IrExpr = builtin; + if (shape.indexValue && typeEquals(shape.indexValue, want)) { + const ownType = lowerer.withUndefinedArmOf(want); + if (ownType?.kind === "union") { + const read: IrExpr = { + kind: "recordKeyGet", + obj: o, + shapeId, + key: { kind: "strLit", value: method, type: STRING, loc }, + overflowOnly: true, + type: ownType, + loc, + }; + const tag = lowerer.armTag(ownType.unionId, want); + const ownLocal = lowerer.declareHiddenLocal("%ownMethod", ownType); + const own = varRef(ownLocal.id, ownType, loc); + stmts.push({ kind: "varDecl", localId: ownLocal.id, init: read, loc }); + result = { + kind: "ternary", + cond: { + kind: "unionIsTag", + unionId: ownType.unionId, + tag, + negated: false, + value: own, + type: BOOL, + loc, + }, + then: { kind: "unionNarrow", unionId: ownType.unionId, tag, value: own, type: want, loc }, + else_: builtin, + type: want, + loc, + }; + } + } + return { kind: "seqExpr", stmts, result, type: want, loc }; +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8360-record-prototype-method-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8360-record-prototype-method-calls.ts.json new file mode 100644 index 000000000..2bbaaaa88 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8360-record-prototype-method-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8360-record-prototype-method-calls.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8361-record-function-table-inherited.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8361-record-function-table-inherited.ts.json new file mode 100644 index 000000000..a9d8bf4b0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8361-record-function-table-inherited.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8361-record-function-table-inherited.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8360-record-prototype-method-calls.ts b/tests/corpus/8360-record-prototype-method-calls.ts new file mode 100644 index 000000000..980e4b5d2 --- /dev/null +++ b/tests/corpus/8360-record-prototype-method-calls.ts @@ -0,0 +1,56 @@ +// Object.prototype's methods called by name on typed records: the built-in +// with the record as its receiver, or an own entry that shadows it. +interface Point { + x: number; + y: number; +} +const counts: Record = { apple: 2, pear: 0 }; +const point: Point = { x: 1, y: 2 }; +const handlers: Record number> = { double: (n) => n * 2 }; +const own: Record string> = { + toString: () => "own toString", + valueOf: () => "own valueOf", +}; + +function line(...parts: unknown[]): void { + console.log(parts.map(String).join(" | ")); +} + +line("toString", counts.toString(), point.toString(), handlers.toString(), String(counts)); +line("toLocaleString", counts.toLocaleString(), point.toLocaleString()); +line("valueOf", counts.valueOf() === counts, point.valueOf() === point); +line( + "hasOwnProperty", + counts.hasOwnProperty("apple"), + counts.hasOwnProperty("pear"), + counts.hasOwnProperty("plum"), + counts.hasOwnProperty("toString"), + point.hasOwnProperty("x"), + point.hasOwnProperty("z"), + handlers.hasOwnProperty("double"), + handlers.hasOwnProperty("valueOf"), +); +line("propertyIsEnumerable", counts.propertyIsEnumerable("apple"), point.propertyIsEnumerable("toString")); +line("isPrototypeOf", counts.isPrototypeOf(point), point.isPrototypeOf(counts)); + +// Own entries shadow the built-ins; toLocaleString calls the own toString. +line("own", own.toString(), own.toLocaleString(), own.valueOf(), own.hasOwnProperty("toString")); +delete own["toString"]; +line("own deleted", own.toString(), own.toLocaleString()); +own.toString = () => "own again"; +line("own again", own.toString(), `${String(own.toString())}`); + +// Keys added at run time are own keys too. +const later: Record = {}; +for (const word of "toString valueOf plain toString".split(" ")) { + later[word] = (later.hasOwnProperty(word) ? later[word]! : 0) + 1; +} +line("later", JSON.stringify(later), later.hasOwnProperty("toString"), later.hasOwnProperty("constructor")); + +// A null-prototype record has no inherited methods. +const groups = Object.groupBy([1, 2, 3], (n): string => (n % 2 ? "odd" : "even")); +try { + line("groups", groups.toString()); +} catch (e) { + line("groups threw", (e as Error).name + ": " + (e as Error).message); +} diff --git a/tests/corpus/8361-record-function-table-inherited.ts b/tests/corpus/8361-record-function-table-inherited.ts new file mode 100644 index 000000000..eecf74f02 --- /dev/null +++ b/tests/corpus/8361-record-function-table-inherited.ts @@ -0,0 +1,40 @@ +// Records of functions used as dispatch tables meet Object.prototype's method +// names: a keyed read answers the inherited method, bound to the record when +// the read is called at once and a plain function value otherwise. +type Command = (arg: string) => unknown; +const commands: Record = { + echo: (arg) => arg, + shout: (arg) => arg.toUpperCase(), +}; +const inputs = process.argv.length > 99 ? [] : ["echo", "toString", "valueOf", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "shout", "nope"]; +function show(v: unknown): string { + return v === commands ? "" : typeof v + ":" + String(v); +} +for (const name of inputs) { + const handler = commands[name]; + if (!handler) { + console.log(name, "unknown handler"); + continue; + } + console.log(name, typeof handler, "direct", show(commands[name](name === "hasOwnProperty" ? "echo" : "x"))); + console.log(name, "optional", show(commands[name]?.("shout"))); + try { + console.log(name, "detached", show(handler("echo"))); + } catch (e) { + console.log(name, "detached threw", (e as Error).name + ": " + (e as Error).message); + } +} +console.log(typeof commands["toString"], typeof commands["valueOf"], commands["toString"] === undefined, commands["toString"] === commands["toString"]); +console.log(String(commands["toString"]("x")), commands["hasOwnProperty"]("shout"), commands["hasOwnProperty"]("nope")); +const literal: Record = {}; +console.log(Object.keys(literal).length, literal["isPrototypeOf"]?.("x"), typeof literal.toString); + +// A word count whose words are not Object.prototype names keeps `?? 0`. +const freq: Record = {}; +for (const w of "a rose is a rose is a rose".split(" ")) freq[w] = (freq[w] ?? 0) + 1; +console.log(JSON.stringify(freq), freq["tulip"] ?? 0); + +// Read by name without a call: a plain function value. +const counts: Record = { a: 1 }; +const named = counts.toString; +console.log(typeof counts.toString, typeof commands.valueOf, String(named())); diff --git a/tests/harness/corpus-costs/8360-record-prototype-method-calls.ts.json b/tests/harness/corpus-costs/8360-record-prototype-method-calls.ts.json new file mode 100644 index 000000000..1b2d59a51 --- /dev/null +++ b/tests/harness/corpus-costs/8360-record-prototype-method-calls.ts.json @@ -0,0 +1 @@ +{ "plain": 3200, "san": 3900 } diff --git a/tests/harness/corpus-costs/8361-record-function-table-inherited.ts.json b/tests/harness/corpus-costs/8361-record-function-table-inherited.ts.json new file mode 100644 index 000000000..1b2d59a51 --- /dev/null +++ b/tests/harness/corpus-costs/8361-record-function-table-inherited.ts.json @@ -0,0 +1 @@ +{ "plain": 3200, "san": 3900 } From f4825e05a7ad5fc48d6bf188ba4d272ec6401d2a Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 11:33:12 -0700 Subject: [PATCH 250/273] Replace a package's declaration files or supply its declarations with type patches A scriptc.typePatches entry can now replace a declaration file of the package (or of its @types twin, written @types//) with a local .d.ts ("files"), or give the package a declaration entry ("types"). A supplied entry answers the package's types file and the declarations TypeScript probes beside its JavaScript entries, and exists for the program host, the resolver and npm-static's untyped-package test, so a package without declarations becomes a typed package. Declaration patches still apply on top. A files target the installed copy does not ship is SC8031. Co-Authored-By: Claude Opus 5.5 --- docs/content/docs/dependencies.mdx | 19 +++ packages/compiler/src/frontend/npm-static.ts | 3 +- packages/compiler/src/frontend/resolve.ts | 4 +- .../compiler/src/frontend/ts7/program-host.ts | 4 +- .../src/frontend/type-patches/layer.ts | 157 +++++++++++++++--- packages/compiler/src/project-config.test.ts | 44 ++++- packages/compiler/src/project-config.ts | 68 +++++++- .../npm/cases/type-patch-files/main.ts.json | 25 +++ .../cases/type-patch-types-entry/main.ts.json | 16 ++ packages/compiler/test/type-patches.test.ts | 55 ++++++ .../node_modules/@effect/platform-node | 1 + tests/fixtures/effect4/node_modules/effect | 1 + .../npm/cases/type-patch-files/main.ts | 10 ++ .../node_modules/eventlog/dist/entry.d.ts | 8 + .../node_modules/eventlog/dist/index.d.ts | 4 + .../node_modules/eventlog/dist/index.js | 17 ++ .../node_modules/eventlog/package.json | 1 + .../npm/cases/type-patch-files/package.json | 13 ++ .../type-patch-files/type-patches/entry.d.ts | 7 + .../npm/cases/type-patch-types-entry/main.ts | 9 + .../node_modules/pointjs/lib/point.js | 17 ++ .../node_modules/pointjs/package.json | 1 + .../cases/type-patch-types-entry/package.json | 10 ++ .../type-patches/pointjs.d.ts | 4 + tests/harness/type-patches.test.ts | 48 +++++- 25 files changed, 513 insertions(+), 33 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-files/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-types-entry/main.ts.json create mode 120000 tests/fixtures/effect4/node_modules/@effect/platform-node create mode 120000 tests/fixtures/effect4/node_modules/effect create mode 100644 tests/fixtures/npm/cases/type-patch-files/main.ts create mode 100644 tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/entry.d.ts create mode 100644 tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/index.d.ts create mode 100644 tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/index.js create mode 100644 tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/package.json create mode 100644 tests/fixtures/npm/cases/type-patch-files/package.json create mode 100644 tests/fixtures/npm/cases/type-patch-files/type-patches/entry.d.ts create mode 100644 tests/fixtures/npm/cases/type-patch-types-entry/main.ts create mode 100644 tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/lib/point.js create mode 100644 tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/package.json create mode 100644 tests/fixtures/npm/cases/type-patch-types-entry/package.json create mode 100644 tests/fixtures/npm/cases/type-patch-types-entry/type-patches/pointjs.d.ts diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index ec1eb0601..9b660c76f 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -172,6 +172,25 @@ Each key names a package and a semver range of the installed versions the patch A declaration is named by its path: `Options.name` for a member of a type, `parse()` for a function's return type, `parse(text)` for a parameter, `list()[]` for an array's elements, and `Env[string]` for an index signature. `export=()` names the return type of the function the module exports, `default` its default export, and a file prefix such as `index.js:parse()` names a declaration inside one of the package's files. One patch applies to the package's own declarations, its JSDoc, and its `@types/` package. The patched type then holds everywhere, so your code has to handle the `undefined` that a widened declaration allows. An optional `"note"` string can link the upstream issue. +For larger fixes, replace a whole declaration file, or give a package without declarations its own: + +```json:package.json +{ + "scriptc": { + "typePatches": { + "some-client@^3.2.0": { + "files": { "dist/types/options.d.ts": "./type-patches/options.d.ts" } + }, + "untyped-helper@1.x": { + "types": "./type-patches/untyped-helper.d.ts" + } + } + } +} +``` + +`files` maps a declaration file inside the package, or inside its `@types/` package written as `@types//`, to a local `.d.ts` file. `types` supplies the package's declaration entry. Paths of local files are relative to the `package.json`, and a replaced file can import the package's other declaration files with the same relative paths as the file it replaces. + Patches change only declarations, never the package's code, and values are still checked against the patched types. A build warns when a patch doesn't apply as written: when no range covers the installed version, when a declaration path matches nothing, when the declaration already has the type, or when a path is ambiguous. Report wrong declarations to the package too, so the patch can be removed later. ## Provenance sources (experimental) diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index ed442be79..8e26e3058 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -23,7 +23,7 @@ import { trackedRealpath, } from "./input-tracker.js"; import { npmBuiltinAliasOf } from "./builtin-modules.js"; -import { packageSourceText } from "./type-patches/layer.js"; +import { packageSourceText, typePatchSuppliesTypes } from "./type-patches/layer.js"; let activePackages: ReadonlySet = new Set(); let declarationOverloads: ReadonlyMap = new Map(); @@ -374,6 +374,7 @@ function packageIsUntyped(path: string): boolean { /* no package.json — keep probing */ } if (untyped && trackedExists(`${pkgDir}/index.d.ts`)) untyped = false; + if (untyped && typePatchSuppliesTypes(pkgDir)) untyped = false; if (untyped) { // the @types twin, hoisted anywhere up the realm chain const mangled = mangledTypesName(dirName); diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts index 5b035b6cb..86252499f 100644 --- a/packages/compiler/src/frontend/resolve.ts +++ b/packages/compiler/src/frontend/resolve.ts @@ -37,9 +37,11 @@ import { trackedRealpath, } from "./input-tracker.js"; import { isRelativeSpecifier, packageNameOfSpecifier } from "./workspace-registry.js"; +import { typePatchFileExists } from "./type-patches/layer.js"; function isFile(path: string): boolean { - return trackedFileExists(path); + // A declaration entry a project type patch supplies (type-patches/layer.ts). + return trackedFileExists(path) || typePatchFileExists(path); } function isDirectory(path: string): boolean { diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index 9e8f51bb3..303092acf 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -46,7 +46,7 @@ import { trackedRealpath, } from "../input-tracker.js"; import { packageShimDeclarationText } from "../package-shims.js"; -import { typePatchedText } from "../type-patches/layer.js"; +import { typePatchedText, typePatchFileExists } from "../type-patches/layer.js"; import { ambientDtsPath, fallbackDtsPath, overridesDtsPath } from "../dts-paths.js"; import { reusedLibDiagnostics } from "./lib-diagnostics.js"; import { ts7Diagnostic } from "./diagnostic.js"; @@ -190,7 +190,7 @@ export class Ts7Host { fileExists: (fileName) => { if (virtualFiles.has(tsgoPath(fileName))) return true; if (shadow !== null && shadow.hideFile(fileName)) return false; - return trackedFileExists(fileName); + return trackedFileExists(fileName) || typePatchFileExists(fileName); }, directoryExists: (path) => trackedDirectoryExists(path), realpath: (path) => diff --git a/packages/compiler/src/frontend/type-patches/layer.ts b/packages/compiler/src/frontend/type-patches/layer.ts index 48ef6ecf0..5fb41f968 100644 --- a/packages/compiler/src/frontend/type-patches/layer.ts +++ b/packages/compiler/src/frontend/type-patches/layer.ts @@ -27,7 +27,7 @@ import { dirname, relative } from "node:path"; import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; import { typePatchDiag } from "../../diagnostics/diagnostic.js"; import { readProjectConfig, type TypePatchEntry } from "../../project-config.js"; -import { trackedReadFile } from "../input-tracker.js"; +import { trackedFileExists, trackedReadFile } from "../input-tracker.js"; import type { SourceFile } from "../ts7/syntax.js"; import { applyPatchEdits, patchFile, type PatchMatch, type PatchRequest } from "./locate.js"; import { parseSelector, parseTypeText, type Selector, type TypePatchOp } from "./selector.js"; @@ -49,6 +49,11 @@ interface CopyInfo { root: string; /** Requests whose entry's range the version satisfies. */ requests: Request[]; + /** Replaced declaration files: package-relative path → local file. */ + files: Map; + /** Declaration entries a `types` patch supplies: package-relative path + * → local file (existing entries are replaced, missing ones appear). */ + entries: Map; } interface MatchRecord extends PatchMatch { @@ -61,9 +66,11 @@ interface State { manifest: string; manifestText: string; parse: TypePatchParse; - byPackage: Map; + byPackage: Map; copies: Map; files: Map; + /** `files` targets that do not exist in a copy. */ + missingFiles: { entry: TypePatchEntry; root: string; rel: string }[]; /** Package copies whose version no patch range covers. */ unmatchedCopies: Map; /** Requests applicable to a copy this load read files of. */ @@ -84,22 +91,23 @@ export function configureTypePatches(entryPath: string, parse: TypePatchParse): state = null; return; } - const byPackage = new Map(); + const byPackage = new Map(); let id = 0; for (const entry of config.typePatches) { + const pkg = byPackage.get(entry.pkg) ?? { entries: [], requests: [] }; + byPackage.set(entry.pkg, pkg); + pkg.entries.push(entry); for (const decl of entry.declarations) { const selector = parseSelector(decl.selector); const op = parseTypeText(decl.type); if ("error" in selector || "error" in op) continue; // reported as SC8004 - const list = byPackage.get(entry.pkg) ?? []; - list.push({ + pkg.requests.push({ id: id++, selector: selector as Selector, op: op as TypePatchOp, entry, offset: decl.offset, }); - byPackage.set(entry.pkg, list); } } state = { @@ -109,6 +117,7 @@ export function configureTypePatches(entryPath: string, parse: TypePatchParse): byPackage, copies: new Map(), files: new Map(), + missingFiles: [], unmatchedCopies: new Map(), touched: new Set(), matches: [], @@ -145,7 +154,7 @@ function versionAt(dir: string): string | null { /** The installed package copy a file belongs to, with the requests that * apply to it; null for files outside node_modules or of unpatched * packages. */ -function copyOf(s: State, path: string): { copy: CopyInfo; rel: string } | null { +function copyOf(s: State, path: string, touch = true): { copy: CopyInfo; rel: string } | null { const marker = "/node_modules/"; const at = path.lastIndexOf(marker); if (at < 0) return null; @@ -159,33 +168,120 @@ function copyOf(s: State, path: string): { copy: CopyInfo; rel: string } | null let copy = s.copies.get(root); if (copy === undefined) { copy = null; - const pkg = name.startsWith("@types/") ? unmangleTypesName(name) : name; - const requests = s.byPackage.get(pkg); - if (requests !== undefined) { + const twin = name.startsWith("@types/"); + const pkg = twin ? unmangleTypesName(name) : name; + const patches = s.byPackage.get(pkg); + if (patches !== undefined) { // A twin describes the runtime package installed beside it. - const version = name.startsWith("@types/") + const version = twin ? (versionAt(path.slice(0, at + marker.length) + pkg) ?? versionAt(root)) : versionAt(root); - const applicable = requests.filter( - (r) => version !== null && satisfies(version, r.entry.range), + const applicable = patches.entries.filter( + (e) => version !== null && satisfies(version, e.range), ); - // Later entries win on the same selector. - const bySelector = new Map(); - for (const r of applicable) bySelector.set(r.selector.text, r); - const chosen = [...bySelector.values()]; - if (chosen.length === 0) { - const entries = [...new Set(requests.map((r) => r.entry))]; - s.unmatchedCopies.set(root, { pkg, version, entries }); + if (applicable.length === 0) { + s.unmatchedCopies.set(root, { pkg, version, entries: patches.entries }); + } else { + // Later entries win on the same selector, file or entry. + const bySelector = new Map(); + for (const r of patches.requests) + if (applicable.includes(r.entry)) bySelector.set(r.selector.text, r); + const files = new Map(); + let types: string | null = null; + const prefix = `${name}/`; + for (const e of applicable) { + for (const [target, local] of e.files) { + // `@types//` names the twin's file; plain paths the + // package's own. + const forTwin = target.startsWith("@types/"); + if (forTwin !== twin) continue; + const rel = forTwin + ? target.startsWith(prefix) + ? target.slice(prefix.length) + : null + : target; + if (rel === null) continue; + if (!trackedFileExists(`${root}/${rel}`)) { + s.missingFiles.push({ entry: e, root, rel }); + continue; + } + files.set(rel, local); + } + if (e.types !== null && !twin) types = e.types; + } + copy = { + pkg, + version, + root, + requests: [...bySelector.values()], + files, + entries: types === null ? new Map() : declarationEntries(root, types), + }; } - copy = chosen.length === 0 ? null : { pkg, version, root, requests: chosen }; } s.copies.set(root, copy); } if (copy === null) return null; - for (const r of copy.requests) s.touched.add(r); + if (touch) for (const r of copy.requests) s.touched.add(r); return { copy, rel }; } +/** The declaration entry paths of a package a `types` patch stands for: + * its `types`/`typings` file and every declaration TypeScript probes + * beside a JavaScript entry (`main`, the `exports` targets of "."). */ +function declarationEntries(root: string, local: string): Map { + const out = new Map(); + let pkg: Record = {}; + try { + pkg = JSON.parse(trackedReadFile(`${root}/package.json`) ?? "{}") as Record; + } catch { + pkg = {}; + } + const add = (target: unknown): void => { + if (typeof target !== "string") return; + const rel = target.replace(/^\.\//, ""); + if (/\.d\.[cm]?ts$/.test(rel)) out.set(rel, local); + else if (/\.[cm]?js$/.test(rel)) out.set(rel.replace(/\.([cm]?)js$/, ".d.$1ts"), local); + else if (!/\.\w+$/.test(rel)) out.set(`${rel}.d.ts`, local); + }; + add(pkg["types"]); + add(pkg["typings"]); + add(typeof pkg["main"] === "string" ? pkg["main"] : "index.js"); + const exportsField = pkg["exports"]; + const dot = + exportsField !== null && typeof exportsField === "object" && "." in exportsField + ? (exportsField as Record)["."] + : exportsField; + const visit = (v: unknown): void => { + if (typeof v === "string") add(v); + else if (v !== null && typeof v === "object") for (const x of Object.values(v)) visit(x); + }; + // Subpath exports ("./x") are not the entry. + if (dot !== null && typeof dot === "object" && Object.keys(dot).some((k) => k.startsWith("."))) + return out; + visit(dot); + return out; +} + +/** True for a declaration entry a `types` patch supplies that does not + * exist on disk (a package without declarations gains one). */ +export function typePatchFileExists(fileName: string): boolean { + const s = state; + if (s === null) return false; + const path = fileName.split("\\").join("/"); + if (!/\.d\.[cm]?ts$/.test(path)) return false; + const located = copyOf(s, path, false); + return located !== null && located.copy.entries.has(located.rel); +} + +/** True when a `types` patch gives the package at `pkgDir` declarations. */ +export function typePatchSuppliesTypes(pkgDir: string): boolean { + const s = state; + if (s === null) return false; + const located = copyOf(s, `${pkgDir.split("\\").join("/")}/package.json`, false); + return located !== null && located.copy.entries.size > 0; +} + /** Names a request's root can appear under in a file's text. */ function rootWords(sel: Selector): string[] { if (sel.root === "export=") return ["exports", "export"]; @@ -205,7 +301,12 @@ export function typePatchedText(fileName: string): string | undefined { let result: string | null = null; const located = copyOf(s, path); if (located !== null) { - const text = trackedReadFile(path); + // A replaced file or a supplied entry reads as the local file; the + // declaration patches then apply to it like to the package's own. + const local = located.copy.files.get(located.rel) ?? located.copy.entries.get(located.rel); + const replaced = local === undefined ? null : trackedReadFile(local); + const text = replaced ?? trackedReadFile(path); + if (replaced !== null) result = replaced; const requests = located.copy.requests.filter( (r) => (r.selector.file === null || r.selector.file === located.rel) && @@ -314,6 +415,16 @@ export function typePatchDiagnostics(): { ); } } + for (const missing of s.missingFiles) { + out.push( + typePatchDiag( + "SC8031", + `type patch '${missing.entry.key}': ${missing.rel} is not a file of ${missing.entry.pkg} at ${relativeToManifest(s, missing.root)}`, + "name a declaration file the installed package ships; the package may have moved it", + loc(s, missing.entry.offset), + ), + ); + } for (const r of s.touched) { const mine = s.matches.filter((m) => m.id === r.id); // A selector that matched somewhere (the .d.ts) is not a problem where diff --git a/packages/compiler/src/project-config.test.ts b/packages/compiler/src/project-config.test.ts index 04a5d2e0b..51d9520a0 100644 --- a/packages/compiler/src/project-config.test.ts +++ b/packages/compiler/src/project-config.test.ts @@ -214,7 +214,7 @@ test("typePatches: malformed keys, ranges, selectors and types are SC8004", () = "A.c": "string\nnumber", "A.d": "| undefined", }, - files: {}, + patches: {}, }, "other@^1.0.0": [], }, @@ -225,7 +225,7 @@ test("typePatches: malformed keys, ranges, selectors and types are SC8004", () = `package.json 'scriptc.typePatches.ms@^banana' has the version range "^banana", which is not a semver range`, "package.json 'scriptc.typePatches.@types/node@*' patches @types/node, whose declarations ship with scriptc or TypeScript", "package.json 'scriptc.typePatches.typescript@5' patches typescript, whose declarations ship with scriptc or TypeScript", - "package.json 'scriptc.typePatches.lib@^1.0.0.files' is not a known type patch setting", + "package.json 'scriptc.typePatches.lib@^1.0.0.patches' is not a known type patch setting", "package.json 'scriptc.typePatches.lib@^1.0.0.declarations.1bad' is not a selector: it must start with a declared name, 'export=' or 'default'", "package.json 'scriptc.typePatches.lib@^1.0.0.declarations.A.b' must map to a type string, not 3", "package.json 'scriptc.typePatches.lib@^1.0.0.declarations.A.c' has a type that must be one line", @@ -241,3 +241,43 @@ test("typePatches: malformed keys, ranges, selectors and types are SC8004", () = }); expect(partial.typePatches[0]!.declarations.map((d) => d.selector)).toEqual(["A.d"]); }); + +test("typePatches: files and types name local declaration files", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-project-config-")); + try { + mkdirSync(join(root, "patches")); + writeFileSync(join(root, "patches", "entry.d.ts"), "export {};\n"); + const text = JSON.stringify({ + scriptc: { + typePatches: { + "lib@^1.0.0": { + files: { + "dist/entry.d.ts": "./patches/entry.d.ts", + "@types/lib/index.d.ts": "patches/entry.d.ts", + "dist/index.js": "./patches/entry.d.ts", + "../outside.d.ts": "./patches/entry.d.ts", + "dist/other.d.ts": "./patches/missing.d.ts", + }, + types: "./patches/entry.d.ts", + }, + "other@^1.0.0": { types: "./patches/entry.ts" }, + }, + }, + }); + const c = parseProjectConfig(join(root, "package.json"), text); + expect(c.diagnostics.map((d) => d.message)).toEqual([ + "package.json 'scriptc.typePatches.lib@^1.0.0.files.dist/index.js' must name a declaration file inside the package", + "package.json 'scriptc.typePatches.lib@^1.0.0.files.../outside.d.ts' must name a declaration file inside the package", + `package.json 'scriptc.typePatches.lib@^1.0.0.files.dist/other.d.ts' names "./patches/missing.d.ts", which does not exist`, + `package.json 'scriptc.typePatches.other@^1.0.0.types' names "./patches/entry.ts", which is not a declaration file`, + ]); + const entry = c.typePatches.find((e) => e.pkg === "lib")!; + expect([...entry.files]).toEqual([ + ["dist/entry.d.ts", join(root, "patches/entry.d.ts")], + ["@types/lib/index.d.ts", join(root, "patches/entry.d.ts")], + ]); + expect(entry.types).toBe(join(root, "patches/entry.d.ts")); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/src/project-config.ts b/packages/compiler/src/project-config.ts index c43a6bd2c..4aad9cbc3 100644 --- a/packages/compiler/src/project-config.ts +++ b/packages/compiler/src/project-config.ts @@ -27,7 +27,7 @@ * to their own readers. */ import { dirname, join, resolve } from "node:path"; import { projectConfigDiag, type ScrDiagnostic } from "./diagnostics/diagnostic.js"; -import { trackedReadFile } from "./frontend/input-tracker.js"; +import { trackedFileExists, trackedReadFile } from "./frontend/input-tracker.js"; import { parseSelector, parseTypeText } from "./frontend/type-patches/selector.js"; import { isValidRange } from "./frontend/type-patches/semver.js"; @@ -79,6 +79,13 @@ export interface TypePatchEntry { pkg: string; range: string; declarations: TypePatchDeclaration[]; + /** `files`: declaration files of the package (or of its `@types/` twin, + * written `@types//`) replaced by local files, by + * package-relative path. The local paths are absolute. */ + files: Map; + /** `types`: a local declaration file that becomes the package's + * declaration entry (absolute), or null. */ + types: string | null; /** Offset of the key in the manifest. */ offset: number; } @@ -341,7 +348,8 @@ export function parseProjectConfig(manifest: string, text: string): ProjectConfi return config; } -const TYPE_PATCH_KEYS = ["declarations", "note"]; +const TYPE_PATCH_KEYS = ["declarations", "files", "types", "note"]; +const DECLARATION_FILE = /\.d\.[cm]?ts$/; /** Packages whose declarations ship with scriptc or TypeScript: a wrong * one is a bug to report, not a project-local patch. */ const UNPATCHABLE = /^(?:@types\/node|typescript|@typescript\/.*)$/; @@ -404,8 +412,64 @@ function readTypePatches( pkg, range, declarations: [], + files: new Map(), + types: null, offset: locate.offsetOf(["scriptc", "typePatches", key]), }; + const base = dirname(config.manifest!); + const localFile = (path: string[], v: unknown): string | null => { + if (typeof v !== "string" || v === "") { + problem( + path, + `must be the path of a local declaration file, not ${shown(v)}`, + `write "./type-patches/${pkg}.d.ts"`, + ); + return null; + } + const abs = resolve(base, v); + if (!DECLARATION_FILE.test(abs)) { + problem( + path, + `names "${v}", which is not a declaration file`, + "use a .d.ts, .d.mts or .d.cts file", + ); + return null; + } + if (!trackedFileExists(abs)) { + problem( + path, + `names "${v}", which does not exist`, + "give the path relative to this package.json", + ); + return null; + } + return abs; + }; + const files = value["files"]; + if (files !== undefined && !isRecord(files)) { + problem( + ["typePatches", key, "files"], + `must be an object mapping declaration files of the package to local files, not ${shown(files)}`, + `write "files": { "dist/index.d.ts": "./type-patches/${pkg}.d.ts" }`, + ); + } else if (files !== undefined) { + for (const target of Object.keys(files)) { + const path = ["typePatches", key, "files", target]; + const rel = target.replace(/^\.\//, ""); + if (!DECLARATION_FILE.test(rel) || rel.startsWith("/") || rel.split("/").includes("..")) { + problem( + path, + "must name a declaration file inside the package", + `write a package-relative .d.ts path such as "dist/index.d.ts", or "@types//index.d.ts" for its @types package`, + ); + continue; + } + const abs = localFile(path, files[target]); + if (abs !== null) entry.files.set(rel, abs); + } + } + if (value["types"] !== undefined) + entry.types = localFile(["typePatches", key, "types"], value["types"]); let valid = true; for (const k of Object.keys(value)) { if (!TYPE_PATCH_KEYS.includes(k)) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-files/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-files/main.ts.json new file mode 100644 index 000000000..4a15a1975 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-files/main.ts.json @@ -0,0 +1,25 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/type-patch-files/main.ts" + ], + "diags": [ + { + "code": "SC0001", + "message": "Property 'toUpperCase' does not exist on type 'number'.", + "loc": { + "file": "/tests/fixtures/npm/cases/type-patch-files/main.ts", + "start": 346, + "end": 357 + } + }, + { + "code": "SC0001", + "message": "This comparison appears to be unintentional because the types 'number' and 'string' have no overlap.", + "loc": { + "file": "/tests/fixtures/npm/cases/type-patch-files/main.ts", + "start": 429, + "end": 447 + } + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-types-entry/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-types-entry/main.ts.json new file mode 100644 index 000000000..bc1622724 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/type-patch-types-entry/main.ts.json @@ -0,0 +1,16 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/type-patch-types-entry/main.ts" + ], + "diags": [ + { + "code": "SC0001", + "message": "Could not find a declaration file for module 'pointjs'. '/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/lib/point.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/pointjs` if it exists or add a new declaration (.d.ts) file containing `declare module 'pointjs';`", + "loc": { + "file": "/tests/fixtures/npm/cases/type-patch-types-entry/main.ts", + "start": 246, + "end": 255 + } + } + ] +} diff --git a/packages/compiler/test/type-patches.test.ts b/packages/compiler/test/type-patches.test.ts index 729a0da37..116715db5 100644 --- a/packages/compiler/test/type-patches.test.ts +++ b/packages/compiler/test/type-patches.test.ts @@ -12,7 +12,9 @@ import { typePatchDiagnostics, typePatchedSpans, typePatchedText, + typePatchFileExists, typePatchMatches, + typePatchSuppliesTypes, } from "../src/frontend/type-patches/layer.js"; const fixtures = join(import.meta.dirname, "../../../tests/fixtures"); @@ -122,3 +124,56 @@ test("an @types twin is matched by the runtime package's installed version", () rmSync(root, { recursive: true, force: true }); } }); + +test("files replaces a declaration file and declarations still apply on top", () => { + const root = join(fixtures, "npm/cases/type-patch-files"); + configureTypePatches(join(root, "main.ts"), parse); + const entry = typePatchedText(join(root, "node_modules/eventlog/dist/entry.d.ts")); + expect(entry).toBe(readFileSync(join(root, "type-patches/entry.d.ts"), "utf8")); + expect(typePatchedText(join(root, "node_modules/eventlog/dist/index.d.ts"))).toContain( + "{ count: number; last: string | undefined }", + ); + expect(typePatchDiagnostics()!.diagnostics).toEqual([]); +}); + +test("types gives a package without declarations an entry beside its main", () => { + const root = join(fixtures, "npm/cases/type-patch-types-entry"); + configureTypePatches(join(root, "main.ts"), parse); + const pkg = join(root, "node_modules/pointjs"); + expect(typePatchFileExists(join(pkg, "lib/point.d.ts"))).toBe(true); + expect(typePatchFileExists(join(pkg, "index.d.ts"))).toBe(false); + expect(typePatchFileExists(join(pkg, "lib/other.d.ts"))).toBe(false); + expect(typePatchSuppliesTypes(pkg)).toBe(true); + expect(typePatchedText(join(pkg, "lib/point.d.ts"))).toBe( + readFileSync(join(root, "type-patches/pointjs.d.ts"), "utf8"), + ); + // Without patches nothing exists that is not on disk. + configureTypePatches(join(tmpdir(), "none", "main.ts"), parse); + expect(typePatchFileExists(join(pkg, "lib/point.d.ts"))).toBe(false); + expect(typePatchSuppliesTypes(pkg)).toBe(false); +}); + +test("a files target the installed package does not ship warns", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-type-patches-")); + try { + const twin = join(fixtures, "npm/cases/type-patch-files"); + writeFileSync(join(root, "local.d.ts"), "export type Entry = {};\n"); + writeFileSync( + join(root, "package.json"), + JSON.stringify({ + scriptc: { + typePatches: { "eventlog@^2.0.0": { files: { "dist/gone.d.ts": "./local.d.ts" } } }, + }, + }), + ); + configureTypePatches(join(root, "main.ts"), parse); + expect(typePatchedText(join(twin, "node_modules/eventlog/dist/index.d.ts"))).toBeUndefined(); + const diags = typePatchDiagnostics()!.diagnostics; + expect(diags.map((d) => d.code)).toEqual(["SC8031"]); + expect(diags[0]!.message).toMatch( + /^type patch 'eventlog@\^2\.0\.0': dist\/gone\.d\.ts is not a file of eventlog at .*type-patch-files\/node_modules\/eventlog$/, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/tests/fixtures/effect4/node_modules/@effect/platform-node b/tests/fixtures/effect4/node_modules/@effect/platform-node new file mode 120000 index 000000000..aaa396705 --- /dev/null +++ b/tests/fixtures/effect4/node_modules/@effect/platform-node @@ -0,0 +1 @@ +../../../../../node_modules/.pnpm/@effect+platform-node@4.0.0_effect@4.0.0_redis@6.3.0/node_modules/@effect/platform-node \ No newline at end of file diff --git a/tests/fixtures/effect4/node_modules/effect b/tests/fixtures/effect4/node_modules/effect new file mode 120000 index 000000000..1b933cdef --- /dev/null +++ b/tests/fixtures/effect4/node_modules/effect @@ -0,0 +1 @@ +../../../../node_modules/.pnpm/effect@4.0.0/node_modules/effect \ No newline at end of file diff --git a/tests/fixtures/npm/cases/type-patch-files/main.ts b/tests/fixtures/npm/cases/type-patch-files/main.ts new file mode 100644 index 000000000..32e041ce0 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-files/main.ts @@ -0,0 +1,10 @@ +// A package declaration file replaced by a local one (scriptc.typePatches +// files), plus a declaration patch on another file of the same package. +import { parseLog, summarize } from "eventlog"; + +const entries = parseLog("10 info ada started\n12 warn - disk low\n15 error lin crashed hard\n"); +for (const e of entries) console.log(e.at, e.level.toUpperCase(), e.user ?? "(system)", e.message); +console.log(entries.filter((e) => e.level !== "info").length, JSON.stringify(entries[1])); +const all = summarize(entries); +const none = summarize([]); +console.log(all.count, all.last ?? "-", none.count, none.last ?? "-"); diff --git a/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/entry.d.ts b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/entry.d.ts new file mode 100644 index 000000000..0a42b298f --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/entry.d.ts @@ -0,0 +1,8 @@ +// Out of date: the JavaScript reports levels as strings and has no +// `user` for system entries. +export type Entry = { + at: number; + level: number; + user: string; + message: string; +}; diff --git a/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/index.d.ts b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/index.d.ts new file mode 100644 index 000000000..c16d59d46 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/index.d.ts @@ -0,0 +1,4 @@ +export type { Entry } from "./entry"; +import type { Entry } from "./entry"; +export declare function parseLog(text: string): Entry[]; +export declare function summarize(entries: Entry[]): { count: number; last: string }; diff --git a/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/index.js b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/index.js new file mode 100644 index 000000000..4c10d88f4 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/dist/index.js @@ -0,0 +1,17 @@ +"use strict"; +function parseLog(text) { + const rows = String(text) + .split("\n") + .filter(Boolean) + .map((line) => { + const [at, level, user, ...rest] = line.split(" "); + return { at: Number(at), level: level, user: user === "-" ? undefined : user, message: rest.join(" ") }; + }); + // Entries travel as JSON (the log format's own wire form). + return JSON.parse(JSON.stringify(rows)); +} +function summarize(entries) { + return { count: entries.length, last: entries.length === 0 ? undefined : entries[entries.length - 1].message }; +} +exports.parseLog = parseLog; +exports.summarize = summarize; diff --git a/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/package.json b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/package.json new file mode 100644 index 000000000..27ebf4625 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-files/node_modules/eventlog/package.json @@ -0,0 +1 @@ +{ "name": "eventlog", "version": "2.3.1", "main": "dist/index.js", "types": "dist/index.d.ts" } diff --git a/tests/fixtures/npm/cases/type-patch-files/package.json b/tests/fixtures/npm/cases/type-patch-files/package.json new file mode 100644 index 000000000..92a38c108 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-files/package.json @@ -0,0 +1,13 @@ +{ + "name": "type-patch-files-case", + "private": true, + "type": "module", + "scriptc": { + "typePatches": { + "eventlog@^2.0.0": { + "files": { "dist/entry.d.ts": "./type-patches/entry.d.ts" }, + "declarations": { "summarize().last": "| undefined" } + } + } + } +} diff --git a/tests/fixtures/npm/cases/type-patch-files/type-patches/entry.d.ts b/tests/fixtures/npm/cases/type-patch-files/type-patches/entry.d.ts new file mode 100644 index 000000000..380338e96 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-files/type-patches/entry.d.ts @@ -0,0 +1,7 @@ +// The project's replacement for eventlog's dist/entry.d.ts. +export type Entry = { + at: number; + level: "info" | "warn" | "error"; + user?: string; + message: string; +}; diff --git a/tests/fixtures/npm/cases/type-patch-types-entry/main.ts b/tests/fixtures/npm/cases/type-patch-types-entry/main.ts new file mode 100644 index 000000000..3d0ab5ac9 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-entry/main.ts @@ -0,0 +1,9 @@ +// A package without declarations, typed by a local declaration file +// (scriptc.typePatches types): user code is checked against it, and the +// values cross the declaration boundary as native records. +import { centroid, point, type Point } from "pointjs"; + +const points: Point[] = [point(0, 0), point(4, 0), point(4, 3)]; +const c = centroid(points); +console.log(c === null ? "none" : `${c.x.toFixed(2)},${c.y.toFixed(2)}`); +console.log(centroid([]) === null, JSON.stringify(points.map((p) => p.x + p.y))); diff --git a/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/lib/point.js b/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/lib/point.js new file mode 100644 index 000000000..f324e9acb --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/lib/point.js @@ -0,0 +1,17 @@ +"use strict"; +// Plain JavaScript without declarations. +function point(x, y) { + return { x: x, y: y }; +} +function centroid(points) { + if (points.length === 0) return null; + var sx = 0; + var sy = 0; + for (var i = 0; i < points.length; i++) { + sx += points[i].x; + sy += points[i].y; + } + return point(sx / points.length, sy / points.length); +} +exports.point = point; +exports.centroid = centroid; diff --git a/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/package.json b/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/package.json new file mode 100644 index 000000000..917576875 --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/package.json @@ -0,0 +1 @@ +{ "name": "pointjs", "version": "1.4.0", "main": "lib/point.js" } diff --git a/tests/fixtures/npm/cases/type-patch-types-entry/package.json b/tests/fixtures/npm/cases/type-patch-types-entry/package.json new file mode 100644 index 000000000..06846a01d --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-entry/package.json @@ -0,0 +1,10 @@ +{ + "name": "type-patch-types-entry-case", + "private": true, + "type": "module", + "scriptc": { + "typePatches": { + "pointjs@1.x": { "types": "./type-patches/pointjs.d.ts" } + } + } +} diff --git a/tests/fixtures/npm/cases/type-patch-types-entry/type-patches/pointjs.d.ts b/tests/fixtures/npm/cases/type-patch-types-entry/type-patches/pointjs.d.ts new file mode 100644 index 000000000..30ae94a5e --- /dev/null +++ b/tests/fixtures/npm/cases/type-patch-types-entry/type-patches/pointjs.d.ts @@ -0,0 +1,4 @@ +// The project's declarations for pointjs, which ships none. +export type Point = { x: number; y: number }; +export declare function point(x: number, y: number): Point; +export declare function centroid(points: Point[]): Point | null; diff --git a/tests/harness/type-patches.test.ts b/tests/harness/type-patches.test.ts index ecd8ee923..816418a96 100644 --- a/tests/harness/type-patches.test.ts +++ b/tests/harness/type-patches.test.ts @@ -4,8 +4,10 @@ * value (the shape of commander's HelpContext.helpWidth), and ms's @return * tags, compile natively and byte-match Node once patched; without the * patches the same declarations throw where Node runs. Patches that do not - * apply as written warn (SC8030-SC8033). The `.d.ts` and `@types` cases - * run in npm.test.ts (type-patch-dts, type-patch-types-twin), and + * apply as written warn (SC8030-SC8033). The `.d.ts`, `@types`, replaced + * file and supplied entry cases run in npm.test.ts (type-patch-dts, + * type-patch-types-twin, type-patch-files, type-patch-types-entry); here + * their unpatched copies show the patches are what makes them compile. And * commander-calc carries the HelpContext.helpWidth patch. */ import { execFile } from "node:child_process"; import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; @@ -121,6 +123,48 @@ describe("type patches", () => { }, ); + test( + "without its patch, code using the out-of-date declaration file does not type-check", + { timeout: 600_000 }, + async () => { + const copy = unpatched(join(repoRoot, "tests/fixtures/npm/cases/type-patch-files")); + try { + mkdirSync(outDir, { recursive: true }); + const result = await compile(join(copy, "main.ts"), { + outPath: join(outDir, "files-unpatched"), + outDir, + sanitize, + }); + expect(result.ok).toBe(false); + if (!result.ok) + expect(result.diagnostics.map((d) => d.message)).toContain( + "Property 'toUpperCase' does not exist on type 'number'.", + ); + } finally { + rmSync(copy, { recursive: true, force: true }); + } + }, + ); + + test( + "without its patch, a package without declarations is not compiled for a typed import", + { timeout: 600_000 }, + async () => { + const copy = unpatched(join(repoRoot, "tests/fixtures/npm/cases/type-patch-types-entry")); + try { + mkdirSync(outDir, { recursive: true }); + const result = await compile(join(copy, "main.ts"), { + outPath: join(outDir, "types-entry-unpatched"), + outDir, + sanitize, + }); + expect(result.ok).toBe(false); + } finally { + rmSync(copy, { recursive: true, force: true }); + } + }, + ); + test( "patches that do not apply as written warn and the build goes on", { timeout: 600_000 }, From be0e13aa1ef057c04ab397aa801a00b7577a1378 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 14:28:23 -0500 Subject: [PATCH 251/273] Run paired row sums two columns per step A paired row-sum nest now also takes two columns per step: each row adds the first column's term and then the second's, and an odd column count runs its last column on its own, so every sum keeps its order of additions. The nest runs paired when all four bounds are integers in [0, 2^25 - 1]; it reads them masked to that range, which proves both counters' ranges, so its counted loops need no run-time guard or second copy of the body. Corpus 8355 covers the column counts and bounds, and corpus 8356 covers worker loops over shared views that run past a view's end or write overlapping views of one buffer. --- packages/compiler/src/ir/row-pairs.test.ts | 37 +++- packages/compiler/src/ir/row-pairs.ts | 200 +++++++++++++----- .../corpus/8355-row-pair-columns.ts.json | 6 + .../8356-shared-view-loop-versions.ts.json | 6 + tests/corpus/8355-row-pair-columns.ts | 73 +++++++ .../corpus/8356-shared-view-loop-versions.ts | 93 ++++++++ 6 files changed, 352 insertions(+), 63 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8355-row-pair-columns.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8356-shared-view-loop-versions.ts.json create mode 100644 tests/corpus/8355-row-pair-columns.ts create mode 100644 tests/corpus/8356-shared-view-loop-versions.ts diff --git a/packages/compiler/src/ir/row-pairs.test.ts b/packages/compiler/src/ir/row-pairs.test.ts index f9db5b89d..8ef01c665 100644 --- a/packages/compiler/src/ir/row-pairs.test.ts +++ b/packages/compiler/src/ir/row-pairs.test.ts @@ -93,12 +93,39 @@ test("a row-sum nest runs two rows per pass behind its bounds and overlap test", // from the array the term reads. expect(JSON.stringify(go.init)).toContain('"fn":"bytes.disjoint"'); expect(branch.else_).toBeNull(); - const pairs = branch.then[0] as IrStmt & { kind: "for" }; + // The four bounds, masked to the range the test proved, then the pairs. + expect(branch.then.slice(0, 4)).toMatchObject( + Array(4).fill({ kind: "varDecl", init: { op: "&", right: { value: 2 ** 25 - 1 } } }), + ); + const pairs = branch.then[4] as IrStmt & { kind: "for" }; expect(pairs.body.filter((s) => s.kind === "bytesSet")).toHaveLength(2); - const inner = pairs.body[2] as IrStmt & { kind: "for" }; - // One read of u[j] for both rows, then the two sums. - expect(inner.body).toHaveLength(3); + const [, , inner, last] = pairs.body as [ + IrStmt, + IrStmt, + IrStmt & { kind: "for" }, + IrStmt & { kind: "if" }, + ]; + const column = inner.init as IrStmt & { kind: "varDecl" }; + // Two columns per step: for each, one read of u[j] for both rows, then + // the two sums; the second column reads `column + 1`. + expect(inner.body).toHaveLength(6); expect(inner.body[0]).toMatchObject({ kind: "varDecl", init: { kind: "bytesIntrinsic" } }); + expect(inner.body[3]).toMatchObject({ + kind: "varDecl", + init: { + kind: "bytesIntrinsic", + args: [{ op: "+", left: { localId: column.localId }, right: { value: 1 } }], + }, + }); + expect(inner.cond).toMatchObject({ op: "<", right: { op: "-", right: { value: 1 } } }); + expect(inner.update).toMatchObject({ value: { op: "+", right: { value: 2 } } }); + // The last column of an odd count, `to - 1`. + expect(last.then).toHaveLength(3); + expect(last.then[0]).toMatchObject({ + kind: "varDecl", + init: { args: [{ op: "-", right: { value: 1 } }] }, + }); + expect(last.else_).toBeNull(); // The second row's term reads `row + 1`. const row = pairs.init?.kind === "varDecl" ? pairs.init.localId : ""; expect((inner.body[2] as IrStmt & { kind: "assign" }).value).toMatchObject({ @@ -130,7 +157,7 @@ test("a row-sum nest runs two rows per pass behind its bounds and overlap test", expect(top.body).toHaveLength(4); expect( result.functions[0]!.locals.map((l) => l.id).filter((id) => id.startsWith("%pair.")), - ).toHaveLength(7); + ).toHaveLength(13); }); test("nests whose term calls, writes or reads the sum keep their loops", () => { diff --git a/packages/compiler/src/ir/row-pairs.ts b/packages/compiler/src/ir/row-pairs.ts index 2e189f42e..e6b3889a5 100644 --- a/packages/compiler/src/ir/row-pairs.ts +++ b/packages/compiler/src/ir/row-pairs.ts @@ -8,10 +8,17 @@ import { type IrStmt, } from "./ir.js"; import { everyExpr, everyExprChild, mapExprChildren, transformStmtList } from "./traverse.js"; +import { PRODUCT_COUNTER_BOUND } from "./integer-loops.js"; /** Inner-loop expressions above this many nodes keep the loop as written. */ const MAX_TERM_NODES = 64; +/** The largest bound a paired nest runs with. Counters below + * PRODUCT_COUNTER_BOUND keep products of their sums exact, so the nest's + * counted loops take integer counters without a run-time guard (and its + * copy of the body). */ +const SMALL_BOUND = PRODUCT_COUNTER_BOUND - 1; + /** The number operators a term may use: they run no other code and cannot * throw. */ const PURE_BINARY = new Set([ @@ -210,7 +217,8 @@ const bin = ( }); /** Runs the row sums of `match` two rows per pass when the test holds and - * as written otherwise. */ + * as written otherwise. Each pass takes two columns per step, then the + * last column when their count is odd. */ function pairRows( s: IrStmt & { kind: "for" }, match: RowSum, @@ -225,42 +233,93 @@ function pairRows( sum = fresh("sum"), next = fresh("next"), column = fresh("column"); - const atColumn = (r: IrExpr & { kind: "varRef" }): IrExpr => - r.localId === match.column ? { ...r, localId: column } : { ...r }; // Element reads that do not depend on the row are read once per column // and shared by both rows. - const shared = new Map(); - const elements: IrStmt[] = []; + const sharedReads: IrExpr[] = []; const collect = (e: IrExpr): boolean => { if (e.kind === "bytesIntrinsic" && e.method === "get" && !readsLocal(e, match.row)) { - const id = fresh("element"); - shared.set(e, id); - elements.push({ kind: "varDecl", localId: id, init: copy(e, atColumn), loc }); + sharedReads.push(e); return true; } return everyExprChild(e, collect, () => true); }; collect(match.term); - // The second row's term reads `row + 1`, so its counter sums stay visible - // to the counted-loop analysis. - const termAt = (rowExpr: () => IrExpr): IrExpr => - copy( - match.term, - (r) => (r.localId === match.row ? rowExpr() : atColumn(r)), - (node) => { - const id = shared.get(node); - return id === undefined ? null : { kind: "varRef", localId: id, type: node.type, loc }; + /** Both rows' additions at the column `columnAt()`: its shared element + * reads, then each row's sum. The second row's term reads `row + 1`, and + * the second column's `column + 1`, so their counter sums stay visible to + * the counted-loop analysis. */ + const step = (columnAt: () => IrExpr): IrStmt[] => { + const at = (r: IrExpr & { kind: "varRef" }): IrExpr => + r.localId === match.column ? columnAt() : { ...r }; + const shared = new Map(); + const elements: IrStmt[] = sharedReads.map((read) => { + const id = fresh("element"); + shared.set(read, id); + return { kind: "varDecl", localId: id, init: copy(read, at), loc }; + }); + const termAt = (rowExpr: () => IrExpr): IrExpr => + copy( + match.term, + (r) => (r.localId === match.row ? rowExpr() : at(r)), + (node) => { + const id = shared.get(node); + return id === undefined ? null : { kind: "varRef", localId: id, type: node.type, loc }; + }, + ); + return [ + ...elements, + { + kind: "assign", + localId: sum, + value: bin( + "+", + ref(sum), + termAt(() => ref(row)), + loc, + ), + loc, }, - ); + { + kind: "assign", + localId: next, + value: bin( + "+", + ref(next), + termAt(() => bin("+", ref(row), num(1), loc)), + loc, + ), + loc, + }, + ]; + }; const innerCond = match.inner.cond as IrExpr & { kind: "bin" }; + const columnStart = (match.inner.init as IrStmt & { kind: "varDecl" }).init!; + // The bounds, as the paired nest reads them (see `small` below). + const low = fresh("low"), + high = fresh("high"), + first = fresh("first"), + last = fresh("last"); + const bounds: IrStmt[] = ( + [ + [low, match.start], + [high, match.end], + [first, columnStart], + [last, innerCond.right], + ] as const + ).map(([id, bound]) => ({ + kind: "varDecl", + localId: id, + init: bin("&", again(bound), num(SMALL_BOUND), loc), + loc, + })); const pairs: IrStmt = { kind: "for", - init: { kind: "varDecl", localId: row, init: again(match.start), loc }, + init: { kind: "varDecl", localId: row, init: ref(low), loc }, cond: { kind: "bin", op: "<", left: ref(row), - right: bin("-", again(match.end), num(1), loc), + right: bin("-", ref(high), num(1), loc), type: BOOL, loc, }, @@ -268,41 +327,40 @@ function pairRows( body: [ { kind: "varDecl", localId: sum, init: { ...match.init }, loc }, { kind: "varDecl", localId: next, init: { ...match.init }, loc }, + // Both column bounds are int32, so `column < to - 1` is + // `column + 1 < to`. Each row adds its columns in order. { kind: "for", - init: { - kind: "varDecl", - localId: column, - init: again((match.inner.init as IrStmt & { kind: "varDecl" }).init!), + init: { kind: "varDecl", localId: column, init: ref(first), loc }, + cond: { + ...innerCond, + left: ref(column), + right: bin("-", ref(last), num(1), loc), + }, + update: { kind: "assign", localId: column, value: bin("+", ref(column), num(2), loc), loc }, + body: [...step(() => ref(column)), ...step(() => bin("+", ref(column), num(1), loc))], + loc, + }, + // An odd column count leaves the last column, `to - 1`. + { + kind: "if", + cond: { + kind: "logical", + op: "&&", + left: { ...innerCond, left: ref(first), right: ref(last) }, + right: { + kind: "bin", + op: "===", + left: bin("&", bin("-", ref(last), ref(first), loc), num(1), loc), + right: num(1), + type: BOOL, + loc, + }, + type: BOOL, loc, }, - cond: { ...innerCond, left: ref(column), right: again(innerCond.right) }, - update: { kind: "assign", localId: column, value: bin("+", ref(column), num(1), loc), loc }, - body: [ - ...elements, - { - kind: "assign", - localId: sum, - value: bin( - "+", - ref(sum), - termAt(() => ref(row)), - loc, - ), - loc, - }, - { - kind: "assign", - localId: next, - value: bin( - "+", - ref(next), - termAt(() => bin("+", ref(row), num(1), loc)), - loc, - ), - loc, - }, - ], + then: step(() => bin("-", ref(last), num(1), loc)), + else_: null, loc, }, { kind: "bytesSet", arr: { ...match.out }, index: ref(row), value: ref(sum), loc }, @@ -316,9 +374,12 @@ function pairRows( ], loc, }; - // Both bounds are int32, so `row < end - 1` is `row + 1 < end`, and the - // nest has at least two rows. The overlap tests run only then, so they never - // read an array the program assigns only when there are rows. Rows are + // Every bound is an integer in [0, SMALL_BOUND], so `row < end - 1` is + // `row + 1 < end` (and the same for the columns), and the masks in + // `bounds` leave the bounds unchanged while proving both counters' ranges. + // The nest has at least two rows. The column bounds' tests and the + // overlap tests run only then, so they never read an array the program + // assigns only when there are rows. Rows are // independent when the written array shares no byte with an array the // term reads; an array that is not there answers false. const int32 = (e: IrExpr): IrExpr => ({ @@ -329,9 +390,22 @@ function pairRows( type: BOOL, loc, }); + const compare = (op: "<=" | ">=", e: IrExpr, value: number): IrExpr => ({ + kind: "bin", + op, + left: again(e), + right: num(value), + type: BOOL, + loc, + }); + const small = (e: IrExpr): IrExpr[] => [ + int32(e), + compare(">=", e, 0), + compare("<=", e, SMALL_BOUND), + ]; const tests: IrExpr[] = [ - int32(match.start), - int32(match.end), + ...small(match.start), + ...small(match.end), { kind: "bin", op: "<", @@ -340,6 +414,8 @@ function pairRows( type: BOOL, loc, }, + ...small(columnStart), + ...small(innerCond.right), ...match.reads.map((read): IrExpr => ({ kind: "libCall", fn: "bytes.disjoint", @@ -394,7 +470,7 @@ function pairRows( }, loc, }, - { kind: "if", cond: { ...paired }, then: [pairs], else_: null, loc }, + { kind: "if", cond: { ...paired }, then: [...bounds, pairs], else_: null, loc }, rest, ], loc, @@ -413,7 +489,15 @@ function pairRows( * store into shares no byte with any array the term reads, so no row reads * another row's result. An odd row count runs its last row as written. * Otherwise the nest runs as written. Two independent sums take turns in - * the inner loop, so neither waits on the other's addition. */ + * the inner loop, so neither waits on the other's addition. + * + * The inner loop takes two columns per step, each row adding the first + * column's term and then the second's, and the last column on its own + * when the count is odd, so the additions keep their order. Both column + * bounds must also be int32 (so `column < to - 1` is `column + 1 < to`). + * Neighboring rows and columns share counter sums (`row + 1 + column` and + * `row + (column + 1)`), and the step's divisions have no dependence + * between them. */ export function pairRowSums(mod: IrModule): IrModule { let changed = false; const functions = mod.functions.map((fn): IrFunction => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8355-row-pair-columns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8355-row-pair-columns.ts.json new file mode 100644 index 000000000..900de6fb0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8355-row-pair-columns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8355-row-pair-columns.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8356-shared-view-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8356-shared-view-loop-versions.ts.json new file mode 100644 index 000000000..76cd99065 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8356-shared-view-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8356-shared-view-loop-versions.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8355-row-pair-columns.ts b/tests/corpus/8355-row-pair-columns.ts new file mode 100644 index 000000000..96ba26153 --- /dev/null +++ b/tests/corpus/8355-row-pair-columns.ts @@ -0,0 +1,73 @@ +// Row sums whose inner loop runs two columns per step: even, odd, zero, one +// and two columns, column ranges that start above zero or at or past their +// end, fractional, negative, NaN and very large column bounds, a column +// bound read from an array's length, terms that read two elements per +// column, and row bounds at and past 2^25. Every sum adds its columns in +// order, so each has the bits of the loop as written. +function rows(u: Float64Array, v: Float64Array, start: number, end: number, from: number, to: number): void { + for (let i = start; i < end; i++) { + let t = 0; + for (let j = from; j < to; j++) t += u[j]! / ((((i + j) * (i + j + 1)) >>> 1) + i + 1); + v[i] = t; + } +} + +function pairs(u: Float64Array, w: Float32Array, v: Float64Array, end: number): void { + for (let i = 0; i < end; i++) { + let t = -0.25; + for (let j = 0; j < u.length; j++) t += u[j]! * w[j + 1]! - (i + 1) / (j + 3); + v[i] = t; + } +} + +const show = (label: string, v: Float64Array): void => { + const parts: string[] = []; + for (const x of v) parts.push(Object.is(x, -0) ? "-0" : x.toPrecision(17)); + console.log(label, parts.join(" ")); +}; + +const n = 9; +const u = new Float64Array(n).map((_, k) => 1 / (k + 1) + k); +for (const [from, to] of [ + [0, n], + [0, n - 1], + [0, 0], + [0, 1], + [0, 2], + [3, n], + [4, n], + [5, 5], + [7, 2], + [0.5, 6], + [0, 6.5], + [-2, 5], + [-0, 4], + [NaN, 4], + [0, NaN], + [0, n + 3], + [2 ** 25, 2 ** 25 + 3], +]) { + const v = new Float64Array(6); + rows(u, v, 0, 6, from!, to!); + show(`columns ${from} ${to}`, v); +} +for (const [start, end] of [ + [2 ** 25 - 3, 2 ** 25 + 1], + [2 ** 25, 2 ** 25 + 2], + [1, 6], +]) { + const v = new Float64Array(8); + rows(u, v, start!, end!, 0, n); + show(`rows ${start} ${end}`, v); +} + +const w = new Float32Array(n + 1).map((_, k) => (k % 4) - 1.5); +for (const end of [0, 1, 4, 5]) { + const v = new Float64Array(5); + pairs(u, w, v, end); + show(`two reads ${end}`, v); +} +const short = new Float64Array(4).map((_, k) => k * 2.5); +const out = new Float64Array(5); +pairs(short, w, out, 5); +show("two reads short", out); diff --git a/tests/corpus/8356-shared-view-loop-versions.ts b/tests/corpus/8356-shared-view-loop-versions.ts new file mode 100644 index 000000000..28f4307fb --- /dev/null +++ b/tests/corpus/8356-shared-view-loop-versions.ts @@ -0,0 +1,93 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +// Loops over typed-array views of shared memory in a worker program: row +// sums and element-wise passes whose accesses are in bounds, the same loops +// running past a view's end, a loop that mixes a shared view with one the +// thread owns, and views of one SharedArrayBuffer that overlap, written in +// an order that shows whether each read sees the previous write. The worker +// writes its results into shared memory, which the main thread reads after +// the worker reports. + +interface Job { + sab: SharedArrayBuffer; + n: number; +} + +function rowSums(u: Float64Array, v: Float64Array, start: number, end: number, n: number): void { + for (let i = start; i < end; i++) { + let t = 0; + for (let j = 0; j < n; j++) t = t + u[j]! / (((i + j) * (i + j + 1)) / 2 + i + 1); + v[i] = t; + } +} + +function scaled(src: Float64Array, dst: Float64Array, k: number, count: number): void { + for (let i = 0; i < count; i++) dst[i] = src[i]! * k + 1; +} + +function mixed(shared: Int32Array, owned: Int32Array, count: number): number { + let sum = 0; + for (let i = 0; i < count; i++) { + owned[i] = shared[i]! * 3; + shared[i] = owned[i]! - i; + sum = (sum + shared[i]! + owned[i]!) | 0; + } + return sum; +} + +function shift(a: Float64Array, b: Float64Array, count: number): void { + for (let i = 0; i < count; i++) a[i + 1] = b[i]! + 1; +} + +function checksum(values: Float64Array): string { + let h = 0; + for (let i = 0; i < values.length; i++) h = (Math.imul(h, 31) + Math.round(values[i]! * 1e6)) >>> 0; + return h.toString(16); +} + +function work({ sab, n }: Job): string[] { + const out: string[] = []; + const u = new Float64Array(sab, 0, n); + const v = new Float64Array(sab, n * 8, n); + for (let i = 0; i < n; i++) u[i] = 1 + (i % 5) / 4; + rowSums(u, v, 0, n, n); + out.push(`rows ${checksum(v)}`); + // Past the end of u: the reads beyond it are undefined, so the sums are NaN. + rowSums(u, v, 0, 4, n + 3); + out.push(`past ${v[0]} ${v[3]} ${checksum(v.subarray(4))}`); + scaled(u, v, 0.5, n); + out.push(`scaled ${checksum(v)}`); + scaled(u, v, 2, n + 2); + out.push(`scaled past ${checksum(v)}`); + const ints = new Int32Array(sab, 2 * n * 8, 32); + for (let i = 0; i < 32; i++) ints[i] = i * 7 - 50; + const own = new Int32Array(32); + out.push(`mixed ${mixed(ints, own, 32)} ${mixed(ints, own, 34)} ${ints[5]} ${own[31]}`); + // a[i + 1] lands on b[i + 1] when b starts one element after a. + const a = new Float64Array(sab, 0, 24); + const b = new Float64Array(sab, 8, 24); + for (let i = 0; i < 25; i++) u[i] = i; + shift(a, b, 20); + out.push(`overlap ${Array.from(a.subarray(0, 8)).join(",")}`); + shift(b, a, 20); + out.push(`overlap back ${Array.from(b.subarray(0, 8)).join(",")}`); + return out; +} + +if (isMainThread) { + const n = 40; + const sab = new SharedArrayBuffer(2 * n * 8 + 32 * 4); + const worker = new Worker(new URL(import.meta.url), { workerData: { sab, n } satisfies Job }); + worker.on("message", (lines: string[]) => { + for (const line of lines) console.log("worker", line); + const v = new Float64Array(sab, n * 8, n); + const ints = new Int32Array(sab, 2 * n * 8, 32); + console.log("main sees", checksum(v), ints[0], ints[31]); + // The same passes on the main thread, over a fresh buffer. + const again = new SharedArrayBuffer(2 * n * 8 + 32 * 4); + for (const line of work({ sab: again, n })) console.log("main", line); + }); + worker.on("exit", (code: number) => console.log("exit", code)); +} else { + parentPort!.postMessage(work(workerData as Job)); +} From 7d458303a632d9b3842e52cea373c3325cff3b19 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 14:32:00 -0500 Subject: [PATCH 252/273] Check type patch files again on a cached configuration, and supply .jsx entries - The project configuration records the local files a type patch names, with whether each existed. A configuration read again from an unchanged package.json is reused only while every answer still holds, so creating or deleting a local declaration file between builds in one process validates again. - A types patch supplies the declaration TypeScript probes for a .jsx JavaScript entry (x.d.ts for x.jsx), as it does for .js, .mjs and .cjs. - Remove the two tests/fixtures/effect4/node_modules links the previous commit added; that directory is ignored and pnpm creates them. --- .../src/frontend/type-patches/layer.ts | 2 ++ packages/compiler/src/project-config.test.ts | 25 +++++++++++++++++ packages/compiler/src/project-config.ts | 16 +++++++++-- packages/compiler/test/type-patches.test.ts | 28 ++++++++++++++++++- .../node_modules/@effect/platform-node | 1 - tests/fixtures/effect4/node_modules/effect | 1 - 6 files changed, 68 insertions(+), 5 deletions(-) delete mode 120000 tests/fixtures/effect4/node_modules/@effect/platform-node delete mode 120000 tests/fixtures/effect4/node_modules/effect diff --git a/packages/compiler/src/frontend/type-patches/layer.ts b/packages/compiler/src/frontend/type-patches/layer.ts index 5fb41f968..9c156b68c 100644 --- a/packages/compiler/src/frontend/type-patches/layer.ts +++ b/packages/compiler/src/frontend/type-patches/layer.ts @@ -242,6 +242,8 @@ function declarationEntries(root: string, local: string): Map { const rel = target.replace(/^\.\//, ""); if (/\.d\.[cm]?ts$/.test(rel)) out.set(rel, local); else if (/\.[cm]?js$/.test(rel)) out.set(rel.replace(/\.([cm]?)js$/, ".d.$1ts"), local); + // TypeScript probes x.tsx and then x.d.ts for x.jsx. + else if (rel.endsWith(".jsx")) out.set(`${rel.slice(0, -".jsx".length)}.d.ts`, local); else if (!/\.\w+$/.test(rel)) out.set(`${rel}.d.ts`, local); }; add(pkg["types"]); diff --git a/packages/compiler/src/project-config.test.ts b/packages/compiler/src/project-config.test.ts index 51d9520a0..f12a8ea89 100644 --- a/packages/compiler/src/project-config.test.ts +++ b/packages/compiler/src/project-config.test.ts @@ -281,3 +281,28 @@ test("typePatches: files and types name local declaration files", () => { rmSync(root, { recursive: true, force: true }); } }); + +test("readProjectConfig checks local type patch files again for an unchanged package.json", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-project-config-")); + try { + writeFileSync( + join(root, "package.json"), + JSON.stringify({ scriptc: { typePatches: { "lib@^1.0.0": { types: "./lib.d.ts" } } } }), + ); + const entry = join(root, "main.ts"); + const missing = readProjectConfig(entry); + expect(missing.diagnostics.map((d) => d.message)).toEqual([ + `package.json 'scriptc.typePatches.lib@^1.0.0.types' names "./lib.d.ts", which does not exist`, + ]); + expect(readProjectConfig(entry)).toBe(missing); + writeFileSync(join(root, "lib.d.ts"), "export {};\n"); + const created = readProjectConfig(entry); + expect(created.diagnostics).toEqual([]); + expect(created.typePatches[0]!.types).toBe(join(root, "lib.d.ts")); + expect(readProjectConfig(entry)).toBe(created); + rmSync(join(root, "lib.d.ts")); + expect(readProjectConfig(entry).diagnostics.map((d) => d.code)).toEqual(["SC8004"]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/src/project-config.ts b/packages/compiler/src/project-config.ts index 4aad9cbc3..b87b67838 100644 --- a/packages/compiler/src/project-config.ts +++ b/packages/compiler/src/project-config.ts @@ -101,6 +101,10 @@ export interface ProjectConfig { wasm: string[]; /** `scriptc.typePatches`, in manifest order. */ typePatches: TypePatchEntry[]; + /** The local files the configuration checked for existence, with the + * answer. A configuration read again from an unchanged package.json is + * reused only while every answer still holds. */ + localFiles: { path: string; exists: boolean }[]; /** SC8004 for every unknown key or value. */ diagnostics: ScrDiagnostic[]; } @@ -153,6 +157,7 @@ export function emptyProjectConfig(): ProjectConfig { nodeIslands: defaultNodeIslands(), wasm: [], typePatches: [], + localFiles: [], diagnostics: [], }; } @@ -435,7 +440,9 @@ function readTypePatches( ); return null; } - if (!trackedFileExists(abs)) { + const exists = trackedFileExists(abs); + config.localFiles.push({ path: abs, exists }); + if (!exists) { problem( path, `names "${v}", which does not exist`, @@ -546,7 +553,12 @@ export function readProjectConfig(entryPath: string): ProjectConfig { const manifest = join(dir, "package.json"); const text = trackedReadFile(manifest); if (text !== null) { - if (cachedConfig !== null && cachedManifest === manifest && cachedText === text) + if ( + cachedConfig !== null && + cachedManifest === manifest && + cachedText === text && + cachedConfig.localFiles.every((file) => trackedFileExists(file.path) === file.exists) + ) return cachedConfig; // A manifest without a `scriptc` key (none spelled out, none behind a // \u escape) has the empty configuration; builds of such projects diff --git a/packages/compiler/test/type-patches.test.ts b/packages/compiler/test/type-patches.test.ts index 116715db5..ddcd5b88d 100644 --- a/packages/compiler/test/type-patches.test.ts +++ b/packages/compiler/test/type-patches.test.ts @@ -2,7 +2,7 @@ * packages: commander's HelpContext.helpWidth @typedef property and ms's * @return tags, patched from the fixtures' package.json; installed-version * selection; and the warnings for patches that do not apply as written. */ -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, test } from "vitest"; @@ -153,6 +153,32 @@ test("types gives a package without declarations an entry beside its main", () = expect(typePatchSuppliesTypes(pkg)).toBe(false); }); +test("types gives a package whose main is a .jsx file an entry beside it", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-type-patches-")); + try { + const pkg = join(root, "node_modules/viewjs"); + mkdirSync(join(pkg, "lib"), { recursive: true }); + writeFileSync( + join(pkg, "package.json"), + JSON.stringify({ name: "viewjs", version: "1.0.0", main: "lib/view.jsx" }), + ); + writeFileSync(join(pkg, "lib/view.jsx"), "export const view = 1;\n"); + writeFileSync(join(root, "viewjs.d.ts"), "export declare const view: number;\n"); + writeFileSync( + join(root, "package.json"), + JSON.stringify({ scriptc: { typePatches: { "viewjs@^1.0.0": { types: "./viewjs.d.ts" } } } }), + ); + configureTypePatches(join(root, "main.ts"), parse); + expect(typePatchFileExists(join(pkg, "lib/view.d.ts"))).toBe(true); + expect(typePatchSuppliesTypes(pkg)).toBe(true); + expect(typePatchedText(join(pkg, "lib/view.d.ts"))).toBe( + "export declare const view: number;\n", + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test("a files target the installed package does not ship warns", () => { const root = mkdtempSync(join(tmpdir(), "scriptc-type-patches-")); try { diff --git a/tests/fixtures/effect4/node_modules/@effect/platform-node b/tests/fixtures/effect4/node_modules/@effect/platform-node deleted file mode 120000 index aaa396705..000000000 --- a/tests/fixtures/effect4/node_modules/@effect/platform-node +++ /dev/null @@ -1 +0,0 @@ -../../../../../node_modules/.pnpm/@effect+platform-node@4.0.0_effect@4.0.0_redis@6.3.0/node_modules/@effect/platform-node \ No newline at end of file diff --git a/tests/fixtures/effect4/node_modules/effect b/tests/fixtures/effect4/node_modules/effect deleted file mode 120000 index 1b933cdef..000000000 --- a/tests/fixtures/effect4/node_modules/effect +++ /dev/null @@ -1 +0,0 @@ -../../../../node_modules/.pnpm/effect@4.0.0/node_modules/effect \ No newline at end of file From 6d34f4f0a5c23fe2001d8c454af7de5217e52104 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 12:41:21 -0700 Subject: [PATCH 253/273] Leave withUnitArm unchanged for pending placeholders The lowerer's optional-union cache relies on withUnitArm answering for a placeholder before it completes and recomputing after the revision moves (lowerer.test.ts). Only the optional-parameter site needs to keep the placeholder, so the refusal goes back out. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/frontend/type-mapper.test.ts | 14 -------------- packages/compiler/src/frontend/type-mapper.ts | 3 --- 2 files changed, 17 deletions(-) diff --git a/packages/compiler/src/frontend/type-mapper.test.ts b/packages/compiler/src/frontend/type-mapper.test.ts index 6e7e339d6..163ba7b9e 100644 --- a/packages/compiler/src/frontend/type-mapper.test.ts +++ b/packages/compiler/src/frontend/type-mapper.test.ts @@ -53,20 +53,6 @@ describe("union arm lookup", () => { }); }); -describe("recursive union placeholders", () => { - test("refuse a unit arm until their arms are known", () => { - const unions = new UnionRegistry(); - const recursive = {} as Type; - const union = unions.recursiveRef(recursive, () => "Recursive"); - const placeholder: IrType = { kind: "union", unionId: union }; - expect(unions.isPendingPlaceholder(union)).toBe(true); - expect(withUndefinedArm(placeholder, unions)).toBeNull(); - unions.finalizeRecursive(recursive, [{ kind: "record", shapeId: "r1" }, UNDEFINED_T]); - expect(unions.isPendingPlaceholder(union)).toBe(false); - expect(withUndefinedArm(placeholder, unions)).toEqual(placeholder); - }); -}); - describe("nullable collection union builders", () => { test.each([mapOf(STRING, F64), setOf(STRING)])( "optional %j fields and generator results share the same union", diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 63eed378d..d819d30a2 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -6799,9 +6799,6 @@ export function withUnitArm( if (t.kind === "union") { const def = unions.get(t.unionId); if (!def) return null; - // A recursive placeholder's arms arrive when its outer frame finishes; - // extending its (still empty) arm list would drop every arm but the unit. - if (unions.isPendingPlaceholder(t.unionId)) return null; if (def.arms.some((a) => a.kind === kind)) return t; const arms = [...def.arms, unit]; // Date and Promise arms beside other data arms have no union home. From 7222c3e900320b99c906b61f0a2987ed7e008d13 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 14:44:05 -0500 Subject: [PATCH 254/273] Skip module-pass work that changes nothing in the functions it walks The LLVM emitter's module passes walk every function, and on large programs most of them find nothing to change in most functions. Each pass now avoids work whose result it does not use, with the same output: - scalarizeLiteralResults built, for every function, a set of all locals and all module globals before looking for a producer call. It now collects the names a fresh binding must avoid when it makes the first one. - specializeCallbackCalls walked every body five times: rebindings, node counts and definitions, call keys, untouched bodies, and callers. One walk now records each body's node count, rebindings, written values and helper calls in walk order; the later steps read those records and walk only the bodies the rewrite changed and the copies it made. - holdCharLocalsAsUnits skips its two object walks in functions without a candidate local, and its mention count where no candidate is declared from a charAt or an optional read (the only ones it can hold). - resolveSelfCaptures only asks CaptureFlow whether bindings are unchanged, so its flow collects the facts of the bodies those questions reach instead of scanning every body. - copyStableCaptures reads the function references of the bodies it kept from its CaptureFlow facts instead of walking them again, and makes the set of local ids a copy must avoid only when it makes a copy. - versionLoopBoundParameters counts call sites only when a function has loop-bound parameters. - pairRowSums and inlineExpressionFunctions look for a row sum or an inlinable call before rebuilding a body. --- .../src/ir/callback-specialization.ts | 168 ++++++++++++------ packages/compiler/src/ir/capture-values.ts | 32 +++- packages/compiler/src/ir/char-unit-locals.ts | 10 +- .../compiler/src/ir/expression-inlining.ts | 19 +- .../compiler/src/ir/parameter-versions.ts | 9 +- packages/compiler/src/ir/row-pairs.ts | 18 +- packages/compiler/src/ir/scalar-records.ts | 12 +- packages/compiler/src/ir/self-captures.ts | 3 +- 8 files changed, 195 insertions(+), 76 deletions(-) diff --git a/packages/compiler/src/ir/callback-specialization.ts b/packages/compiler/src/ir/callback-specialization.ts index f4421f34b..ba579061c 100644 --- a/packages/compiler/src/ir/callback-specialization.ts +++ b/packages/compiler/src/ir/callback-specialization.ts @@ -33,6 +33,22 @@ const REBINDING_KINDS: readonly IrNodeKind[] = [ "closure", ]; +/** The rebindings one function's body makes: locals it writes after + * declaring them, and the closures it creates over captured bindings. */ +interface FunctionRebindings { + ids: Set; + sites: { owner: string; fnName: string; captures: string[] }[]; +} + +/** Records `node`'s rebinding into `into` (owner `fnName`). */ +function noteRebinding(fnName: string, node: IrExpr | IrStmt, into: FunctionRebindings): void { + if (node.kind === "assign" || node.kind === "forOf") into.ids.add(node.localId); + else if (node.kind === "tryCatch" && node.catchLocalId !== null) into.ids.add(node.catchLocalId); + else if (node.kind === "assignExpr" || node.kind === "incDec") into.ids.add(node.localId); + else if (node.kind === "closure" && node.captures.length) + into.sites.push({ owner: fnName, fnName: node.fnName, captures: node.captures }); +} + function rebindings( mod: { readonly functions: readonly IrFunction[] }, nodes?: IrNodeIndex, @@ -41,21 +57,31 @@ function rebindings( /** Bindings of each function that a closure it creates writes. */ closureWritten: Map>; } { + const scan = nodes?.scan(REBINDING_KINDS); + const each = mod.functions.map((fn) => { + const own: FunctionRebindings = { ids: new Set(), sites: [] }; + eachNodeOfKinds(fn, scan, (node) => noteRebinding(fn.name, node, own)); + return own; + }); + return closeRebindings(mod, each); +} + +/** The module's rebindings from each function's own (`each`, in module + * order): + * a binding a closure writes through its environment is written by the + * function that created the closure, transitively. */ +function closeRebindings( + mod: { readonly functions: readonly IrFunction[] }, + each: readonly FunctionRebindings[], +): { written: Map>; closureWritten: Map> } { const written = new Map>(); const closureWritten = new Map>(); const sites: { owner: string; fnName: string; captures: string[] }[] = []; - const scan = nodes?.scan(REBINDING_KINDS); - for (const fn of mod.functions) { - const ids = new Set(); - eachNodeOfKinds(fn, scan, (node) => { - if (node.kind === "assign" || node.kind === "forOf") ids.add(node.localId); - else if (node.kind === "tryCatch" && node.catchLocalId !== null) ids.add(node.catchLocalId); - else if (node.kind === "assignExpr" || node.kind === "incDec") ids.add(node.localId); - else if (node.kind === "closure" && node.captures.length) - sites.push({ owner: fn.name, fnName: node.fnName, captures: node.captures }); - }); - written.set(fn.name, ids); - } + mod.functions.forEach((fn, index) => { + const own = each[index]!; + written.set(fn.name, own.ids); + for (const site of own.sites) sites.push(site); + }); const functions = new Map(mod.functions.map((fn) => [fn.name, fn])); for (let changed = true; changed;) { changed = false; @@ -130,9 +156,10 @@ export function specializeCallbackCalls( symbolName: (name: string) => string = (name) => name, ): IrModule { const functions = new Map(mod.functions.map((fn) => [fn.name, fn])); - const { written, closureWritten } = rebindings(mod); const copies = new Map(); - const candidates = new Map(); + // Helpers that may take callbacks: their callback parameters are final + // once the walk below finds which of them the body rebinds. + const helpers = new Map(); for (const fn of mod.functions) { if ( !(fn.ownsPrototype || fn.name.startsWith("%arr.")) || @@ -142,19 +169,13 @@ export function specializeCallbackCalls( fn.captures !== undefined || fn.classCaptures?.length || fn.fastRead !== undefined || - fn.speculativeDispatch + fn.speculativeDispatch || + !fn.params.some((param) => param.type.kind === "func") ) continue; - const nodes = countNodes(fn); - if (nodes > MAX_FUNCTION_NODES) continue; - const rebound = written.get(fn.name)!; - const slots: number[] = []; - fn.params.forEach((param, index) => { - if (param.type.kind === "func" && !rebound.has(param.localId)) slots.push(index); - }); - if (slots.length) candidates.set(fn.name, { fn, slots, nodes }); + helpers.set(fn.name, fn); } - if (!candidates.size) return mod; + if (!helpers.size) return mod; // Function values, exports, entry points and methods: an original // reachable this way stays even when every direct call moves to a copy. @@ -162,16 +183,17 @@ export function specializeCallbackCalls( for (const cls of mod.classes ?? []) for (const method of ["constructor", ...(cls.methods ?? [])]) referenced.add(`%${cls.name}.${method}`); - // Single definitions of function-typed locals and globals. - const definitions = new Map>(); const globalIds = new Set(mod.globals?.map((global) => global.id)); const globalWrites = new Map(); - // The sum of countNodes over every function, counted by the walk below. - let totalNodes = 0; - for (const fn of mod.functions) { + // One walk of every body records what the steps below read: its node + // count (countNodes), its rebindings, the values written into each of its + // bindings, and its direct calls of helpers, in walk order. + const each = mod.functions.map((fn) => { let nodes = 0; + const own: FunctionRebindings = { ids: new Set(), sites: [] }; const values = new Map(); const counts = new Map(); + const calls: (IrExpr & { kind: "call" })[] = []; const record = (id: string, value: IrExpr | null, weight: number): void => { counts.set(id, (counts.get(id) ?? 0) + weight); if (value) { @@ -183,6 +205,7 @@ export function specializeCallbackCalls( everyStmtList(fn.body, { stmt: (stmt) => { nodes++; + noteRebinding(fn.name, stmt, own); if (stmt.kind === "varDecl") record(stmt.localId, stmt.init, stmt.init ? 1 : 0); if (stmt.kind === "assign") { if (globalIds.has(stmt.localId)) { @@ -199,15 +222,42 @@ export function specializeCallbackCalls( }, expr: (expr) => { nodes++; + noteRebinding(fn.name, expr, own); if (expr.kind === "assignExpr" || expr.kind === "incDec") { record(expr.localId, null, 2); if (globalIds.has(expr.localId)) globalWrites.set(expr.localId, []); } + if (expr.kind === "call" && helpers.has(expr.callee)) calls.push(expr); const name = exprFunctionReference(expr); if (name !== undefined) referenced.add(name); return true; }, }); + return { fn, nodes: Math.min(nodes, MAX_FUNCTION_NODES + 1), own, values, counts, calls }; + }); + const { written, closureWritten } = closeRebindings( + mod, + each.map((entry) => entry.own), + ); + const nodesOf = new Map(each.map((entry) => [entry.fn.name, entry.nodes])); + const candidates = new Map(); + for (const fn of helpers.values()) { + const nodes = nodesOf.get(fn.name)!; + if (nodes > MAX_FUNCTION_NODES) continue; + const rebound = written.get(fn.name)!; + const slots: number[] = []; + fn.params.forEach((param, index) => { + if (param.type.kind === "func" && !rebound.has(param.localId)) slots.push(index); + }); + if (slots.length) candidates.set(fn.name, { fn, slots, nodes }); + } + if (!candidates.size) return mod; + + // Single definitions of function-typed locals and globals. + const definitions = new Map>(); + // The sum of countNodes over every function. + let totalNodes = 0; + for (const { fn, nodes, values, counts } of each) { const single = new Map(); const locals = new Set(fn.locals.map((local) => local.id)); const shared = closureWritten.get(fn.name); @@ -221,7 +271,7 @@ export function specializeCallbackCalls( ) single.set(id, list[0]!); definitions.set(fn.name, single); - totalNodes += Math.min(nodes, MAX_FUNCTION_NODES + 1); + totalNodes += nodes; } const globalDefinitions = new Map(); for (const global of mod.globals ?? []) @@ -288,23 +338,18 @@ export function specializeCallbackCalls( // unknown callbacks or function-value entries. const keysOf = new Map>(); const unknown = new Set(); - for (const fn of mod.functions) - everyStmtList(fn.body, { - stmt: () => true, - expr: (expr) => { - if (expr.kind !== "call") return true; - const candidate = candidates.get(expr.callee); - if (!candidate) return true; - const key = callKey(fn, new Map(), expr, candidate); - if (key === "") unknown.add(expr.callee); - else if (key !== null) { - const keys = keysOf.get(expr.callee) ?? new Set(); - keys.add(key); - keysOf.set(expr.callee, keys); - } - return true; - }, - }); + for (const { fn, calls } of each) + for (const call of calls) { + const candidate = candidates.get(call.callee); + if (!candidate) continue; + const key = callKey(fn, new Map(), call, candidate); + if (key === "") unknown.add(call.callee); + else if (key !== null) { + const keys = keysOf.get(call.callee) ?? new Set(); + keys.add(key); + keysOf.set(call.callee, keys); + } + } const specialized = new Set(); for (const [name, keys] of keysOf) if (keys.size > 1 || unknown.has(name) || referenced.has(name)) specialized.add(name); @@ -419,12 +464,9 @@ export function specializeCallbackCalls( return owner.body.map(stmt); } - const functionsOut = mod.functions.map((fn) => { + const functionsOut = each.map(({ fn, calls }) => { // Most bodies call no specialized original; keep their nodes. - const untouched = everyStmtList(fn.body, { - stmt: () => true, - expr: (expr) => expr.kind !== "call" || !specialized.has(expr.callee), - }); + const untouched = calls.every((call) => !specialized.has(call.callee)); return untouched ? fn : { ...fn, body: rewriteBody(fn, new Map(), new Set(), false) }; }); for (let i = 0; i < pending.length; i++) { @@ -437,19 +479,29 @@ export function specializeCallbackCalls( // Drop originals no remaining code reaches: every direct call moved to a // copy and nothing names them as a value. const callers = new Map>(); - for (const fn of functionsOut) + const call = (callee: string, caller: string): void => { + if (!specialized.has(callee)) return; + const set = callers.get(callee) ?? new Set(); + set.add(caller); + callers.set(callee, set); + }; + functionsOut.forEach((fn, index) => { + // A body the rewrite kept has the calls the walk recorded, and its + // closures are already in `referenced`. + const kept = each[index]; + if (kept !== undefined && kept.fn === fn) { + for (const site of kept.calls) call(site.callee, fn.name); + return; + } everyStmtList(fn.body, { stmt: () => true, expr: (expr) => { - if (expr.kind === "call" && specialized.has(expr.callee)) { - const set = callers.get(expr.callee) ?? new Set(); - set.add(fn.name); - callers.set(expr.callee, set); - } + if (expr.kind === "call") call(expr.callee, fn.name); if (expr.kind === "closure" && specialized.has(expr.fnName)) referenced.add(expr.fnName); return true; }, }); + }); const live = new Set(); const work: string[] = []; for (const fn of functionsOut) diff --git a/packages/compiler/src/ir/capture-values.ts b/packages/compiler/src/ir/capture-values.ts index bcc587f1b..19ec41363 100644 --- a/packages/compiler/src/ir/capture-values.ts +++ b/packages/compiler/src/ir/capture-values.ts @@ -86,7 +86,12 @@ export function copyStableCaptures(mod: IrModule): IrModule { // A closure body whose every use became a direct call is only reachable // through its copy now. const referenced = new Set(moduleFunctionReferences(mod)); - for (const fn of functions) + functions.forEach((fn, index) => { + // A body the rewrite kept names what the flow recorded of it. + if (fn === mod.functions[index]) { + for (const name of flow.references(fn)) referenced.add(name); + return; + } everyStmtList(fn.body, { stmt: () => true, expr: (e) => { @@ -95,6 +100,7 @@ export function copyStableCaptures(mod: IrModule): IrModule { return true; }, }); + }); const clones = flow.directClones(); return { ...mod, @@ -251,6 +257,9 @@ interface FunctionFacts { captureIndex: Map; classCaptures: Set; params: Set; + /** Functions the body names: direct call callees and function values + * (exprFunctionReference). */ + references: Set; } interface CreationSite { @@ -273,17 +282,26 @@ export class CaptureFlow { /** Environment-free bodies called directly, and their copies' names. */ private readonly clones = new Map(); - constructor(functions: readonly IrFunction[]) { + /** With `scan` false, each body's facts are collected when a question + * first needs them, and only unchanged() may be asked: the other + * questions read creation sites, which come from scanning every body. */ + constructor(functions: readonly IrFunction[], scan = true) { for (const fn of functions) this.functions.set(fn.name, fn); this.receivers = new AmbientReceiverReaders(this.functions); // Creation sites are collected while scanning each body. - for (const fn of functions) this.factsOf(fn); + if (scan) for (const fn of functions) this.factsOf(fn); } directClones(): ReadonlyMap { return this.clones; } + /** The functions the body of `fn`, one of the functions this flow was + * made from, names (FunctionFacts.references). */ + references(fn: IrFunction): ReadonlySet { + return this.factsOf(fn).references; + } + /** A lifted body that captures nothing and can be entered like a declared * function: synchronous, never naming its own closure, and unable to * observe the ambient receiver a closure call would bracket. */ @@ -459,6 +477,7 @@ export class CaptureFlow { captureIndex: new Map(), classCaptures: new Set((fn.classCaptures ?? []).map((c) => c.localId)), params: new Set(fn.params.map((p) => p.localId)), + references: new Set(), }; (fn.captures ?? []).forEach((c, index) => { if (!facts.captureIndex.has(c.localId)) facts.captureIndex.set(c.localId, index); @@ -511,6 +530,8 @@ export class CaptureFlow { facts.writes.set(id, (facts.writes.get(id) ?? 0) + 1); }; const expr = (e: IrExpr): boolean => { + const referenced = e.kind === "call" ? e.callee : exprFunctionReference(e); + if (referenced !== undefined) facts.references.add(referenced); switch (e.kind) { case "varRef": facts.reads.set(e.localId, (facts.reads.get(e.localId) ?? 0) + 1); @@ -707,8 +728,11 @@ export class CaptureFlow { const splits = new Map(); const entry: IrStmt[] = []; const locals: IrLocal[] = []; - const used = new Set(fn.locals.map((l) => l.id)); + // The local ids a copy must avoid, collected for the first copy: most + // functions make none. + let used: Set | null = null; const fresh = (local: IrLocal): IrLocal => { + used ??= new Set(fn.locals.map((l) => l.id)); let k = 0; while (used.has(`${local.name}.${k}`)) k++; const id = `${local.name}.${k}`; diff --git a/packages/compiler/src/ir/char-unit-locals.ts b/packages/compiler/src/ir/char-unit-locals.ts index aff952909..fc1d5873b 100644 --- a/packages/compiler/src/ir/char-unit-locals.ts +++ b/packages/compiler/src/ir/char-unit-locals.ts @@ -300,6 +300,8 @@ function rewriteFunction(fn: IrFunction): IrFunction { // or as copies of another such local (a switch's discriminant temp). const reads = new Map(); const copyOf = new Map(); + // Whether a candidate string is declared from a charAt. + let charAtDeclared = false; const collect = (value: unknown): void => { if (Array.isArray(value)) for (const item of value) collect(item); else if (value !== null && typeof value === "object") { @@ -307,6 +309,7 @@ function rewriteFunction(fn: IrFunction): IrFunction { if (node.kind === "varDecl" && candidates.has(node.localId as string) && node.init) { const id = node.localId as string; const init = node.init as IrExpr; + if (locals.get(id)?.type.kind === "string" && isCharAt(init)) charAtDeclared = true; const read = optionalCharRead(init); if (read) reads.set(id, read); else if (init.kind === "varRef" && reads.has(init.localId)) { @@ -317,7 +320,8 @@ function rewriteFunction(fn: IrFunction): IrFunction { for (const key in node) if (key !== "loc") collect(node[key]); } }; - collect(fn.body); + // Both walks only record candidates: a function without one holds none. + if (candidates.size > 0) collect(fn.body); const localTags = (operand: IrExpr): OptionalCharRead | null => operand.kind === "varRef" ? (reads.get(operand.localId) ?? null) : null; @@ -344,7 +348,9 @@ function rewriteFunction(fn: IrFunction): IrFunction { for (const key in node) if (key !== "loc") count(node[key]); } }; - count(fn.body); + // Only candidates declared from a charAt or an optional read can be held + // (understoodMention), and count() only decides which are. + if (reads.size > 0 || charAtDeclared) count(fn.body); // A copy is held only with its source, and the source's mention in the // copy's initializer is understood only while the copy is held. const held = new Set(); diff --git a/packages/compiler/src/ir/expression-inlining.ts b/packages/compiler/src/ir/expression-inlining.ts index 377cfaf3f..1282cdb79 100644 --- a/packages/compiler/src/ir/expression-inlining.ts +++ b/packages/compiler/src/ir/expression-inlining.ts @@ -128,15 +128,24 @@ export function inlineExpressionFunctions(mod: IrModule): IrModule { const functions = mod.functions.map((fn) => { const locals = new Map(fn.locals.map((local) => [local.id, local])); const inLoops = callsInLoops(fn.body); - if (inLoops.size === 0) return fn; + // Each call the rewrite inlines: of an expression function, in a loop, + // with arguments the copy may repeat. + const inlinable = (e: IrExpr & { kind: "call" }): boolean => { + if (e.callee === fn.name) return false; + const target = expressions.get(e.callee); + return ( + target !== undefined && + target.fn.params.length === e.args.length && + e.args.every((arg) => duplicableArgument(arg, locals)) + ); + }; + if (![...inLoops].some((e) => e.kind === "call" && inlinable(e))) return fn; let inlined = false; const body = transformStmtList(fn.body, { stmt: (s) => s, expr: (e) => { - if (e.kind !== "call" || e.callee === fn.name || !inLoops.has(e)) return e; - const target = expressions.get(e.callee); - if (target === undefined || target.fn.params.length !== e.args.length) return e; - if (!e.args.every((arg) => duplicableArgument(arg, locals))) return e; + if (e.kind !== "call" || !inLoops.has(e) || !inlinable(e)) return e; + const target = expressions.get(e.callee)!; const args = new Map(target.fn.params.map((param, i) => [param.localId, e.args[i]!])); inlined = true; return copy(target.value, (ref) => { diff --git a/packages/compiler/src/ir/parameter-versions.ts b/packages/compiler/src/ir/parameter-versions.ts index 88232251c..5156d4bff 100644 --- a/packages/compiler/src/ir/parameter-versions.ts +++ b/packages/compiler/src/ir/parameter-versions.ts @@ -184,7 +184,8 @@ export function versionLoopBoundParameters( symbolName: (name: string) => string = (name) => name, ): IrModule { const candidates: { fn: IrFunction; params: Set }[] = []; - const sites = callSites(mod); + // Call sites are read only for functions with loop-bound parameters. + let sites: ReturnType | null = null; 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) { @@ -207,7 +208,11 @@ export function versionLoopBoundParameters( 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)) + if ( + params.size === 0 || + size(fn) > MAX_FUNCTION_NODES || + keepsSingleBody(fn, (sites ??= callSites(mod)), functions) + ) continue; candidates.push({ fn, params }); } diff --git a/packages/compiler/src/ir/row-pairs.ts b/packages/compiler/src/ir/row-pairs.ts index 2e189f42e..d5342d113 100644 --- a/packages/compiler/src/ir/row-pairs.ts +++ b/packages/compiler/src/ir/row-pairs.ts @@ -7,7 +7,13 @@ import { type IrModule, type IrStmt, } from "./ir.js"; -import { everyExpr, everyExprChild, mapExprChildren, transformStmtList } from "./traverse.js"; +import { + everyExpr, + everyExprChild, + everyStmtList, + mapExprChildren, + transformStmtList, +} from "./traverse.js"; /** Inner-loop expressions above this many nodes keep the loop as written. */ const MAX_TERM_NODES = 64; @@ -419,6 +425,16 @@ export function pairRowSums(mod: IrModule): IrModule { const functions = mod.functions.map((fn): IrFunction => { if (fn.async || fn.generator) return fn; const locals = new Map(fn.locals.map((local) => [local.id, local])); + // Most bodies hold no row sum: find one before copying the body. + let found = false; + everyStmtList(fn.body, { + expr: () => !found, + stmt: (s) => { + if (s.kind === "for" && matchRowSum(s, locals) !== null) found = true; + return !found; + }, + }); + if (!found) return fn; const added: IrLocal[] = []; const ids = new Set(fn.locals.map((local) => local.id)); const fresh = (name: string): string => { diff --git a/packages/compiler/src/ir/scalar-records.ts b/packages/compiler/src/ir/scalar-records.ts index 5d57cb3ec..088fd8461 100644 --- a/packages/compiler/src/ir/scalar-records.ts +++ b/packages/compiler/src/ir/scalar-records.ts @@ -554,17 +554,23 @@ export function scalarizeLiteralResults(mod: IrModule): IrModule { } if (producers.size === 0) return mod; let changed = false; + const globalIds = new Set((mod.globals ?? []).map((g) => g.id)); const functions = mod.functions.map((fn): IrFunction => { if (fn.async || fn.generator) return fn; const locals = new Map(fn.locals.map((l) => [l.id, l])); - const used = new Set([...locals.keys(), ...(mod.globals ?? []).map((g) => g.id)]); - collectLabels(fn.body, used); + // The names a fresh binding must avoid, collected when the first one is + // made: most functions inline no producer. + let used: Set | null = null; let next = 0; const fresh = (): string => { + if (used === null) { + used = new Set(locals.keys()); + collectLabels(fn.body, used); + } let id: string; do { id = `%literal.${next++}`; - } while (used.has(id)); + } while (used.has(id) || globalIds.has(id)); used.add(id); return id; }; diff --git a/packages/compiler/src/ir/self-captures.ts b/packages/compiler/src/ir/self-captures.ts index 74f634d4a..70242cb8b 100644 --- a/packages/compiler/src/ir/self-captures.ts +++ b/packages/compiler/src/ir/self-captures.ts @@ -107,7 +107,8 @@ export function resolveSelfCaptures(mod: IrModule): IrModule { if (creator.captures?.some((c) => c.localId === source)) return false; if (creator.classCaptures?.some((c) => c.localId === source)) return false; if (declarations.get(owner)!.get(source) !== 1) return false; - flow ??= new CaptureFlow(mod.functions); + // Only unchanged() is asked: the flow reads just the bodies it needs. + flow ??= new CaptureFlow(mod.functions, false); return flow.unchanged(owner, source, definition.writes); }); if (qualifies) { From 6b51aa883f99337b45a7e854f0c27eef1f6b2f65 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 14:46:01 -0500 Subject: [PATCH 255/273] Keep -0 row and column starts out of the paired row-sum nest The paired nest reads its bounds masked to [0, 2^25 - 1], which turns a -0 start into +0, and a term can tell the two apart (1 / i on the first row). The bounds test now requires 1 / b > 0, which -0 fails, so such a nest runs as written. Corpus 8355 adds row and column starts of -0 with terms that see the sign. --- packages/compiler/src/ir/row-pairs.ts | 13 ++++++++----- tests/corpus/8355-row-pair-columns.ts | 25 +++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/ir/row-pairs.ts b/packages/compiler/src/ir/row-pairs.ts index e6b3889a5..d54f37b0b 100644 --- a/packages/compiler/src/ir/row-pairs.ts +++ b/packages/compiler/src/ir/row-pairs.ts @@ -203,7 +203,7 @@ function copy( } const bin = ( - op: "+" | "-" | "&" | "|", + op: "+" | "-" | "/" | "&" | "|", left: IrExpr, right: IrExpr, loc: IrExpr["loc"], @@ -374,9 +374,10 @@ function pairRows( ], loc, }; - // Every bound is an integer in [0, SMALL_BOUND], so `row < end - 1` is - // `row + 1 < end` (and the same for the columns), and the masks in - // `bounds` leave the bounds unchanged while proving both counters' ranges. + // Every bound is an integer in [0, SMALL_BOUND] and not -0, so `row < + // end - 1` is `row + 1 < end` (and the same for the columns), and the + // masks in `bounds` leave the bounds unchanged while proving both + // counters' ranges. // The nest has at least two rows. The column bounds' tests and the // overlap tests run only then, so they never read an array the program // assigns only when there are rows. Rows are @@ -398,9 +399,11 @@ function pairRows( type: BOOL, loc, }); + // `1 / e > 0` holds for +0 and every positive bound, but not for -0: + // masking would turn a -0 start into +0, which a term can tell apart. const small = (e: IrExpr): IrExpr[] => [ int32(e), - compare(">=", e, 0), + { kind: "bin", op: ">", left: bin("/", num(1), again(e), loc), right: num(0), type: BOOL, loc }, compare("<=", e, SMALL_BOUND), ]; const tests: IrExpr[] = [ diff --git a/tests/corpus/8355-row-pair-columns.ts b/tests/corpus/8355-row-pair-columns.ts index 96ba26153..dccd9ca6d 100644 --- a/tests/corpus/8355-row-pair-columns.ts +++ b/tests/corpus/8355-row-pair-columns.ts @@ -2,8 +2,9 @@ // and two columns, column ranges that start above zero or at or past their // end, fractional, negative, NaN and very large column bounds, a column // bound read from an array's length, terms that read two elements per -// column, and row bounds at and past 2^25. Every sum adds its columns in -// order, so each has the bits of the loop as written. +// column, row bounds at and past 2^25, and row and column starts of -0 +// with terms that tell -0 from +0. Every sum adds its columns in order, +// so each has the bits of the loop as written. function rows(u: Float64Array, v: Float64Array, start: number, end: number, from: number, to: number): void { for (let i = start; i < end; i++) { let t = 0; @@ -20,6 +21,16 @@ function pairs(u: Float64Array, w: Float32Array, v: Float64Array, end: number): } } +// 1 / (i * 4 - j) is -Infinity at a -0 row and +0 column, and +// 1 / (j - i * 4) at a -0 column and +0 row: the first pass sees the sign. +function signs(v: Float64Array, start: number, end: number, from: number, to: number): void { + for (let i = start; i < end; i++) { + let t = 0; + for (let j = from; j < to; j++) t += 1 / (i * 4 - j) + 1 / (j - i * 4) + 1 / (i * 4 + j + 1); + v[i] = t; + } +} + const show = (label: string, v: Float64Array): void => { const parts: string[] = []; for (const x of v) parts.push(Object.is(x, -0) ? "-0" : x.toPrecision(17)); @@ -71,3 +82,13 @@ const short = new Float64Array(4).map((_, k) => k * 2.5); const out = new Float64Array(5); pairs(short, w, out, 5); show("two reads short", out); +for (const [start, from] of [ + [0, 0], + [-0, 0], + [0, -0], + [-0, -0], +]) { + const v = new Float64Array(4); + signs(v, start!, 4, from!, 5); + show(`signs ${Object.is(start, -0) ? "-0" : start} ${Object.is(from, -0) ? "-0" : from}`, v); +} From f791c1d0061b946c4f1d2f24ca9673ca50e6efba Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 14:47:24 -0500 Subject: [PATCH 256/273] Address review feedback --- .../src/frontend/lowering/record-inherited.ts | 144 +++++++++++++++--- .../8362-record-inherited-order.ts.json | 6 + tests/corpus/8362-record-inherited-order.ts | 40 +++++ .../record-inherited/member-signature.ts | 12 ++ .../8362-record-inherited-order.ts.json | 1 + tests/harness/record-inherited-member.test.ts | 32 ++++ 6 files changed, 211 insertions(+), 24 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8362-record-inherited-order.ts.json create mode 100644 tests/corpus/8362-record-inherited-order.ts create mode 100644 tests/fixtures/record-inherited/member-signature.ts create mode 100644 tests/harness/corpus-costs/8362-record-inherited-order.ts.json create mode 100644 tests/harness/record-inherited-member.test.ts diff --git a/packages/compiler/src/frontend/lowering/record-inherited.ts b/packages/compiler/src/frontend/lowering/record-inherited.ts index 2c01ccd0a..65d070f1d 100644 --- a/packages/compiler/src/frontend/lowering/record-inherited.ts +++ b/packages/compiler/src/frontend/lowering/record-inherited.ts @@ -104,14 +104,19 @@ export function withInheritedMethods( if (form === null || !shape || shape.tuple) return read; const loc = read.loc; const stmts: IrStmt[] = []; - const stable = (value: IrExpr, name: string): IrExpr => { - if (isSafeToRepeat(value) || value.kind === "strLit") return value; + const repeatable = (value: IrExpr): boolean => isSafeToRepeat(value) || value.kind === "strLit"; + const stable = (value: IrExpr, name: string, keep: boolean): IrExpr => { + if (keep) return value; const local = lowerer.declareHiddenLocal(name, value.type); stmts.push({ kind: "varDecl", localId: local.id, init: value, loc }); return varRef(local.id, value.type, loc); }; - const obj = stable(read.obj, "%inheritedRecv"); - const key = stable(read.key, "%inheritedKey"); + // The receiver is evaluated before the key: when computing the key runs + // code (it may reassign the receiver's variable), the receiver is held + // first, even when reading it again would be safe on its own. + const keyRepeatable = repeatable(read.key); + const obj = stable(read.obj, "%inheritedRecv", repeatable(read.obj) && keyRepeatable); + const key = stable(read.key, "%inheritedKey", keyRepeatable); const helper = inheritedReadHelper(lowerer, read.shapeId, read.type, form, bound, loc); const fallback: IrExpr = { kind: "call", callee: helper, args: [obj, key], type: read.type, loc }; if ( @@ -481,19 +486,52 @@ function builtinAnswer( // No object is created with a typed record as its prototype. return { kind: "boolLit", value: false, type: BOOL, loc }; default: { - const key = propertyKeyOf(lowerer, args[0], loc); - return { + // hasOwnProperty and propertyIsEnumerable: ToPropertyKey keeps a + // symbol a symbol, and a record's own keys are strings, so a symbol + // key is no entry of it. + const no: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; + const arg = args[0]; + if (arg?.type.kind === "symbol") return sequence(arg, no, loc); + const has = (key: IrExpr): IrExpr => ({ kind: "call", callee: recordHasOwnHelper(lowerer, shapeId, loc), args: [o, key], type: BOOL, loc, + }); + if (arg === undefined || !mayBeSymbol(arg.type)) return has(propertyKeyOf(lowerer, arg, loc)); + const id = declare("%protoKey", DYN); + const held = varRef(id, DYN, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: id, init: lowerer.coerceToExpected(arg, DYN), loc }], + result: { + kind: "ternary", + cond: { kind: "dynTest", test: "symbol", value: held, type: BOOL, loc } as IrExpr, + then: no, + else_: has(propertyKeyOf(lowerer, held, loc)), + type: BOOL, + loc, + }, + type: BOOL, + loc, }; } } } -/** ToPropertyKey of a built-in's key argument (absent: "undefined"). */ +/** Whether a value of `type` may be a symbol at run time. */ +function mayBeSymbol(type: IrType): boolean { + return !( + isUnitType(type) || + type.kind === "string" || + type.kind === "f64" || + type.kind === "bool" + ); +} + +/** ToPropertyKey of a built-in's key argument (absent: "undefined"), for + * keys that are not symbols. */ function propertyKeyOf(lowerer: Lowerer, arg: IrExpr | undefined, loc: SrcLoc): IrExpr { if (arg === undefined || isUnitType(arg.type)) return { @@ -552,6 +590,7 @@ function callOwnOrBuiltin( want: IrType, calleeText: string, loc: SrcLoc, + resolved?: { has: IrExpr; own: IrExpr }, ): IrExpr { const shape = lowerer.shapes.get(shapeId)!; const field = shape.fields.find((f) => f.name === method); @@ -569,7 +608,47 @@ function callOwnOrBuiltin( }; const builtinAsWant = convertTo(lowerer, builtin, want) ?? builtin; if (ownType === undefined || !callableType(lowerer, ownType)) return builtinAsWant; - const own: IrExpr = field + const own = resolved?.own ?? ownEntryRead(lowerer, o, shapeId, method, loc); + const called = callOwn(lowerer, declare, own, args, builtinAsWant.type, calleeText, loc); + return { + kind: "ternary", + cond: resolved?.has ?? ownEntryTest(lowerer, o, shapeId, method, loc), + then: called, + else_: builtinAsWant, + type: builtinAsWant.type, + loc, + }; +} + +/** Whether the record holds an own entry named `method`. */ +function ownEntryTest( + lowerer: Lowerer, + o: IrExpr, + shapeId: string, + method: string, + loc: SrcLoc, +): IrExpr { + return { + kind: "call", + callee: recordHasOwnHelper(lowerer, shapeId, loc), + args: [o, { kind: "strLit", value: method, type: STRING, loc }], + type: BOOL, + loc, + }; +} + +/** The record's own entry named `method`: its field, or its overflow entry + * (undefined when absent). */ +function ownEntryRead( + lowerer: Lowerer, + o: IrExpr, + shapeId: string, + method: string, + loc: SrcLoc, +): IrExpr { + const shape = lowerer.shapes.get(shapeId)!; + const field = shape.fields.find((f) => f.name === method); + return field ? { kind: "recordGet", obj: o, shapeId, field: method, type: field.type, loc } : { kind: "recordKeyGet", @@ -580,21 +659,6 @@ function callOwnOrBuiltin( type: lowerer.withUndefinedArmOf(shape.indexValue!) ?? shape.indexValue!, loc, }; - const called = callOwn(lowerer, declare, own, args, builtinAsWant.type, calleeText, loc); - return { - kind: "ternary", - cond: { - kind: "call", - callee: recordHasOwnHelper(lowerer, shapeId, loc), - args: [o, { kind: "strLit", value: method, type: STRING, loc }], - type: BOOL, - loc, - }, - then: called, - else_: builtinAsWant, - type: builtinAsWant.type, - loc, - }; } /** Call the own entry `own` (present, of any stored type) with `args`. A @@ -727,6 +791,25 @@ export function lowerRecordInheritedCall( const recvLocal = lowerer.declareHiddenLocal("%protoRecv", receiver.type); stmts.push({ kind: "varDecl", localId: recvLocal.id, init: receiver, loc }); const o = varRef(recvLocal.id, receiver.type, loc); + // `o.m(...)` reads the member before its arguments run: an argument that + // adds, deletes or replaces the own method does not change the callee. + const field = shape.fields.find((f) => f.name === method); + const ownType = field ? field.type : shape.indexValue; + let resolved: { has: IrExpr; own: IrExpr } | undefined; + if (ownType !== undefined && callableType(lowerer, ownType)) { + const shapeId = receiver.type.shapeId; + const hasLocal = lowerer.declareHiddenLocal("%protoHas", BOOL); + stmts.push({ + kind: "varDecl", + localId: hasLocal.id, + init: ownEntryTest(lowerer, o, shapeId, method, loc), + loc, + }); + const read = ownEntryRead(lowerer, o, shapeId, method, loc); + const ownLocal = lowerer.declareHiddenLocal("%protoOwn", read.type); + stmts.push({ kind: "varDecl", localId: ownLocal.id, init: read, loc }); + resolved = { has: varRef(hasLocal.id, BOOL, loc), own: varRef(ownLocal.id, read.type, loc) }; + } const args: IrExpr[] = []; for (const node of call.arguments.nodes) { const arg = lowerer.lowerExpr(node); @@ -744,6 +827,7 @@ export function lowerRecordInheritedCall( want, ts.getTextOfNode(access), loc, + resolved, ); return { kind: "seqExpr", stmts, result, type: result.type, loc }; } @@ -751,7 +835,10 @@ export function lowerRecordInheritedCall( /** `r.toString` and the other Object.prototype methods read by name (not * called) on an index-signature record: an own entry of the member's * function type when the record holds one, else the built-in as a plain - * function value. Null when the checker's member type is not a function. */ + * function value. An own entry of another type than the member's (a + * function of another signature) throws the checked-conversion TypeError: + * the program's types claimed the member. Null when the checker's member + * type is not a function. */ export function lowerRecordInheritedMember( lowerer: Lowerer, receiver: IrExpr, @@ -810,6 +897,15 @@ export function lowerRecordInheritedMember( loc, }; } + } else if (shape.indexValue && callableType(lowerer, shape.indexValue)) { + result = { + kind: "ternary", + cond: ownEntryTest(lowerer, o, shapeId, method, loc), + then: nodeThrowExpr(1, "", OTHER_ARM_FAILURE_MESSAGE, want, loc), + else_: builtin, + type: want, + loc, + }; } return { kind: "seqExpr", stmts, result, type: want, loc }; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8362-record-inherited-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8362-record-inherited-order.ts.json new file mode 100644 index 000000000..e080ae4d1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8362-record-inherited-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8362-record-inherited-order.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8362-record-inherited-order.ts b/tests/corpus/8362-record-inherited-order.ts new file mode 100644 index 000000000..33a72eb91 --- /dev/null +++ b/tests/corpus/8362-record-inherited-order.ts @@ -0,0 +1,40 @@ +// Object.prototype's methods on typed records keep JavaScript's evaluation +// order and key conversions. + +// A function-table key that reassigns the receiver reads the table +// evaluated before the key. +let table: Record string> = { a: () => "first" }; +const other: Record string> = { a: () => "second" }; +function key(): string { + table = other; + return "a"; +} +console.log(String(table[key()]())); + +// hasOwnProperty and propertyIsEnumerable with a symbol key on a record of +// functions test the symbol key. +const fns: Record number> = { double: (n) => n * 2 }; +const sym = Symbol("s"); +console.log(fns.hasOwnProperty(sym as unknown as string), fns.propertyIsEnumerable(sym as unknown as string)); + +// A call by name reads the method before its arguments run: an argument +// that adds, replaces or deletes the own method does not change the callee. +type Has = Record boolean>; +function add(r: Has): string { + r.hasOwnProperty = () => true; + return "zzz"; +} +function swap(r: Has): string { + r.hasOwnProperty = (key) => key === "b"; + return "a"; +} +function drop(r: Has): string { + delete r["hasOwnProperty"]; + return "zzz"; +} +const added: Has = {}; +console.log(added.hasOwnProperty(add(added)), added.hasOwnProperty("zzz")); +const swapped: Has = { hasOwnProperty: (key) => key === "a" }; +console.log(swapped.hasOwnProperty(swap(swapped)), swapped.hasOwnProperty("a")); +const removed: Has = { hasOwnProperty: () => true }; +console.log(removed.hasOwnProperty(drop(removed)), removed.hasOwnProperty("zzz")); diff --git a/tests/fixtures/record-inherited/member-signature.ts b/tests/fixtures/record-inherited/member-signature.ts new file mode 100644 index 000000000..c9c65f8ad --- /dev/null +++ b/tests/fixtures/record-inherited/member-signature.ts @@ -0,0 +1,12 @@ +// A dot read of a prototype-named member whose own entry has another +// signature than the member's (tests/harness/record-inherited-member.test.ts). +const plain: Record number> = { double: (x) => x * 2 }; +const builtin = plain.toString; +console.log(typeof builtin); +const numeric: Record number> = { toString: (x: number) => x + 1 }; +try { + const own = numeric.toString; + console.log(typeof own); +} catch (e) { + console.log(e instanceof TypeError, (e as Error).message); +} diff --git a/tests/harness/corpus-costs/8362-record-inherited-order.ts.json b/tests/harness/corpus-costs/8362-record-inherited-order.ts.json new file mode 100644 index 000000000..8bcdf4a18 --- /dev/null +++ b/tests/harness/corpus-costs/8362-record-inherited-order.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 2000 } diff --git a/tests/harness/record-inherited-member.test.ts b/tests/harness/record-inherited-member.test.ts new file mode 100644 index 000000000..ed14524d2 --- /dev/null +++ b/tests/harness/record-inherited-member.test.ts @@ -0,0 +1,32 @@ +/* Object.prototype's methods on typed records (frontend/lowering/ + * record-inherited.ts): a dot read of a prototype-named member answers the + * built-in when the record has no own entry, and throws the checked + * conversion's TypeError when its own entry is a function of another + * signature than the member's, which the program's types claimed. Node reads + * the own function there, so the program cannot be a corpus case. */ +import { execFile } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; +import { OTHER_ARM_FAILURE_MESSAGE } from "../../packages/compiler/src/frontend/lowering/coercions/unions.js"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const entry = join(repoRoot, "tests/fixtures/record-inherited/member-signature.ts"); +const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/record-inherited-member"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +test( + "a prototype-named member read with an own function of another signature throws", + { timeout: 300_000 }, + async () => { + mkdirSync(outDir, { recursive: true }); + const result = await compile(entry, { outPath: join(outDir, "main"), outDir, sanitize }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const { stdout } = await execFileAsync(result.binaryPath, [], { encoding: "utf8" }); + expect(stdout).toBe(`function\ntrue ${OTHER_ARM_FAILURE_MESSAGE}\n`); + }, +); From c21fc0120180fb89bf6ff2251bba30d6739f3c7b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 14:49:55 -0500 Subject: [PATCH 257/273] Key bound alias instances by the declared alias The compiler compiles itself, and a Map keyed by another Map is outside the key domains it compiles (SC2009 in the self-hosted frontend). The Lowerer's table is now keyed by the declared alias reference: each entry lists the instances that instantiate it with their ts-level bindings maps, and the resolver picks the current instance's by identity. --- .../frontend/lowering/generic-functions.ts | 7 ++-- .../compiler/src/frontend/lowering/lowerer.ts | 32 ++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index a2eda839a..1769e8562 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -1041,11 +1041,12 @@ function unifySignatureBindings( iAliasArgs.length === dAliasArgs.length ) { if (tsBindings && declared !== inst) { - let pairs = lowerer.boundAliasInstances.get(tsBindings); - if (!pairs) lowerer.boundAliasInstances.set(tsBindings, (pairs = new Map())); + let entries = lowerer.boundAliasInstances.get(declared); + if (!entries) lowerer.boundAliasInstances.set(declared, (entries = [])); // A body forwarding its own alias (`inner(g)` returning `Catalog` inside an instance) resolves through the enclosing one. - if (!pairs.has(declared)) pairs.set(declared, lowerer.boundAliasTsResolver(inst) ?? inst); + if (!entries.some((entry) => entry.tsBindings === tsBindings)) + entries.push({ tsBindings, instance: lowerer.boundAliasTsResolver(inst) ?? inst }); } dAliasArgs.forEach((da, i) => { const ia = iAliasArgs[i]; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 216851c9c..c77741997 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2233,16 +2233,19 @@ export class Lowerer { * the body needs — indexed accesses (`T[K]` needs K's literal key) and * keyed record reads (`o[k]` where k's type is a literal-bound K). */ typeParamTsBindings: Map | null = null; - /** Generic alias references of a call-keyed instance's declared signature - * (`Catalog`), each paired with the same alias as the call's - * resolved signature instantiates it (`Catalog<{ g1: … }, "acme">`), - * keyed by the instance's ts-level bindings (typeParamTsBindings). The - * checker keeps the alias symbolic inside the generic body and offers no - * way to instantiate it with the bound arguments, but alias - * instantiations are interned: the body's `x as Catalog` is the - * very type the signature declares, so the pair recorded during binding - * unification is that re-instantiation. */ - readonly boundAliasInstances = new Map, Map>(); + /** Generic alias references of call-keyed instances' declared signatures + * (`Catalog`), each with the same alias as every call's resolved + * signature instantiates it (`Catalog<{ g1: … }, "acme">`), together with + * that instance's ts-level bindings map (typeParamTsBindings), which every + * scope that lowers the instance installs. The checker keeps the alias + * symbolic inside the generic body and offers no way to instantiate it + * with the bound arguments, but alias instantiations are interned: the + * body's `x as Catalog` is the very type the signature declares, so + * the pair recorded during unification is that re-instantiation. */ + readonly boundAliasInstances = new Map< + ts.Type, + { tsBindings: Map; instance: ts.Type }[] + >(); /** Non-null while an IMPLICIT-ANY instance body lowers (npm-static JS; * see generic-functions.ts): bound param symbol → * the call site's checker type, consulted by typeOf for identifier @@ -9563,10 +9566,15 @@ export class Lowerer { /** The concrete instantiation of a generic alias reference (`Catalog`) the current call-keyed instance's signature declares - * (boundAliasInstances). Inert outside generic function instantiation. */ + * (boundAliasInstances), or null when its signature never instantiated + * that reference. Inert outside generic function instantiation. */ readonly boundAliasTsResolver = (t: ts.Type): ts.Type | null => { if (!this.typeParamTsBindings || t.getAliasTypeArguments().length === 0) return null; - return this.boundAliasInstances.get(this.typeParamTsBindings)?.get(t) ?? null; + const tsBindings = this.typeParamTsBindings; + return ( + this.boundAliasInstances.get(t)?.find((entry) => entry.tsBindings === tsBindings)?.instance ?? + null + ); }; irTypeOf(node: ts.Node): IrType { From aead30fc2635a67d62764fbd1a61147cec16e592 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 15:10:32 -0500 Subject: [PATCH 258/273] Mark emitted functions sanitize_address only in sanitized builds The sanitized lane's -fsanitize=address link needs the attribute to instrument the emitted functions. Without a sanitizer pass it is not inert: LLVM does not shrink-wrap functions that carry it, so every call saved and restored all of a function's callee-saved registers on entry, including on paths that return before any call, and it blocks speculative loads. The LLVM helper already dropped it for builds that import runtime bitcode (speed); release builds kept it. - The new sanitizeAttributes option (default true for direct emission) is set from --sanitize wherever objectAudit is. - Without it and without a target CPU level, attribute group #0 holds a string attribute LLVM ignores, since a group cannot be empty. --- packages/cli/test/target-cpu.test.ts | 4 +++- .../compiler/src/backend/llvm/emit-module.ts | 20 ++++++++++--------- .../compiler/src/backend/llvm/emitter.test.ts | 13 ++++++++++++ packages/compiler/src/backend/llvm/emitter.ts | 9 +++++++++ packages/compiler/src/backend/llvm/shapes.ts | 5 +++-- packages/compiler/src/index.ts | 3 +++ packages/compiler/src/native/prepare.ts | 2 ++ .../compiler/test/generator-emission.test.ts | 2 +- 8 files changed, 45 insertions(+), 13 deletions(-) diff --git a/packages/cli/test/target-cpu.test.ts b/packages/cli/test/target-cpu.test.ts index ca5243564..5bc6717c3 100644 --- a/packages/cli/test/target-cpu.test.ts +++ b/packages/cli/test/target-cpu.test.ts @@ -50,9 +50,11 @@ test.runIf(process.platform === "linux" && process.arch === "x64")( const expected = `${(await execFileAsync(process.execPath, [entry])).stdout}`; const baseline = await build([]); expect(baseline).not.toContain('"target-cpu"'); + // Only --sanitize builds mark the functions for ASan instrumentation. + expect(baseline).not.toContain("sanitize_address"); expect(await build(["--target-cpu=baseline"])).toBe(baseline); const v3 = await build(["--target-cpu=x86-64-v3"]); - expect(v3).toMatch(/^attributes #0 = \{ sanitize_address "target-cpu"="x86-64-v3" \}$/m); + expect(v3).toMatch(/^attributes #0 = \{ "target-cpu"="x86-64-v3" \}$/m); if (runsV3) expect((await execFileAsync(outPath)).stdout).toBe(expected); // The bootstrap's cached route builds without a level. expect(await build([], true)).toBe(baseline); diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index af00ebc2c..8145429f1 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -196,15 +196,17 @@ function readRange(dir: string, range: number): { result: ChunkResult; texts: st /** The attribute groups of the emitted functions: #0 for every function, * #1 for coroutine bodies and #2 for out-of-line record clones. Each carries - * the build's `--target-cpu` level. */ + * the build's `--target-cpu` level, and `sanitize_address` in sanitized + * builds. A group needs one attribute: without either, #0 holds a string + * attribute that LLVM ignores. */ function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean): string[] { const cpu = targetCpuAttribute(emitter.targetCpu); + const asan = emitter.sanitizeAttributes ? " sanitize_address" : ""; + const base = `${asan}${cpu}` || ` "scriptc-function"`; return [ - `attributes #0 = { sanitize_address${cpu} }`, - ...(emitter.usesCoroutines - ? [`attributes #1 = { sanitize_address presplitcoroutine${cpu} }`] - : []), - ...(hasNoInlineRecordClone ? [`attributes #2 = { noinline sanitize_address${cpu} }`] : []), + `attributes #0 = {${base} }`, + ...(emitter.usesCoroutines ? [`attributes #1 = {${asan} presplitcoroutine${cpu} }`] : []), + ...(hasNoInlineRecordClone ? [`attributes #2 = { noinline${asan}${cpu} }`] : []), ]; } @@ -1179,9 +1181,9 @@ export function emitModuleParts(emitter: LlEmitter): string[] { ); for (const line of emitter.inlineRcTail(flushedDecls)) out.push(line); out.push( - // sanitize_address is inert under the plain pipeline; the sanitized - // lane's -fsanitize=address link activates instrumentation over the - // emitted functions too (the runtime TUs get theirs from clang). + // The sanitized lane's -fsanitize=address link instruments the + // functions marked sanitize_address (the runtime TUs get theirs from + // clang). ...functionAttributes(emitter, hasNoInlineRecordClone), ``, ); diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index 876476cc1..880685bf2 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -211,6 +211,19 @@ test("a target CPU level reaches every function attribute group and nothing else expect(emitLlvmModule(module, { targetCpu: "baseline" })).toBe(plain); }); +test("only sanitized builds mark the emitted functions sanitize_address", () => { + const module = moduleFor([call()]); + const sanitized = emitLlvmModule(module); + expect(emitLlvmModule(module, { sanitizeAttributes: true })).toBe(sanitized); + expect(sanitized).toMatch(/^attributes #0 = \{ sanitize_address \}$/m); + const plain = emitLlvmModule(module, { sanitizeAttributes: false }); + expect(plain).not.toContain("sanitize_address"); + // A group needs an attribute: an ignored string attribute stands in. + expect(plain).toMatch(/^attributes #0 = \{ "scriptc-function" \}$/m); + const v3 = emitLlvmModule(module, { sanitizeAttributes: false, targetCpu: "x86-64-v3" }); + expect(v3).toMatch(/^attributes #0 = \{ "target-cpu"="x86-64-v3" \}$/m); +}); + test("inline RC inlines every fast path at speed and keeps box releases as calls in release", () => { for (const boxed of [false, true]) { const module = moduleFor(Array.from({ length: 4 }, call), { boxed }); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 5569599ee..f1f1fa7d6 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -511,6 +511,12 @@ export interface LlvmTargetOptions { inlineRc?: InlineRcMode; /** Define the inline RC fast-path helpers `noinline` (sanitized builds). */ rcHelpersNoinline?: boolean; + /** Mark every emitted function `sanitize_address`, which the sanitized + * lane's `-fsanitize=address` link needs to instrument it. Without a + * sanitizer pass the attribute still turns off shrink-wrapping and + * speculative loads, so plain builds pass false. Defaults to true for + * direct emission. */ + sanitizeAttributes?: boolean; /** Define `scr_detached_exit` so the Linux runtime hands the address-space * teardown after `process.exit` to a helper process (`--detached-exit`). * Executables only; the runtime's weak default keeps it off. */ @@ -1269,6 +1275,8 @@ export class LlEmitter { readonly runtimeAbiMarker: boolean; readonly detachedExit: boolean; readonly targetCpu: TargetCpu | undefined; + /** Emitted functions carry `sanitize_address` (LlvmTargetOptions). */ + readonly sanitizeAttributes: boolean; readonly wasmBoundsChecks: boolean; /** Interned string literals: UTF-8 text → { symbol, byte length } — * first-use order, the runtime ABI’s determinism discipline. */ @@ -1971,6 +1979,7 @@ export class LlEmitter { this.runtimeAbiMarker = options.runtimeAbiMarker === true; this.detachedExit = options.detachedExit === true; this.targetCpu = options.targetCpu; + this.sanitizeAttributes = options.sanitizeAttributes !== false; this.wasmBoundsChecks = options.wasmBoundsChecks === true || wasmBoundsChecks({ diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 7498235b0..6d03db222 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -216,8 +216,9 @@ export interface RegionHooks { classDefinitions(cls: IrClassDef, sizeOf: string): string[]; } -/** Every emitted function/helper carries #0 = { sanitize_address } — see - * the emitter header. */ +/** Every emitted function/helper carries attribute group #0: the build's + * target CPU level, and `sanitize_address` in sanitized builds (see + * emit-module.ts functionAttributes). */ export const FN_ATTRS = "#0"; export { computeTraced } from "../cycle-analysis.js"; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index ffd56a1bf..11b0f6d13 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -854,6 +854,7 @@ async function prepareExecutableInput( // the pending flag, since their link may not add an unwinder. unwindAbi: buildPlatform === "linux" && outputKind !== "obj", objectAudit: opts.sanitize === true, + sanitizeAttributes: opts.sanitize === true, lowRegion: lowRegionTarget(buildPlatform, opts.sanitize === true), ...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}), ...inlineRcOption(opts.optimization, opts.sanitize === true), @@ -925,6 +926,7 @@ async function prepareExecutableInput( runtimeAbiMarker: useRuntimePack, unwindAbi: buildPlatform === "linux", objectAudit: opts.sanitize === true, + sanitizeAttributes: opts.sanitize === true, lowRegion: lowRegionTarget(buildPlatform, opts.sanitize === true), ...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}), ...inlineRcOption(opts.optimization, opts.sanitize === true), @@ -2357,6 +2359,7 @@ async function compileLibraryTracked( pointerBits: buildPlatform === "wasi" ? 32 : 64, wasi: buildPlatform === "wasi", objectAudit: opts.sanitize === true, + sanitizeAttributes: opts.sanitize === true, ...inlineRcOption(profile.optimization, opts.sanitize === true), }); timing("llvm-emit", { diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index 11b8f9dd1..749027b80 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -431,6 +431,7 @@ export function prepareNativeExecutable( (outputKind === "exe" || outputKind === "llvm" || outputKind === "asm") && toolchain.target.platform === "linux", objectAudit: options.sanitize === true, + sanitizeAttributes: options.sanitize === true, lowRegion: lowRegionTarget(toolchain.target.platform, options.sanitize === true), functionWorkers: emitWorkers(), forkPrimitives: emitForkPrimitives(), @@ -545,6 +546,7 @@ export function prepareNativeLibrary( pointerBits: toolchain.target.pointerBits, wasi: toolchain.target.platform === "wasi", objectAudit: options.sanitize === true, + sanitizeAttributes: options.sanitize === true, ...inlineRcOption(profile.optimization, options.sanitize === true), }); timing("llvm-emit"); diff --git a/packages/compiler/test/generator-emission.test.ts b/packages/compiler/test/generator-emission.test.ts index 8c43925ea..47e5f16c5 100644 --- a/packages/compiler/test/generator-emission.test.ts +++ b/packages/compiler/test/generator-emission.test.ts @@ -50,7 +50,7 @@ console.log(sum); expect(definition(llvm, "sc_f_range")).not.toContain("@scr_coro_yield_f64("); expect(llvm).not.toContain("@scr_gen_take_out_f64("); expect(llvm).toContain("%ScrGenHead = type { i64, ptr, ptr, i32, %ScrGenSlot }"); - expect(llvm).toContain("attributes #1 = { sanitize_address presplitcoroutine }"); + expect(llvm).toContain("attributes #1 = { presplitcoroutine }"); expect(definition(llvm, "sc_gs_range")).toContain("call ptr @scr_gen_new_stackless("); expect(llvm).not.toContain("@scr_gen_yield_f64("); }); From a4ae5f9e27f89e238ca24d6dd91273674a7ba092 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 15:17:20 -0500 Subject: [PATCH 259/273] Run the linker job directly for host Linux links with lld A Linux executable link runs `clang -fuse-ld=lld ...`, and the driver only starts ld.lld with a job it derives from its arguments, its environment and the installed toolchain. Starting the driver costs about 10 ms of a small program's 29 ms link. LinkerJobs (native/linker-job.ts) records that job in the native cache, with the link's own inputs and output as positions. A traced link (the Linux executable cache's `-v -Wl,-t` link) records it from the job line the driver prints with -v; a link the executable cache does not trace lists it with -v -### first. The entry also keeps the identities of the files and directories the job and the driver's -v lines name (ld.lld, startup objects, library directories, the GCC installation, the driver's directory): the set the Linux executable cache records from its traced link. It is keyed by the driver's identity, the link's arguments, the toolchain environment, PATH and the working directory. A later link with the same key whose recorded paths still have those identities runs ld.lld with the job directly, and the executable cache traces it with the job's -t output and the recorded identities. Every other link runs through the driver as before: cross-target links, links without lld, a driver that is not clang, SCRIPTC_LINKER, sanitizer links, a toolchain environment that names mutable inputs, a listing without exactly one ld.lld job naming each of the link's paths once, a recorded path whose identity changed, and a job that fails (the driver then reports the error). The linker gets the same arguments, so the executables are identical. --- packages/compiler/src/native/cache.ts | 1 + packages/compiler/src/native/compiler.ts | 8 +- .../src/native/executable-cache.test.ts | 42 +- .../compiler/src/native/executable-cache.ts | 28 +- .../compiler/src/native/linker-job.test.ts | 408 ++++++++++++++++++ packages/compiler/src/native/linker-job.ts | 286 ++++++++++++ 6 files changed, 760 insertions(+), 13 deletions(-) create mode 100644 packages/compiler/src/native/linker-job.test.ts create mode 100644 packages/compiler/src/native/linker-job.ts diff --git a/packages/compiler/src/native/cache.ts b/packages/compiler/src/native/cache.ts index e204b8a6b..0250e9618 100644 --- a/packages/compiler/src/native/cache.ts +++ b/packages/compiler/src/native/cache.ts @@ -168,6 +168,7 @@ export class NativeCache { "sanitizer", "executable", "kept", + "linker-job", "runtime-pack", "binary", "dsym", diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts index ff49e2ac9..0b56b0a53 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -70,6 +70,7 @@ import { prepareNativeExecutable, prepareNativeLibrary } from "./prepare.js"; import { buildSanitizedRuntime, sanitizerDriver, sanitizerFlags } from "./sanitizer.js"; import { lldLinkerArgs } from "../backend/lld.js"; import { openNativeExecutableCache } from "./executable-cache.js"; +import { LinkerJobs } from "./linker-job.js"; import { nativeFileIdentity } from "./file-identity.js"; /** The first partition keeps the whole-object key used before partitioning. */ @@ -530,11 +531,14 @@ export class NativeCompiler { "-o", stagedOutput, ]; - const cacheable = executableCache?.trace(linkArgs, stage) ?? false; + // Host Linux links with lld run the linker job the driver would. + const jobs = LinkerJobs.open(this.cache, toolchain, lld, options.sanitize ?? false); + const job = jobs?.link([...plan.inputs, stagedOutput], stage); + const cacheable = executableCache?.trace(linkArgs, stage, job) ?? false; timing("link-inputs"); // A successful dependency trace already produced this executable. // Keep its verified output instead of asking the linker to repeat it. - if (!cacheable) { + if (!cacheable && (job?.run(linkArgs, true) ?? null) === null) { const driver = options.sanitize ? sanitizerDriver(toolchain) : toolchain.linker; try { runNativeTool(driver, linkArgs); diff --git a/packages/compiler/src/native/executable-cache.test.ts b/packages/compiler/src/native/executable-cache.test.ts index 8bec9916e..4f950e5b5 100644 --- a/packages/compiler/src/native/executable-cache.test.ts +++ b/packages/compiler/src/native/executable-cache.test.ts @@ -15,7 +15,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, test, vi } from "vitest"; import { contentDigest, NativeCache } from "./cache.js"; -import { NativeExecutableCache } from "./executable-cache.js"; +import { identity, NativeExecutableCache } from "./executable-cache.js"; vi.mock("node:child_process", async (original) => ({ ...(await original()), @@ -331,3 +331,43 @@ test("a Linux build that keeps its output does not read the deferred inputs", () expect(open().restore(f.destination)).toBe(true); expect(deferred).toHaveBeenCalledTimes(3); }); + +test("a Linux link that runs as its linker job is traced from the job's run, and otherwise records it", () => { + const f = fixture(); + const lib = join(f.root, "lib"); + mkdirSync(lib); + const crt = join(lib, "Scrt1.o"); + writeFileSync(crt, "startup object"); + const program = join(f.stage, "program.o"); + writeFileSync(program, "program"); + const job = { + run: vi.fn((_args: string[], _list: boolean) => ({ + output: [crt, program].join("\n") + "\n", + driverInputs: [identity(crt), identity(lib)], + })), + record: vi.fn(), + }; + const open = () => new NativeExecutableCache(f.cache, f.key, "clang", false, [f.runtime], true); + const entry = open(); + expect(entry.trace(["-o", f.output], f.stage, job)).toBe(true); + // The job runs the traced link, and the driver does not. + expect(job.run.mock.calls[0]).toEqual([["-o", f.output, "-v", "-Wl,-t"], false]); + expect(spawnSync).not.toHaveBeenCalled(); + expect(job.record).not.toHaveBeenCalled(); + entry.recordOutput(f.output); + const recorded = ( + JSON.parse(f.cache.read("kept", f.key)!.toString()) as { inputs: { path: string }[] } + ).inputs.map((saved) => saved.path); + expect(recorded).toEqual(expect.arrayContaining([crt, lib])); + expect(recorded).not.toContain(program); + expect(open().keep(f.output)).toBe(true); + // A link without a job runs through the driver, and records the job from + // the driver's output. + const record = vi.fn(); + expect(open().trace(["-o", f.output], f.stage, { run: () => null, record })).toBe(true); + expect(spawnSync).toHaveBeenCalledTimes(1); + expect(record).toHaveBeenCalledWith(["-o", f.output, "-v", "-Wl,-t"], f.library + "\n\n", [ + identity(f.sdk), + identity(f.library), + ]); +}); diff --git a/packages/compiler/src/native/executable-cache.ts b/packages/compiler/src/native/executable-cache.ts index bafd2c020..a26bc1d87 100644 --- a/packages/compiler/src/native/executable-cache.ts +++ b/packages/compiler/src/native/executable-cache.ts @@ -14,8 +14,9 @@ import type { NativeOptimization } from "../backend/optimization.js"; import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js"; import type { NativeToolchain } from "./toolchain.js"; import { contentDigest, NativeCache } from "./cache.js"; +import type { LinkerJobLink } from "./linker-job.js"; -interface InputIdentity { +export interface InputIdentity { path: string; canonical: string; kind: "file" | "directory"; @@ -33,7 +34,7 @@ interface ExecutableEntry { symbols: string | null; } -function identity(path: string): InputIdentity { +export function identity(path: string): InputIdentity { path = resolve(path); const info = statSync(path); if (!info.isFile() && !info.isDirectory()) @@ -63,7 +64,7 @@ function sameIdentity(a: InputIdentity, b: InputIdentity): boolean { ); } -function stillMatches(inputs: InputIdentity[]): boolean { +export function stillMatches(inputs: InputIdentity[]): boolean { try { return inputs.every((input) => sameIdentity(identity(input.path), input)); } catch { @@ -71,7 +72,7 @@ function stillMatches(inputs: InputIdentity[]): boolean { } } -function commandPath(command: string): string { +export function commandPath(command: string): string { if (command.includes("/") || command.includes("\\")) return resolve(command); for (const directory of (process.env["PATH"] ?? "/usr/bin:/bin").split(delimiter)) { const candidate = join(directory || process.cwd(), command); @@ -92,7 +93,7 @@ function toolOutput(executable: string, args: string[]): string { return result.stdout + "\n" + result.stderr; } -function tracePaths(output: string, driver: boolean, roots: string[]): string[] { +export function tracePaths(output: string, driver: boolean, roots: string[]): string[] { const paths = new Set(); for (const line of output.split(/\r?\n/)) { for (const candidate of driver ? driverTraceCandidates(line) : linkTraceCandidate(line)) { @@ -134,7 +135,7 @@ interface KeptEntry { inputs: InputIdentity[]; } -function validIdentity(input: InputIdentity): boolean { +export function validIdentity(input: InputIdentity): boolean { return ( input !== null && typeof input === "object" && @@ -290,8 +291,10 @@ export class NativeExecutableCache { * Private build inputs already belong to the LLVM/runtime cache identity. * A true result means the requested output was linked successfully and its * inputs remained stable. The caller can publish that output directly. On - * false, the caller links the output itself. */ - trace(args: string[], stage: string): boolean { + * false, the caller links the output itself. On Linux, `job` runs the + * traced link as its recorded linker job when it can, and otherwise records + * the job the driver ran (see LinkerJobs). */ + trace(args: string[], stage: string, job?: LinkerJobLink): boolean { this.traced = false; try { this.addDeferredInputs(); @@ -300,8 +303,13 @@ export class NativeExecutableCache { ? [] : tracePaths(toolOutput(this.linker, [...args, "-###"]), true, roots).map(identity); // On Linux, -v prints the jobs that -### would, while the link runs. - const linked = toolOutput(this.linker, [...args, ...(this.linux ? ["-v"] : []), "-Wl,-t"]); - if (this.linux) driverInputs = tracePaths(linked, true, roots).map(identity); + const traced = [...args, ...(this.linux ? ["-v"] : []), "-Wl,-t"]; + const direct = this.linux ? (job?.run(traced, false) ?? null) : null; + const linked = direct?.output ?? toolOutput(this.linker, traced); + if (this.linux) { + driverInputs = direct?.driverInputs ?? tracePaths(linked, true, roots).map(identity); + if (direct === null) job?.record(traced, linked, driverInputs); + } const paths = tracePaths(linked, false, roots); if (paths.length === 0 || !stillMatches(this.inputs) || !stillMatches(driverInputs)) return false; diff --git a/packages/compiler/src/native/linker-job.test.ts b/packages/compiler/src/native/linker-job.test.ts new file mode 100644 index 000000000..b9d718c03 --- /dev/null +++ b/packages/compiler/src/native/linker-job.test.ts @@ -0,0 +1,408 @@ +import { spawnSync } from "node:child_process"; +import { + chmodSync, + mkdirSync, + mkdtempSync, + readFileSync, + renameSync, + rmSync, + statSync, + utimesSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, test } from "vitest"; +import { + LINUX_ARM64_GNU_TARGET, + LINUX_X64_GNU_TARGET, + MACOS_ARM64_TARGET, + type NativeTargetSpec, +} from "../backend/targets.js"; +import { NativeCache } from "./cache.js"; +import { LinkerJobs } from "./linker-job.js"; +import type { NativeToolchain } from "./toolchain.js"; + +const directories: string[] = []; +const saved = new Map(); +const variables = [ + "SCRIPTC_LINKER", + "COMPILER_PATH", + "LOG", + "LD", + "CRT", + "LIB", + "FAIL", + "BROKEN", + "DOUBLE", +]; +beforeEach(() => { + for (const name of variables) { + saved.set(name, process.env[name]); + delete process.env[name]; + } +}); +afterEach(() => { + for (const [name, value] of saved) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + saved.clear(); + for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true }); +}); + +function script(path: string, text: string): string { + writeFileSync(path, text); + chmodSync(path, 0o755); + return path; +} + +/** A driver named `name` and an ld.lld that log their arguments to $LOG. The + * driver's job runs $LD with `-pie`, the startup object $CRT, `-L$LIB` and + * its own arguments (without `-fuse-ld=lld` and `-v`, `-Wl,` removed). With + * -### it prints that job as clang does, every argument quoted; with -v it + * prints the job it runs as clang does, only the program quoted. With + * $BROKEN, -### prints the job twice; with $DOUBLE, it names the path $DOUBLE + * once more. The linker writes its inputs to its output, prints them with + * `-t`, and fails with $FAIL. */ +function fixture(name = "clang") { + const root = mkdtempSync(join(tmpdir(), "scriptc-linker-job-")); + directories.push(root); + const bin = join(root, "bin"); + const lib = join(root, "lib"); + // The recorded inputs include the parents of the toolchain's paths (here + // `root`); stages are elsewhere, as a build's are. + const stages = join(root, "stages"); + for (const directory of [bin, lib, stages]) mkdirSync(directory); + const log = join(root, "log"); + writeFileSync(log, ""); + const driver = script( + join(bin, name), + `#!/bin/sh +echo "driver $*" >> "$LOG" +job=" \\"$LD\\" \\"-pie\\" \\"$CRT\\" \\"-L$LIB\\"" +raw=" \\"$LD\\" -pie $CRT -L$LIB" +listing=0; verbose=0 +for a; do + case "$a" in + -'###') listing=1 ;; + -v) verbose=1 ;; + -fuse-ld=lld) ;; + -Wl,*) job="$job \\"\${a#-Wl,}\\""; raw="$raw \${a#-Wl,}" ;; + *) job="$job \\"$a\\""; raw="$raw $a" ;; + esac +done +if [ $listing = 1 ]; then + echo "InstalledDir: $(dirname "$0")" >&2 + [ -z "$DOUBLE" ] && echo "$job" >&2 + [ -n "$BROKEN" ] && echo "$job" >&2 + [ -n "$DOUBLE" ] && echo "$job \\"$DOUBLE\\"" >&2 && exit 0 + exit 0 +fi +[ $verbose = 1 ] && echo "$raw" >&2 +eval "exec $job" +`, + ); + const ld = script( + join(bin, "ld.lld"), + `#!/bin/sh +echo "ld.lld $*" >> "$LOG" +[ -n "$FAIL" ] && exit 1 +out=""; prev="" +for a; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done +printf '%s\\n' "$@" > "$out" +for a; do [ "$a" = "-t" ] && printf '%s\\n' "$@" | grep '^/'; done +exit 0 +`, + ); + const crt = join(lib, "Scrt1.o"); + writeFileSync(crt, "startup object"); + Object.assign(process.env, { LOG: log, LD: ld, CRT: crt, LIB: lib }); + // A directory's identity is its stat: an old time makes a later change visible. + utimesSync(lib, 0, 0); + const cache = new NativeCache(join(root, "cache")); + const toolchain = (target: NativeTargetSpec = LINUX_X64_GNU_TARGET, helper = target) => + ({ + target, + helper: helper.helper, + linker: driver, + }) as unknown as NativeToolchain; + let count = 0; + /** A stage with a program object, the link's paths and its driver arguments. */ + const stage = () => { + const directory = join(stages, `stage-${++count}`); + mkdirSync(directory); + const program = join(directory, "program.o"); + const output = join(directory, "out"); + writeFileSync(program, "program"); + return { + directory, + output, + paths: [program, output], + args: ["-fuse-ld=lld", "-pthread", program, "-lm", "-o", output], + }; + }; + const calls = () => + readFileSync(log, "utf8") + .split("\n") + .filter((line) => line !== ""); + return { root, bin, lib, crt, ld, driver, cache, toolchain, stage, calls }; +} + +test.skipIf(process.platform === "win32")( + "a link records the driver's job once, and later links run ld.lld with their own paths", + () => { + const f = fixture(); + const jobs = LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + expect(jobs).not.toBeNull(); + const first = f.stage(); + const run = jobs.run(first.args, first.paths, first.directory, true); + expect(run).not.toBeNull(); + // The driver only listed the job; ld.lld linked. + expect(f.calls()).toEqual([ + `driver ${first.args.join(" ")} -v -###`, + `ld.lld -pie ${f.crt} -L${f.lib} -pthread ${first.paths[0]} -lm -o ${first.output}`, + ]); + expect(readFileSync(first.output, "utf8")).toContain(first.paths[0]); + const recorded = run!.driverInputs.map((input) => input.path); + expect(recorded).toEqual(expect.arrayContaining([f.ld, f.bin, f.crt, f.lib])); + expect(recorded.some((path) => path.startsWith(first.directory))).toBe(false); + // Another build, with other paths, runs the recorded job without the driver. + const second = f.stage(); + const again = LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + expect(again.run(second.args, second.paths, second.directory, true)).not.toBeNull(); + expect(f.calls().slice(2)).toEqual([ + `ld.lld -pie ${f.crt} -L${f.lib} -pthread ${second.paths[0]} -lm -o ${second.output}`, + ]); + // Other arguments are another job. + const third = f.stage(); + expect( + again.run([...third.args, "-Wl,-t"], third.paths, third.directory, true)?.output, + ).toContain(third.paths[0]); + expect(f.calls().slice(3)[0]).toBe(`driver ${third.args.join(" ")} -Wl,-t -v -###`); + }, +); + +test.skipIf(process.platform === "win32")( + "a driver link records its job from its -v output, and later links run it", + () => { + const f = fixture(); + const open = () => LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + const first = f.stage(); + const traced = [...first.args, "-v", "-Wl,-t"]; + // Without an entry, a link that is not listed runs through the driver. + const link = open().link(first.paths, first.directory); + expect(link.run(traced, false)).toBeNull(); + expect(f.calls()).toEqual([]); + const linked = spawnSync(f.driver, traced, { encoding: "utf8" }); + expect(linked.status).toBe(0); + link.record(traced, linked.stdout + "\n" + linked.stderr); + const second = f.stage(); + const run = open() + .link(second.paths, second.directory) + .run([...second.args, "-v", "-Wl,-t"], false); + expect(run?.output).toContain(second.paths[0]); + expect(run!.driverInputs.map((input) => input.path)).toEqual( + expect.arrayContaining([f.ld, f.bin, f.crt, f.lib]), + ); + expect(f.calls().slice(2)).toEqual([ + `ld.lld -pie ${f.crt} -L${f.lib} -pthread ${second.paths[0]} -lm -o ${second.output} -t`, + ]); + }, +); + +test.skipIf(process.platform === "win32")( + "a changed startup object, library directory or linker sends the link to the driver", + () => { + for (const change of ["crt", "lib", "ld"] as const) { + const f = fixture(); + const open = () => LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + const first = f.stage(); + expect(open().run(first.args, first.paths, first.directory, true)).not.toBeNull(); + if (change === "lib") writeFileSync(join(f.lib, "libm.so"), "new library"); + else { + const path = change === "crt" ? f.crt : f.ld; + const before = statSync(path); + writeFileSync(path + ".new", readFileSync(path)); + chmodSync(path + ".new", 0o755); + utimesSync(path + ".new", before.atime, before.mtime); + renameSync(path + ".new", path); + } + const second = f.stage(); + const count = f.calls().length; + expect(open().run(second.args, second.paths, second.directory, true)).toBeNull(); + expect(f.calls().length).toBe(count); + } + }, +); + +test.skipIf(process.platform === "win32")("a changed driver records the job again", () => { + const f = fixture(); + const first = f.stage(); + expect( + LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!.run( + first.args, + first.paths, + first.directory, + true, + ), + ).not.toBeNull(); + writeFileSync(f.driver, readFileSync(f.driver, "utf8") + "\n"); + const second = f.stage(); + expect( + LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!.run( + second.args, + second.paths, + second.directory, + true, + ), + ).not.toBeNull(); + expect(f.calls()[2]).toBe(`driver ${second.args.join(" ")} -v -###`); +}); + +test.skipIf(process.platform === "win32")( + "a job that fails sends this and the build's later links to the driver", + () => { + const f = fixture(); + const first = f.stage(); + const open = () => LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + expect(open().run(first.args, first.paths, first.directory, true)).not.toBeNull(); + process.env["FAIL"] = "1"; + const jobs = open(); + const second = f.stage(); + expect(jobs.run(second.args, second.paths, second.directory, true)).toBeNull(); + const count = f.calls().length; + expect(jobs.run(second.args, second.paths, second.directory, true)).toBeNull(); + expect(f.calls().length).toBe(count); + }, +); + +test.skipIf(process.platform === "win32")( + "a listing without one ld.lld job naming each path once keeps the driver, and is not asked again", + () => { + for (const listing of ["two jobs", "another linker", "a path twice"] as const) { + const f = fixture(); + if (listing === "two jobs") process.env["BROKEN"] = "1"; + if (listing === "another linker") + process.env["LD"] = script(join(f.bin, "ld.bfd"), readFileSync(f.ld, "utf8")); + const open = () => LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + const first = f.stage(); + if (listing === "a path twice") process.env["DOUBLE"] = first.paths[0]; + expect(open().run(first.args, first.paths, first.directory, true)).toBeNull(); + expect(f.calls()).toEqual([`driver ${first.args.join(" ")} -v -###`]); + // The answer is stored: the next build does not list the job again. + expect(open().run(first.args, first.paths, first.directory, true)).toBeNull(); + expect(f.calls().length).toBe(1); + delete process.env["BROKEN"]; + delete process.env["DOUBLE"]; + } + }, +); + +test.skipIf(process.platform === "win32")( + "arguments that name the stage besides the link's paths keep the driver", + () => { + const f = fixture(); + const first = f.stage(); + const jobs = LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + const script = `-Wl,--version-script=${join(first.directory, "exports")}`; + expect(jobs.run([...first.args, script], first.paths, first.directory, true)).toBeNull(); + expect(f.calls()).toEqual([]); + }, +); + +test.skipIf(process.platform === "win32")( + "only host Linux links through a clang driver with lld run the job", + () => { + const f = fixture(); + const lld = ["-fuse-ld=lld"]; + expect(LinkerJobs.open(f.cache, f.toolchain(), lld, false)).not.toBeNull(); + // A cross-target link: the arm64 target with the x64 host's helper. + expect( + LinkerJobs.open( + f.cache, + f.toolchain(LINUX_ARM64_GNU_TARGET, LINUX_X64_GNU_TARGET), + lld, + false, + ), + ).toBeNull(); + expect( + LinkerJobs.open( + f.cache, + f.toolchain(LINUX_X64_GNU_TARGET, LINUX_ARM64_GNU_TARGET), + lld, + false, + ), + ).toBeNull(); + expect(LinkerJobs.open(f.cache, f.toolchain(MACOS_ARM64_TARGET), lld, false)).toBeNull(); + // The driver's default linker, a sanitizer link, no cache. + expect(LinkerJobs.open(f.cache, f.toolchain(), [], false)).toBeNull(); + expect(LinkerJobs.open(f.cache, f.toolchain(), lld, true)).toBeNull(); + expect(LinkerJobs.open(null, f.toolchain(), lld, false)).toBeNull(); + // A linker that is not a clang driver. + const gcc = fixture("gcc"); + expect(LinkerJobs.open(gcc.cache, gcc.toolchain(), lld, false)).toBeNull(); + // An explicit linker, or an environment that names mutable toolchain inputs. + process.env["SCRIPTC_LINKER"] = f.driver; + expect(LinkerJobs.open(f.cache, f.toolchain(), lld, false)).toBeNull(); + delete process.env["SCRIPTC_LINKER"]; + process.env["COMPILER_PATH"] = f.bin; + expect(LinkerJobs.open(f.cache, f.toolchain(), lld, false)).toBeNull(); + }, +); + +const hostTarget = + process.arch === "x64" + ? LINUX_X64_GNU_TARGET + : process.arch === "arm64" + ? LINUX_ARM64_GNU_TARGET + : null; +const hasLld = + process.platform === "linux" && + spawnSync("clang", ["-fuse-ld=lld", "-###", "-x", "c", "/dev/null"], { + encoding: "utf8", + }).stderr?.includes("ld.lld") === true; + +test.skipIf(hostTarget === null || !hasLld)("the job links the executable the driver links", () => { + const root = mkdtempSync(join(tmpdir(), "scriptc-linker-job-clang-")); + directories.push(root); + const stage = join(root, "stage"); + mkdirSync(stage); + const program = join(stage, "program.o"); + const compiled = spawnSync("clang", ["-c", "-x", "c", "-", "-o", program], { + input: '#include \nint main(void) { puts("linked"); return 0; }\n', + }); + expect(compiled.status).toBe(0); + const toolchain = { + target: hostTarget, + helper: hostTarget!.helper, + linker: "clang", + } as unknown as NativeToolchain; + const args = (output: string) => ["-fuse-ld=lld", "-pthread", program, "-lm", "-o", output]; + const viaDriver = join(stage, "driver"); + expect(spawnSync("clang", args(viaDriver)).status).toBe(0); + const cache = new NativeCache(join(root, "cache")); + // Recorded from the -v output of a traced link, and listed with -###. + const traced = (output: string) => [...args(output), "-v", "-Wl,-t"]; + const tracedOutput = join(stage, "traced"); + const linked = spawnSync("clang", traced(tracedOutput), { encoding: "utf8" }); + expect(linked.status).toBe(0); + LinkerJobs.open(cache, toolchain, ["-fuse-ld=lld"], false)! + .link([program, tracedOutput], stage) + .record(traced(tracedOutput), linked.stdout + "\n" + linked.stderr); + for (const [name, list] of [ + ["first", false], + ["second", true], + ["third", true], + ] as const) { + const output = join(stage, name); + const link = LinkerJobs.open(cache, toolchain, ["-fuse-ld=lld"], false)!.link( + [program, output], + stage, + ); + const run = list ? link.run(args(output), true) : link.run(traced(output), false); + expect(run).not.toBeNull(); + expect(readFileSync(output).equals(readFileSync(viaDriver))).toBe(true); + expect(spawnSync(output, { encoding: "utf8" }).stdout).toBe("linked\n"); + } +}); diff --git a/packages/compiler/src/native/linker-job.ts b/packages/compiler/src/native/linker-job.ts new file mode 100644 index 000000000..e1195e4c4 --- /dev/null +++ b/packages/compiler/src/native/linker-job.ts @@ -0,0 +1,286 @@ +import { spawnSync } from "node:child_process"; +import { realpathSync } from "node:fs"; +import { basename, extname, isAbsolute, resolve } from "node:path"; +import { + toolchainEnvironmentCachePolicy, + toolchainEnvironmentFingerprint, +} from "../backend/toolchain-environment.js"; +import type { NativeToolchain } from "./toolchain.js"; +import { contentDigest, type NativeCache } from "./cache.js"; +import { + commandPath, + identity, + type InputIdentity, + stillMatches, + tracePaths, + validIdentity, +} from "./executable-cache.js"; + +/** A recorded linker job. In `job`, a number is the index of one of the link's + * own paths (its inputs and output); `inputs` are the identities of the + * other files and directories the job and the driver's `-v` lines name. */ +interface LinkerJobEntry { + schema: "scriptc.linker-job.v1"; + /** null when the driver's job could not be recorded: the link runs through + * the driver. */ + job: (string | number)[] | null; + inputs: InputIdentity[]; +} + +/** A link that ran as its recorded job: the linker's output, and the + * identities the job depends on besides the link's own paths. */ +export interface LinkerJobRun { + output: string; + driverInputs: InputIdentity[]; +} + +/** One link's use of LinkerJobs, with its own paths and stage. */ +export interface LinkerJobLink { + run(args: string[], list: boolean): LinkerJobRun | null; + record(args: string[], output: string, driverInputs?: InputIdentity[]): void; +} + +const MAX_ENTRY_BYTES = 1024 * 1024; + +function validEntry(value: unknown, paths: number): value is LinkerJobEntry { + if (value === null || typeof value !== "object") return false; + const entry = value as Partial; + if (entry.schema !== "scriptc.linker-job.v1" || !Array.isArray(entry.inputs)) return false; + if (!entry.inputs.every(validIdentity)) return false; + if (entry.job === null) return true; + if (!Array.isArray(entry.job) || typeof entry.job[0] !== "string") return false; + const used = new Set(); + for (const token of entry.job) { + if (typeof token === "string") continue; + if (!Number.isInteger(token) || token < 0 || token >= paths || used.has(token)) return false; + used.add(token); + } + return used.size === paths; +} + +function underRoots(text: string, roots: string[]): boolean { + return roots.some((root) => text.includes(root)); +} + +/** The arguments of one job line as the driver prints them: with `-###` + * each argument is quoted, with `-v` only the program and the arguments that + * contain a space, `"`, `\` or `$`; a quoted argument escapes `"`, `\` and + * `$` with `\`. Null for any other line. */ +function jobArguments(line: string): string[] | null { + const pattern = /\s*(?:"((?:\\.|[^"\\])*)"|([^\s"\\]+))/y; + const args: string[] = []; + let end = 0; + let match: RegExpExecArray | null; + while ((match = pattern.exec(line)) !== null) { + args.push(match[1] === undefined ? match[2]! : match[1].replace(/\\(.)/g, "$1")); + end = pattern.lastIndex; + } + return args.length === 0 || line.slice(end).trim() !== "" ? null : args; +} + +/** + * Linux links through a clang driver with lld. The driver only starts ld.lld + * with a job it derives from its arguments, its environment, its own + * installation and the system's (startup objects, the GCC installation, + * library directories). The job is recorded once per driver identity, + * arguments and environment, with the identities of the files and + * directories it and the driver's `-v` lines name. A later link whose recorded + * paths still have those identities runs ld.lld with the job directly; every + * other link runs through the driver. + */ +export class LinkerJobs { + /** A job run that failed in this build: later links run through the + * driver, which reports the error. */ + private failed = false; + + private constructor( + private readonly cache: NativeCache, + private readonly driver: string, + private readonly driverIdentity: InputIdentity, + ) {} + + /** Null unless the link is a host Linux link through a clang driver with + * `-fuse-ld=lld` (`lld`, the link's lld arguments), outside the sanitizer, + * with a native cache and a toolchain environment that allows complete + * artifacts. */ + static open( + cache: NativeCache | null, + toolchain: NativeToolchain, + lld: readonly string[], + sanitize: boolean, + ): LinkerJobs | null { + const target = toolchain.target; + if ( + cache === null || + sanitize || + target.platform !== "linux" || + // A cross-target link keeps the driver. + target.llvmTriple !== toolchain.helper.defaultTarget || + !lld.includes("-fuse-ld=lld") || + process.env["SCRIPTC_LINKER"] !== undefined || + !toolchainEnvironmentCachePolicy().completeArtifacts + ) + return null; + try { + const driver = commandPath(toolchain.linker); + if (!/^clang(-\d+)?$/.test(basename(realpathSync(driver)))) return null; + return new LinkerJobs(cache, driver, identity(driver)); + } catch { + return null; + } + } + + /** The link whose own inputs and output are `paths`, in `stage`. */ + link(paths: readonly string[], stage: string): LinkerJobLink { + return { + run: (args, list) => this.run(args, paths, stage, list), + record: (args, output, driverInputs) => this.record(args, paths, stage, output, driverInputs), + }; + } + + /** The entry key of the link `args` with its own `paths`, or null when + * the arguments name the stage besides those paths. */ + private key(args: readonly string[], paths: readonly string[], roots: string[]): string | null { + const index = new Map(paths.map((path, i) => [path, i])); + if (index.size !== paths.length || !paths.every((path) => args.includes(path))) return null; + if (args.some((arg) => !index.has(arg) && underRoots(arg, roots))) return null; + const shape = args.map((arg) => { + const at = index.get(arg); + return at === undefined ? arg : { path: at, extension: extname(arg) }; + }); + return contentDigest( + JSON.stringify({ + schema: 1, + driver: this.driverIdentity, + args: shape, + environment: toolchainEnvironmentFingerprint(), + cwd: process.cwd(), + path: process.env["PATH"] ?? null, + }), + ); + } + + /** + * Runs the link that the driver arguments `args` describe as its recorded + * job. `paths` are the link's own inputs and output among `args`, which may + * differ from build to build; `stage` is the directory they are in. Without + * an entry, `list` records the job first with the driver's `-v -###`; + * otherwise the caller links through the driver and passes its `-v` output + * to record(). Null when the link has to run through the driver: no job is + * recorded, a recorded path changed, or the job failed. + */ + run( + args: readonly string[], + paths: readonly string[], + stage: string, + list: boolean, + ): LinkerJobRun | null { + if (this.failed) return null; + try { + const roots = [resolve(stage), realpathSync(stage)]; + const key = this.key(args, paths, roots); + if (key === null) return null; + let entry = this.read(key, paths.length); + if (entry === null && list) { + const result = spawnSync(this.driver, [...args, "-v", "-###"], { + encoding: "utf8", + stdio: "pipe", + }); + if (result.error !== undefined || result.status !== 0 || result.signal !== null) + return null; + entry = this.store(key, result.stdout + "\n" + result.stderr, paths, roots); + } + if (entry === null || entry.job === null || !stillMatches(entry.inputs)) return null; + const job = entry.job.map((token) => (typeof token === "number" ? paths[token]! : token)); + const result = spawnSync(job[0]!, job.slice(1), { + encoding: "utf8", + stdio: "pipe", + }); + if (result.error !== undefined || result.status !== 0 || result.signal !== null) { + this.failed = true; + return null; + } + return { output: result.stdout + "\n" + result.stderr, driverInputs: entry.inputs }; + } catch { + return null; + } + } + + private read(key: string, paths: number): LinkerJobEntry | null { + const bytes = this.cache.read("linker-job", key, MAX_ENTRY_BYTES); + if (bytes === null) return null; + try { + const entry: unknown = JSON.parse(bytes.toString("utf8")); + return validEntry(entry, paths) ? entry : null; + } catch { + return null; + } + } + + /** Records the job of the link `args` from `output`, the `-v` output of + * the driver's successful link, unless an entry exists. `driverInputs` + * are the identities of the paths `output` names, when the caller has + * them. */ + record( + args: readonly string[], + paths: readonly string[], + stage: string, + output: string, + driverInputs?: InputIdentity[], + ): void { + try { + const roots = [resolve(stage), realpathSync(stage)]; + const key = this.key(args, paths, roots); + if (key !== null && this.read(key, paths.length) === null) + this.store(key, output, paths, roots, driverInputs); + } catch { + /* The next link records it. */ + } + } + + /** Stores the entry for `output`, the driver's `-v` lines and job. */ + private store( + key: string, + output: string, + paths: readonly string[], + roots: string[], + driverInputs?: InputIdentity[], + ): LinkerJobEntry { + const lines = output.split(/\r?\n/).filter((line) => line.startsWith(' "')); + const job = lines.length === 1 ? this.jobOf(jobArguments(lines[0]!), paths, roots) : null; + const entry: LinkerJobEntry = { + schema: "scriptc.linker-job.v1", + job, + inputs: job === null ? [] : (driverInputs ?? tracePaths(output, true, roots).map(identity)), + }; + this.cache.write("linker-job", key, JSON.stringify(entry)); + return entry; + } + + /** `job` with each of `paths` as its index: null unless the job runs + * ld.lld and names each of the paths once, as one whole argument, and names + * nothing else in the stage. */ + private jobOf( + job: string[] | null, + paths: readonly string[], + roots: string[], + ): (string | number)[] | null { + if (job === null || job.length === 0) return null; + if (!isAbsolute(job[0]!) || basename(job[0]!) !== "ld.lld") return null; + const index = new Map(paths.map((path, i) => [path, i])); + const used = new Set(); + const tokens: (string | number)[] = []; + for (const arg of job) { + const at = index.get(arg); + if (at === undefined) { + if (underRoots(arg, roots)) return null; + tokens.push(arg); + } else { + if (used.has(at)) return null; + used.add(at); + tokens.push(at); + } + } + return used.size === paths.length ? tokens : null; + } +} From e842da2ceb7bcb8e7c914e2d20203624497ff06a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 15:24:38 -0500 Subject: [PATCH 260/273] Batch the program's identifier symbols before collection --- .../compiler/src/frontend/lowering/lowerer.ts | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 2a04b33c3..d272b7014 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -5221,11 +5221,11 @@ export class Lowerer { const sourceFiles = parts.map((p) => p.sf); const fnDecls = parts.flatMap((p) => p.fnDecls); // This fixed-point scan intentionally sees deferred bodies before - // reachability is known. Batch their binding queries through the - // facade's panic fence, then keep lookup free of lowering side effects: - // resolveValueSymbol also flushes deferred diagnostics and applies - // merged-namespace fences, whose authority remains ordinary lowering. - this.checker.prefetchSymbolRoots(sourceFiles, true); + // reachability is known. Its binding queries were batched through the + // facade's panic fence before collection (prefetchProgramSymbols); keep + // lookup free of lowering side effects: resolveValueSymbol also flushes + // deferred diagnostics and applies merged-namespace fences, whose + // authority remains ordinary lowering. // The facts each fixed-point site reads, by the symbols, nodes and // element keys they belong to (created with the site list below). let siteDependencies: SiteDependencies | null = null; @@ -8027,6 +8027,7 @@ export class Lowerer { if (this.remainder) { for (const fp of parts) this.checker.prefetchSourceFile(fp.sf); } + this.prefetchProgramSymbols(parts); this.collectProgram(parts); this.speculativeTypeQueries(() => { extendAssertedRecordMembers( @@ -8486,6 +8487,7 @@ export class Lowerer { this.onEdge = (name) => { collectionEdges.add(name); }; + this.prefetchProgramSymbols(parts); this.collectProgram(parts); this.speculativeTypeQueries(() => { extendAssertedRecordMembers( @@ -12183,6 +12185,17 @@ export class Lowerer { return lowerClassExpressionInfo(this, expr); } + /** Batch-prefetches the symbols of every runtime identifier of the + * program's files. Collection's early-binding scan and the runtime- + * optional analysis look them up across every body, reached or not; + * one batch answers both, instead of one request per identifier. */ + private prefetchProgramSymbols(parts: FileParts[]): void { + this.checker.prefetchSymbolRoots( + parts.map((fp) => fp.sf), + true, + ); + } + /** Depth of the whole-program analyses that query types across every * file before reachability is known (speculativeTypeQueries). */ private speculativeTypeQueryDepth = 0; From 11de08b93116e4758c4d66d114fd859d4ca2f606 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 15:46:27 -0500 Subject: [PATCH 261/273] Skip worker cancellation polls in short loops inside another loop In a worker program, a loop inside another loop of the same function needs less than its own budget, because the enclosing loop polls at least once every 64 of its iterations: - A loop whose counters' proven ranges bound its runs, and those of the loops in its body, to 64 x 64 emits no poll. This covers loops with constant bounds, such as mandelbrot's lane and iteration loops. The bound uses the counted and two-counter loop ranges, or the range of a counter that the update alone steps by a constant where the condition reads it. Loops whose counted form needs a run-time guard don't count. - An innermost counted loop is strip-mined like a counted loop over typed arrays, whatever its body. A run of at most 64 iterations never polls, and the budget's countdown leaves the body. A run of at most 16384 iterations is one block and polls once as it leaves. A longer run polls between blocks of 64, but not after its last block. Between two polls there are then at most 64 iterations of the enclosing loop, and in each of them at most 16384 unpolled iterations of each strip-mined inner loop and 64 x 64 runs of each bounded nest. A loop that no other loop of its function encloses polls as before. New corpus program: native-worker-terminate-bounded-loops.ts. --- .../src/backend/llvm/emit-statements.ts | 142 +++++++++++++++++- packages/compiler/src/backend/loop-polls.ts | 4 +- packages/compiler/src/ir/integer-loops.ts | 22 +++ .../test/counted-loop-emission.test.ts | 91 +++++++++++ ...ive-worker-terminate-bounded-loops.ts.json | 6 + .../native-worker-terminate-bounded-loops.ts | 91 +++++++++++ ...ive-worker-terminate-bounded-loops.ts.json | 1 + 7 files changed, 349 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-bounded-loops.ts.json create mode 100644 tests/corpus/native-worker-terminate-bounded-loops.ts create mode 100644 tests/harness/corpus-costs/native-worker-terminate-bounded-loops.ts.json diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index a43cf3534..e0db764e8 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -50,6 +50,7 @@ import { matchIntegerBytesForLoop, matchIntegerCountedForLoop, matchIntegerPairedForLoop, + steppedLoopCounter, } from "../../ir/integer-loops.js"; import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js"; import { @@ -1218,7 +1219,24 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { // has no exit besides its own bound, which LLVM can vectorize. A typed // array's length is fixed, so each block can take it as its limit when // the loop does not assign the array's binding. + // + // Inside another loop of this function, which polls at least once + // every WORKER_POLL_BLOCK of its own iterations, a loop needs less: + // - a loop whose counters' ranges bound its runs, and those of the + // loops in its body, to WORKER_POLL_BLOCK^2 needs no poll; + // - an innermost counted loop is strip-mined, typed arrays or not, + // and does not poll after its last block. A run of at most + // WORKER_POLL_BLOCK iterations then never polls, and the countdown + // leaves its body. A run of at most WORKER_UNPOLLED_RUN iterations + // is one block, polled once as it leaves. + const enclosed = + emitter.mod.workers === true && + emitter.jumpTargets.some((target) => target.kind === "loop"); + const bounded = + enclosed && boundedLoopRuns(emitter, s) <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK; + const innermost = enclosed && !containsLoop(s.body); const stripMined = + !bounded && capturedLets.length === 0 && integerSlot !== null && ((countedLoop !== null && @@ -1226,16 +1244,21 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { Math.abs(countedLoop.step) * WORKER_POLL_BLOCK <= 2 ** 52) || (integerLoop !== null && integerArrayLoop === null && integerLoop.stableReceiver)) && emitter.workerLoopPolls(s) && - loopAccessesTypedArrays(s); - const workerBudget = stripMined ? null : emitter.workerLoopBudget(s); + (loopAccessesTypedArrays(s) || (countedLoop !== null && innermost)); + const workerBudget = stripMined || bounded ? null : emitter.workerLoopBudget(s); // One slot for every copy of the loop: a speculative copy resumes an // iteration in another copy's body, which then reads this bound. let blockBound: string | null = null; + let runSlot: string | null = null; if (stripMined) { blockBound = B.slot(); B.entryAllocas.push( `${blockBound} = alloca ${countedLoop ? "i64" : emitter.sizeType} ; cancellation poll block bound`, ); + if (innermost) { + runSlot = B.slot(); + B.entryAllocas.push(`${runSlot} = alloca i1 ; cancellation poll after the run`); + } } freshenBindings(); emitter.emitLoopCopies(s, capturedLets.length === 0, le, (exit) => { @@ -1271,7 +1294,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { return len; }, }; - emitStripMinedCountedLoop(emitter, s, counter, blockBound!, exit); + emitStripMinedCountedLoop(emitter, s, counter, blockBound!, exit, runSlot); return; } const lc = B.newLabel("loop.c"); @@ -2628,6 +2651,51 @@ function emitInt32KindLoop( /** Iterations between cancellation polls of a strip-mined counted loop. */ const WORKER_POLL_BLOCK = 64; +/** The longest run of a strip-mined loop inside another loop that polls + * only as it leaves. */ +const WORKER_UNPOLLED_RUN = 16384; + +/** At most how many times a `for` loop and the loops in its body run their + * bodies per entry, from the counters' proven ranges: its iterations times + * one plus the nested loops' runs. Infinity when a count has no bound, or + * depends on a run-time guard (the guard's slow copy steps a double). */ +function boundedLoopRuns(emitter: LlEmitter, loop: IrStmt): number { + if (loop.kind !== "for") return Infinity; + const counted = matchIntegerCountedForLoop( + loop, + emitter.numericLocals, + emitter.integerRanges, + LOOPS.of(emitter).unaliasedArrays, + ); + let trips = Infinity; + if (counted && !counted.guarded) + trips = Math.floor((counted.range.max - counted.range.min) / Math.abs(counted.step)) + 1; + else if (!counted) { + // Each iteration closes the gap between the two counters by at least two. + const paired = matchIntegerPairedForLoop(loop, emitter.numericLocals, emitter.integerRanges); + if (paired && !paired.guarded) + trips = Math.floor((paired.range.max - paired.range.min) / 2) + 1; + } + if (trips === Infinity) { + // A counter with a proven range where the condition reads it, such as + // one compared with a module constant. + const stepped = steppedLoopCounter(loop, emitter.currentLocals); + const range = stepped && emitter.integerRanges.get(stepped.ref); + if (stepped && range) trips = Math.floor((range.max - range.min) / Math.abs(stepped.step)) + 1; + } + if (!(trips <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK)) return Infinity; + let nested = 0; + const visit = (s: IrStmt): boolean => { + if (s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf") { + nested += boundedLoopRuns(emitter, s); + return nested !== Infinity; + } + return everyStmtChild(s, () => true, visit); + }; + if (!loop.body.every(visit)) return Infinity; + return trips * (1 + nested); +} + /** The counter of a strip-mined loop: its slot, the LLVM integer type and * signedness of its values, its step, the counters stepped beside it, and * the limit, emitted in the outer header before each block. */ @@ -2645,13 +2713,20 @@ interface StripMinedCounter { * tests the counter against the loop's limit, each block runs the body up * to the nearer of the limit and WORKER_POLL_BLOCK steps on, and the poll * follows the block. It polls as often as the budget it replaces, and the - * inner loop's only exit besides `break` and `return` is its bound. */ + * inner loop's only exit besides `break` and `return` is its bound. + * + * With `runSlot` (for a loop that another loop of the function encloses), a + * block that ends at the limit leaves the loop without polling, and a run + * of at most WORKER_UNPOLLED_RUN iterations is one block, polled once as + * it leaves when it took more than WORKER_POLL_BLOCK iterations. `runSlot` + * holds whether the current block is such a run. */ function emitStripMinedCountedLoop( emitter: LlEmitter, s: Extract, counter: StripMinedCounter, boundSlot: string, exit: string, + runSlot: string | null, ): void { const B = emitter.B; const { slot, type, signed, step } = counter; @@ -2684,7 +2759,27 @@ function emitStripMinedCountedLoop( `${stepped} = call ${type} @${saturating}(${type} ${start}, ${type} ${step * WORKER_POLL_BLOCK})`, ); B.line(`${bound} = call ${type} @${nearer}(${type} ${stepped}, ${type} ${limit})`); - B.line(`store ${type} ${bound}, ptr ${boundSlot}`); + if (runSlot === null) B.line(`store ${type} ${bound}, ptr ${boundSlot}`); + else { + // The steps left to the limit: positive, as the header tested. + const left = B.tmp(), + whole = B.tmp(), + long = B.tmp(), + polled = B.tmp(), + end = B.tmp(); + const stride = Math.abs(step); + B.line( + ascending + ? `${left} = sub ${wrap} ${type} ${limit}, ${start}` + : `${left} = sub ${wrap} ${type} ${start}, ${limit}`, + ); + B.line(`${whole} = icmp ule ${type} ${left}, ${stride * WORKER_UNPOLLED_RUN}`); + B.line(`${long} = icmp ugt ${type} ${left}, ${stride * WORKER_POLL_BLOCK}`); + B.line(`${polled} = and i1 ${whole}, ${long}`); + B.line(`${end} = select i1 ${whole}, ${type} ${limit}, ${type} ${bound}`); + B.line(`store ${type} ${end}, ptr ${boundSlot}`); + B.line(`store i1 ${polled}, ptr ${runSlot}`); + } B.br(bc); B.startBlock(bc); const index = B.tmp(); @@ -2693,7 +2788,8 @@ function emitStripMinedCountedLoop( B.line(`${index} = load ${type}, ptr ${slot}`); B.line(`${end} = load ${type}, ptr ${boundSlot}`); B.line(`${inBlock} = icmp ${compare} ${type} ${index}, ${end}`); - B.condBr(inBlock, lb, poll); + const after = runSlot === null ? poll : B.newLabel("loop.end"); + B.condBr(inBlock, lb, after); B.startBlock(lb); emitter.jumpTargets.push({ kind: "loop", @@ -2716,11 +2812,45 @@ function emitStripMinedCountedLoop( B.line(`store ${type} ${next}, ptr ${stepSlot}`); } B.br(bc); + if (runSlot !== null) { + // A speculative copy may enter the body without passing the header, so + // the limit is read again here. + B.startBlock(after); + const at = B.tmp(); + const more = B.tmp(); + const run = B.tmp(); + const done = B.newLabel("loop.done"); + const last = B.newLabel("loop.poll.run"); + B.line(`${at} = load ${type}, ptr ${slot}`); + B.line(`${more} = icmp ${compare} ${type} ${at}, ${counter.limit()}`); + B.condBr(more, poll, done); + B.startBlock(done); + B.line(`${run} = load i1, ptr ${runSlot}`); + B.condBr(run, last, exit); + B.startBlock(last); + emitter.emitWorkerPoll(); + B.br(exit); + } B.startBlock(poll); emitter.emitWorkerPoll(); B.br(lc); } +/** Whether `body` holds a loop outside its closures. */ +function containsLoop(body: IrStmt[]): boolean { + const inStmt = (s: IrStmt): boolean => + s.kind === "while" || + s.kind === "doWhile" || + s.kind === "for" || + s.kind === "forOf" || + !everyStmtChild( + s, + () => true, + (child) => !inStmt(child), + ); + return body.some(inStmt); +} + function enterLoopHeader(emitter: LlEmitter, header: string): void { const dispatch = emitter.speculationHeader; emitter.speculationHeader = null; diff --git a/packages/compiler/src/backend/loop-polls.ts b/packages/compiler/src/backend/loop-polls.ts index c582a4974..866711fb7 100644 --- a/packages/compiler/src/backend/loop-polls.ts +++ b/packages/compiler/src/backend/loop-polls.ts @@ -110,8 +110,8 @@ export function loopTestsEveryIteration(loop: Loop, tested: (call: IrExpr) => bo /** True when the body of `loop`, outside its nested loops and closures, * reads or writes a typed-array element: the loops whose cancellation poll * the emitter moves between blocks of iterations, so that LLVM can - * vectorize them. Other loops keep the budget in their body, which costs - * less in a loop of a few iterations. */ + * vectorize them. Outside another loop, other loops keep the budget in + * their body, which costs less in a loop of a few iterations. */ export function loopAccessesTypedArrays(loop: Loop): boolean { const inExpr = (e: IrExpr): boolean => { if (e.kind === "bytesIntrinsic" && e.method === "get") return true; diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index ea866a134..427f6879c 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -618,6 +618,28 @@ function writesLocal(body: IrStmt[], localId: string): boolean { }); } +/** The counter of a `for` loop that its condition reads and its update + * steps by a constant, when nothing else in the loop writes it: each test + * of the condition then sees a different value. The local must not be + * boxed (a closure could write it). */ +export function steppedLoopCounter( + stmt: IrStmt & { kind: "for" }, + locals: ReadonlyMap, +): { ref: IrExpr & { kind: "varRef" }; step: number } | null { + const cond = stmt.cond; + if (cond?.kind !== "bin") return null; + for (const side of [cond.left, cond.right]) { + if (side.kind !== "varRef") continue; + const local = locals.get(side.localId); + if (local === undefined || local.boxed) continue; + const step = constantStep(stmt.update, side.localId); + if (step === null || writesLocal(stmt.body, side.localId)) continue; + if (writesLocal([{ kind: "exprStmt", expr: cond, loc: cond.loc }], side.localId)) continue; + return { ref: side, step }; + } + return null; +} + function isUnitIncrement(update: IrStmt | null, localId: string): boolean { // The frontend normally lowers `i++` to `i = i + 1` before backend // emission. Accept the incDec form too so this analysis remains valid for diff --git a/packages/compiler/test/counted-loop-emission.test.ts b/packages/compiler/test/counted-loop-emission.test.ts index f617c6ab4..794edd1af 100644 --- a/packages/compiler/test/counted-loop-emission.test.ts +++ b/packages/compiler/test/counted-loop-emission.test.ts @@ -136,3 +136,94 @@ test("worker programs poll counted loops over typed arrays between blocks of ite await rm(dir, { recursive: true, force: true }); } }); + +test("worker programs poll no loop whose proven ranges bound its runs inside another loop", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-bounded-loop-polls-")); + try { + const entry = join(dir, "bounded.ts"); + await writeFile( + entry, + [ + 'import { isMainThread, Worker } from "node:worker_threads";', + "const LIMIT = 50;", + "export function rows(cr: Float64Array, n: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++)", + " for (let i = 0; i < 8; i += 2) {", + " const c = cr[i]!;", + " for (let j = 0; j < LIMIT; j++) {", + " t = (t * 31 + j + c) % 1000003;", + " if (t === 7) break;", + " }", + " }", + " return t;", + "}", + "export function open(cr: Float64Array, n: number, k: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++)", + " for (let i = 0; i < 8; i += 2) {", + " const c = cr[i]!;", + " for (let j = 0; j < k; j++) {", + " t = (t * 31 + j + c) % 1000003;", + " if (t === 7) break;", + " }", + " }", + " return t;", + "}", + "export function wide(n: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++) for (let j = 0; j < 5000; j++) t = (t * 31 + j) % 1000003;", + " return t;", + "}", + "export function single(): number {", + " let t = 0;", + " for (let j = 0; j < 50; j++) t = (t * 31 + j) % 1000003;", + " return t;", + "}", + "const cr = new Float64Array(8).fill(0.5);", + 'const k = Number(process.argv[2] ?? "50");', + "console.log(rows(cr, 3), open(cr, 3, k), wide(2), single());", + "if (!isMainThread) new Worker(new URL(import.meta.url));", + ].join("\n"), + ); + const outPath = join(dir, "bounded.ir.json"); + const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const body = (name: string): string => + [ + ...ll.matchAll( + new RegExp( + `define internal [^\\n]+ @sc_b?f_${name}(?:\\.i32)?\\([^\\n]*\\n([\\s\\S]*?)\\n\\}`, + "g", + ), + ), + ] + .map((match) => match[1]!) + .join("\n"); + const polls = (text: string): number => text.match(/@scr_exc_flag/g)?.length ?? 0; + // The i and j loops of `rows` run at most 4 × (1 + 50) bodies: only the + // r loop polls, and the i loop over the typed array is not strip-mined. + const rows = body("rows"); + expect(rows).not.toMatch(/^loop\.poll\d+:$/m); + expect(polls(rows)).toBeGreaterThan(0); + // With a limit known only at run time the inner loops keep their polls. + // The innermost one is strip-mined, and a block that ends at the limit + // leaves it without polling: only a block that ends short of it polls. + const open = body("open"); + expect(polls(open)).toBeGreaterThan(polls(rows)); + expect(open).toMatch(/^loop\.poll\d+:$/m); + const ends = [...open.matchAll(/^loop\.end\d+:$[\s\S]*?^\S/gm)].map((match) => match[0]); + expect(ends.length).toBeGreaterThan(0); + for (const end of ends) expect(end).toMatch(/br i1 %\w+, label %loop\.poll\d+, label %/); + // A run of up to 16384 iterations is one block, polled as it leaves when + // it took more than 64. + expect(open).toMatch(/^loop\.poll\.run\d+:$/m); + // A nest over the bound, or a loop no other loop encloses, polls. + expect(polls(body("wide"))).toBeGreaterThan(0); + expect(body("wide")).toMatch(/^loop\.poll\d+:$/m); + expect(polls(body("single"))).toBeGreaterThan(0); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-bounded-loops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-bounded-loops.ts.json new file mode 100644 index 000000000..2e52a519c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-bounded-loops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/native-worker-terminate-bounded-loops.ts" + ], + "diags": [] +} diff --git a/tests/corpus/native-worker-terminate-bounded-loops.ts b/tests/corpus/native-worker-terminate-bounded-loops.ts new file mode 100644 index 000000000..d244e021d --- /dev/null +++ b/tests/corpus/native-worker-terminate-bounded-loops.ts @@ -0,0 +1,91 @@ +// worker.terminate() stops a worker whose loop runs short inner loops: +// counted loops of a few iterations, a two-counter loop, an inner loop +// whose length grows past 64 iterations, an inner loop too long to finish, +// and a nest of loops with constant bounds. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +function counted(values: number[]): number { + let acc = 0; + for (let round = 0; ; round++) { + for (let i = 0; i < values.length; i++) acc = (acc + values[i]! * i) % 1000003; + for (let i = values.length - 1; i >= 0; i -= 2) acc = (acc * 7 + values[i]!) % 1000003; + if (round === 200) parentPort!.postMessage("ready"); + } + return acc; +} + +function paired(values: number[]): number { + let flips = 0; + for (let round = 0; ; round++) { + for (let i = 0, j = values.length - 1; i < j; i++, j--) { + const t = values[i]!; + values[i] = values[j]!; + values[j] = t; + flips++; + } + if (round === 200) parentPort!.postMessage("ready"); + } + return flips; +} + +function growing(): number { + let acc = 0; + for (let n = 1; ; n++) { + const limit = n % 200; + for (let i = 0; i < limit; i++) acc = (acc + i) % 1000003; + if (n === 200) parentPort!.postMessage("ready"); + } + return acc; +} + +function long(limit: number): number { + let acc = 0; + for (let round = 0; ; round++) { + for (let i = 0; i < limit; i++) { + acc = (acc * 7 + i) % 1000003; + if (i === 100000) parentPort!.postMessage("ready"); + } + } + return acc; +} + +const ROUNDS = 50; + +function constant(row: Float64Array): number { + let acc = 0; + for (let y = 0; ; y++) { + for (let i = 0; i < 8; i += 2) { + const c = row[i]!; + for (let j = 0; j < ROUNDS; j++) { + acc = (acc * 3 + j + c) % 1000003; + if (acc === -1) break; + } + } + if (y === 200) parentPort!.postMessage("ready"); + } + return acc; +} + +if (isMainThread) { + for (const kind of ["counted", "paired", "growing", "long", "constant"]) { + await new Promise((resolve) => { + const worker = new Worker(new URL(import.meta.url), { workerData: kind }); + worker.once("message", () => { + void worker.terminate().then((code: number) => console.log(kind, "terminate", code)); + }); + worker.once("exit", (code: number) => { + console.log(kind, "exit", code); + resolve(); + }); + }); + } +} else { + const values = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8]; + let result = 0; + if (workerData === "counted") result = counted(values); + else if (workerData === "paired") result = paired(values); + else if (workerData === "growing") result = growing(); + else if (workerData === "long") result = long(1e15); + else result = constant(new Float64Array(8).fill(0.25)); + console.log("unexpected result", result); +} diff --git a/tests/harness/corpus-costs/native-worker-terminate-bounded-loops.ts.json b/tests/harness/corpus-costs/native-worker-terminate-bounded-loops.ts.json new file mode 100644 index 000000000..bb744e4ed --- /dev/null +++ b/tests/harness/corpus-costs/native-worker-terminate-bounded-loops.ts.json @@ -0,0 +1 @@ +{ "plain": 4034, "san": 5956 } From 829f35bf851bd691078b1fa3ed8226e56c7543a0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 15:55:57 -0500 Subject: [PATCH 262/273] Pass sanitizeAttributes where the serialized-IR test emits directly The test emits each corpus program from its serialized IR with its own emitter options and compares the text with compile()'s. compile() now marks functions sanitize_address only in sanitized builds, so the test passes the same setting as objectAudit. --- .../compiler/src/backend/llvm/serialized-ir-emission.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/compiler/src/backend/llvm/serialized-ir-emission.test.ts b/packages/compiler/src/backend/llvm/serialized-ir-emission.test.ts index 6d2457c57..1d35caa05 100644 --- a/packages/compiler/src/backend/llvm/serialized-ir-emission.test.ts +++ b/packages/compiler/src/backend/llvm/serialized-ir-emission.test.ts @@ -44,6 +44,7 @@ test("sort-heavy programs emit equal LLVM from memory and from serialized IR", a // compile() emits Linux textual LLVM with the unwinding ABI. unwindAbi: process.platform === "linux", objectAudit: sanitize, + sanitizeAttributes: sanitize, ...inlineRcOption(options.optimization, sanitize), }); const text = typeof emitted === "string" ? emitted : emitted.join(""); From d1f0113e49ce58cb2ea00c38b6521041c4d2a3bd Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 15:58:14 -0500 Subject: [PATCH 263/273] Check only the files whose diagnostics the tsc gate keeps The tsc gate (preflight's errorsOf, and the original program of a JSX or declaration-projection build) asked for every file's semantic diagnostics, then dropped all of those in an opted-in package's files, in unselected package JavaScript and in package shims. With --npm-static that is nearly the whole check: on three.js's nodes fixture the 82 package files took 1.17 s and reported 13,022 diagnostics, the program's own files 0.1 s. getPreEmitDiagnostics takes the files whose diagnostics its caller keeps. When some file is left out, the server checks only the kept files (getDiagnosticsOfCheckedFiles), and their semantic diagnostics come in the full check's order. When a stored answer for the supplied declaration files applies to the program, it stands in for checking those files, as in the full check; the storage rule is unchanged. A global diagnostic can come from checking any file, so when the partial check reports one, the full check answers as before. At load time, before workspace packages are known, the gate leaves out only package and shim files. The kept diagnostics are unchanged. --- packages/compiler/src/frontend/program.ts | 21 +++++- .../src/frontend/ts7/lib-diagnostics.test.ts | 40 ++++++++++ .../src/frontend/ts7/lib-diagnostics.ts | 37 +++++++++- .../src/frontend/ts7/program-host.test.ts | 55 +++++++++++++- .../compiler/src/frontend/ts7/program-host.ts | 74 ++++++++++++++++--- 5 files changed, 208 insertions(+), 19 deletions(-) diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index b93e3f217..284b2ce06 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -665,6 +665,14 @@ function createRequireProgramRoots7(program: ts.Program): string[] { return [...roots].sort(); } +/** Whether the tsc gate can keep a file's diagnostics when the program + * loads: it drops all of an opted-in package's files and of the package + * shims (keptErrors in preflight also drops unselected package JavaScript, + * which is not known yet here, so those files are still checked). */ +function tscGateFileKept(fileName: string): boolean { + return npmStaticPackageOfPath(fileName) === null && !isPackageShimFile(fileName); +} + /** Classify the explicitly selected installed package's own sources. */ export function entryPackageFilePredicate(entryPath: string): (file: string) => boolean { const entryFile = tsgoPath(entryPath); @@ -934,7 +942,7 @@ function loadProgram7( 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 diagnostics = ts.getPreEmitDiagnostics(original, tscGateFileKept); const errors = diagnostics.some((d) => d.category === ts.DiagnosticCategory.Error); tscWorld = { program: original, @@ -982,7 +990,7 @@ function loadProgram7( } else { tscWorld = { program: original, - diagnostics: ts.getPreEmitDiagnostics(original), + diagnostics: ts.getPreEmitDiagnostics(original, tscGateFileKept), projectWorld: null, declWorldFallback: true, }; @@ -5438,7 +5446,14 @@ function preflight7(load: LoadResult): { const sf = p.getSourceFile(d.fileName); return sf !== undefined && isCheckedJsSourceFile(sf, projectCheckJs); }; - const errorsOf = (p: ts.Program): ts.Diagnostic[] => keptErrors(p, ts.getPreEmitDiagnostics(p)); + // keptErrors drops every diagnostic of these files: the gate does not ask + // the checker for them. + const diagnosticsKept = (fileName: string): boolean => + npmStaticPackageOfPath(fileName) === null && + !unselectedPackageJsFile(fileName) && + !isPackageShimFile(fileName); + const errorsOf = (p: ts.Program): ts.Diagnostic[] => + keptErrors(p, ts.getPreEmitDiagnostics(p, diagnosticsKept)); 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. diff --git a/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts b/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts index e87db5f1a..83abaeddc 100644 --- a/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts +++ b/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts @@ -4,6 +4,7 @@ import { join } from "node:path"; import { afterAll, afterEach, expect, test } from "vitest"; import type { SourceFile } from "./ast-types.js"; import { + reusedKeptLibDiagnostics, reusedLibDiagnostics, setLibDiagnosticsStore, type LibDiagnosticsStore, @@ -118,6 +119,45 @@ test("a stored answer stands in for checking the supplied files, merged by file expect(full?.semantic).toEqual(reused?.semantic); }); +test("with kept files, a stored answer needs checking only the kept other files", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const pkg = join(dir, "node_modules", "pkg", "index.d.ts"); + const semantic = new Map([ + [lib, [diagnostic(lib, 1)]], + [fallback, []], + [main, [diagnostic(main, 5)]], + [pkg, [diagnostic(pkg, 3)]], + ]); + const withPackage = files({ + [main]: "export const n: number = 'x';\n", + [pkg]: "export declare const p: number;\n", + }); + const all = [lib, fallback, main, pkg]; + const keep = new Set([lib, fallback, main]); + // Without a stored answer nothing is checked or stored. + const cold = sessionProgram({ semantic }); + expect(reusedKeptLibDiagnostics(cold, all, supplied, withPackage, keep)).toBeNull(); + expect(cold.asked).toEqual([]); + expect(store.entries.size).toBe(0); + reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, withPackage); + expect(store.entries.size).toBe(1); + const warm = sessionProgram({ semantic }); + expect(reusedKeptLibDiagnostics(warm, all, supplied, withPackage, keep)).toEqual({ + semantic: [diagnostic(lib, 1), diagnostic(main, 5)], + global: [], + }); + expect(warm.asked).toEqual([`checked ${main}`]); + // A left-out file that can declare something global still prevents reuse. + const augmenting = files({ + [main]: "export const n: number = 'x';\n", + [pkg]: "export {};\ndeclare global {\n var NaN: number;\n}\n", + }); + const blocked = sessionProgram({ semantic }); + expect(reusedKeptLibDiagnostics(blocked, all, supplied, augmenting, keep)).toBeNull(); + expect(blocked.asked).toEqual([]); +}); + test("files merge in code point order, which differs from UTF-16 order", () => { const store = memoryStore(); setLibDiagnosticsStore(store, "checker"); diff --git a/packages/compiler/src/frontend/ts7/lib-diagnostics.ts b/packages/compiler/src/frontend/ts7/lib-diagnostics.ts index 30c6e81f0..c3e563fef 100644 --- a/packages/compiler/src/frontend/ts7/lib-diagnostics.ts +++ b/packages/compiler/src/frontend/ts7/lib-diagnostics.ts @@ -64,6 +64,31 @@ export function reusedLibDiagnostics( fileNames: readonly string[], supplied: (fileName: string) => boolean, sourceFile: (fileName: string) => SourceFile | undefined, +): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + return reuse(program, fileNames, supplied, sourceFile, () => true, false); +} + +/** reusedLibDiagnostics for a caller that keeps only the diagnostics of the + * `kept` files: a stored answer then needs checking only the kept other + * files, and without a stored answer the result is null (nothing is checked + * or stored). */ +export function reusedKeptLibDiagnostics( + program: Ts7SessionProgram, + fileNames: readonly string[], + supplied: (fileName: string) => boolean, + sourceFile: (fileName: string) => SourceFile | undefined, + kept: ReadonlySet, +): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + return reuse(program, fileNames, supplied, sourceFile, (name) => kept.has(name), true); +} + +function reuse( + program: Ts7SessionProgram, + fileNames: readonly string[], + supplied: (fileName: string) => boolean, + sourceFile: (fileName: string) => SourceFile | undefined, + keep: (fileName: string) => boolean, + storedOnly: boolean, ): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { if (configured === null || process.env["SCRIPTC_LIB_DIAGNOSTICS"] === "0") return null; const { store, checker } = configured; @@ -104,12 +129,16 @@ export function reusedLibDiagnostics( if (globals !== null) writeEntry(store, key, globals, Object.fromEntries(stored.files)); } if (stored !== null && (scripts.length === 0 || (globals !== null && !collides(globals)))) { - const checked = program.checkedFileDiagnosticData(own); + const checkedNames = storedOnly ? own.filter(keep) : own; + const checked = program.checkedFileDiagnosticData(checkedNames); if (checked === null) return null; - const byFile = new Map(stored.files); - own.forEach((name, index) => byFile.set(name, checked.files[index] ?? [])); + const byFile = new Map( + storedOnly ? [...stored.files].filter(([name]) => keep(name)) : stored.files, + ); + checkedNames.forEach((name, index) => byFile.set(name, checked.files[index] ?? [])); return { semantic: inFileOrder(byFile), global: checked.global }; } + if (storedOnly) return null; // The full check, as before. Its answers for the supplied files are stored // when it reports no global diagnostics and the program's scripts declare // none of the supplied files' globals (so nothing of the program reached @@ -339,7 +368,7 @@ function readEntry( /** Each file's diagnostics (already in the checker's order) in the order the * checker sorts files: by file name, compared by code point. */ -function inFileOrder(byFile: ReadonlyMap): Ts7DiagnosticData[] { +export function inFileOrder(byFile: ReadonlyMap): Ts7DiagnosticData[] { const names = [...byFile.keys()].sort(compareCodePoints); const out: Ts7DiagnosticData[] = []; for (const name of names) for (const d of byFile.get(name)!) out.push(d); diff --git a/packages/compiler/src/frontend/ts7/program-host.test.ts b/packages/compiler/src/frontend/ts7/program-host.test.ts index 80c4d2bcf..2ab6a0293 100644 --- a/packages/compiler/src/frontend/ts7/program-host.test.ts +++ b/packages/compiler/src/frontend/ts7/program-host.test.ts @@ -3,7 +3,12 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, test, vi } from "vitest"; import { Ts7Api } from "./rpc-api.js"; -import { Ts7Host, type Ts7ApiOptions, type Ts7CompilerOptions } from "./program-host.js"; +import { + getPreEmitDiagnostics, + Ts7Host, + type Ts7ApiOptions, + type Ts7CompilerOptions, +} from "./program-host.js"; import { ModuleDetectionKind, ModuleKind, ModuleResolutionKind, ScriptTarget } from "./enums.js"; import { tsgoPath } from "./session-path.js"; @@ -70,6 +75,54 @@ test("implementation discovery leaves declaration trees lazy without skipping th expect(() => program.getSourceFileNames()).toThrow("disposed"); }); +test("pre-emit diagnostics for kept files check only those, unless a global diagnostic appears", () => { + const { directory, entry } = fixture(); + const other = join(directory, "other.ts"); + const later = join(directory, "later.ts"); + writeFileSync( + entry, + 'import "./other.js";\nimport "./later.js";\nexport const n: number = "x";\n', + ); + writeFileSync(other, "export const s: string = 1;\n"); + writeFileSync(later, "export const b: boolean = 0;\n"); + const { host } = makeHost(directory); + const program = host.createProgram([entry], options); + try { + const full = getPreEmitDiagnostics(program); + const keep = (name: string): boolean => name !== tsgoPath(other); + const checked = vi.spyOn(program.project.program, "checkedFileDiagnosticData"); + const kept = getPreEmitDiagnostics(program, keep); + // The full answer without the left-out file's diagnostics, in the same order. + expect(full.some((diagnostic) => diagnostic.fileName === tsgoPath(other))).toBe(true); + expect(kept.map((diagnostic) => [diagnostic.fileName, diagnostic.code])).toEqual( + full + .filter((diagnostic) => diagnostic.fileName === undefined || keep(diagnostic.fileName)) + .map((diagnostic) => [diagnostic.fileName, diagnostic.code]), + ); + expect(kept).toHaveLength(2); + expect(checked).toHaveBeenCalledTimes(1); + expect(checked.mock.calls[0]![0]).not.toContain(tsgoPath(other)); + // Keeping every file is the full check. + expect(getPreEmitDiagnostics(program, () => true)).toEqual(full); + expect(checked).toHaveBeenCalledTimes(1); + // A global diagnostic from the partial check, or a server without it: the full check. + for (const answer of [ + { + files: [], + global: [{ fileName: undefined, pos: 0, end: 0, code: 2318, category: 1, text: "global" }], + }, + null, + ]) { + checked.mockReturnValueOnce( + answer as ReturnType, + ); + expect(getPreEmitDiagnostics(program, keep)).toEqual(full); + } + } finally { + program.dispose(); + } +}); + test("the factory receives one connection with the host's filesystem and timing", () => { const { directory, entry } = fixture(); const factory = vi.fn((connection: Ts7ApiOptions) => new Ts7Api(connection)); diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index 9e8f51bb3..bd6921afc 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -48,7 +48,7 @@ import { import { packageShimDeclarationText } from "../package-shims.js"; import { typePatchedText } from "../type-patches/layer.js"; import { ambientDtsPath, fallbackDtsPath, overridesDtsPath } from "../dts-paths.js"; -import { reusedLibDiagnostics } from "./lib-diagnostics.js"; +import { inFileOrder, reusedKeptLibDiagnostics, reusedLibDiagnostics } from "./lib-diagnostics.js"; import { ts7Diagnostic } from "./diagnostic.js"; import type { Ts7DiagnosticData } from "./session-schema.generated.js"; @@ -423,17 +423,31 @@ export class Ts7Program { /** The semantic and global diagnostics with the stored diagnostics of the * declaration files scriptc supplies (lib-diagnostics.ts), or null. */ reusedLibDiagnostics(): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + return this.libDiagnostics(null); + } + + /** reusedLibDiagnostics for the `kept` files only: null without a stored + * answer. */ + reusedKeptLibDiagnostics( + kept: ReadonlySet, + ): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + return this.libDiagnostics(kept); + } + + private libDiagnostics( + kept: ReadonlySet | null, + ): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { this.snapshot.ensureActive(); const program = this.project.program; const declarations = new Set([ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]); - return reusedLibDiagnostics( - program, - this.getSourceFileNames(), - (name) => - declarations.has(tsgoPath(name)) || - program.getSourceFileMetadataByPath(name)?.isDefaultLibrary === true, - (name) => this.getSourceFile(name), - ); + const names = this.getSourceFileNames(); + const supplied = (name: string): boolean => + declarations.has(tsgoPath(name)) || + program.getSourceFileMetadataByPath(name)?.isDefaultLibrary === true; + const sourceFile = (name: string): SourceFile | undefined => this.getSourceFile(name); + return kept === null + ? reusedLibDiagnostics(program, names, supplied, sourceFile) + : reusedKeptLibDiagnostics(program, names, supplied, sourceFile, kept); } isSourceFileDefaultLibrary(file: SourceFile): boolean { @@ -464,10 +478,35 @@ export class Ts7Program { /** 5.9.3's getPreEmitDiagnostics, composed from 7.0.2's split surface (the * package dropped the aggregate). Order mirrors 5.9.3: config, options/ - * program, global, syntactic, semantic. */ -export function getPreEmitDiagnostics(program: Ts7Program): readonly Diagnostic[] { + * program, global, syntactic, semantic. + * + * With `keep`, a caller that discards every diagnostic of the other files + * asks for semantic diagnostics of the kept files only: the server checks + * only those (getDiagnosticsOfCheckedFiles), or only those not answered by + * stored diagnostics of the supplied declaration files, and their + * diagnostics come in the full check's order. A global diagnostic can come + * from checking any file, so when that partial check reports one, the full + * check answers. */ +export function getPreEmitDiagnostics( + program: Ts7Program, + keep?: (fileName: string) => boolean, +): readonly Diagnostic[] { const config = program.getConfigFileParsingDiagnostics(); const options = program.getProgramDiagnostics(); + if (keep !== undefined) { + const kept = program.getSourceFileNames().filter((name) => keep(name)); + if (kept.length < program.getSourceFileNames().length) { + const partial = + program.reusedKeptLibDiagnostics(new Set(kept)) ?? keptFileDiagnostics(program, kept); + if (partial !== null && partial.global.length === 0) + return [ + ...config, + ...options, + ...program.getSyntacticDiagnostics(), + ...partial.semantic.map(ts7Diagnostic), + ]; + } + } const reused = program.reusedLibDiagnostics(); if (reused !== null) return [ @@ -486,6 +525,19 @@ export function getPreEmitDiagnostics(program: Ts7Program): readonly Diagnostic[ ]; } +/** The semantic diagnostics of the kept files, in the full check's order, + * and the global diagnostics of checking them; null when the server cannot + * check only some files. */ +function keptFileDiagnostics( + program: Ts7Program, + kept: readonly string[], +): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + const checked = program.project.program.checkedFileDiagnosticData(kept); + if (checked === null) return null; + const byFile = new Map(kept.map((name, index) => [name, checked.files[index] ?? []])); + return { semantic: inFileOrder(byFile), global: checked.global }; +} + /** ts.findConfigFile: nearest configName walking up from searchPath. */ export function findConfigFile( searchPath: string, From a96fbcfb641ec841f1ff5e98f5b8cd855782734e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 16:14:48 -0500 Subject: [PATCH 264/273] Refuse static member reads of MIMEType and MIMEParams in JavaScript JavaScript sources hold node:util's MIMEType and MIMEParams as identity tokens, so a read such as MIMEType.prototype or MIMEType.name answered undefined where Node returns the class's member. Those reads now report SC2020, as the TypeScript spelling does at compile time; in JavaScript the refusal throws when the read runs. The node24-mime-errors fixture reads MIMEType.prototype to take the getters' descriptors, so its expected refusal is now SC2020. --- .../compiler/src/frontend/lowering/builtins/mime.ts | 11 +++++++++++ .../frontend/lowering/expressions/property-access.ts | 2 ++ tests/harness/npm.test.ts | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/builtins/mime.ts b/packages/compiler/src/frontend/lowering/builtins/mime.ts index 27f95226c..4c36d6cc5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/mime.ts +++ b/packages/compiler/src/frontend/lowering/builtins/mime.ts @@ -134,6 +134,17 @@ export function lowerMimeCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr }; } +/** Reports SC2020 when `access` reads a static member of MIMEType or + * MIMEParams (`MIMEType.prototype`, `MIMEType.name`); null otherwise. + * JavaScript sources hold either class as an identity token, which has no + * members, so the read would answer undefined. MIMEType.parse calls lower + * in lowerMimeCall and never reach the member read. */ +export function fenceMimeStaticRead(lowerer: Lowerer, access: ts.PropertyAccessExpression): null { + const cls = mimeClassOf(lowerer, access.expression); + if (cls !== null) lowerer.noLowering(`util.${cls}.${access.name.text}`, access); + return null; +} + /** `v instanceof MIMEType` / `v instanceof MIMEParams`. */ export function lowerMimeInstanceof(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr | null { const cls = mimeClassOf(lowerer, expr.right); diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index df62da2c8..cfd7ae297 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -63,6 +63,7 @@ import { } from "../../../ir/ir.js"; import { lowerNavigatorMember } from "../builtins/navigator.js"; import { lowerX509PublicKey } from "../builtins/x509.js"; +import { fenceMimeStaticRead } from "../builtins/mime.js"; import { NODE_COMPAT_VERSION } from "../builtins/process.js"; import { inlineNamespaceRequireOf, @@ -942,6 +943,7 @@ export function lowerPropertyAccessExpr( // prototype that no lowering above claimed keeps the SC2020 fence // rather than reading undefined through the field and dynamic reads. fenceBuiltinConstructorStaticRead(lowerer, expr) ?? + fenceMimeStaticRead(lowerer, expr) ?? lowerer.lowerFieldRead(expr); // A union-typed field read narrows like an identifier when the // checker has narrowed this use to one arm. diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index d51a2e242..8e133f6e2 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -74,7 +74,7 @@ const KNOWN: Record = { "node24-file-url-conversion": { build: "SC1090" }, "node24-fs-descriptors": { run: "SC2020" }, "node24-mime": { run: "SC2020" }, - "node24-mime-errors": { run: "SC1090" }, + "node24-mime-errors": { run: "SC2020" }, "node24-no-error-assertions": { run: "SC2020" }, "node24-parseargs": { run: "SC1090" }, "node24-promise-timers": { run: "SC2020" }, From a9ea71df7ddec01a9e0c9984fe3981f4d4b68cf9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 16:22:25 -0500 Subject: [PATCH 265/273] Compare unions that hold promises as untyped values A promise converted into a union's promise arm is a new promise, so two optional slots set to one promise each hold their own conversion, and a value narrowed out of such a slot may be one too. Union equality compared those arm payloads by pointer and answered false where Node answers true. Any comparison between a union with a promise arm and a promise or another such union now compares both sides as untyped values, where a converted promise answers with its origin's identity. --- .../frontend/lowering/expressions/binary.ts | 27 ++++++++++--------- .../union-promise-function-arm-widening.ts | 9 +++++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 01b48554e..4e46b4d5f 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -2388,19 +2388,22 @@ export function lowerStrictEquality( loc, }; } - // A plain promise against a union whose promise arm has a different - // payload (`chains.get(id) === tail` with a `Map>`): - // wrapping would convert the promise into that arm, and the converted - // promise is a new object. Promises are objects to ===, so compare both - // sides as untyped values, where a converted promise answers with its - // origin's identity (the plain promise-vs-promise rule below). - const plainPromise = - left.type.kind === "promise" ? left : right.type.kind === "promise" ? right : null; + // A promise against a union with a promise arm, or two such unions: + // converting a promise into a union's promise arm of another payload + // (`chains.get(id) === tail` with a `Map>`, or + // `a.pending === b.pending` after both were set to one promise) builds a + // new promise, so either side may hold a conversion of the other's + // promise, and a value narrowed out of the union may be one too. + // Promises are objects to ===, so compare both sides as untyped values, + // where a converted promise answers with its origin's identity (the plain + // promise-vs-promise rule below). + const holdsPromise = (type: IrType): boolean => + type.kind === "promise" || + (type.kind === "union" && + (lowerer.unions.get(type.unionId)?.arms ?? []).some((arm) => arm.kind === "promise")); if ( - !bothUnion && - plainPromise !== null && - lowerer.armTag(ut.unionId, plainPromise.type) < 0 && - unionArms.some((arm) => arm.kind === "promise") && + holdsPromise(left.type) && + holdsPromise(right.type) && lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type) ) { diff --git a/tests/corpus/union-promise-function-arm-widening.ts b/tests/corpus/union-promise-function-arm-widening.ts index 751af6f41..d3b8f618d 100644 --- a/tests/corpus/union-promise-function-arm-widening.ts +++ b/tests/corpus/union-promise-function-arm-widening.ts @@ -58,6 +58,15 @@ async function main(): Promise { console.log(store.pending === other); console.log(await store.pending); + // One promise in two optional slots: each holds its own conversion. + const second: Store = {}; + second.pending = work; + console.log(store.pending === second.pending, store.pending !== second.pending); + const slots: Store[] = [store, second, { pending: other }]; + console.log(slots.map((slot) => slot.pending === slots[0]!.pending).join(",")); + second.lookup = findName; + console.log(store.lookup === second.lookup); + const lookup = store.lookup; if (lookup) console.log(await lookup("abcd"), await lookup("ab")); console.log(store.scale?.(21)); From e3f02c73c9cde475b83fae2db150f2cc5f0af083 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 16:32:27 -0500 Subject: [PATCH 266/273] Probe hashed number-key maps without normalizing integer keys A number-key Map or Set read whose key has an integer view (sc_map_entry_f64_int) fell back to the same hashed probe as any double key, which tests for NaN and -0 before hashing. The fallback is now sc_map_entry_f64_hashed_int, which takes the integer and converts it to the key word directly: an integer is never NaN, and zero converts to +0. Both hashed probes test the absent-key filter only before scanning a map that keeps no bucket table. The filter keeps two of its 64 bits per key, so it rejects few absent keys once a map has a bucket table, and the probe's first bucket read answers those. The map layout and the runtime lookup are unchanged. Corpus 8331 covers integers the compiler tracks as integers on hashed maps and absent keys on small bucket tables, and matches Node's output. --- .../compiler/src/backend/llvm/emit-module.ts | 254 ++++++++++-------- .../backend/llvm/map-number-lookup.test.ts | 22 +- .../src/backend/llvm/map-number-lookup.ts | 7 +- ...331-map-number-hashed-integer-keys.ts.json | 6 + .../8331-map-number-hashed-integer-keys.ts | 102 +++++++ ...331-map-number-hashed-integer-keys.ts.json | 1 + 6 files changed, 276 insertions(+), 116 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8331-map-number-hashed-integer-keys.ts.json create mode 100644 tests/corpus/8331-map-number-hashed-integer-keys.ts create mode 100644 tests/harness/corpus-costs/8331-map-number-hashed-integer-keys.ts.json diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index af00ebc2c..66e419fb9 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -1210,6 +1210,126 @@ export function emitModuleParts(emitter: LlEmitter): string[] { return out; } +/** The out-of-line hashed probe of a number-key map (scr_map_find_f64): + * `keyLines` define the normalized key word %kb from the parameter, and + * `runtimeKey` is the double passed to the runtime lookup. A map with at + * most SCR_MAP_LINEAR_LIMIT entries keeps no bucket table: the absent-key + * filter is tested before its entries are scanned. A bucket table is + * probed without the filter: the filter keeps two of its 64 bits per key, + * so past a few dozen keys it rejects few absent keys, and the probe's + * first bucket read answers those. Compact tables tag each 24-bit entry + * index (+1) with the hash's top byte; wider tables take the runtime + * lookup. */ +function hashedNumberProbe( + name: string, + param: string, + sz: string, + keyLines: string[], + runtimeKey: string, +): string[] { + return [ + `define internal ptr @${name}(ptr %m, ${param}) noinline ${FN_ATTRS} {`, + `entry:`, + ...keyLines, + ` %x1s = lshr i64 %kb, 30`, + ` %x1 = xor i64 %kb, %x1s`, + ` %x2 = mul i64 %x1, -4658895280553007687`, + ` %x3s = lshr i64 %x2, 27`, + ` %x3 = xor i64 %x2, %x3s`, + ` %x4 = mul i64 %x3, -7723592293110705685`, + ` %x5s = lshr i64 %x4, 31`, + ` %x5 = xor i64 %x4, %x5s`, + ` %hz = icmp eq i64 %x5, 0`, + ` %h = select i1 %hz, i64 1, i64 %x5`, + ` %nbp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 13`, + ...(sz === "i64" + ? [` %nb32 = load i32, ptr %nbp`, ` %nb = zext i32 %nb32 to i64`] + : [` %nb = load i32, ptr %nbp`]), + ` %eps = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`, + ` %ents = load ptr, ptr %eps`, + ` %linear = icmp eq ${sz} %nb, 0`, + ` br i1 %linear, label %filter, label %buckets`, + `filter:`, + ` %f1s = lshr i64 %h, 44`, + ` %f1a = and i64 %f1s, 63`, + ` %f1 = shl i64 1, %f1a`, + ` %f2s = lshr i64 %h, 50`, + ` %f2a = and i64 %f2s, 63`, + ` %f2 = shl i64 1, %f2a`, + ` %fb = or i64 %f1, %f2`, + ` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`, + ` %filter.bits = load i64, ptr %fp`, + ` %fm = and i64 %filter.bits, %fb`, + ` %fin = icmp eq i64 %fm, %fb`, + ` br i1 %fin, label %scan, label %miss`, + // A number-key entry has no cached hash; a dead entry's key matches no key. + `scan:`, + ` %nep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`, + ...(sz === "i64" + ? [` %ne32 = load i32, ptr %nep`, ` %ne = zext i32 %ne32 to i64`] + : [` %ne = load i32, ptr %nep`]), + ` br label %scan.loop`, + `scan.loop:`, + ` %si = phi ${sz} [ 0, %scan ], [ %si.next, %scan.next ]`, + ` %sdone = icmp eq ${sz} %si, %ne`, + ` br i1 %sdone, label %miss, label %scan.entry`, + `scan.entry:`, + ` %se = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %si`, + ` %sk = load i64, ptr %se`, + ` %skq = icmp eq i64 %sk, %kb`, + ` br i1 %skq, label %scan.found, label %scan.next`, + `scan.found:`, + ` ret ptr %se`, + `scan.next:`, + ` %si.next = add ${sz} %si, 1`, + ` br label %scan.loop`, + `buckets:`, + ` %wide = icmp ugt ${sz} %nb, 16777216`, + ` br i1 %wide, label %runtime, label %probe`, + `probe:`, + ` %bp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 14`, + ` %bk = load ptr, ptr %bp`, + ` %mask = sub ${sz} %nb, 1`, + ` %ht = lshr i64 %h, 32`, + ` %ht32 = trunc i64 %ht to i32`, + ` %tag = and i32 %ht32, -16777216`, + sz === "i64" ? ` %hi = add i64 %h, 0` : ` %hi = trunc i64 %h to ${sz}`, + ` %i0 = and ${sz} %hi, %mask`, + ` br label %probe.loop`, + `probe.loop:`, + ` %pi = phi ${sz} [ %i0, %probe ], [ %pi.next, %probe.next ]`, + ` %bep = getelementptr inbounds i32, ptr %bk, ${sz} %pi`, + ` %b = load i32, ptr %bep`, + ` %bempty = icmp eq i32 %b, 0`, + ` br i1 %bempty, label %miss, label %probe.tag`, + `probe.tag:`, + ` %bt = and i32 %b, -16777216`, + ` %btq = icmp eq i32 %bt, %tag`, + ` br i1 %btq, label %probe.entry, label %probe.next`, + `probe.entry:`, + ` %bix = and i32 %b, 16777215`, + ` %bix1 = sub i32 %bix, 1`, + sz === "i32" ? ` %bi = add i32 %bix1, 0` : ` %bi = zext i32 %bix1 to ${sz}`, + ` %pe = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %bi`, + ` %pk = load i64, ptr %pe`, + ` %pkq = icmp eq i64 %pk, %kb`, + ` br i1 %pkq, label %probe.found, label %probe.next`, + `probe.found:`, + ` ret ptr %pe`, + `probe.next:`, + ` %pi.inc = add ${sz} %pi, 1`, + ` %pi.next = and ${sz} %pi.inc, %mask`, + ` br label %probe.loop`, + `miss:`, + ` ret ptr null`, + `runtime:`, + ` %r = call ptr @scr_map_entry_f64(ptr %m, double ${runtimeKey}) memory(read) nounwind willreturn`, + ` ret ptr %r`, + `}`, + ``, + ]; +} + /** The shared abort helpers (emitted only when referenced): the OOM * abort of untraced shape allocation and the invalid-union-tag abort — * both print the runtime ABI’s exact message on fd 2 and abort. */ @@ -1349,118 +1469,34 @@ export function helperDefs(emitter: LlEmitter): string[] { ` %dense.any = icmp ne i64 %nd, 0`, ` br i1 %dense.any, label %miss, label %slow`, `slow:`, - ` %hashed = call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)`, + ` %hashed = call ptr @sc_map_entry_f64_hashed_int(ptr %m, i64 %ki)`, ` ret ptr %hashed`, `}`, ``, - // Hashing (scr_map_find_f64): the SameValueZero key word, its - // scr_key_mix hash (zero remapped to one) and the absent-key filter. - `define internal ptr @sc_map_entry_f64_hashed(ptr %m, double %k) noinline ${FN_ATTRS} {`, - `entry:`, - ` %nan = fcmp uno double %k, %k`, - ` %zero = fcmp oeq double %k, 0.0`, - ` %kz = select i1 %zero, double 0.0, double %k`, - ` %kzb = bitcast double %kz to i64`, - ` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`, - ` %x1s = lshr i64 %kb, 30`, - ` %x1 = xor i64 %kb, %x1s`, - ` %x2 = mul i64 %x1, -4658895280553007687`, - ` %x3s = lshr i64 %x2, 27`, - ` %x3 = xor i64 %x2, %x3s`, - ` %x4 = mul i64 %x3, -7723592293110705685`, - ` %x5s = lshr i64 %x4, 31`, - ` %x5 = xor i64 %x4, %x5s`, - ` %hz = icmp eq i64 %x5, 0`, - ` %h = select i1 %hz, i64 1, i64 %x5`, - ` %f1s = lshr i64 %h, 44`, - ` %f1a = and i64 %f1s, 63`, - ` %f1 = shl i64 1, %f1a`, - ` %f2s = lshr i64 %h, 50`, - ` %f2a = and i64 %f2s, 63`, - ` %f2 = shl i64 1, %f2a`, - ` %fb = or i64 %f1, %f2`, - ` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`, - ` %filter = load i64, ptr %fp`, - ` %fm = and i64 %filter, %fb`, - ` %fin = icmp eq i64 %fm, %fb`, - ` br i1 %fin, label %table, label %miss`, - `table:`, - ` %nbp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 13`, - ...(sz === "i64" - ? [` %nb32 = load i32, ptr %nbp`, ` %nb = zext i32 %nb32 to i64`] - : [` %nb = load i32, ptr %nbp`]), - ` %eps = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`, - ` %ents = load ptr, ptr %eps`, - ` %linear = icmp eq ${sz} %nb, 0`, - ` br i1 %linear, label %scan, label %buckets`, - // Small maps keep no bucket table: scan the entries' keys. A number-key - // entry has no cached hash; a dead entry's key matches no key. - `scan:`, - ` %nep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`, - ...(sz === "i64" - ? [` %ne32 = load i32, ptr %nep`, ` %ne = zext i32 %ne32 to i64`] - : [` %ne = load i32, ptr %nep`]), - ` br label %scan.loop`, - `scan.loop:`, - ` %si = phi ${sz} [ 0, %scan ], [ %si.next, %scan.next ]`, - ` %sdone = icmp eq ${sz} %si, %ne`, - ` br i1 %sdone, label %miss, label %scan.entry`, - `scan.entry:`, - ` %se = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %si`, - ` %sk = load i64, ptr %se`, - ` %skq = icmp eq i64 %sk, %kb`, - ` br i1 %skq, label %scan.found, label %scan.next`, - `scan.found:`, - ` ret ptr %se`, - `scan.next:`, - ` %si.next = add ${sz} %si, 1`, - ` br label %scan.loop`, - // Compact tables tag each 24-bit entry index (+1) with the hash's - // top byte; wider tables take the runtime lookup. - `buckets:`, - ` %wide = icmp ugt ${sz} %nb, 16777216`, - ` br i1 %wide, label %runtime, label %probe`, - `probe:`, - ` %bp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 14`, - ` %bk = load ptr, ptr %bp`, - ` %mask = sub ${sz} %nb, 1`, - ` %ht = lshr i64 %h, 32`, - ` %ht32 = trunc i64 %ht to i32`, - ` %tag = and i32 %ht32, -16777216`, - sz === "i64" ? ` %hi = add i64 %h, 0` : ` %hi = trunc i64 %h to ${sz}`, - ` %i0 = and ${sz} %hi, %mask`, - ` br label %probe.loop`, - `probe.loop:`, - ` %pi = phi ${sz} [ %i0, %probe ], [ %pi.next, %probe.next ]`, - ` %bep = getelementptr inbounds i32, ptr %bk, ${sz} %pi`, - ` %b = load i32, ptr %bep`, - ` %bempty = icmp eq i32 %b, 0`, - ` br i1 %bempty, label %miss, label %probe.tag`, - `probe.tag:`, - ` %bt = and i32 %b, -16777216`, - ` %btq = icmp eq i32 %bt, %tag`, - ` br i1 %btq, label %probe.entry, label %probe.next`, - `probe.entry:`, - ` %bix = and i32 %b, 16777215`, - ` %bix1 = sub i32 %bix, 1`, - sz === "i32" ? ` %bi = add i32 %bix1, 0` : ` %bi = zext i32 %bix1 to ${sz}`, - ` %pe = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %bi`, - ` %pk = load i64, ptr %pe`, - ` %pkq = icmp eq i64 %pk, %kb`, - ` br i1 %pkq, label %probe.found, label %probe.next`, - `probe.found:`, - ` ret ptr %pe`, - `probe.next:`, - ` %pi.inc = add ${sz} %pi, 1`, - ` %pi.next = and ${sz} %pi.inc, %mask`, - ` br label %probe.loop`, - `miss:`, - ` ret ptr null`, - `runtime:`, - ` %r = call ptr @scr_map_entry_f64(ptr %m, double %k) memory(read) nounwind willreturn`, - ` ret ptr %r`, - `}`, - ``, + // Hashing (scr_map_find_f64): the SameValueZero key word and its + // scr_key_mix hash. sc_map_entry_f64_int's probe converts the integer + // view to the key word, which is never NaN or -0, and so skips the + // normalization. + ...hashedNumberProbe( + "sc_map_entry_f64_hashed", + "double %k", + sz, + [ + ` %nan = fcmp uno double %k, %k`, + ` %zero = fcmp oeq double %k, 0.0`, + ` %kz = select i1 %zero, double 0.0, double %k`, + ` %kzb = bitcast double %kz to i64`, + ` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`, + ], + "%k", + ), + ...hashedNumberProbe( + "sc_map_entry_f64_hashed_int", + "i64 %ki", + sz, + [` %kf = sitofp i64 %ki to double`, ` %kb = bitcast double %kf to i64`], + "%kf", + ), ); } // Map/Set iteration steps read the entries array inline: an index is diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts index 7457db42d..f7bd87af1 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts @@ -168,8 +168,10 @@ test("hashed number-key lookups probe inline and call the runtime only for wide expect(hashed).toContain("select i1 %nan, i64 9221120237041090560, i64 %kzb"); expect(hashed).toContain("mul i64 %x1, -4658895280553007687"); expect(hashed).toContain("mul i64 %x3, -7723592293110705685"); - // Filter (field 10), small-map scan of the entry count (field 8), and - // compact buckets (fields 13 and 14) tagged with the hash's top byte. + // Filter (field 10) before the small-map scan of the entry count (field + // 8) only, and compact buckets (fields 13 and 14) tagged with the hash's + // top byte. + expect(hashed).toContain("br i1 %linear, label %filter, label %buckets"); expect(hashed).toContain("getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10"); expect(hashed).toContain(`load i32, ptr %nep`); expect(hashed).toContain(`load i32, ptr %nbp`); @@ -192,11 +194,23 @@ test("a key with an integer view tests the direct index on the integer", () => { integral.module, )![0]; // An unsigned compare also rejects negative keys; the hashed probe takes - // the double. + // the integer. expect(helper).toContain("icmp ult i64 %ki, %nd"); expect(helper).toContain(`getelementptr inbounds i32, ptr %dense, ${sz} %i`); - expect(helper).toContain("call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)"); + expect(helper).toContain("call ptr @sc_map_entry_f64_hashed_int(ptr %m, i64 %ki)"); expect(helper).not.toContain("fptosi"); + // Its key word is the integer's double, which needs no NaN or -0 + // normalization; the runtime lookup gets that double. + const hashed = + /^define internal ptr @sc_map_entry_f64_hashed_int\(ptr %m, i64 %ki\) noinline [^]*?^}/m.exec( + integral.module, + )![0]; + expect(hashed).toContain("sitofp i64 %ki to double"); + expect(hashed).not.toContain("fcmp"); + expect(hashed).toContain("mul i64 %x1, -4658895280553007687"); + expect(hashed).toContain( + "call ptr @scr_map_entry_f64(ptr %m, double %kf) memory(read) nounwind willreturn", + ); // A fractional key and a key without a proof keep the double lookup. for (const key of [{ kind: "numLit", value: 0.5, type: F64, loc } as IrExpr, ref("k", F64)]) { const { work } = emitted(lookupFixture(key), bits); diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.ts b/packages/compiler/src/backend/llvm/map-number-lookup.ts index afc8ec606..17a1563c3 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.ts @@ -4,9 +4,10 @@ * direct index in the runtime (scr_map.c). Reads go through the internal * sc_map_entry_f64 helper (emitter.ts helperDefs), which LLVM inlines: it * resolves directly indexed maps without a call and other maps through - * the internal sc_map_entry_f64_hashed probe, which hashes the key as the - * runtime does and defers to scr_map_entry_f64 only for wide bucket - * tables. Both only read memory. The generated code then reads the entry's + * the internal sc_map_entry_f64_hashed probe (sc_map_entry_f64_hashed_int + * for a key with an integer view), which hashes the key as the runtime + * does and defers to scr_map_entry_f64 only for wide bucket tables. Both + * only read memory. The generated code then reads the entry's * value slot itself and retains a reference value with the value type's own * retain, which is the adapter the collection was constructed with. * String-keyed reads use the same entry contract through the internal diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8331-map-number-hashed-integer-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8331-map-number-hashed-integer-keys.ts.json new file mode 100644 index 000000000..86822b641 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8331-map-number-hashed-integer-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8331-map-number-hashed-integer-keys.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8331-map-number-hashed-integer-keys.ts b/tests/corpus/8331-map-number-hashed-integer-keys.ts new file mode 100644 index 000000000..8af7293e9 --- /dev/null +++ b/tests/corpus/8331-map-number-hashed-integer-keys.ts @@ -0,0 +1,102 @@ +// Number-keyed Map and Set lookups whose keys are integers the compiler +// tracks as integers (loop counters, literals, integer arithmetic) on +// hashed storage, and lookups of absent keys on small bucket tables. + +// Keys spread past the direct index's range, so the maps hash them. +const spread = new Map(); +for (let i = 0; i < 500; i++) spread.set(i * 104729 - 26000000, i); +let found = 0, + absent = 0, + total = 0; +for (let i = -10; i < 520; i++) { + const v = spread.get(i * 104729 - 26000000); + if (v === undefined) absent++; + else { + found++; + total += v; + } +} +console.log("spread", spread.size, found, absent, total); +console.log("spread literals", spread.get(-26000000), spread.get(-25895271), spread.get(0), spread.has(-1)); + +// Zero stored through -0 and through an integer; read back through both. +const zeros = new Map(); +for (let i = 1; i <= 20; i++) zeros.set(i * 1000003, "k" + i); +zeros.set(-0, "minus zero"); +let z = 0; +console.log("zeros", zeros.size, zeros.get(z), zeros.get(0), zeros.get(-0), zeros.get(z * -1)); +zeros.set(z, "zero"); +console.log("zeros overwrite", zeros.size, zeros.get(-0), [...zeros.keys()].includes(0)); + +// Large and negative integer keys. +const wide = new Map(); +for (let i = 0; i < 64; i++) wide.set(2 ** 40 + i * 3, i); +for (let i = 0; i < 64; i++) wide.set(-(2 ** 33) - i, -i); +let wideUp = 0, + wideDown = 0, + wideMissing = 0; +for (let i = 0; i < 200; i++) { + const a = wide.get(2 ** 40 + i); + const b = wide.get(-(2 ** 33) - i); + if (a === undefined) wideMissing++; + else wideUp += a; + if (b === undefined) wideMissing++; + else wideDown += b; +} +console.log("wide", wide.size, wideUp, wideDown, wideMissing, wide.get(1099511627776), wide.get(-8589934592)); + +// Absent keys on bucket tables just past the linear scan, and as they grow. +for (const size of [9, 12, 16, 33, 64, 100, 257, 400]) { + const m = new Map(); + for (let i = 0; i < size; i++) m.set(i * 7919 + 1000000, i); + let hits = 0, + misses = 0; + for (let i = 0; i < size * 4; i++) { + if (m.has(i * 7919 + 1000000)) hits++; + else misses++; + if (m.get(i * 7919 + 1000001) !== undefined) hits += 1000; + } + for (let i = 0; i < size; i += 2) m.delete(i * 7919 + 1000000); + let after = 0; + for (let i = 0; i < size; i++) if (m.has(i * 7919 + 1000000)) after++; + console.log("size", size, hits, misses, m.size, after, m.get(1000000), m.get(1007919)); +} + +// Small maps that scan their entries, with integer keys. +const tiny = new Map(); +for (let i = 0; i < 8; i++) tiny.set(i * 5000011, "t" + i); +let tinyHits = 0; +for (let i = 0; i < 40; i++) if (tiny.has(i * 5000011)) tinyHits++; +console.log("tiny", tiny.size, tinyHits, tiny.get(0), tiny.get(35000077), tiny.has(5000012)); + +// Sets with integer keys. +const seen = new Set(); +let dupes = 0; +for (let i = 0; i < 3000; i++) { + const k = ((i * 2654435761) % 1000003) - 500000; + if (seen.has(k)) dupes++; + else seen.add(k); +} +let present = 0; +for (let i = -500000; i < 500003; i += 997) if (seen.has(i)) present++; +console.log("set", seen.size, dupes, present, seen.has(-500000), seen.has(500002)); + +// A counting loop like a k-mer histogram: get, then update the found entry. +class Count { + n = 0; +} +const counts = new Map(); +let key = 0; +for (let i = 0; i < 20000; i++) { + key = ((key * 4) % 4194304) + (i % 7 === 0 ? 3 : i % 3); + const c = counts.get(key); + if (c === undefined) { + const fresh = new Count(); + fresh.n = 1; + counts.set(key, fresh); + } else c.n++; +} +let most = 0, + mostKey = -1; +for (const [k, c] of counts) if (c.n > most) [most, mostKey] = [c.n, k]; +console.log("counts", counts.size, most, mostKey); diff --git a/tests/harness/corpus-costs/8331-map-number-hashed-integer-keys.ts.json b/tests/harness/corpus-costs/8331-map-number-hashed-integer-keys.ts.json new file mode 100644 index 000000000..961fe0dad --- /dev/null +++ b/tests/harness/corpus-costs/8331-map-number-hashed-integer-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 3441, "san": 1752 } From 3a900ed9b6d8b43cdb7b764228dcd134df7f76d7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 16:38:27 -0500 Subject: [PATCH 267/273] Keep the poll in nested loops whose counter can stop changing at 2^53 The range analysis caps a counter that steps by 1 at 2^53, where `j + 1 === j`, so a loop such as `for (let j = 2 ** 53 - 2; j < 2 ** 53 + 10; j++)` has a range of three values at its condition and was taken for a loop of three runs. It never ends, and without its poll worker.terminate() could not stop it. A loop's runs now count as bounded only when a step from every value in its counter's range gives a different value. --- .../src/backend/llvm/emit-statements.ts | 15 ++++++++++++--- .../test/counted-loop-emission.test.ts | 19 ++++++++++++++++++- .../native-worker-terminate-bounded-loops.ts | 15 +++++++++++++-- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index e0db764e8..cc0af8ebb 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -2655,6 +2655,14 @@ const WORKER_POLL_BLOCK = 64; * only as it leaves. */ const WORKER_UNPOLLED_RUN = 16384; +/** Whether a step of `step` from every value in `range` gives a different + * value. Beyond 2^53 a double can stop changing (`j + 1 === j` from 2^53), + * so a counter there can run without end inside a range of a few values. */ +function exactSteps(range: { min: number; max: number }, step: number): boolean { + const size = Math.abs(step); + return range.min >= size - 2 ** 53 && range.max <= 2 ** 53 - size; +} + /** At most how many times a `for` loop and the loops in its body run their * bodies per entry, from the counters' proven ranges: its iterations times * one plus the nested loops' runs. Infinity when a count has no bound, or @@ -2668,12 +2676,12 @@ function boundedLoopRuns(emitter: LlEmitter, loop: IrStmt): number { LOOPS.of(emitter).unaliasedArrays, ); let trips = Infinity; - if (counted && !counted.guarded) + if (counted && !counted.guarded && exactSteps(counted.range, counted.step)) trips = Math.floor((counted.range.max - counted.range.min) / Math.abs(counted.step)) + 1; else if (!counted) { // Each iteration closes the gap between the two counters by at least two. const paired = matchIntegerPairedForLoop(loop, emitter.numericLocals, emitter.integerRanges); - if (paired && !paired.guarded) + if (paired && !paired.guarded && exactSteps(paired.range, 1)) trips = Math.floor((paired.range.max - paired.range.min) / 2) + 1; } if (trips === Infinity) { @@ -2681,7 +2689,8 @@ function boundedLoopRuns(emitter: LlEmitter, loop: IrStmt): number { // one compared with a module constant. const stepped = steppedLoopCounter(loop, emitter.currentLocals); const range = stepped && emitter.integerRanges.get(stepped.ref); - if (stepped && range) trips = Math.floor((range.max - range.min) / Math.abs(stepped.step)) + 1; + if (stepped && range && exactSteps(range, stepped.step)) + trips = Math.floor((range.max - range.min) / Math.abs(stepped.step)) + 1; } if (!(trips <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK)) return Infinity; let nested = 0; diff --git a/packages/compiler/test/counted-loop-emission.test.ts b/packages/compiler/test/counted-loop-emission.test.ts index 794edd1af..d7ebc97fa 100644 --- a/packages/compiler/test/counted-loop-emission.test.ts +++ b/packages/compiler/test/counted-loop-emission.test.ts @@ -175,6 +175,18 @@ test("worker programs poll no loop whose proven ranges bound its runs inside ano " for (let r = 0; r < n; r++) for (let j = 0; j < 5000; j++) t = (t * 31 + j) % 1000003;", " return t;", "}", + "export function exact(n: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++)", + " for (let j = 9007199254740900; j < 9007199254740950; j++) t = (t + (j % 7)) % 1000003;", + " return t;", + "}", + "export function stalled(n: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++)", + " for (let j = 9007199254740990; j < 9007199254741002; j++) t = (t + (j % 7)) % 1000003;", + " return t;", + "}", "export function single(): number {", " let t = 0;", " for (let j = 0; j < 50; j++) t = (t * 31 + j) % 1000003;", @@ -182,7 +194,7 @@ test("worker programs poll no loop whose proven ranges bound its runs inside ano "}", "const cr = new Float64Array(8).fill(0.5);", 'const k = Number(process.argv[2] ?? "50");', - "console.log(rows(cr, 3), open(cr, 3, k), wide(2), single());", + "console.log(rows(cr, 3), open(cr, 3, k), wide(2), exact(2), stalled(0), single());", "if (!isMainThread) new Worker(new URL(import.meta.url));", ].join("\n"), ); @@ -219,6 +231,11 @@ test("worker programs poll no loop whose proven ranges bound its runs inside ano // A run of up to 16384 iterations is one block, polled as it leaves when // it took more than 64. expect(open).toMatch(/^loop\.poll\.run\d+:$/m); + // Steps of 1 below 2^53 are exact, so 50 runs near it are bounded. From + // 2^53, `j + 1 === j`: a counter ranging up to it can run without end + // and keeps its poll. + expect(polls(body("exact"))).toBe(polls(body("single"))); + expect(polls(body("stalled"))).toBeGreaterThan(polls(body("exact"))); // A nest over the bound, or a loop no other loop encloses, polls. expect(polls(body("wide"))).toBeGreaterThan(0); expect(body("wide")).toMatch(/^loop\.poll\d+:$/m); diff --git a/tests/corpus/native-worker-terminate-bounded-loops.ts b/tests/corpus/native-worker-terminate-bounded-loops.ts index d244e021d..ec1e4c736 100644 --- a/tests/corpus/native-worker-terminate-bounded-loops.ts +++ b/tests/corpus/native-worker-terminate-bounded-loops.ts @@ -1,7 +1,8 @@ // worker.terminate() stops a worker whose loop runs short inner loops: // counted loops of a few iterations, a two-counter loop, an inner loop // whose length grows past 64 iterations, an inner loop too long to finish, -// and a nest of loops with constant bounds. +// a nest of loops with constant bounds, and an inner loop whose counter +// stops changing at 2 ** 53 (`j + 1 === j` there), so it never ends. import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; function counted(values: number[]): number { @@ -66,8 +67,17 @@ function constant(row: Float64Array): number { return acc; } +function stalled(): number { + let acc = 0; + for (let round = 0; ; round++) { + if (round === 0) parentPort!.postMessage("ready"); + for (let j = 9007199254740990; j < 9007199254741002; j++) acc = (acc + (j % 7)) % 1000003; + } + return acc; +} + if (isMainThread) { - for (const kind of ["counted", "paired", "growing", "long", "constant"]) { + for (const kind of ["counted", "paired", "growing", "long", "constant", "stalled"]) { await new Promise((resolve) => { const worker = new Worker(new URL(import.meta.url), { workerData: kind }); worker.once("message", () => { @@ -86,6 +96,7 @@ if (isMainThread) { else if (workerData === "paired") result = paired(values); else if (workerData === "growing") result = growing(); else if (workerData === "long") result = long(1e15); + else if (workerData === "stalled") result = stalled(); else result = constant(new Float64Array(8).fill(0.25)); console.log("unexpected result", result); } From 5f76d41a7181348e2e14d60a2ec3377f825bc3d5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 16:57:11 -0500 Subject: [PATCH 268/273] Keep U+11A7 after a Hangul LV syllable in NFC and NFKC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit libunicode composes an LV syllable with U+11A7 as if it were a trailing consonant (U+11A8..U+11C2), which drops it: "가ᆧ".normalize() gave "가" where Node gives "가ᆧ". U+11A7 is a class-0 vowel that composes with nothing, so for the composing forms the code points between occurrences of it normalize on their own and it is kept as it is. The guard sits in front of the vendored library (scr_unicode_normalize), for String.prototype.normalize and the NFC step of IDNA mapping. 8009-normalize-hangul-jungseong-o-yae prints the code points of all four forms for U+11A7 after LV and LVT syllables, conjoining jamo, other starters and combining marks. --- ...9-normalize-hangul-jungseong-o-yae.ts.json | 6 ++ packages/runtime/src/scr_regex.c | 55 ++++++++++++++++++- .../8009-normalize-hangul-jungseong-o-yae.ts | 28 ++++++++++ ...9-normalize-hangul-jungseong-o-yae.ts.json | 1 + 4 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts.json create mode 100644 tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts create mode 100644 tests/harness/corpus-costs/8009-normalize-hangul-jungseong-o-yae.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts.json new file mode 100644 index 000000000..872a85396 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index 2a98e92c6..37f94bd65 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -2418,6 +2418,57 @@ static void *scr_normalize_realloc(void *opaque, void *ptr, size_t size) { return realloc(ptr, size); } +/* unicode_normalize, except that U+11A7 never composes. libunicode composes + * an LV syllable with U+11A7 as a trailing consonant (those are U+11A8 to + * U+11C2), which drops it: "\uAC00\u11A7" became "\uAC00". U+11A7 is a + * class-0 vowel that composes with nothing, so for the composing forms the + * code points between occurrences of it normalize on their own and it is + * kept as it is, as in Node. */ +static int scr_unicode_normalize(uint32_t **pdst, const uint32_t *src, int len, + UnicodeNormalizationEnum mode) { + int at = 0; + if (mode == UNICODE_NFC || mode == UNICODE_NFKC) + while (at < len && src[at] != 0x11A7) at++; + else + at = len; + if (at == len) return unicode_normalize(pdst, src, len, mode, NULL, scr_normalize_realloc); + uint32_t *out = NULL; + size_t count = 0, cap = 0; + int start = 0; + for (int i = 0; i <= len; i++) { + if (i < len && src[i] != 0x11A7) continue; + uint32_t *part = NULL; + int n = 0; + if (i > start) { + n = unicode_normalize(&part, src + start, i - start, mode, NULL, scr_normalize_realloc); + if (n < 0) { + free(out); + return -1; + } + } + size_t need = count + (size_t)n + 1; + if (need > cap) { + size_t grown = cap ? cap * 2 : 16; + if (grown < need) grown = need; + uint32_t *bigger = realloc(out, grown * sizeof *out); + if (!bigger) { + free(part); + free(out); + return -1; + } + out = bigger; + cap = grown; + } + if (n > 0) memcpy(out + count, part, (size_t)n * sizeof *out); + count += (size_t)n; + free(part); + if (i < len) out[count++] = 0x11A7; + start = i + 1; + } + *pdst = out; + return (int)count; +} + ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form) { UnicodeNormalizationEnum mode; if (form->len == 3 && !memcmp(form->data, "NFC", 3)) mode = UNICODE_NFC; @@ -2442,7 +2493,7 @@ ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form) { offset += step; } uint32_t *output = NULL; - int count = unicode_normalize(&output, input, length, mode, NULL, scr_normalize_realloc); + int count = scr_unicode_normalize(&output, input, length, mode); free(input); if (count < 0) scr_regex_oom(); ScrJsonBuf buffer; @@ -2455,7 +2506,7 @@ ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form) { uint32_t *scr_nfc_code_points(const uint32_t *input, size_t length, size_t *out_len) { if (length > INT32_MAX) scr_regex_oom(); uint32_t *output = NULL; - int count = unicode_normalize(&output, input, (int)length, UNICODE_NFC, NULL, scr_normalize_realloc); + int count = scr_unicode_normalize(&output, input, (int)length, UNICODE_NFC); if (count < 0) scr_regex_oom(); *out_len = (size_t)count; return output; diff --git a/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts b/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts new file mode 100644 index 000000000..b7cb5418a --- /dev/null +++ b/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts @@ -0,0 +1,28 @@ +// U+11A7 (HANGUL JUNGSEONG O-YAE) is a vowel that composes with nothing: +// after an LV syllable, after a leading consonant, and around trailing +// consonants it stays as it is in every normalization form, as in Node. + +const samples = [ + "가ᆧ", + "가ᆧ", + "가ᆧᆨ", + "각ᆧ", + "ᄀᆧ", + "ᆧ", + "ᆧᆧ", + "x가ᆧy각z", + "café 가ᆧ é", + "ガᆧ가", +]; + +function points(text: string): string { + return Array.from(text, (ch) => ch.codePointAt(0)!.toString(16)).join("."); +} + +for (const sample of samples) { + console.log( + points(sample), + ["NFC", "NFD", "NFKC", "NFKD"].map((form) => points(sample.normalize(form))).join(" "), + ); +} +console.log("가ᆧ".normalize() === "가ᆧ", "가ᆧ".normalize().length); diff --git a/tests/harness/corpus-costs/8009-normalize-hangul-jungseong-o-yae.ts.json b/tests/harness/corpus-costs/8009-normalize-hangul-jungseong-o-yae.ts.json new file mode 100644 index 000000000..6c20e6ad7 --- /dev/null +++ b/tests/harness/corpus-costs/8009-normalize-hangul-jungseong-o-yae.ts.json @@ -0,0 +1 @@ +{ "plain": 2503, "san": 2110 } From 605e312e788131d8a3067227a0abdd1a2fbfb0fe Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 14:00:29 -0700 Subject: [PATCH 269/273] Let boxable function arms ride a checked-dynamic union A union with a checked-dynamic arm (`unknown`, fetch's AbortSignal) maps to the checked-dynamic value when every other arm is one the checked-dynamic tree holds faithfully. Function arms were excluded, so `Progress | AbortSignal | undefined` was refused (SC2009). The tree boxes a function whose parameters check in and whose result converts out by reference (canBoxFuncIntoDyn), and `typeof x === "function"` narrows it back out to the typed function. Such arms now join the collapse. The corpus program checks calls, identity (`===` against the original), and the other arm's members against Node. pi's SessionManager.listAll takes `onProgressOrSignal?: SessionListProgress | AbortSignal`. The refusal rejected the whole SessionManager class. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/frontend/type-mapper.ts | 11 +++++ .../corpus/dyn-union-function-arms.ts.json | 6 +++ tests/corpus/dyn-union-function-arms.ts | 48 +++++++++++++++++++ .../dyn-union-function-arms.ts.json | 1 + 4 files changed, 66 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-union-function-arms.ts.json create mode 100644 tests/corpus/dyn-union-function-arms.ts create mode 100644 tests/harness/corpus-costs/dyn-union-function-arms.ts.json diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 17f9d0d11..03d85ba7d 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -10,6 +10,7 @@ import { isDynTypedRefType, BOOL, bytesOf, + canBoxFuncIntoDyn, canConvertToDyn, canDynCheckTo, CHILD_T, @@ -8341,6 +8342,16 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean { // `string | URL | Request`, fetch's own input type under @types/node. case "url": return true; + // A function the checked-dynamic tree can box (each parameter checks + // in, the result converts out) is held by reference there, and + // `typeof x === "function"` narrows it back out: `Progress | AbortSignal` + // beside fetch's handles. + case "func": + return canBoxFuncIntoDyn( + arm, + (id) => ctx.shapes.get(id), + (id) => ctx.unions.get(id), + ); case "record": case "array": // `T | Promise` with T bound to `unknown`: the checked-dynamic value diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-union-function-arms.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-union-function-arms.ts.json new file mode 100644 index 000000000..80675e5e6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-union-function-arms.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/dyn-union-function-arms.ts" + ], + "diags": [] +} diff --git a/tests/corpus/dyn-union-function-arms.ts b/tests/corpus/dyn-union-function-arms.ts new file mode 100644 index 000000000..e85cc2e6b --- /dev/null +++ b/tests/corpus/dyn-union-function-arms.ts @@ -0,0 +1,48 @@ +// A union of a function type with a value the checked-dynamic tree holds +// (AbortSignal, `unknown`) rides that representation: the function is held +// by reference, `typeof x === "function"` narrows it back out, and calls, +// identity and the other arm's members behave as in Node. + +type Progress = (loaded: number, total: number) => void; + +function listAll( + dirOrProgress?: string | Progress, + progressOrSignal?: Progress | AbortSignal, + signal?: AbortSignal, +): string { + const dir = typeof dirOrProgress === "string" ? dirOrProgress : "/default"; + const progress = + typeof dirOrProgress === "function" + ? dirOrProgress + : typeof progressOrSignal === "function" + ? progressOrSignal + : undefined; + const abortSignal = + typeof dirOrProgress === "string" || typeof progressOrSignal === "function" + ? signal + : (progressOrSignal ?? signal); + progress?.(1, 3); + progress?.(3, 3); + return `${dir} aborted=${abortSignal?.aborted ?? "none"}`; +} + +const seen: string[] = []; +const report: Progress = (loaded, total) => seen.push(`${loaded}/${total}`); +const controller = new AbortController(); +console.log(listAll()); +console.log(listAll("/x", report)); +console.log(listAll(report, controller.signal)); +controller.abort(); +console.log(listAll("/y", undefined, controller.signal)); +console.log(listAll(report, controller.signal), seen.join(" ")); + +function same(handler: Progress | AbortSignal | undefined): boolean { + return handler === report; +} +console.log(same(report), same(controller.signal), same(undefined), same((a, b) => void (a + b))); + +function describe(value: unknown | ((n: number) => string)): string { + if (typeof value === "function") return `function -> ${(value as (n: number) => string)(7)}`; + return `value ${JSON.stringify(value)}`; +} +console.log(describe((n: number) => `n=${n}`), describe({ a: 1 }), describe("s")); diff --git a/tests/harness/corpus-costs/dyn-union-function-arms.ts.json b/tests/harness/corpus-costs/dyn-union-function-arms.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/dyn-union-function-arms.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } From 3375304945e1b6e548d501b3c9d403f1c5d155fa Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 14:40:32 -0700 Subject: [PATCH 270/273] Erase lambdas that implement a generic method through their context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A lambda with no type parameters of its own, contextually typed by a generic signature (`send: (m) => …` for an interface's `send(m: Message): string`), takes that signature's type parameters: its parameter is `Message`. A generic lambda written with its own type parameters already lowers once, erased, with them bound to `unknown`, but this one tried to map `Message` with `T` unbound and was refused (SC2009). It now lowers the same way, erased, binding the contextual signature's type parameters to `unknown`. Parameters bound in the enclosing scope keep their bindings. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-functions.ts | 42 +++++++++++++++++++ .../generic-member-lambda-impls.ts.json | 6 +++ tests/corpus/generic-member-lambda-impls.ts | 40 ++++++++++++++++++ .../generic-member-lambda-impls.ts.json | 1 + 4 files changed, 89 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-member-lambda-impls.ts.json create mode 100644 tests/corpus/generic-member-lambda-impls.ts create mode 100644 tests/harness/corpus-costs/generic-member-lambda-impls.ts.json diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index bd9b5f55d..7f4c8dfb7 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -167,6 +167,23 @@ function declareFunctionArguments( } } +/** The type parameters a lambda without its own takes from a generic + * contextual signature, unbound in the current scope. */ +function contextualTypeParameterSymbols( + lowerer: Lowerer, + node: ts.ArrowFunction | ts.FunctionExpression, +): ts.Symbol[] { + const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(node)); + if (signatures.length !== 1) return []; + const symbols: ts.Symbol[] = []; + for (const parameter of signatures[0]!.getTypeParameters()) { + const symbol = parameter.getSymbol(); + if (!symbol || lowerer.typeParamBindings?.has(symbol)) return []; + symbols.push(symbol); + } + return symbols; +} + /** Lifts an arrow function / function expression / nested declaration / * object-literal shorthand method to a module-level function and yields * the `closure` expression creating it. */ @@ -202,6 +219,31 @@ export function lowerLambda( lowerer.typeParamBindings = previous; } } + // A lambda contextually typed by a GENERIC signature (`send: (m) => …` + // for `send(m: Msg): string`) takes that signature's type + // parameters as its own; it lowers once, erased, like a generic lambda. + if ( + (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && + !node.typeParameters?.nodes.length && + !lowerer.erasingLambdas.has(node) + ) { + const contextual = contextualTypeParameterSymbols(lowerer, node); + if (contextual.length > 0) { + const previous = lowerer.typeParamBindings; + const bindings = new Map(previous ?? []); + for (const symbol of contextual) bindings.set(symbol, DYN); + lowerer.typeParamBindings = bindings; + lowerer.erasingLambdas.add(node); + lowerer.erasingDepth++; + try { + return lowerLambda(lowerer, node, checkedReceiver); + } finally { + lowerer.erasingDepth--; + lowerer.erasingLambdas.delete(node); + lowerer.typeParamBindings = previous; + } + } + } const prepared = ts.isArrowFunction(node) || ts.isFunctionExpression(node) ? lowerer.preparedClassFactories.get(node) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-member-lambda-impls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-member-lambda-impls.ts.json new file mode 100644 index 000000000..bc87c6ea0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-member-lambda-impls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/generic-member-lambda-impls.ts" + ], + "diags": [] +} diff --git a/tests/corpus/generic-member-lambda-impls.ts b/tests/corpus/generic-member-lambda-impls.ts new file mode 100644 index 000000000..fd25bae96 --- /dev/null +++ b/tests/corpus/generic-member-lambda-impls.ts @@ -0,0 +1,40 @@ +// An object literal implements an interface's generic method with a plain +// lambda (`send: (m) => …` for `send(m: Message): string`). The +// lambda takes the contextual signature's type parameters as its own and +// lowers once, erased, like a generic lambda: its parameters hold whatever +// each call passes. + +interface Message { + content: string; + details?: T; +} + +interface Sender { + name: string; + send(message: Message): string; + notify(message: Message): void; + pick(keys: K[], fallback: K): K; +} + +const log: string[] = []; + +function makeSender(name: string): Sender { + return { + name, + send: (message) => `${name} sent ${message.content} (${message.details === undefined ? "no details" : JSON.stringify(message.details)})`, + notify: (message) => { + log.push(`${name} notified ${message.content}`); + }, + pick: (keys, fallback) => keys[0] ?? fallback, + }; +} + +const senders: Sender[] = [makeSender("first"), makeSender("second")]; +for (const sender of senders) { + console.log(sender.send({ content: "hi", details: { n: 3 } })); + console.log(sender.send({ content: "bare" })); + console.log(sender.send({ content: "list", details: [1, 2] })); + sender.notify({ content: "ping", details: true }); + console.log(sender.pick(["a", "b"], "z"), sender.pick([], "z")); +} +console.log(log.join("; ")); diff --git a/tests/harness/corpus-costs/generic-member-lambda-impls.ts.json b/tests/harness/corpus-costs/generic-member-lambda-impls.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/generic-member-lambda-impls.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } From 1733a4c84bf815fa283ddcb8672d4ef3df18f5fe Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 25 Oct 2026 15:17:09 -0700 Subject: [PATCH 271/273] Name new corpus programs instead of numbering them Parallel changes kept taking the same next corpus number (6992 twice, the Node island programs renumbered several times). The harness never needed the numbers: it discovers every entry by glob, and 818 programs already have descriptive names. AGENTS.md now makes a name, without a number, the convention for new programs and keeps the numbered ones as they are. The two scripts that sampled programs by number skipped named programs. They now key a named program by a stable hash of its name, so they sample it at the same rate; the numbered programs they pick are unchanged. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 ++ scripts/lowering-order.mjs | 24 ++++++++++++++++++++---- scripts/refresh-cli-profile.mjs | 29 ++++++++++++++++++++++------- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2350d7337..55ccb2eab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,8 @@ SCRIPTC_TEST_WORKERS=4 SCRIPTC_SAN=1 pnpm test # sanitized lane Corpus programs are differential tests against Node: every program runs under Node and as a compiled native binary, and stdout, stderr, and exit codes must match byte-for-byte. A new feature lands with corpus programs that pin its behavior both ways. +Name a new corpus program after what it tests, without a number: lowercase words joined by hyphens, such as `tests/corpus/intersection-restated-members.ts`, or a directory such as `tests/corpus//main.ts` for a multi-file program. Parallel changes kept taking the same next number; with names, two changes collide only when they add the same name, which git reports as a conflict. Leave existing numbered programs as they are: never rename or renumber them, and name a new program even when it extends a numbered family. + Record a new or remeasured program's CI cost (used to balance the test shards) in `tests/harness/corpus-costs/.json`, not in `tests/harness/corpus-costs.json`; see the README there. Before opening a PR that adds or changes corpus, diagnostic, or frontend fixtures, run `pnpm test:ts7 --baselines-only`. This fast completeness check uses the same fixture discovery as the parity suite; it does not type-check or replace behavioral parity. Generate missing preflight/order baselines, review their diagnostics and module order, and run the affected `order-parity.test.ts` batches before pushing. Keep existing baseline entries unchanged unless the corresponding behavior intentionally changes. The full `pnpm test:ts7` command also checks completeness before starting its sweep. diff --git a/scripts/lowering-order.mjs b/scripts/lowering-order.mjs index 81611e6a4..6d68530cb 100644 --- a/scripts/lowering-order.mjs +++ b/scripts/lowering-order.mjs @@ -16,13 +16,16 @@ * [--seeds 1,2,3] [--jobs 8] [--entry ]... [--out ] * [--fail-on names|content] * + * --stride and --offset sample the top-level corpus programs by number, or by + * a hash of the name of a program without one. + * * 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 { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; import { cpus, tmpdir } from "node:os"; import { basename, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -306,12 +309,25 @@ if (values["fail-on"] !== undefined && !["names", "content"].includes(values["fa const out = resolve(values.out); mkdirSync(out, { recursive: true }); const seeds = values.seeds.split(",").filter(Boolean); +/** A top-level corpus program's sampling key: its number, or, for a program + * named without one, a stable hash of its name (FNV-1a). */ +const programKey = (name) => + /^\d+-/.test(name) + ? Number.parseInt(name, 10) + : [...name].reduce((h, c) => Math.imul(h ^ c.charCodeAt(0), 16777619) >>> 0, 2166136261); +/** Numbered programs by number, then the named ones by name. */ +const programOrder = (a, b) => + /^\d+-/.test(a) !== /^\d+-/.test(b) + ? /^\d+-/.test(a) + ? -1 + : 1 + : programKey(a) - programKey(b) || (a < b ? -1 : a > b ? 1 : 0); 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)) + .filter((name) => /^[\w-]+\.ts$/.test(name) && statSync(join(corpus, name)).isFile()) + .filter((name) => programKey(name) % Number(values.stride) === Number(values.offset)) + .sort(programOrder) .slice(0, Number(values.limit)) .map((name) => join(corpus, name)), ...values.entry.map((entry) => resolve(entry)), diff --git a/scripts/refresh-cli-profile.mjs b/scripts/refresh-cli-profile.mjs index 1285b5503..dde4e21b9 100644 --- a/scripts/refresh-cli-profile.mjs +++ b/scripts/refresh-cli-profile.mjs @@ -7,8 +7,9 @@ * Builds an instrumented native compiler for this Linux host, runs it on the * training builds below, and merges the raw profiles it writes into one * sparse indexed profile. The training builds are tsc-ts (a cold `speed` - * build, then a rebuild after an edit), every seventh corpus program at the - * default optimization and a further slice in `dev`, and the runtime + * build, then a rebuild after an edit), every seventh top-level corpus + * program (by number, or by a hash of the name of a program without one) at + * the default optimization and a further slice in `dev`, and the runtime * benchmark workloads. Run it on a quiet Linux x64 host after changes that * move much compiler code; a stale profile only loses part of its gain. A * refresh takes about 11 minutes on 16 cores. @@ -126,16 +127,30 @@ async function pool(tasks) { return passed; } -const programNumber = (name) => Number.parseInt(name, 10); +/** A top-level corpus program's sampling key: its number, or, for a program + * named without one, a stable hash of its name (FNV-1a). */ +const programKey = (name) => + /^\d+-/.test(name) + ? Number.parseInt(name, 10) + : [...name].reduce((h, c) => Math.imul(h ^ c.charCodeAt(0), 16777619) >>> 0, 2166136261); +/** Numbered programs by number, then the named ones by name. */ +const programOrder = (a, b) => + /^\d+-/.test(a) !== /^\d+-/.test(b) + ? /^\d+-/.test(a) + ? -1 + : 1 + : programKey(a) - programKey(b) || (a < b ? -1 : a > b ? 1 : 0); const corpus = readdirSync(join(root, "tests/corpus")) - .filter((name) => /^\d+-.*\.ts$/.test(name)) - .sort((a, b) => programNumber(a) - programNumber(b)); + .filter( + (name) => /^[\w-]+\.ts$/.test(name) && statSync(join(root, "tests/corpus", name)).isFile(), + ) + .sort(programOrder); const training = [ ...corpus - .filter((name) => programNumber(name) % 7 === 0) + .filter((name) => programKey(name) % 7 === 0) .map((name) => ({ file: join(root, "tests/corpus", name), optimization: "release" })), ...corpus - .filter((name) => programNumber(name) % 14 === 7) + .filter((name) => programKey(name) % 14 === 7) .map((name) => ({ file: join(root, "tests/corpus", name), optimization: "dev" })), ...readdirSync(join(root, "benchmarks/runtime/workloads")) .filter((name) => name.endsWith(".ts")) From d33b083a3b4f49c258e4ff2a911235bcd3abda8a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 17:30:44 -0500 Subject: [PATCH 272/273] Cache the zone offset at UTC midnights for local-time conversions Local setters and the field constructor convert a local time value to UTC by bracketing it with the zone offset a day before and after, then checking the candidate instants: three to five localtime(3) calls per conversion. In glibc each call takes the zone lock and, for dates past the zone file's last transition, parses the zone's rule string again. The offsets now come from a per-thread cache of offsets at UTC midnights: a day whose two midnights agree has that offset throughout, given at most one transition per day, which the conversion already assumes. A day with a transition still asks localtime for the instant. A change of process.env.TZ clears the cache. --- packages/runtime/src/scr_lib.c | 58 ++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index a9fd25915..1a625cdf1 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -11588,8 +11588,8 @@ double scr_date_get_timezone_offset(double ms) { * from local fields reads those fields back. */ /* The host zone's offset (local - UTC) in ms at UTC instant `ms` (an - * integral time value within about ±8.65e15). */ -static bool scr_date_offset_at(double ms, double *out) { + * integral time value within about ±8.65e15), from the host's localtime. */ +static bool scr_date_offset_at_uncached(double ms, double *out) { long long cms = (long long)ms; double secd = (double)(cms / 1000 - (cms % 1000 < 0)); struct tm tmv; @@ -11611,6 +11611,60 @@ static bool scr_date_offset_at(double ms, double *out) { return true; } +/* Offsets at UTC midnights, for scr_date_offset_at. A day whose two + * midnights have the same offset has that offset throughout, given at most + * one transition per day (scr_date_local_to_utc already takes transitions + * to be at least a day apart). Each local setter asks for three to five + * offsets around one time value, and consecutive ones land on the same or + * neighbouring days, so most answers come from here instead of a + * localtime(3) call: glibc takes its zone lock in each and, for dates past + * the zone file's last transition, parses the zone's rule string again. A + * change of process.env.TZ starts a new generation. */ +#define SCR_DATE_MIDNIGHTS 64 +typedef struct { + long long day; + double offset; + bool valid; +} ScrDateMidnight; +typedef struct { + bool valid; + unsigned generation; + ScrDateMidnight entries[SCR_DATE_MIDNIGHTS]; +} ScrDateMidnightCache; +static SCR_TL ScrDateMidnightCache scr_date_midnight_cache; + +static bool scr_date_midnight_offset(ScrDateMidnightCache *c, long long day, double *out) { + ScrDateMidnight *e = &c->entries[(unsigned long long)day % SCR_DATE_MIDNIGHTS]; + if (e->valid && e->day == day) { + *out = e->offset; + return true; + } + if (!scr_date_offset_at_uncached((double)day * 86400000.0, out)) return false; + e->valid = true; + e->day = day; + e->offset = *out; + return true; +} + +static bool scr_date_offset_at(double ms, double *out) { + ScrDateMidnightCache *c = &scr_date_midnight_cache; + unsigned generation = scr_date_tz_generation; + if (!c->valid || c->generation != generation) { + memset(c, 0, sizeof *c); + c->valid = true; + c->generation = generation; + } + long long cms = (long long)ms; + long long day = cms / 86400000 - (cms % 86400000 < 0); + double start, end; + if (scr_date_midnight_offset(c, day, &start) && scr_date_midnight_offset(c, day + 1, &end) && + start == end) { + *out = start; + return true; + } + return scr_date_offset_at_uncached(ms, out); +} + static volatile const double scr_date_day_ms = 86400000.0; static double scr_date_local_to_utc(double t) { From 1a2eb5201607dbb10ebde85f0883276624c412ca Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 19:54:03 -0500 Subject: [PATCH 273/273] Start nested Node entries below the frames a fiber switch left live A host callback marks where it runs on Node's stack, and a call into Node.js from a fiber starts below that mark. Runtime code the callback runs may go deeper on Node's stack before it starts or resumes a fiber, and those frames stay live while the fiber runs: a call into Node.js from the fiber overwrote them. scr_switch now reports where it leaves Node's stack, and the mark moves down to it for the rest of the callback. 6823 runs a timer callback 200 frames deep that starts an async function calling into Node.js before its first await. --- .../main.ts.json | 6 +++++ packages/runtime/src/scr_async.c | 5 ++++ packages/runtime/src/scr_node_island.c | 10 +++++-- packages/runtime/src/scr_node_island.h | 5 ++++ .../6823-node-island-nested-stack/echo.cjs | 8 ++++++ .../6823-node-island-nested-stack/main.ts | 26 +++++++++++++++++++ .../main.ts.json | 1 + 7 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6823-node-island-nested-stack/main.ts.json create mode 100644 tests/corpus/6823-node-island-nested-stack/echo.cjs create mode 100644 tests/corpus/6823-node-island-nested-stack/main.ts create mode 100644 tests/harness/corpus-costs/6823-node-island-nested-stack/main.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6823-node-island-nested-stack/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6823-node-island-nested-stack/main.ts.json new file mode 100644 index 000000000..aa900980e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6823-node-island-nested-stack/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6823-node-island-nested-stack/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 0f18756e1..3e3c7f265 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -28,6 +28,7 @@ #include "scr_runtime.h" #ifdef SCR_NODE_ISLANDS #include "scr_node_bridge.h" +#include "scr_node_island.h" #endif #include @@ -1659,6 +1660,10 @@ static void scr_switch(ScrCtx *from, ScrCtx *to, ScrFiber *to_fiber) { uintptr_t guard = scr_stack_guard; if (to_fiber) SCR_STACK_GUARD_SET(scr_stack_guard_for((uintptr_t)to_fiber->stack, SCR_FIBER_STACK)); #endif +#ifdef SCR_NODE_ISLANDS + /* Leaving Node's stack keeps this frame and those above it live. */ + scr_node_island_leave_stack((uintptr_t)__builtin_frame_address(0)); +#endif #ifdef _WIN32 /* SwitchToFiber snapshots the outgoing fiber's state inside its own * fiber object — `from` has nothing to record. */ diff --git a/packages/runtime/src/scr_node_island.c b/packages/runtime/src/scr_node_island.c index 3b21dffff..cad9d80e3 100644 --- a/packages/runtime/src/scr_node_island.c +++ b/packages/runtime/src/scr_node_island.c @@ -865,8 +865,10 @@ static bool ni_stack_has(uintptr_t sp) { return ni_stack != NULL && sp > (uintptr_t)ni_stack && sp <= (uintptr_t)ni_stack + NI_STACK; } -/* The lowest address of Node's stack its frames use while runtime code - * they called runs (a host callback below), 0 outside them. Runtime code +/* The lowest address of Node's stack in use while runtime code Node's + * frames called runs (a host callback below), 0 outside them: the + * callback's frame, or lower, where that code switched to a fiber's stack + * (scr_node_island_leave_stack) and left its own frames live. Runtime code * may resume an async fiber there, and the fiber may call into Node again: * that entry runs on Node's stack below this mark, so every frame of Node * stays on the one stack V8 scans (heap::base::Stack, bounded by @@ -883,6 +885,10 @@ static uintptr_t ni_mark = 0; } while (0) #define NI_MARK_END (ni_mark = ni_saved_mark) +void scr_node_island_leave_stack(uintptr_t sp) { + if (ni_mark != 0 && ni_stack_has(sp) && sp < ni_mark) ni_mark = sp; +} + /* A nested entry: job(arg) on Node's stack below the mark, from a fiber. */ static void (*ni_nested_job)(void *) = NULL; static void *ni_nested_arg = NULL; diff --git a/packages/runtime/src/scr_node_island.h b/packages/runtime/src/scr_node_island.h index 6c6dddf22..0374508e6 100644 --- a/packages/runtime/src/scr_node_island.h +++ b/packages/runtime/src/scr_node_island.h @@ -49,6 +49,11 @@ void scr_loop_hosted_io_state(struct ScrNodeIoState *state); void scr_loop_hosted_io(void); void scr_loop_hosted_microtasks_done(void); +/* Runtime code switches stacks (scr_switch) at `sp`: when that is Node's + * stack, the frames above stay live until it switches back, so a call + * into Node.js from the other stack runs below them. */ +void scr_node_island_leave_stack(uintptr_t sp); + /* Process events island code listens to (SCR_NODE_EVENT_* of the bridge * ABI). Their listeners sit in the runtime's registries (the * SCR_NODE_ISLANDS build of scr_events.c) at their registration position, diff --git a/tests/corpus/6823-node-island-nested-stack/echo.cjs b/tests/corpus/6823-node-island-nested-stack/echo.cjs new file mode 100644 index 000000000..f74a40e4e --- /dev/null +++ b/tests/corpus/6823-node-island-nested-stack/echo.cjs @@ -0,0 +1,8 @@ +// Returns its argument after using some of Node's stack. +function down(n, depth) { + return depth === 0 ? n : down(n, depth - 1); +} + +exports.echo = function (n) { + return down(n, 500); +}; diff --git a/tests/corpus/6823-node-island-nested-stack/main.ts b/tests/corpus/6823-node-island-nested-stack/main.ts new file mode 100644 index 000000000..f9539187f --- /dev/null +++ b/tests/corpus/6823-node-island-nested-stack/main.ts @@ -0,0 +1,26 @@ +// @requires-libnode +// A timer on Node's loop runs runtime code deep below the callback's own +// frame, and an async function it starts calls into Node.js before its +// first await: that call's frames go below the deepest live runtime frame +// on Node's stack, so the frames above it return intact. +import { callSync } from "@scriptc/node"; + +async function viaNode(n: number): Promise { + const echoed = callSync("./echo.cjs", "echo", n); + await null; + return echoed; +} + +const started: Promise[] = []; +function deep(depth: number, n: number): number { + const local = [depth, n, depth * n]; + if (depth > 0) return deep(depth - 1, n) + local[2]!; + started.push(viaNode(n)); + return local[0]! + local[1]!; +} + +console.log("loaded", callSync("./echo.cjs", "echo", 1)); +setTimeout(() => { + console.log("deep", deep(200, 3)); + void Promise.all(started).then((values) => console.log("echoed", values.join(","))); +}, 0); diff --git a/tests/harness/corpus-costs/6823-node-island-nested-stack/main.ts.json b/tests/harness/corpus-costs/6823-node-island-nested-stack/main.ts.json new file mode 100644 index 000000000..5a61731bd --- /dev/null +++ b/tests/harness/corpus-costs/6823-node-island-nested-stack/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 20000 }

  • o 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 %!!-sel)E{ z36scqcldnKyWiM<%lyWRTd#qeynZ1!tfD3{96>O=%o2$^Cw$%K{Gz0|2-yjKgW1&I z{$!PgRVsx)x1e}c);N_VMVd?8IbnXC^~)>AMNPx4{0-W_@V`OxE~=11)&(e*pjCpD zWsXly8*)Z$L4r_xwB_L^rr$|;1M2cbv0X-$S^}c*jY%)Ha^}J@Ia_c4 zAICP;unqRGZbPidxt;%X)`M@icn2D$7+bZtUpsmm2Q-v*gm zZp<{zZOohPODvG3o4kT zXgUB;ol6Ve0Y-miq|XFKwWh<5e!Zb(5^QG=qyv1B*8;)6f6v~8cm4^#4pEFka|%h& zs-!VDp8e(2`kHC5Ee~3D6bF$j*W;{|!IEEZzcIs=Q*#Vd>Qr%uWEvNaaR@>oXqw~z zCDL9+CLi}K`swv)D-v;*K-VHE?*B;P+6cev9b*Zkcf=#v*Z)_qU%$MeY1m_SNufCw z09g|i5j|#KjUA}|{>fhqJ-P585iIuxoOX)PL}+Uy7Aeq7?i+5sC{>#3)PzI)HHCQX_14VuXiT?ijZd zrxUc(8QbmYq5aBGtDw6rJVS{%12bOHx(~y(%d&-ZJ??gNKSh}@(f;qirHx-WpTDvu z9A%{(lwA@6=VP?|A`#&`Pg*~Ih_bxm@(ouB)#pI@1SqHs^snpv#L)YyHOH&u3d?XZ z3&4d9IWKVEe0NpN!n(P%2&;6vM*0U28dBeA05#JGg0BMx*_`H^o30x<=Y{Woj`KW( zls(_NWU%~f=(?*e$~RTJIYRY`e|CCI*&GfY9+7{^;j|u@hwy(R0^i^2&aY|z2UlE3 z=USM1^JM<>O_qD@et+hV#`nYW-;1}5&Icm+ntW?yn+sTv@l$O4GR`eyK4Vw^T!#-j zPzLPS?Hc7z^gp%y_(ZsGoQqR=uq^2v=F0_{zHHFPZ(QKf{_K0YJhD~mJUs!juS1r~ zq^CP{>W*;-Jg(9Hjazb?E~ySzlqT*Fni7`sX8KH^OPSZ`d3we%Ql=jmn+|XO`RQ)! zD?{^YsMLpw6P(X0-18Jf$U?OW^=&qrapjCUM*EGmD?D$0hcmn8m~dv-Lu_eptp8Z2 zNjn}-!Rw$&k>)9pBq@Ot>0q_|+c)Qa>nLPouJxxZb%zbMP_+|kO4yo zqVUYQ+y4Dyjb{L|)Ow03u|~RNrywZ~sv<;_=nB0vNI~jPiLf6jHj=uhn3DYEt|=(2 z;7$s_%LKpLW!Qmzw?d9KF_5^l4i+ywgOo`1B}iSnult&j}Q2N_KpM0iK3h2 z`f+p+IC>QUaeJ{KT|k=jqJ}NATp*Vm>8A)-DWZa+NKp{@ML|?liWS9*4Nz1>!H)3% zqKKjhBL5d|=k7AeWRttxQQXJ(@oqM|Q{KFJ^XAQ)Hyb)UpPiG9WDGHdq|=k0FRt}> zo@IIHgF>up2uaBrphSVy2=-{3VRueA@qDRk2y`p>wtk|;UFUE7;cpxv7<&?dqH2A6oF>+B*lu-we}OO#%YLNDP5)hCPSP zTJ^vAd(Jg~?1V!vgwqU3LEljD^pzAz1$N2ra}V_Z4fvFT@q4cNz}wH02UqKjZ!9S} z6B&B_-zdS`Hbg#8t1xMt{+6DZTYu>v!>g>^jyy>bGQ+Wy0vH%)$gVp6n%wEpOW&yp zwc6v>*cgT5Nlt3$`$pUM>7~E9{mq(ZZKp138?8FUjBo^is&Nb@Q38kRX`fzEg2_v8 zlq~X(S;n;7WXX?avT{lksc;O>lMIk08RfFJHk|tI@o%+HT{LV(X)nbP@FdOIeZ@sU z9R{>sWxK3=UdifV;;x)PQEaveyUHRgttJgu+e!6AcG0L!trU(qsG@3ekaOnDf zgypr4=;9CE#=}KjOV#s=LY9F-kt`%C{mpiq+i<4nZPNYhpnH}{_iCYg*1weAXA8fZ zbU(9nPdDjaEp$(>U_Y63PYu7PRa1M;6*yPvZZYX@(%q!{c|UKP{BF|Sq`UF^CjG02 z?k1hjEWevz(%q!93Fix)P5v<9e1tL|8NYwm&S8VLE{9>BVmNJjeIC2RrGk*17APQ* zw7{_p+0{W)SB$qzpZI*oT$tSo9WT%^3@w4%rd(lAJ)0%qzoGD^O%wO!kRzne=MM`9G<^G6!tM(Z?xgS;jOAN>=$O_uXID|e>?tx*K0ae^$!H?-@1c*AcK zvWHf)fXS`oDEqfTE~fp>&r|x8vGjERy780AmnnFN>FKheRdUf9?ig2xZ*JZETV0_R zw6R`hjgKLOkdsR`TcqD)vIj;I{oQK0&A*^=)1E&pjM3ESAZ#S1DuhfafYYd7);xU0 z+aLJT(dZYa8plsVxZa>Q?oN=cjNU8j)jRwXUf%zowf*A-+!HB*X9Cf2y-#=do;e!Vf0?%^{Pq75S5K(imU5|l7V=pn9^c4)ntVTGKuzpH8G@Ansn8-#6B>F@rfbrLjk@Qx8|%E;w%pi^npbIpCp1P< z6bFr!bp5=dek=Uzua0oLM{SP_0Zsfz>y!5xZ{>z{K^34xc!;ARF z#L!7uhOoQbuEAiUbg>ToVl+F4wH?scE7A>#6Iz9GL?9<(*iI4@$q(ptr+Y zAww=)1X`9vLZS$I z%g|9uc^#y?t{6?X#@AoV2(r|_HGT6BPbe+$lga5LP7YUCDIccvl?olsVl))>12e4& zcEB?KZFJtN|GNi&te&Mro@Gdq6hx92g=BAwv|5(?zi!rU+)r=f1L~5LK#?NH@GQrZ zLR1lB^v2Y~7O~3z!HPN8etA8#*mTERY&3xb5)nQ(!xH<}pB&e&FPq*5$vE)%nHd*o zxiF1;ZUhr6Xi7)F!&zw3EdTL0-F@5aP0iz&3d1)qQ=A}CB+b)2J^p9kTjfuk+oD!% z(d8r~Q~u*$v-di#X00YMROwQQ!U`Nq0zr`lUfZ!?nK)^@w>19h{`w`yPq*pm-g?cp zVGDda{i=TVUC<@y}Ya3dfKnL2DczbpdX_g}A)e_}ldHcieQS^eWMem_5r-geWRNrbVvtsm*OZ z?%bf}22+7Z2+V`?CRC>X)2oQD($A9iovZzF?wg0#`I~<wwdc^S|GDX9cm8gBue!0x&dSHx5Po@{%CIcjhbJx|k$ zNb?M>iKrHRXMzN?S)TS=j@Nm4<6^jsnPG{$R_959BRNF_1Sfn@Z(;4J2R2*slI$&8 zihBELf3qJR-uTb;Skt2>vV=tQD#wcgYI?L-Vq1#w9lzc`R(q)b5v^`CYw3h?xEww^ zMWB%4k-Ju`Lk=hUQqBq*zr#TXI2?WV2ab@b6xI;Oq5I!8J!H zCwiquk(}M@FDi0-d_W!U_IniY+P=OWuFf29aa7SLK2S~4!pLr#i{aB$n<5$+{NAyW z`R5`G0@Dpk*FY7URnSYwM14d&fINqnch}bOK>hj8NPbsg`X9#+b&&RaQ%O3#fZ#I) zjWf%5Ec^7@wd9`#kqQvn8l+L%Kx-u33Q4y_(&%P#z5)NDf(VU)TNl%hiKbDvAk@I0 zmt?wzK|05vy{J*Awn2T9O($Dlqx$Dy%!P@jGY!%i23lyEfgWI#HtBz6>EF;`{3T1D zT!XX`{jv?xS=CJMs-pi3{!`sO_t{o=($il@n~0T%@pUF@4pPj5`Nb-kNQTsQ-)p&42!UvP+|U z{4%bPr6{{1Ih_E6qy9T*_8nP1r1ghym*4?04EQJziL=pSm9NQ{>Bvft9y{m>K3;zUXeJ7yP2s$EiL z9te^^@VpXs%U-Z&n9yxaPN`qp-FKHX_ktC*F~oe+-}l~WKfQG;t3Qe1(&BLW+!4_; zMZ(_8>2?9=MBn6m*I>7M8HyAco~FY8 zu@p@tYdzfIpF3L`x4Sf)X)ISTn&IqzFYxx7gXM{Qob=Yu8jBoxV$s>FyIDX}~%p@!CvjiXEY+@JHE|M%t#VmP#x$zW09 z0n3pLp@;yQPEm)6s}XF$q(g{kK=qc+mDJEPG4r_9@}<9S_q^?MHe9SfiQx+t%d@V? zDLGujfCfCkrGPf=M>Rk7ew`dRMXEX}SL$>azqADFVZ1Ht_1jbXUsBTOW(!L2x*Mz! zzyG0I2DU6J!O!aTzOh{iQ@!RZ!54g8g6r)m`F_;)5}ej9vK(U``^Nw1_ulqjd>S`? zGe>ZM2Z8`dKu1N^y}3Rn>+k%Zb!mIW^qcSwsO2VgRF8lc9!*Smw0!UHH}sF*y|VCL zCp^uuJWJ4&7#0>HDkJ{Z;}EaP#TBvNKlIsq?rrdCK3?jD@9<@5No7QZQTV7>$D*G1 z5Ui8(kN)eP`u4kiW1tPFU#x)hEN54XT|GP#{e{5g^M-YWIq(1O?&osk^f6fLRCWid zx_0u?tGFE|#OA;>%R&ETM;g7oQ_e@s>ycb8Nt~$i3@r;R*?Ykv`I)Yhvzj6Ho~bFW zW9_q)W(WOW&wJ#}^(v&;V=?6d$1@~L%9O~H^uFU{=H-QN9KVoMTnh*ts6?jf$n6UR3I-?v+i%;EO{1^4%P7JqVXGoft zC;&8$Cs+!dA;)JY*OgYwVgLP8iJCuu5r6vPX@=rtQ3R^2pwri;4n}4C>VN#%eS?4b z3~G`woo@-27e$T~WkQf~nq)?Twmc)(+l zO-4u}#Yn2aN;E*%WbM*%S)cHK-EmgkiCy3na3J;yMq){h;3bk_Im?0<^63xqEU+>w z29@9^^!7ZXZ_aH+^zv0YhI=FRY>{=jMPCWE&_5{kK^RhkyIo#_N4-*luhq+O>P!1B zb9JMJp6*ST;7|4Z=IE+bmjsWx+AOF1%4;nTOn)k_mp9-AUScRhVn_=0@_vmf=>RxM z!gXW^Op&MjkFR{{*IOQq+s`Ub$t*|n65s^1pZj1B)20VP`=1?r@HpH#4V8IXtBRc@ zNxSS;iyt1lL0sHG&0DL#HBNT`$*WhrhkIH!h+WC4l<%g$ z??bZj>4SI_lJ7;xhNqAg+k{_HxJd7a3zNLoA<^|s=L)WcgP5`4LQIzEom@E>ow`WJ zy#D(RJ^iV^{Me{--9cUDdIR(QV4d%3>87MP|CHc+q4l=j`Ehs-w*Ow89)6@h{yE3n zm+?{w-u0r+4?2EF$8TWpRt(NxT!IgENe=&%AU=rTA0sa#w|w6vK3=Hz-~9+aksgVo z<#R@0+TmMPjXcskdcWcH&9*F8{$^Xdo3Kv=zP7up4K^QKReDmsn?Ci8N{_^FH19Vs z29#ANGf?aPFEVeM4wv}{V(-UkPRTb&Dzfu5&8nQJ@CqY{&mJAU_E7IEUjK9yU9~*c z`{Lx@!@Z9_4;j?EUS{R$9ZHfeyQ?@t49p*`7k&k+l*)lw_4*`s)!E1;k0gP7s$aGnkt!O^PB z0+Ju2_E4##K$d(R}fvuXbQfUjIKY zKLtf}jH4&zkAqN*DsT!Hj>N%!`uU{|%b!7kup`}wk?IFF(``R#@k-lhlUb}n0VAn2 zMGFkik+b)%doTE0t8{2gyX*{)j*3Q_J6mmi)qNvMMg_k+M`EOy5M>W(5-9k7@#-%* zIE(Zv8~o)1x{)kt?_XHx_k}~|^mPEI>SgI!2ls8-TIz`McUEj%D)rJLrCwts{8>GN zv21jHp>`$Hwa)Q$%}SsYwvPON( z-~e8Jv&i(*$nT#c%KHLjg|!b!8=c>*)hhn)sFNn@{KXsOZ{&81dSs`9%=Ur6 zF<&RJ59<`ns46c~q{c8bi8=*0NRo4`mac)$&1KixjWELZ@@Nx(AxV?r%_3(Hr_-%S z;gI43for0#p!fFA*KIqzy=IX)6iqk-U^7WzFuE@Z93c6jL}%#0)i~PdR!9?^eWG9T zsA2g$)Z_NKn?3&3@So={O?{$N|%mvXo>*8AA?7f_l*W0_;B^5f9@O>qc zPbsjw+@8YI?t&_O1iU+*554eR<9&J1QmBWLu{2Bh(F-=P?)I-|Tr(+NumXl8X+qRQ ziDDI0{U;lij2f|SB#thL3*5CL@2)>~Cm0x&B7vp>LRDzMHGk^-Pc?Af=jYqRfQ$h6Hdj(1Z8ZFhKwI~@wR_}JL%LC4?fTi+k(1uW9jU+C@I4(Vu(J!9;IK8gD8tMz{QOkaq)b#wbh zougV8m3+`%XOP}K03_1~H zV<(xu0p}a{d~%SAYtB(6RZ>)5V@-?*-@x~ej()o1k@(@Q zDUpy#nqX*!;Lz~a*$LCHU!ciXFV5Rr3zjuBc3C_va0JT$hUOod_0c1*K7U8eOvLO{ zx59ncKQQC!m#-P|Tl_l6f)+l2#fpp~K4`r@N8B_p7fuLs6D$)SA8tsX2Ju7Rx|g&^ zBn}d7L6wSpc246-hE`dc;#j+*u*jLYbNsHpCue6t@;5#j-^zqLEImBy-9wk8eLKvh zPfJ4rhb9(%vzUiQp?T4jDr8vE?g5j4;v4Rhd=4cX3(2m?#0N{Vo_=Dn&6-!PoJ4SC z0e_BvbYS$Fo^!e>@!6UqBmrnZ;wV`|*_zN2W2w;r`HO#=ET2^`dgMk%B4>UXRJVzA z;`!1c`>QUHPg;{@;xU2R>-xR;_I{`iJstn-2Cb+BCrXs8iZYe4{+?%h7vvB~=J%eV z<5rWcvD3KcI2f1Y?=XE~1?zo}wQ}E~^}z?u8yk4$#{N><<5=S`4+u_F6$S_tiyDWo z)Q@^JN|?9C2kxH#-F5YTXp0w+5&qu1{H;;-tYn(VoHseg&=J^e#&Bp{2 z&OX>M#iW5L=S(oc1XIqK^3DWvJ!X!76HNGbKsVh!*4Am#!US{vGv^f(&LuuF`87Fu znDk1m{7?UvjI*<)KWr&(e|n1ZIam2M-Q+_POu1FwVE)nDWUp}rn> zVNbVOytl*uej9V)Be^e_3iKHTGi0j?CYWH#T%AK;W?j^*W2(~BD2aOuO_s;s=Wp~^+<4C=n}5!%=clY| zf6n~WXZ)P$|NlP06Cm4mmv4QLfUiF@Po!`pI@jy%js`^%A;#dU1|}K?=$BaJ&RR)E zL9cLw$r|Vfs_@<}TjR~Iy@T)I;j8^wu*Jk&rAYh<{*p{tyEGm4PX}6JT66EsVrnRJ zqDJ$DH0rsbXjNi^?AcgY;u^5d>mUC*idpo{WW(3e`H3{rkC-J$+C{6;tESDlnQ>fI z5v@q^^ZESJV}`7%6JC%??ic8;`JYWeJ@I_dN6fC(T+wnzPZCh|pKp-IBWY(~nM>;* zs)F8GZRmbFoeOF+M~8}s#G?)`M`B258mv?fb*ANN!=>3oZSz}+&)j~^j8P;!fROU^ z6Y`)-#EH%eB{(yY>JUh*2+gx3Jm?NRyip}?#}=Isp6QBrKqev(iID}eCViQKer^Tq zhJGBy@qvj5^!_d2PnHNp4*18^#cp{&viYBUZ$elAzF(p?f}vzT)(N_T!2Q;<4wL#} z`7co;2FzdISVJ`#|Fe1lWO~@*CP(f8KcfVy1+yqqbpcFad4EfZP3PZ}O?BzyR)*qr ziDqn?L}H>!4B-{^%^UZ%;lmRujVla1*id=lyZ7oo^5^)qr-CqDkwgcn^hfwUaUMd# zV5@pCUiW*OH$LP|=)rI=W39|&8ne~0`9I*&^B&^;tmuzysg21 z{dpanjL++;CGe>a5b1mj<%W3-WjYz#iI{KPPrT=^tj(7rQm-S@3<6sm8dUZN;cgP2 zPQiDFGErqY^afHhhD2_~y&I5ns6)23R*U0m#O1%%JiTo=-Sf@`u@>FgB4-g(FpZDT z!k?Vmv7YhqSuo3m6?yMi=e<_7a@U_3D{8cU;TQ(nl~Eo9AvK(~)pp&_lTz?_qWU?A zQg@$DXz0b}&ElnPx&asaag(~jAAs+TcCbou-h&GF&F9*Z+pG%1^9-02=#yXSGRS}9 zLB1?JbuLxXmK1R@dv4NW33Rtr{Tx@3y_Sxi9uks4xMnPfX<&-R{y0Xl0&G4!M!uRx zpkekRVNQg5FVy-!e%N8#9!j8~i=R z|0uZ`kV*6Bz|J4$@iLzu_{}Um<~sRKd+5>gY3+Ed!Y20`lW1SvzB5@{%N%5|n+Wh$ zzS%8U8gMpoUmg!0$i7XmiJGZSa>g8^MeV_eZiu<+_4k@x?tN)oe7oTa@AleukNML) zgid5TJFJ|O2-_u9+fZj#AvxbsIlT?MLLl9@rhzaOcHLyIFP<)OSBzy|kC(;UI&I;* zJWnN_a$?|(m{-69UE>IEgDLGn1}z8XSO>4O?Dt=><25+X5oF~kv^TGS=Y5?GpU|!z zDY$;$_3tUZcPb+_IN^%TN0Opo%Tv%I@nzn&H=BZjx^xy;da4|Sr2)BtwRH5L^4Y-Y zs84ceajC>*8$)~LAA;hC9Xf9UT}F1+&F*EY*WEYfQsp(dsjQ_d5ZDLmF%i(_v?a*-x@1PW8#jU<$!wgZ%>>5k%8HemweH1~eb?voj zZx$FfkwXNCBPq?lf8XSLu>#(a83LjBff5r*B~>}C30SVi)ea3^$hIUcyGtR#9(-&!;dpAgKfwdx z!NR=_j(uI#K6*_}LU+DpfmxsOkzzx%R+r2_DNKMtQC%?KUEkJ#87d&`CSGFoabb3?qxi&i`m}ycR5(&#l zCF&Td%5Wzn+^YLcK6Cr_54w3XKW(IfYyKd(LKo4(7D!T|Rx8VC1t4q&Zt}_)v=d`X z?qB64Uwb0jjn8-1meY^D7Am0^M(e8oMdQ-JTVuAVR?82ZD97XD=E5zQ#`)J%8)j5| z;BEA+FUQN)=HAn1Y{MQV{Lm)m2en3q6^S;spLKGb?}we0@3Lck@+|&ILr~SltEB0aLHA$?F6O4d5)v*}w_k_nGj_$xI2` zPIlH<`m?pZ2lCqQ)+?YkNgO3E$&TEf*;4U4_kR0aNbwM%NNe%E6MQO(D7KCY7cj-fOo`H6FKk8zskkNgUt97MHscSe zbqkq9z)YHJw$;&pgWxx>xbq_)#LwUz4{n49+MW}oVOOHT3WkryWp^=Owpd?6o0_fW zR&{n>DiTr1tZQfy@KvETeOIF9Ypd{xxGY^Ks{b?U5n<ADfsRU(%oH7cSgyfuy4bULVL+X*I;1_*-c=35{?l5^*zYL{x!F@Vu` zIv>l_YC}Ay7`mgNH#C_edgAi_7QQ$D`%&CG=XmoHuBL7^X7ShX`_cd1KmC2no8iLj z+UehYXJQGjO2q(eHQQ=8J`F1#Ifj)w(_oEcPc?)RNJ5J=yOIQ!Cx3S2%I5#!YrF?X za!axH4$yY6^L;XP>v2Rcef>^6p0@v80*Jr!8)G`M@))Z+eD+h3PiZemJenS>*}Q8K zMO#2Kd2gYbpjg@m2C-G={;ThM%8E2i*Xf0}roi}Ra&RIo;1aiS9(taNvIK9|sV&)( ztIwl5=fHXQe%_>W61vqCoo#QEM|)~Gg}w0z#XGjj)R-hGnlkg%-_Gn>TXHo^x4U3FVH-1_$ctjpr#aTWU4}6q2*YbCq$hK)g$?(= z4{Jp^bL^sI(hSDBxL1)ox6nc~mx|I9w`OQZUA!_eeR43C)Z-yCElc*wFrq&vs7L~e(7GG8N|S)?~X03vOOX>rK|uW z&G?0-s`0&c0ZxGbLJrF*>%7QiSf6vo167mW(9!d2_qykM;9xM{5`95MWlmfKiCI!f zx&PXwBM5SIdTqh{Dsg?*VKczkwJ?LfJURY;}7w*6(TC&+0Hn;M8P>B}9m}jh2Vy)mX)W z$DuWjmhDBB5tDBTT+O7&v}=PF0AafGauxLDVgIl=9*K<`5EQ3(|l7?uq9)oLIU{~O3z{TwYtX%wDy`P_A4=2s>3oJ5^ZGKK6V<(8xl>PBn7n=L`27b|` zmP!&mRz`pNgP9Od+i3j8DnAx)Yr4NHc20PvFRtGb7i02shOaXry&5~tO$>&dN&x(4%e}9TGENh!ctX&Q6Tkp zpW_Z!Zw+TBmG0jg85w{ZGYLK@=jO|148Jvw^;rdKbMgL~U;qcO<@iLpD!9Nds_HRS zCvj3zmgDoEAg|P?W9gx|a1rC}e_&gUH!0g#t9KW{!$viM4*yIweoNV>9p1JDxrP>* zry`l>B%Bd+iVn975X;+{u0&ech zn^_tbm<**c$LSV$;s5D*+|*jB4Z&tmnl}w8f{Z&wheODC0|m_`^1^RE^}Job6#mIj zH~mTgOC|>>#ea-2W7ow&@ni4{@_%cy-+{DMtJb(P`7>6aBVuWP@I)r zm^z91mpq|UeDC$WNgB~cabx22K8MwqU$aSVN@^8TfF_H=4iB{Cdh(H$ocVmn-y;H; zlXwi^aOb(=G)5Ir&GGjj@bRTN_9%a~xbRr5?Q@Ce)0w&%52LC-g zju*lg!VZMMu5iYI`cJ=XsKFeF?())U?)&cgjZdCgKlKI4cH5jWndlyepb;r@USPeF zruC06z(@af7dXVmQmAoawk(H!O%JK8$>84AhO>;*6m9SG7ElIkJpd!!`Dj%V`|5f#KBqO8y45+c+M^~ zH*h*?a>0<&lh{D*M~K=ORJo+FZzE^5d*2%~+vb%oJcT>!Ju}nb{YD?F*?*>a&tBt` z|4RVVbvuohw2d&VhBffaHlH!XNF7%J$0y$MSAzhi>Zfo|%2#KBlkSGuQ6+zSm<4XW9YV4)0of{fy5iWmM4-cY z5ACnvM(cUD=&hM!PO3Zd4R$=5Z-0(~U@Hqpu>zP=wW!n;ABaaBx6f-T!s|=1E4w9@ z7!tCG0+_`7C&6v(mbbkMf_9>hPX8~<<}4^{ENXYlPej`YL;8JgyivHaf{HQhN?<`B z-@ceMYduFj)q5vt*5cw*t`9to)ZIb1KZ6t!x7m`yIg0KINFD((XUYv_s@}MCwbuPI z7~hT7``z(T4LC#l#43a)0cqxf$;mX&Tg`?9ygEOn9?H9@k4u?`M^(U=rgB>vqY?k9_nSffr8xQL%XAdD zW=CEmIy1$;Y*@DKkPyo-e7M3U@p+^Or&p}@ix0coPMLn%f)F-IE;XRKh;<}17GV|%OLp+|{o}{tlgs9s;Eu1Mx#Yw zq;La5^BKgBirXP>*PkBMI$2$vozz%&JLHTLi-djQR1>PeJq3yg1#jq*1^Y`?u9qjR zQS2)17&A7s-+@&y8PuT3Th`F@QxlPuI3I`gfVhu#`Y+Q2(O+P&!h%ZRdy&;{Q|$*} zdN>Vt(xiZN=(H`uzfPxf7<|b4x>v-r1;mYseNo6Ex>t!mbqv8dp(oC=`cc-naLg8O z9jw1N0X%qHn>Sy{$M)r)I@CYeB+W&2&0_OVk}oLsTn~@2g|%_)7Xt(CyDr_t9g2b4 zgq}f}z|qDvt1wNJwOAAjMN_ayGxr3Ks}7#`D}ath2i^)46Bv$)qdBYO?QBa)=`mLp zSe?dQ5J38j^Sm4DihplhcQayGHg_QC*ld2#;+~ANI1rX zR1=8wb9f~t`3(Lm^J<09#_+dGvB7c%+T)U>++4=uVn)Rh38QT`np7{fST17ll^2VY|xZe_s-Rs$*g-P$%uwxMja$+snFGhL#H=k z)fw8W&!CQCLTld@>?&mRLq}wT7#TbK`gHsd_eP&<>Y*8z*s03WLzvM1!0sW*NAVhH zej*dICu$gC21we>0g!Iz$RBK}2!4nn{p?ob=N59%YI{cOD1F=?ywyf*|60xwXe|Ii zha5jz0V~7wN;eGV%^X|a+lL1zIx))^sRy*d3Q&n*qow%+AjL9gYX`G)N!~5#0OBYa z&G3-t8)VT`YvrvXj!Ii{RJiHkI>xo-BdgN{`|$pE3W`$B*?$#=YkNmZa)@g_uZf0LMQ& zdp$EUzjv@D&P&Hf?$~!2ENZB`cm7)o?0JVY_&^i?-6&dCsg7XJ5kj3r0TZ|f7LA z>g()avIi%=eC-SYQ+wO#!(Y9#XXfRhSB!!|CMHxF0^=%eSO0v-b*pnA+y^9r%${S+ zzQ|IOV}K=nvmB_l1L&l+4Tlai!%83}Bb-z?UgD;Ot?S=}?R!9Z{cWovU>$}qwf6uV zmn{#bKqL~9W@2+D{TwE>;xZ;AhduxIc=lxVcYY;@UzQXU&c|^D=gOry?6df&FwRT-UafbNAb1(tu)(m_sztU$gF#v=Cx(Fn`?hn*m8Q z0G3r4Q*|u)WZ}xHa8I6gEppBY2Kb(q8Y-m|5%)hE0hj@bm^yN7Kj5ck)qR6V=1WV`@};N~^|?M%;xvv+P^0n=Mee&4u+cbF_TPJqCry6@s2rG0Mu?mqRHE}F&MDs&%gp|^Qzz`mao z#$1iHj84?V*jT4#7gkQeI?Uyvzm%TEJC)o#Uk>&X5&X#16PNWG%iaK1E-*r#{1g!z zk5(F~7X;Cw-r5V+2}k_?USOhW87`BNJh)iK)Q( z@pQE*JF_8qv6>3HIu2g!=PhupdH&RdSU;CFp2(o{6sD_h{7@hzFBY0S`G@ zQyH37ZKl{#y?`(4bdIg1_|P5C>mTnB8rZ*kXemg0xfYuLB4vX>&YrUE!-UH`;|3m_ zA@k(0+i6;y{WCWUX%2Vn3eigeEBa{ekCefD8g?!2=}*?|Z5GxARtEgCIW(@6m zK11ZIAIMVt&#+k=!~45DX2sPDtQVE^$MXK;F4PFv3obh-bFNYNl@vIGFum?-3A^Zc zGE4|JnBcm$r-)Y*p$3d`!=Btz({GCgH>dmgtL>aaOx+-PTfM)_>47xv;7<{!+1tmR>4yEy>c`3r|Nj)^q(wzCvN|lS&>12P`g{L{+{# zXvfQBd$7&@upV!5P|=EI?(q|Ohz;shWY+zbym_ml>VLhPwQ;f(m|VR3>jgwg_|sNo ztg?3o-k5{8dbLGS?GYX_tT|i>xrqQnjbx23XAfwg;5#C?#58k-6{B`=e@8`sjSL(6 z&sT7I0~~h!|r;-?}=9 zbOs7U;1}F5vI1M$$6`L|AjesKxn*N;U|cI;!51yA7dM2=Bc}j7bYDE6u#g^=v&^al z@;4p^4C~*)sqKwM1vpq|_XBpsMZAslL2Khc zVAn6=4$kg*q4b1-{^-K~BN88SeXW@A#Bw|v_x1ZuIxRzKFEs`(IV#-*#1h0=&$s?* zNit7B(}?^foCy^9GXP{X(WhWCRs~2eonu8>gO2Qs`h!q>P?)geKRrZSw z(u$$;GmN*Hp0&YOk=uyi^it{cCZrpjKYx%7j?y*`o0p>1Njr(2OWvu))ONaCA}yAee!xJ9c6_#X>a<-IkS*g2njI zitxQDENt!yaiYCC9(r0l2OrJ6Ql2VXH&IO3 zkZ|+kfpdcT)Z{USIEg#TMR=#tnGvse!%7LyR{D4Kn8`^Iq?=C1GmQC$L#IbHVp7Qc zpc*9_z>k;0uN}02_n;WNom7ttyI1!Va_>Ls;-GzwkrS5lRZTxt7jKqQqCCRdyVfeK zK|9q{`?4SX?;-t0X>nXjtKb`$kz1mX2B|H4(&M2W!?=FN3ehR$eoEjbUQh_Co0DiA~+syNBa8?#aksS?e1 z{hja>w-8li{7A}T%Gj4-Vgv5MsihE6gJHgWv#;lzT61!04l+DLmPsB;C(p=_a@j_1 zD*c<}!WmUcQfCyWzD*k39O}HHc1*$i`~{@KG>yh}1IgGmfD-fU*U!u<20>AC57hf} zH=egNJB7e26H0lQFul#B6x>#6rGlPY)?F)>(W^{F{&3GERrQ5~h}08D zSgHcf-sCQ1>Z8=L7e~IS;qkX&YSqplXi{4cVRgW@T}w$pDJpc9rbbu{JnBRmv0k~h zLdomHm9R=^*Ke2!2wRhjr6iq`>Yfc^VO^>Wl4MBxl#2 zr0%DpoQo}5C&ud)9R5mI0LPcmcNibtrs9Kn6q=8OU8V2rK7!shnI`BC@4PzxAA8dZ zJrZ@ia;m9Gh`>RxPL%#;(WfP=Y@Yl)URm7w zh6|ietX(UaW{69_LE877camDYsB@|bW`@2+202t@G%^N;KpcWKv>aqE(Kj0-0H>|) z6Wa-Hn(7roLc|rOjCrb_83^VoC&}qOl22_}aL||AEp*@CdyT~v`x!T63gU4)OTW|A zZ1~&P3H^JRcA=2SO4~E968QKj%|d>i7ij zJ>d$h^e|9eEmkQ@$5CQ>ALsuhE)5c`h);2z=s_lPD|;#Jbn&kPCmG^xE}!9An)OD- z^Xvbf{8k!WlhpL>6sSdd?riS+agRqHHZ zKR*it_t-t?M0qK3iO+r9e35DeGVq?Sxo|*YUD#WX>xm;D(TEzz%V*Cj`9y>*p~py6{-b2a%FWXHtxee5zojc_x%DBYXHGHvUW(% z?|-e{ZB_F&pWJ{nS*?F4v!(QdlTri^IAjS_W4T)0l|$ebkeShN%+VOUqy3*%5PSZs zt-Jy2WM!ATQ;H~vhM{pD6{=;8Wv25FcD|EudD3H#nO%h!-Km0A6 zDo+JP&y?U2Z`*OEA7t+X9OAmWz!$HpBI?=Te>Y77<5C}2PbSRx z88IE|9P!QtYP6>0lpws&$Iv>BOm~_G@Yo?uTYGrt4q&7Nr?o%2#t#(1%&>oe!ZYRI zuoh!Ewr?)$!@_=MYM8B-!7{j6TCs`}hIx>Y8O`V3kZbGRlGv5`8bR0dxuV(a}%pN)s)rRvT&cY?gN zEOq~cyz|*3>sFVhbmtl1wmd=M`_a5PcTCb@{4K<0ER<~%Nymt%6W858H+BBN$qufo zzy@sP6sv-G8s^9OU-;jK{K#wYWXb?oBL|tgLCHFcd#?wY_jTzj;0xof&}{FnF?Rqo z_Ru3GuNbm={4;#iQ_J2?-jPY?dp=4ML^(9=7!;SdY$C$Myys?zU{o=@@d{fd|e?VlrP1nle$Ht^0w z!u7{ro)t=1FP?D~dNo<62K8&eTXz3p;_0~-b3~bEMTINN~Ub@X>)ttSIhae=x8ZJDchDcRQD6*j?sI^h3R2B=IQ^k zxp04^@mu1toiq}v7RMEa0?8Hn%~`G0GJLC5CvmZ*HG-5TjKfx!&_7V>JeY&blZ7y@ zg^WS_Bb&GeS#2tCnoRe!N&U-tKfIpC^yS9hywiC9aAR$15teKb*QCmrkZclXs(`Zy zx#q9V3^ID<3)FiV0lzpLH51n)#?(lV(2OrYCz*oBAQ=s0d45;hypiORA{7%}_$j)* zc^e?&87RWNPU7xSh|xNEXfsp6?yzz8IzX3zzsrqdjmnf|?L9}991TpmraFDy`n30+ zqAA?^FR4~e)APd|X#JjmJADg?68s^jXv!5f&D~d2q|Mzo5Iwrx%%X7>9iv~;3f?yH zEBvtkXlEQ1Mp~vif}ISd>ViY@e9|V~X}rB1fg?p5to~VH)jCY|CxAXDce%-u8hM60Thl^VHR;d*zHPAg6RWro#-r9a_xv4Cw z$Lym;5L+let<{fOI105uE-!w`3H^0bxE%kRtRhpWoDOzAvQL(_?w12Th^-lV8Py9i zmZN$1K7Kib0hO>|qD%QaiW7_f$)S5`BfE?mOAa(k=|8glMash(n>zm}G}D?pe;DOe z`}^EpvM%oCZZKfw!oD-qa}$L}anHg-=kiV>awh-5RUAdfwst08m?J_H*%_&dAVc`s z0GloF_;8Cg`m@4VDU}o&VU}55d7x0O zElyAwhsH3tk%CfwTXg=E+zPVWY?n4-Br-V5k8D1u3;%j3^u?OHI4WT#0lMR$0nDCdc>hFwQq=M^0>JHF_xN4On@+VDil z-&w&Bl)<-=`Adbd`NVUiP5Yhc6hV?7p|iAdz#)a;IR~?$yVWjqf^prCeLE<}} zn<#S^3nh~Z#8bw~V?n5J+6Q14tkt=kcK(`V?MjZqQ#dO8F7yS%Lnf8iaIg__($jpd zQX4?07)utk!lrAC3`fw%4F3cKlSH>5R&#moAKq@htfb#Wf~iNK55yjq6=bF8FIcA{ z3TH+%4nffI2RN@npV zg6T7R4uIS?V&!UCvG$=Vl?Ez)Q$&tUfrb~edXNX~;k4n}d4S5@t8EY|+I$+u;}dFkPU z3K@)j83>pqRo!#k-dCXuBQCLu5-YAcX>en9nsJOw*d`rDJ(}*oPhJ#u5w_oZByQqOms3N-Do5k1&EPC;Q zY}QKMh+HX*{gHCZ$E1?-&Lm27!8X%_u=!#+o*#c)s=7crR$?<(S5UFP(GU2vCorgNp_bqr8?YY(aVeTc2n>_@^A~RymY_xPJHDc`+Bndgu`NDW-Ez)jKiy0?zZ1}iW z!o-}oB7o?8WBh}SxF=^~53od~xTm{?o;F^F3WI`HZkkB3pRGbT`cg#0dbnnUP_axENL@~&nU#vxk|qWhmls{POimKu(%Ju4Dr4OI?_eN;sSkBx@sVJ|uLIs1dV z*~9hh%l8Pu>AqEqO(#@sOzc<|eP^{mYUK@FZD|^*gGX;!Pjh+PeYdhCN`N{)aw&!yt<9lXs?$UeF|NYATsjeizIWW z5B+8G{UqO&jc@C!V;MB5*d%{I7+|omTAsRpA2zy1xpgIkY%m1E1|I*X3bz3)i^FB+ zyl*;R3x)1$+YUPWFvSC)|9C;wb&*RjR{nwce4E5DJ^DJSSQgI#>l%Tk&KH<@TF>2dnfu$%BZUaal0?GkFUQ0d_HZm1goxpY zz!w!{I24^~ae)sdKt(qaf;5xP*zjfFHWJX+h%ul{MFl!1bO1H1WJSL?5ZbAFw_P$p zY!Ru-`Gqq|s^`dT72G=pElc1IwUd+N;eLl9vPHgfo<&nn&?Dy0&|y^qyNdthj7tB9HuOx1r1|cI?m-${5c1v9OU_vpc?I|Q zY+zqfCVu-{#w|nDG>$`RTpPqmI#@aVh4+YtY*=u9T#Z!XCuCqq3n~27MD@12%Hf5b!i2o~-?~VI$U$!F&mqw1 z-Iq&r2(<{3YbgnkrzBZDK`5%nJ3Af|Qjg;n=d;aX%%7{nf?}dM)L{l4lIGRemA*`b~#Lsr8`5 zkF>MeTokO!CZA?XmJYZ(m&&&O%KkIp4er5LK$XHwEXjXBjgfVQ@N^SAlQTtQW=#5Ai4 zd^>cB6OB4xle&hXr!Z0o-tU|Joq~g2Yo2;oBDzX}A6cQL%M6eH_Xd1^ZKR&>XAnVS zVdGP|>rWx{FlN0~qcnW?&cm{rYwr@VE*Z4$k%`Ejh6s_I9!k_PA9B)9zWQROb&azZ zc-goR8CCuVH_D#MkC@KSZY$%AkdE$Y`(^S`l6gSDI42&N5FlyKDAr*Hm!WQLN<7E ziK>uScrEAc7z{rsz!rwRdE2$Wy+hk!Hh>3eM~!s($+Jg@TG|~ohY%Rs=||S3;wUxd zNwAA>8GIP|%jC}+BSbMNt*YM&l!R*_5k8vx8xPw-F<-V9qa~rIzKX~Uja!PV`NO}0 zYcC7|^vpE8sA1uhUn;ut>He7of;vGA~s3FQv6drKkogmoDR7~p!uY%6L@t#~-=6V%)KkSg^>fEBM zvrDGVEp*SjM@V0s-$f4@aT$N4>2NW6US+9H;k9*V2M-b=Av?B2&SQ+edjKWYllN4% z-qq4k|AqJjrR&nK82f|o5n@kqhG)N0Lw`KR50VZ(wLy~l;E!l8cohKLzNTk>QA1B9 zk;hUcv}B=vFrB5|!NqS_X@s!4|WQYKfW?5?-VA= zSB6y;dF{i&=fNXmbWw;zC#OLKP~it<*aQ(d^|Q7^eNYr?z;6wZyXUfFxFgEBrT^ zaa{mP1#`f=+iOr@2P*)w-=|frO#cvMygrH{b1lFQWQJC`S3E@PF1qH`6iLxmqqzA& zd5d?OaHJVp7obsd2d^|8>_e@7N|-q6tXdY!tkF(_@pa>U%tkA3(ioK<&N64fQxBj5 zBRd$|Q?Jn+8J;-X+KI07Ib>k;LcWUraQo@Q5pcVX>G)&HOOkb;zu0ZX^tn+>@ z*LeXDT(s%*UgzUpu8$~152+GMpb|(dSkFh?28pksr?R#lw%-wl+G4A-gf!Y|2={K` znTO=G1`CeEY*$mw_(-sEQo?LThQn8bvl%N5kOBD zf532nix3fJ&->vM_^K07Oz;kU)N^~0t003sX3VMnq6@XDPl#OL#~sF{4ibt5VHa4j z7U`U>gV8->7j3s*Fmx3kyzeaUOmcC6 zyQU6!eN67c=vWR0Qww|pBcm>8*C?lk#x6FhRY>%vS@fyfA-!-Idj;TLk$Fm{G!18j zn&Sa;*RM<1UcBYB>?6V@L^}h%GOV=xqx}hI&|`o80cygfoe5g_zCeOoc*#^H(BpL4~6A)Gw?h7O&6se;jrUo$ttv3|l9}XmT6_%$h zyCHh}9JtyfOOkd?nncszAz)+6-17K8U{o(AieSF5d&>6Jz|&gySCfWaP`az)VgMoe z*oWl)p5>YM+ZuMx`FQQk3)T?@hUR`!V`YXNPod8B=QuV!Mc`Y2WsBHu7+$N7+VBNr zUT@*@&4ZnA2IzRN=$;K_uLccZjkFn*k}*ILPM876a8e4X8)z&@5$L|@?U9vzM*f`# zRR?>EVBphE;DJwC&+~|aQ?JiV6cGOv*w?s#okI?bE#M7qUgFPLLY7>6g#=Kz@zV6` z$!EHjahd4fAA)^)7Ci#P5#(J8TIRfnnR_*RaNE_|htKw1O5&@M=j-==;1ah>Ny^J| z1WDXz_~l8hyttt}^ujDSYosLX!5_~MvFzhUH1N}di4n!hd#uiVl`TA%}H35*+#_Dml~8HT5C zKXi*|8p;tAwRf$PcDh|}!z2Sj?LImTBD3CQ#)>@jS zk;u+PAJCI*8m4t`;H(0fRTed@y*b$K7$zso?2-?er?cYLRW+5ljlC`zj=WVq{-+um zP|GT)XO5bXvte=!k8|kUMsTAtYb~9Al+$NLwb_AZz{DeAVa7%0E?i;KpOZ0fae42! z8z1~yDQ~9>8$lcY+oj7JH_*Tvzp?0SHW((HWoy4}-NAf+%v06@78Yqe{@#QTOuP6N zskmCcnbwii!N(UX^nx9K-cgs(sRKL~(lQgWGQ@H!{#!KlxUhm1>1;>evZKg=b@Cax zCKcPwmUZL}Ge4!NE9unj`i8fy2|-c#tK7sbzFi)7?P+H%d$&MPrKR@-=Ik%JNGE)w z<xr+;mwuUB4qR7&%N)0X~8 z`u4JswLjECR{#2RchuJAr%3dM)7I=QdteSO7%u8T#^H`7jJME}q|L`7Uz6-@QD@pJ4o zNtRa+f4?@f|Em&;vc1dsn(n~t^N#w4ewNiz+d6%aJ+T-Z#%xu>4>fvY)<;rw18lUW zV_#Rz&D+M`&f~1=pG<4~b8;ANlkkN?v>6sOiF#s58K9Sdt>y3)RO1nO$XhpS2H2P= z;-+UFg6*qzD+wRztD^^A;E~jb;K~59gIl-r(9yH&8hQM}aR&Pcg8+A%2~B#^uWDp| zVt~=A4WP;X;aZVio6xo7=yx_Tx3=TiyCKSqG91RvLVSg{x=*dbaGgtra8mPw=aQr>7 z(~NRyihb^Lw)elN`o`!wgDvdFW@9#ujmEZZv$5@@VPm7wiEaDDZfu>XF-~lJx%czG z`S;FR@2uIgpZ(xTIwd~MV~o%HBbFQ1` zU7`;Hm&vC4fKR1kjW~+&cq^F5hZ}lN`AeO~!Qe z#+danw)`n4$@9HGo?CAOXfG`U1337_w(%tPkRuzRC3+jFv|bL`e&X`0@j73Pc)R*? zh~*o5hnuqmf7eN)M8F(&Du^|tcBIXD0~hJ|l8S$VwIPCY_}RnUQ6|oKLu9e#abg8t z`L{Re@mnP=QgZo!3QV~pRhs1cRIO|_e%_cK=X|(lyh?k{6fk06OGco}UJ?y_2J2M( zozF4!wzwoGR1E?@tE{E#An)Frgz*q0NJpCvL7ME=0zN2G&@wvaziuKe=S0#*g@RlQ3L zLS?bmPQ~_KjM$Ph9 zs+4|Lrs+KA4uUfaqjUJJd~`|gF{YvZFCt>esh0pCS>WB>v{Ms}YS4u}M(!1)VFg>t z5*fczJ~uNse~96jmNq3=|1o&sag@`hmv}Z$+|b%Pg-+DBgGWh^jOM9NP7y_1Y+ZV< zy=?INXBD(q1Apuuv`jKqtOUyvfB+RTa`${LgnVJwUa$9}UB@kBe2-4`47zkTbX1Lw z*?1Za^>(G%R7H|roSZ--$%#;7HqG7HV6EaVfF3+;y{Hq`P;-eDe4kw z=#*h4+?f<}fcBf!0GN!r?d6{Ewvpn9*J569U)>hY7Wi9k**L}Q%J?>4n;^{-)?I8O#m=ms2!K|CLbwoeDKj($8y8SV zs|#F|*)Ic*>-}vX46bzlR&x&QF(_Rjl3nq4ErO7Xam0bamS9@{;npe*I5ZcXJesIM zt0XF(?4dI0M8JY(=f#~ejUcl-m%-M-V+gBW@nx{^CE}P25khMP$3kp-ghREkcb?lw z7T@oU089B4eyK!cg!mO;QKpjG-OK2 zGGp@*g%|V4B(chC=xE!tug9G%h7^Cz{j>SrA@fTsRdN<5fx!|DAm1kC?H?v0 zSsi!9FgG5DOW#4ov*LySAev&q3z4GXu2G^b46aRd`KT=>jm^kzK6>}4cjFJ0r9q~q z!D3D_#u-SY#HR@(mt#?}uspJp^oDNHeW%&v_&FOkrB=^BS5GnA$UcO@t)Sw>ypey` zFY=T5Rfmq`?d+kPv^j1_&^d$J9Pkq%(oD&=12s0cwf6Wcy$b|wC9gFFs7E&UsqH8| zeD7C{qk|8RVf(0}na3>UO90;cXG~&$Om>cjxPwv#Y-cm1h1{%~mmeUwMbUT%X4wY* z=xwij)-z1uTk}Ge_3Rjj%A>NH`a`rUUuGG)jk=mQqKUFoYh318C)HSn)PY~6!I$9y z`NDb2A(uPP;|ndE-0W3?szF;i_7@WSj5EXFS$3*Dx)4_W_KWM76ly_UbrOFs7*Tbu zzR=9Xy7+Vw9k-jP(|h;G1(Be)w1H#yAngMpll0)(hL?_P!`5%!0(ItMB_e-0CnD#> z-r}3UQ*O%uEGg5am|5*{!KS*v!~sHeCGW0acJokfoy4v}L<5k*^xrqLCCZWN-dAcU zU1YVqg@VSh#ma|B)sNp&MV(x3F<8SQ)pFUW>GKy@8xG5Ee2PDy72pd(RWKj`AG5?c zfpTgj8@xM@oZr$vwf%fXD%Mf?v_bh4*G>8X=Lx5Co{s0*0l@%3vUUd-J%2ppqo=?` zu^@UPmT1pI_;sBmA?o)<4XG<z`z$H*gLP1e;i7dZp5#6Y2{{EIOli)?+IEYZ-o|j<@8v>hF+Wc#dX(55 z9yRE@wIn4=rrN@K2Qj{;=ft(TFwT}O0DxinzsZq@pYC;Q1C3A$8`wrlR3 zPIJR>3xF`W+%g5EgJEy!OCKJkTu(QwHCz{S9UZNSZ5AFwRjQvTj6oNO^3B`@ieAzI zBy$4mf789igddh4J?iJas^lI(adJjf$v?53U#?ld&r`QYXNJ+3_tvL?>NR01o`KXS zf$Klt8H#w-D9mM2STd+1;9c=2#RoB27~t5iiRrrn7O=yl?{H-5*~hN%;+z>Kj4@8C zCkeKk$d%cz@8oE^>b=!|85^tn(<(OdlO<9VVrq4J9oKc%@T5ZCLK#iU$7~g8XFdZr z)HlYSgqlv`%#ZQ}$VGM-22_bQS|Bp5m&Bnv?N2%%x1+)Q8^DdYZ_l#;HU;OGGiWJH z29XLPrqPL-mGx~f5~zF~HvhHti2rwBZH3&!>Y_T+i8QbreEJoP{8(@Pw-8m6j0M4x zC0iBxkNdL7RHFrubKsd!K93ugHe}J_1X0J=7x70fkVj zk}q@I1DZN$V%{Xu!Pp0u=jQ6O`0x9=Xyj?* z|9DpAm+YrBViQcwd91jawr*{dQoxnSE1-6BRBLtL#c9RY!?M=SC9_Z4ar`BnOI}ba z1FNC5sDTBt`T`n{yGX&Fk#pD^JwC10^n?Sd=YQn-A+j~q>S~G}AF?@}sn1C0_ ziUG6-wx(gf+Q{CD*n#!l#$66Nkb+RKu3aFfR$pM5#c3`ef+u^pqO|DXK?%yT z-ISs`Rz~%^w5u;3BLjNZA9?`45Iv@$ zqEg2qxF?rB!7d2qmYPL00Cg_>7m}W%uy^-!(XdQUYH|GMAEziM;#TG6zla~w0j0xk z@o6>NjFGgip|)%22Xw97#>+qRSAKh7uvOBq@a-!Nj4^wZoETZ@0#83Q;nOhSJH<5eZ+hP>$q zwf35Ac=)bO762WVr=E>P5vjg|E_Ci&ETKStwXwC+ZSM~Y)#In~hRhbzJwrKaZXsBg zTOp{ajs(vBv8)xinGsGP>g7cHjR3D1p3a2_q}}oZE}X)H zh&SJ=?_!*#JU9Kxhl%}#eP~*Kh^2082Nz*wCt_Toc z94+yXan)hJdh4D%T-7ixZ_bi)F^dR%(g8gy!*1_#yujHvLa=F~v;7k@+~!)uiry(& zm%K$7Q~5Vw9d<`b6n=z6^566!z|9#E3y|(` z9Zcy@E*6e{`)lkh+#}!!A|lAcbJ@?r?}oYD)3nlE@r0Xi8R*;In!fDl)ZMWL;KG9 zS8L0RlVKYD)mERY#p>N_qZwGc$OE^JcDjDomK)dPX-c>$55b!id`+YB@ME0OL!d0G zm&VoXn&NNcj{(c@iLb^VP*QNYHN|}S?_hPYJFs8cT21M?<*bFTZ{|i?FGf{rJs+b zlW?Qy>-!wWYd-B-kiY<8im76IL%_^ZPdXFJOFz8+M!Usji=fy)7na&ME9?Ly%-%Rr zYlUXCaLKfzYX|!fT3soO#YyVF^ZxXC%aUe7?KSAfbPfT)~u zWg(qTa~<(f9zT+ADy@#39O0z0&PWgk0!pr=46_oX_Vm4e|1rqL@`w6WQUV74xg}=y zC*dIfnVZbQNXf5&!ybA-D{Yx&;2_Fa6eu8#vst39(oRJI$21E&+Vq=~?BHGXYg1gQ zc>0ZFjrpBIOTcR@fo74SjPm<5yUa3l^w3;)+A`WoNQ^DX`>O>FF=TPCZS8S*k;_bP z+KN|(lf{|Y*PIXHBz;G9cQ|!c!>=|t6?%R)Dc^E!(oN!`6Cb8IUjC)Hb_E)@{|$Az z)ULEJ)V1Xi`eh=~J)ZibJq@qm*IR)3gM#}mk&MSFuZhV;G%TuWLb*2Y9Vu zuuGDX0M_3t?Al>KFNLxaf4O(-F`#hv8*37x2-P$tk+% zjtV3enO>zrVj~L$iTKQ5txVIbIENo41_5CdA-;4#(J}<)A2kSw8rC@QPO%bOXJdak zx@fi`nf3xhL3TJA4ZRE{&hY10CP}M%O=f>kWrsZIDM%x)Wf*)qIsGqO@c&tfqCiIQ zO<4Jy$4@iMiSR2YEXmcqj^sB9q%cMPK!?aROq-g$mfT}9CgYsSW1LlgqTTPgyTUH6 zdTKqS+fKyV`0^m_q0+oAhp<+|drn7%8N)DxSrhU8M(q3XF1~lhE}r3|Y_h(&?0E;Y z?Wlbru|o4K5+6`-YLbt}8;(q}!KIM7@*+|b+GOzrx3N#-E2hm)nx(aUiEr$=&gZ`* zK_a#7b@=K_&HWjxl1>Jiw`eUUE$A|}y_*{`|17qR=$u&NR8?v3g4e;70^R8luzuz+ zFV3B*q7s)h9<9I1zfac}P|Or;svTtl9t_~FYit#Ze+8WBQS*mE@b|2Rn@T|W$B!CK z6Zw+23Gzp6!aLr|RI8$I0(*Ee%FQ>KZm#9Kmn{9kZ3gFpEl|j7LUeOMd8;Q(p%IbV z2A%CnL)*w#``OQX$3a;lXs(I23&%nd>;vncG7EZ#A$w1H60IF}bGM*@MoZC~=IX{; zS%@OfdC^LdzXSeX4ULgr_Yd_9zK@AIb)Jv251lv4K{L-lPB=V*@ayGYe!y&2>5b zFYdn%cZXhsRp6A} z+h34WgL1H~B=U6(6QB#+Up%=!*iL_^SfIP4=%<%;S>AREBBX6m!#3*;@Y*)T)BZiJ ze}Ok<-J|1`OFKipxo_s_>AHT+?x-XF7Z+*%_jLKUD(24uGymd`l5XGiixx7yFliIL zgfP?EoC2$~AuobLZ?Yca;|}0g;1pyCh>L?ic94ClltlfaXIHeYm?oBPNsfDxCbSja*e@PGRE$-J`1-8!?hpj8UIk`A$6r|-!Rm3W{0IP-4D2yCr>a*Ruz z1A7FqP99q&kLyKO?Mt0}6Sp-sQ1fSYw9a6H;V$Z{u+-xfuHA_|-4CBmg_{<15*a!o zimW>aevBuA)i=z-`Z4!&Qyf;V`u(kcg!v0K)H3y9&a%E1j*5+gFe)b38A*OPf1)@9C^Qkm4BKV?%!hh8LI zx|b~0dhJGM`^cU;-gSH)67Ke22CQDxCAW3b%wzy6Je^fuT@LBfOs#)_{xDbnzS5S@ zBnhF4!W?!Ai-uzNE!g8Mww5lF8whlEo}S0Ur!}B4o*DhS^uca5pmp(jdCC?|6gx4} zJq!1gq3<9(4Op}!{viIa68NU=hi&kF(-wurm!E(|MvtXb?iQc+!pX1AD=0Os$&Vzv z_eKm8nVDINk9BXC=0|#?-MwXGp_fN#_6dFHXyC9!BFhfKd>CAAO7U5i7VL`knUbBA zT8H$3?RdA=zd;x2*9wFo6}v!q32AVn8qf*zrsGB~{oVoR!9A#aNsKIxyEat++Cu zKEj2uJxROX3HuTCueOls0M&Pwo!}-K3P;j?cfRbWkn9H4wGoZo6ajOwZKlL7Ca$gm zb8`H~frlkVsQ}O_;$Z4K`}kuLku@_ioqg~p8~5|-@faO1GnF8?o3~8o(wlE3sU&|J z&f-F(bq;U@aj;89p>Bob0`(3ba;nWDc-V*5Hkr~r6RNnoQ5W;ue4EDt%f0sHGK`Bs zCGHQk1WM0!4v(@-wjV2}wiC9B`!+c?87G8W+fNU0ZVo{J;fnbwM2ccWNQGzy6r~i- zh=$VVVf&Z!A;DHRi3w)&&EVuv|Acut%ukriFy6qUGfIjMgxUHPR9rg^uSCwugy(9q zuEf?M%(u!iJTd3d`n`SQ8hwasO{)so-3z-_nOm3k?x4oI;wh8RvKZn*htviiBBxDX zd$MT>X6;HFgIYw*AB>W7bFZP)5=}Ipa03@=02+q4*X8Hf9}Ki+a>P)AjG%YG?pa(kf$!$rJe@a2aoW@;DD`C!qZtX z(3h%WMT-TL7z$QsxgCbs$TtvA?ap`zaaA34ggWse+Nj#hjmeA!vh+Mk9tzqvH7WH9 zb=}}96=Rpac2X!KoAAr9*f|f(zAJh~7i369x`Z+y*7dyRcspiYkVIuk) zyZB5lNq6R(iolnHRb?l36BMD{nHK}Jn)x|W4(BHrdvl$FedgfD??<)vGbI)j>KB|e z@tM=a1@IPERyw^ZeLcEQ>nQIISOh1psL<=3y9^^oo>y~`2z3dBmz$c7<4HR=y$9u3 z-QEok_g=p{e+~g^z0Qk^jG1vZqUL>P-;%Soeq84A3dh&`*qW+CQ6Wkd7W~%Hkz*D* zyL|4yeV^}bMZmb9#2^1S>Yth4og?#)JG1OIHt&;?{;q${s}~tQm2_q4MF?Ei$QTy% zoqzPE8{Xm0kKNMjBr9~ae~c^=X}Rj#2I$*6-)q@$EOLRiO2wu{fb_RSG&7|lJzMbI zTqsup6(pQiYL1l!Pia*y_KSmPHuOidtzV1xDo&<-cwd*ox1+D_LuNO7+bgPccJ+Yf z!^_Fx!-66s@R-z;F;w`-ArHXhmco0Woc}!R`_5AldbT@fbQ3T0cl=JcvkECJUt*@x z()V*?JFkS!=gyIVLl8$i+!mZj%`*jbcz6j5C(Nn7? zl3|)C4Qs%zhH8MxAT@D1I6wf(oU%h$ZyD8m_+7wI*uV9=0FMA%dPqyU_WsLryFyS; zAS4Ph>j;2*HzwXufNL+FS5^y zi1L_B6;*xZ4774|wRNG{z{AX}EP?v(xaK1|&qdgMbjwdT=6?-e7qE@LgIn^b*@mtm z!^{{;cE{`c$M~qbe$c|0|8ZN{9$bs53;we~0d!H`(qkB=vWPMdnKm%yyUZn)_V8RC z_xeC6|rqy$3oqZE^fQwoF7iNhucispT zSiTV8v1dMdea4^VH6p`Oq;k%-puE^a^yMwNF2EP$RWNeV=?QAet*NcQj3pHX z9JoIlIXaYn2P#D-YZVQxI%-o+u+sJX`4lF8$qk=zaJno1>K0~N{mpWv2o0xT-u<9l zrTrsuD#MY~aAdmS?jvy~^}fdB?8&9R5s-wS+xuE67`M`u3nBGh`1Buc(1*E;8Unt%TJt^P}( zhpvt5`jW&He2Tv_FZNJGVTkfsSxrgrv*=jWGll67{u4-OO%D1%-{T~LSk91(?_Nph z)}s4_#_6QocjAVd84&@tdP65m6E2v(9}U?j`wkjmxG7wGogU6N6&FUrS}gwfLoY5I z^T(^G_F|95?5a&Vf|u@Dcdkq8@lbK3`*VlZq(5cL2TqT9dxjdC(-o(z;c5Mptr02Y zwbIKujdMsJ8P*kJ+Fl);zyh-TsWZZmm_wL<(DnT!PImP!d|jqwIC0OAcHQl?`?`g$ z6+puKeve4oAlE`!oQ9%0BI*c+Dd(*~p^I9QNh5{IEaMw`TLWE{2PHq(h{HoBqE!}` za(XTS7q4>)MSWQm*(|?UYAE@Y0o{Ei{QLdq)MaCo?j+%I8NmMW(#shzBKSJhk>_;=ArpckS7)QnFjza*ga#yCTbq6w!S@4~i$|_=|o${&p00J}uO7@63O| zl%|GvF!7lwChTgL=8|^{d^sp#kf#m6tdPJ`)S^nzhN3g{#b+YJSq zy{B2u^@!eoi>^mjMCVtUVHkE|f!(H$m0WXu-z%)ixV$ewGV=*ehDpSA7X6MLDd8P# z8+}w7=%=M%vv~J>{{;ePb4`o!il!lF&`L7J3t-05EOu~1tVk<#&?1FA2%<}S9Ibi; ziFKm^UfgiP;Lu<7z@3OTPt%^n0n@D^r19O2IO;OnI;kq^to`?m#U{J0&v2eEc88X_osy5}z!zt7wyAjlmbh3rhKON} zJSk(_4R-Cv%VaKGz=rAt%D?gub6V{qLd_(3db;#|Bkre5kYsh1R1Gw?q%YPGJYLuh zyI3-pZciH>o;ws){$Vc#h;EgE=iNU{z|mn9M?XqC{C;qBw5x)RTcNzlC%enyU%rq3 zd@rVrI)A0!7_drj41A*5u#tIvf-}+Td89vjHLd4zL|yVlA8{SCV2!q656c?YpusV| zv#Zfp=GAC&T=FP(&Y*Orsdkz z{D^)l--I6zo}C5`%T@V}qFSNP;VK;0dJac(W>c4OiGvus3)u~oQEI9+nu92IwgM_AIl)q*VmVK(qxH1Ad-0UE&MLG%gc)7 zR#YPa8fue!hhVZY+OVH5&^?t7H(4jcmVmvbGj2|{&hb#2e}xBrwE$-;qk+9-yLZ=` zn;)*g#hQ1A!0=cp`y8Q?Y}0GBWWcA{-rql5mINPc`vr|zBnpB74k8L+{yzj@{!>Z+ zFKlQ@-^A6~8|n(btv6p1P-l@(yXy_YUjzKSnYW8j%o=_&@P*D9(VOmq9--tLyJ0Zo z`;$UfFwY-yyT&wixQH@FlIwLm2L070gnx!&?6ohY((HjND);m%pUMIkFzdHw<<05( zs=(4K^q*)T*Y}M7@9UrY_$-q70Rn_W8-cC&%cn2u@oGH#Qvn=~TJV-VO`|MCWHgv# zD)CDn?q*ll{kL3AN*_6-z?|It;$(JQ8B(i@D$+SaC_qA-t*&oM{IGTAJx*I~2kI3` zVR3IE{@L4&!z71pTFD&@==u7a=j)egNEBvRYuZ=clW;oZSLbuZPs0)}kQSLA)pOf>D;9{1Mc9;&-bKm1#c0rtC8ql>_qPvRow(ADrE* z!k-a(E2)fFcNl_22XBzvfwm|LF%vsmD7&?nq03WB3%(PzaWEDPuJroRn%qnW>fux} z@=18IU*e((TRKl>q?A0yzbG>M#_Mqs%a7fiN?ta=+bX4mM2qin21tIdzyQ zzP#8|_xNm|1$2O5l`5Mm8jOTyq{UNYKY5K7SV**5Z(FrQ`)KXI9`+4z#N&Ao-BXxh z@SGUE9XYC6J_#Cw-j&bWRZ!nHeq9PyG~;`RUTfyF>14sd^$PkXj%jv(OS&v`+(m}n zQZFoH?sQwVD1W=v=lbZ$oy$;m6asip)f)zH`1>PC|3U*pNouHPQXoW{Bar=&d$!d* zUAVC=hyRt-w%5EBDz>Da`er$#t$)(9&(?4|eV8RiZx1sGH!y%+6!8Ojtey^Hgd4s| z1bcJ+W7oElZ&L^O$Iu~Gm7N~FO%8M*-O4N~w{C)05;eY|a?P{1cb2l^(o;=?+ky8d0aYLBZIECh(%ZA;cgsY+LwcC$ z3wwT@_|*z!_@S4^cL~YfD$nZ2+w;$nsc`)agsjR8>gt8a`=XZPrnaYJD%|B$0|( zQ)qKo>$r9;1yw_W=d%81h4z=~5nIe8&Xj5RDFbZeyF;`7{`C3%YtVCgK6x7pw@3qC zkQ%>CoxZpqwoK*YFUhi5f@7h;Oz=3fuDh_Bby*FvrFht&wFD(}M{Rlg^wpk=yDE&NE>+}>I9@%bHj7-60A2YL1Mzf!Rk3iXx zh*^~h?a32}J7hcPVaH{*kCpl<4EF)#0-iqteG;2Ttb(cVvJ1u{K6!2__=>)Kl<3Zo)F9gLq{ zf{q?^w=QoUvd2k4xo`3Yk1VuWz&(!XF2V;ZkNd zMh9!r9i89GYi$7L(Im1m^HbzCt(j9wvNiP4nAZ}IyjCnJgZ2(`+_-y z@-7th7W;s}IP-?L;T8p1Im2Hl+tlH;C0(<;_Sa?1R0j@$_C-fUg$bKLKsZ^(Id@|Gz>A)E_-XWCdqPmORLU-`E_z0qT0=8%Wv#OM`Vot zmjeyoaj}k72J{2dhtM1X+_e8zbJR*Re72(U$#e`(9+EizSs&ma`wzieAva`CKpszw z1wxd0-RQB)hHiKv23y6t-$$QsZD;pU#Y$Hi6&4IG4L}v)AT=^)OxY0WS3TwLWnD(3 zSMdAedzu1uJxyi8a5noUx5VlQ_oggxU{7NsYNJ*8(kmF_f?s8sQsA7}D}XDh<>C^J zPyX*Y4>Q#kg#>E4Ok_TVM9essQt_Z7`4wp2=&H{Eq?a`U0`EnQULl55a!V=L2|L&l zU`|v3L^egE%=15TKS7^&Nj>(-S#CvEypcq_fmsH5*JW}1d1YjzH!aT`j*>e*`x$$L zD2Jqp<15}S%1@-wM|{UA7D=-h&t^vSV?JJJ=HxM~9zkD2N3tI}EEWGbsq0lCKOX`@ ze)%4@jgmjs$ltX!@VJJ!)(^x&HIe|2KVqf73mqKJGu$JMXmGMM?>StdVwO-Sh0Z@z zhbOVEonYQuxQrl6Za%pGzHUU=b@KR#mxL~M)5j9c5Vj*vp+-=N-@z`)QoJ9q&tW=Q zg^4l|wzba^#d?dd9;+`f?gY|M+@80?T!7e(Dn&Q%RaqK41!8i!2|5fW zeoDvMybpQR!y$NayQ}_ahWW&wJ5Y}DR86fE6baVK;@SZ$1dJFQay}$GntrD6>Zw zCNsi$rq8%;?3@ZIt72#4%VceSeI)s%KmZucC~dp(P%L&&&Td zfmDN^ipaYS8JjKG6kdA8{2c#sgVr_&Hq9K_aQ%TLiC!RnP1o*wWh3ZjscNLw<6r39 zf1&dqO@-~7mO=B;%B2!=jbZ5Dy+5N-p>#>iz_$gnn%Ro#3awg*h{Xxh8qH?NIYz2K zm0IpKhNrK-@rvv3N6e#T{m`XJm^F0YMNFMij=sXKYXx0c2kx=|Sx}PtkAI__Zqk)XD~6w$ABY^On%wk=Wf-k%t<2S}n3ob=X~QeY_r!wWJ+= zS#21d#gZFs=>@Vl9piPazuDk{V~u}(rHhuGLSD9Sc`Xm=%u#_K z${co3_T!I$p3OjJfe$6aPHAl8qStUWtdX71p#r-=<-cc=(}vTcVE0p>>epd}347mH zKD1jK7K))?I#r|e3}njLbO_vbIGs&N^yQ$3t28ta*K-cY7gytE9CA-=Hfi!Y|D#{3 zgnz@WItW6Y8$bb_k-{Al4p-NJX<$JaDAk>tLGfC?$QOX|s0{U({{dLWA<>hB)Ko^d z-x4<~`YuZ(W}Fn$ah6a+K35`zp<${+UX=cEpV;#PWMT%jVU)Y=dxigW$~K2jGh@Xx zqyjE^cx)z&u)npXw_sU7@C1Ylbp`Pk2fVK@4?Xjy6ZL3@y3Ok;9~#My0W-CELc*OjvuD#$p_dP} z$g7FB@{j?Q`>ugzBjp?WMjr7COZl}FxUlnmfO&#h)1mXb^Nblk`=dz|yB-zye*qt7 zdeNopTHv`u2#0E8tq!kphJtCdmpdR{3xEQPfyTrY3h4 zvooaFzAYi|(SaQwiSA$5B`WUeIBWQaZwh{~7L3;$?;%z`Dw1KR{C=g(3KPO{7x%!I zz+jM4g%(r8lXJls);)A*q<_(%h#Gj8dO^P7=s~u^07umbs1W~0)^W(HTSOuH4u6Y^4B`lP4B2r6 z5_cB6mQeh6tM`Zx#H-lC0n#{o1bPNq8xT7bo76->GN7&Gr7zv zVU@Eu9(Bp>euopjqE;oNiX8GHrg6FlD>(8maRlueQ@%fNW4!on!{;cP7aD|6N%_|nkE+U zfF+C*z}cMuVZQ#E4TP&gVe82)FckHV zSVL9+%*B^$lC>wYk|*Gpy4`C##lnDe zB0lmGuKA9r#|_GDK0J4ea}Sgx!lf^-hY`D6H5`2Tww*9LPr5J+=hq{7_@anTe9lR; z4cR4Gt;8Ip`X?qf$u&*!iDe@@uNOfNsyTcvGngS7fdj(l9d4W}F33GzC`RF(HL5lx z!3zSJ36ouBw@Nm|mx{5((MW{l1BN_H)7qQ~#j zT4inSPkshg7o|gS?ssfNhrUxM9uuW|o(?$^i)|!5Fm7GE2t#=bjnJHQTksUHqqaSBQYngF@yF*ejANQF?~D@1nHPnw9l(0-4fjy(ghEy^lNvW z5ZeZyFCY;lS#|DcJlHj&NZjGBEo&Hlvz(X6eY+K`77+2t8{qNXf|STCXoG`K`bO{J zIK%q9i0asv8az*i0x$Xd&DkH^8fQsLBh&)Oz~9Pl)zyVnh1E{2ofk$P*0NIxbt6IY z(MU%0!Nq~~Zl{q}lcal+J%)ETtek`+_3x4!w;~R06|*j2Q99(Qc-12-S7k;E#APm` ze(d!AtK1z8Se)Z^nx}^g0ILM-aovJy)Q?=~JX$GevBIv{`eZOX@mv#OT9KOUyU(|P zwT2}E_z~GhRsJce6rXelnXJouzac#)78Iw>DACZ8O2aNUZI-_gRDfT$F+(E^@a}R~qAAjgB z8wL}b==vU13{ps1NP=qCwan-ddSS$3@_&xSCF5=G_8JMazM!)u@4i0B^d_e0rozu-_!Z^P0Y`@Gu61Lm<=buXr4(6 zdJGO8r%6!`_OwfDR+aa1_eU6b`levha*kW30V`gGLFvXVG;p>>r(R_y=vd9D?KGCG zx94tS<-M9VEy+3xLnf|tnCVmj9SPZpuo zBs>u^=F{mRWm%OLsQdE9fBwd^-f!w z*d9?IWX?eWd-6n0>JV8&)O1ACEf4=Y+WiQk<({vI{IDvdDL^pfjvIBKmY6ad#A!j0 zQd@iDZ&Lxn9D;ezZVwzNpk*CIiBXe?w8TptA`kved=g3b*dQEj5;BM5aU28c;>$MF zdg_QvXPpqNnG-~H3{C$RodBfug*!RBsuI|@BN{uH(Ekw%J1CpQS7l2R{Xwr*8wd*{ zo9^z%;rJC@7O~*uo3yGacTB!|oVpfD`|iPly?!Q0bJfP_@9cApNf0)i+%kVFxSyt? z^-|iL$b?i~U40jOP_(#q0w0;|JeH(be!AASqy~?R?($$DnZ)Zd_z$%bqL>^5wG$qr z%%yg`gii=Qv9=T4R=}PCE999e`|u5S+&uS~3kFF&R3>}F*P!o1Y*gF4oY!#{BQsaU z!8?_8#wpU_!+3E)DqUqmRdrMGai;RySz)KlZM8-r(w`yk>O4eCl8{=;G=cnPkjwTR zr`NoJkf3`Fb_s^UBxtE^$Qn#6S&Vg9S;mN0Gjx;wqD0-HC#)R5Frb-!p5BtbZ8$I( z2L?ZL7%#k#Ciov7RliE2!taw2n0MN9?viP#yy&P|MS!^qdPuz)Od9Tz?(4w)t4=n= zZ_3Ap@I_UY0ChvK0Mz8`vvaYLTNic<8=-+Cd##DDaWm7X#hj#G;0BO8wV~T9FKW%(>u^t5rp3$ zrq%4Gq3Op_ryD9#Jr=fj3zy6uDm7xuM2C6kMcFE3FplXCCTPyv_J{of}sf zr3FkuP@h~~^D}@ic12(O6CzDI@A2m9R>=(Gaq(Cb@y1;u$4-#WDrtfpWk#DK*EErs zrO4yt1a-0Ho=oNlj-e}~pqz`j^fB@iD{Fmd#8$OIFihdqNA-I~XEnK3Zi*L#G{eWh zl~F0-0~7H5xYgArVQ-vw|8%mia%nU4a}jBj-kHl`E_aZq?{tPeR8QK|iJ>&fz4jqt z+CKEL?7%iiBGt~`_kPntyd0Y=^TRX~52N}F%B8pl#%d|~y{F&u3L}mN4F=Jov!T-; zqK1e?>>fV_`RW-os+??5xY#pC_x6WL8V7~Z8B2(K8yUsGyFH11X&_{eTQKwP5)F93 zlr7Ru8k8{nxzG}SzDZm$>GLob-LLvZirqH)il92Cvo*#GC*C%^Y&fNt%UvxnqXs}) zE*lzCV>Kt(a4Xw)uCn1li0Cb(uT5fSI}kG9YRRdl8B?$(CFHqh_1DMe=EDa zgI%Oo{3Ul!dgbQn2z$X3z451ppw!L*Sl=R{=6isoJ;Ydg0 zH#4YAed+GodJ(|ye!V*0p&eJ7F18;~!y;OkJ7s^3;Q($~+FEbo*;<%`YHft|DCLp< z_PFkmoSrPHsO#+)bc9~94@E;#B6_KtYX;bOAwqct(5vpL6VMQ>v|L7!PZngGrpj3b zSHX7oB>nk2fT_!w(w|_jy?Tgt_F1K;xj5xlx`avv zLXRG7aE46$zQy~*1%H)Mjm@qjIDfJd`RbFmAvVONT5)YwTGPXDFOxc#`2UoQLUdZnLug{;MB-(B=gu%-Yj%@nJM!lQ@S{ z6h`qJgYy^$BWRsbR`bWwQB*NMFvEWD;o0Y*71S(4(G15jILmVwA7QI7%vOe?5%QdD zL157jnVZJG9>oPs1!tjoPNW$U7dRRF^}>1mLtW6IQZz@nBXIfKtp-kcy?yK@OLN{Y z(-OzYB1N$hGymi@Q~A43TH#fzO@fzz+9dhoPkG zS|G$cgJVcRA!QC@ae{_5oO6(_@b3&PYuivs-KrQ2`$52@1^6%11tmSseRg?0KD zXpHYK!vfxt!1Cg5`##vzErfl+lY+u2yHswtF36rfzW#+uk>d1vx;sds#Yh&*PRf)g_nG$$RC16jA9S!cMmIfYrc3-QB z8)L>D76n04DU7Evg+;|3Cf8?%r7*zH_0EX?mjyawS>T19r0=F1J46btQSD!Og}`Kj z=2QWLk6}L41fMKKZ!8OB%o%f0*5;W06A4BYNJ5qgnG#|Di9czUtsS)M+E>@LhUFvY z@x6gB@t1dYyCbFuMT#<^5I84u3J!}<>yV=O%0NlyFDwsdbdS=SFuN_1rR;r!M>u_5 zPLJ1JA_Y(N`#C(tLCa~liRtO#rM;aJo{yyF`vTL?E8Qa10ZkkjtydLN5d>NxXiOI8 zKDh4SZ%;JN2DH^!<4`wfr&3EaJ?*fx@H~wAzQA20)9>Z}M6zTSrxlV_ahVcvIGoaP zt%EZRv*gDEt)Kg;<>M<_qGxRpMPPQtIc=;^SfY&f6)W((^&7_aMBu}>kGwUOVH(Ms@JWnHOm}8W|I?Sa1tPqk?xt;2F8kikb6)tSX=2V(e)-# zzTPmw^|E1tc*iEVPK?!|BR%79)D?#@mNOg1#QA)1f_y#85?ya#wEr%ttw{Fi4%u5^=QvR!a8af(j_dcz15-nLn6t)* z`n7;MbMqHB{TkDxlapl;XJn3*Bsr={XA5h13IllF3~AE&dSK<88yAlL8g%-&lxUqU z;+V>a3?Y&nne7|(*y`VZPs=|`W?7=r#HTtR#%t=;sfNUwDF)x0li=>HMJ30>1Mh}3 z3hl1L)0Swycs8enm+RENe^ATcqElYH;|QA`g0qrrT?Y6@;D@jF{?_~#&|`J7qVjoK zU}c;YgFYyFO@EyJYwPQ|mqMOtu7Eo6QLr=621(YC9@g3nY*tFNE#upZ%hFz&Gll%Ch}!%c-bcV1iG}(STrC3j7fUr$HdZ^fYW2Ueian*Bfi2PA184rPYZU}X zWFKm5H%B`(ykBeYrP|ROIK*uZ5oHgp-m6ohk1?=*0#;3*q=(}jIG`+*wEWfD`@5lX z&em~&9SKg<(nmB`M}4)sO=C5~+=F@H<1;TM`dn#{UOnS%j_#QAp57io$+A?|` zejmV5?RhgurR>-G?;2b5-@)^v+0nn#!=Gq*;w#bD?g#3I)6f0Z=yVF`(r*i=Ge9?B zK7x4<<~sl|ZvgGp>0dw3tT>F=UeM`A)H)NRCFhodhm(WXeaCpw7=y*-+Ss9 za6Clv6i4znO)3J8ay(RBb_FP?Z!lj!!Fne<%32-6l(H-EfD)KKYfbE%eWFV8f`U<^ z!iaycKjP%hobo$qr!YgYu8(CTphc4$xP=_1vgyT1TpkLN4P9%k$67{DASnKZFjeW^bg_oi5xDof+Djt&!KE!Pg=uM z7~to6S5DG&@~LLNO7ba|y@9KKTDS5i6-=X9QKyj{yxJt4cCAF#x#Y>KOOCd%)``K~ z69GLtX;ALoz_<5Vh;ChD*H~g`g{Bx8BN=)1uj}z1BQoltYTVCY4f`3aai{DY=GmQp zHccz9an})p>gTosMh!?u;h|QSYLOTA1iq<9q73KLz)ix=@fpuHj;ukM&mEP*Wq}n5 zL7|kU7k3^Wv=0VoHyj90uh!CQbkTku=-I2q`g(_Bxvb$8TxAJ{;RJ?&UDou~SXnP9 zja0(#58U0++o5}Zut}H{<&p?Vu=ZlNtH|L|db^7hc}&p2c7)pbrc>|#qO{5FqN9`q zzD4pNHv6o>(a1LV9`OJDp+O~QHX^NTJ(9JQ?+*;fYnQQpa4er(1kax~Ze}S=fY+KV zL~23gjmBBBTD}hK%bLmeSO}KGNzs;Jg(7%HmP851an#LAl#kcUIIEUf{@V|MxewoP z!z&Lpj+Ac7CafT-9Hz1iPYU$9r=DCM^hE{al?*7=Q?sWgX{4=|9|Il6Uisv*F5qIo zjz~^dgp-(ERwS3K3@-^@T%0m--E(bczLf!*TK5x8DAJhJWdozv+GenIwrZkttL0F@ zYJbrB)Jy1#^E}VuEX~m*hLJcH;Y~MPBY+2xhmA)9*4*LP-lKqILvDx$eGr%XC#LAK zdoEYHU0#>uD)h|k_Sa?4^juaKkXTw)^mIjL?B54M-vc-vLe9bJ+D=17=jZBhp)>XO zBY}^e+STLELtttCF_Pj#y>c^@T`6*SM->QeMIPZE>XL-Qp)Ntbcvzl#&By-S=8ynv z@X$SilcYl5!}f93Bu#x(J*>z{@^c`q=dI+g+d)g`R-6`xc(WE*6=N_`Aw+S>)LZ(k z-qEMt6liOj3MrleTnyzUtFiCW5Z~WwsoeL4mHfWH!S9WhqeoJ!pt1W~OJCN~?NB){ zL*GAQ5D)n72E_aDb3M5Q{{FDe`$H@y=qcwi!lw2wfv?*&DcSdQ6nDRomM${R@f5>y zgd&LWy|HUkqjQH8%ddfkZMXcl^<<2YMM1ABiYyU9>m5pz;OlT-G>N@T{~CDns;`u9 zC$tDzxT0XdSb`OCNmeOlUDKccWjiYw=Rut^&7kHL7Q9-Z*b(P5ind-augOcZgJ#R*)<-!x&>yp=>` zhtH)((A)yhxYFHmJaB5{vX?)5Sf{QcM-wW?Ndm(^_gAln|1R8@2I>mD9m2s{&ey@+ zEAG0U3^1P`zOcbpKns6ml%Rzqh1=cAbhpE+3@CA)zRBIaSc zX&un3Md&?kTw+9AA}9_giTCOZ+5gEW1Jce*uu@GD{BEG;kLaNUH@<~WSu4qDEHj#4 z>z~qTe7i`sUxgL`54sU|<`*ib_o*XKYXe~)0WUW)iqbm6dGzVy>!-xhG*YC>@I0xK zEE%C`?EQ7asA(jolo|d#y>8z$sVQ<*!Dy18X`ZDpf`|xwGEw{IRb*3AXDIB|tFC>~ zpjSqUQbbNxS%H?sM;E_f4=ye!uZvafqO_tBX)~^RZFk*nGs+n_`WRKj2omElnWGqD z`Q#%bHaEF9wIc$-#}~%VCks$`DvSTT7Rjj}Tn;)uOZto(KHog^zw-0!E$D%eaEuUS zRmB8`dbial^6R;K%3VeP__1T`is}7cSuk2Yc(1bc?bXCmD*&zs%6n(}jQd{u`r3m% zb$sjMyi77d@3BIJZ{7OV=xcuD9M78Z$n<4DUN$mzGV>Rd&t{ z{$YRnK6Q*tk%-HbCd*fFPkGub3g17s> z<;7l#Y?izkPfxgW#f)`e!G0`?G!l&Q;Sq|IQHoc7U}0L*RVOk)I5|n17?V=M^OC{E z6ug$GL@ps5hvv<=V&hNlj7wtrnvw)n<}i*VX%2^dO&#l9mM*+JTu=H;dU!r^x-_5h z@90gf7ca_;ocOY2Cujnm=X;SeujLGtzT9!=AOqo@NS2`)oaJy*;ock_GS``-7y!k$1&irqB6S(`MDyKi&R9YGq9Q<&n9zVA52~^U^zJGlN1}42;p%8DHx@)f4a@+C+A>CiHtiUs@uF?$4qP`(f zuA^GmDx10cEJ+`9{ML{X3Q{H%R;NjoQBeuSCsq7)Jcp!#3{P|SEAzK@eE!`~HBpSf z>k_Hz9HXH%nK3C^Q*(FfpKsp0mcu%oBo&cT8J!>ml|!9QTB4q_)^6*?kDmHppW1j) zTsBKH);znCDcLhLHUIOS7w(xkA`O1f3)J~wyfGz8cnQ8Oj1qLOGSWc7ANt$6XEWba zImd?db;~L`#mbUM$pnkj^W8oq&X8t4ZR`ttZYshFhEj0yg0!u>W%Wfb9e5w^OOFI= zpe#uYrP8`UOMV4tujQl0t#+(~HmxS{bBiHG0Y3NKYc}Ku)(-w&q`hcg@EhqC(zOTf z99BDUsTI*dmw8na2uUM(29A!xM49o7r1}|ID z8C@m`fuXj1JLH=lv+s{vk!Wn|Kud5YWiqHN9o!iW1>6U`GT0B!h95rfPhSMF-yQ?s z-3)Fep9B?EyVL&l z4hRg?P7web2ieLc;Qf#47pX@xS7#m&_ZtNvn?D4%pPSD3(_Gn~#;-B<<2|I^aD1Dk z#mbMQ{Mb79chjTQNwIl|W**848kcf;sA#f|>Ds`x?$?b@e@lCPoW4%h$BpJW`+8|_ zj9HH(;ezPiis0 zw@we;&f-G2NGFL1-v^;ZgoN<6gzKx`WXwJVi-n*47~%M#X1p`5aeX%`>dLGl@8R9>V|-SpprYu(?DV1aEwp7)HB9-1)ca0q)NY|opPlC`g_ARL{7 zV2`joblq6Xxcv;Wp5Z>*0C^9MGmq*yG&RMbEFcN`Ax55eE8}`!Ll$bAFwY}3TZQ(S z{Y&{kI4#bY7JEH|{;8-v6_n4_s}R-xb<6(HskwsxVDwdef4S4BR6`2+TbM_`$BqYh z|D^=u`$88>$P}&TAu=^+eXw1Be&iph+Ov)4Loe_jUhe@=4$w+ITG9XhInXYcqV3>7 zeajGf3RC!A0iB#4yqs!C`LKOZ{_+@R{JuSH?B_?2j;9#qL{MIe;$2s>k?wabZ>)D5 zc;5vGZzmYf2b5PE5a3zBpN_e?@ptq+-21riaqq)+zYWr@@y0yLcZO?EL!|tvNFMH= z&xj~3v zqZiVCNRN{cc@ex{gK~H%*M#?f@IDXDKfE`9a!+_42j!!P8Ak#Ag7=175PlxsAHw@R zcn|49&Jj?q3h@BmSHOG7$;rX#A-u1E!vp69;uYNYwxIk8m>77k3GXi5}yr)b=&KK}L6UG0DaYnogh)qz>STAf>56rr}-yB>%4e=ZHGai{Y z*uE45?;w6c{JI5WeUgN<1J1Y06ddp1xI+9x@eblE4)5T+SH+woAwEI0fp`bkF~sX7 z=HPe-@dm{^i0^Pc!g+^y1lJwJ`(6kgf$I^aw-BEp-a`5S@eJY>dY`ZZiLV0b4#Z1H zZ$2t#OrJ#Kfnfw0sPX0k4E@AC-<=Tr+;kdQx47qhBaE5hf#ZSsy#nTWINt&_5V>nO zK)0*wtv#g$@LV_tnXmsU+*Zu-D=mmc2#>+_40x9e>Fy2q&MOYeQXwT#6F;3vG|PyC-Z|!*uEl9Y_QTh77&HThpDcK{={s&Cp!dskCHu zWoq`}F3F|&91$cZjaq6eVsL%B_&fEmXqV!sHZv9=Fi86GibkTjxN30P-u_W+x<(s^qnXEfjE&FI2=BEMVZqy zf~O^(la;kwl8>hSRDctMpYwBqGdfg>sBWAQZRH;hcVy!i=hRMyzx8o*&I`}Be+ciV zm19Jj6=*@ASw>(3+~&stk)U}9uKRu6gC=^#+xuXvAUT#NMVjRZkz*J}K&z+)*v0Xd z;8nz#`n$I*WhWfy7&q)e4ga&hj94%IQ%ff1DPwURf12LY7rlVD+H&4hxe8);q-gX#PhP zF7Jg?|4GoPr%8=wWl_`eOQ_p1rDvC2GUmd%s?Kro!q*_$B8v zSoixQmk2@PR8pdOflIw*<)w}9?oCB`#sN_d$3=5=zZW)rq&wr8W#nIdpfXWV7pW}g ziyfh5Xbyj*lped5AF5fzdBELqMq)lV4p-f6?%!{&?obsnkR>RrjFhi-$F@_6bXtJXF1Ld9FdEQg30auK5OSJ=$SczU_EiKQz%XZjwD@6?#gkV>>+GmCP#l8#!0iB@<+d}X92lUFA@27oUcXv`a+ZVDI0?#K!Q!a_I7JkA{$uN# zXLV6LcNnWc^U<^B6Wx!`NoYhj z3VDuJWmy#!PLluptGtd zs|qi4-aU6tQIx8Qpb#G+O2wJ(a67*B0?~nn?NkU;5U8OaNlyHs< z1)zQg)8&X;d-xnexB%rT+l~1~$aUdqgnbx*$;Yu}FRllBbY75hh<1)mB6DC(ID3oo@Q8-LEbs>*8+|vMGVJ=H@tU zz8Fv=q>QS~&su+7U%kNB=#W(BLbO$c}HgDTzm^la2T2v7@nh8mL++P5K)42G|4FNM5~zMW-d-@ z{OiJ|fudV5Zc0v8meuCc9K*A6Tvm;sWP;IIk)gO_XSlqr4=pPPdC*#Oj5OtqVa61F zjsoua)xRy8>TYq!{=$h{D+Y4zE(bs-$!15{^~+ds=XCd>k!xrDS`O0n&6NZ3lV)ja zR?Za7F(S)3JV%+Nxdv%dP8?j)aMQ$;MCwK}Q;$ImVi1EE#2^O$T3BC9^J-LTx*RD7 z74El-4Rs7+5Q7-RAO zMhsBe`V^$2K{^A;2cpq^?o+!vJ!qK{I=C{cvxFouzDqD>TxEJ^QH{;6mPk={wmW^< zl%(PBVHF%`ffq%Q6Iqtwr~rLFZ)bmc8Y_~0*gbc|h`m*pMHE$&Aw-(iIf3SMFbyp&qR2@Ksi4H^C4h~skt$kas$A&a_@njbpC5*ca;qg6 zY0?Rr*93tUSe}}`nOvHcz9n&Zgq?%-wa`x*&QBNgv-4U#V++4Twt#%hx(%mi`4UJ) z@M2|PP>mx;_lF@T$yaHWEiUGFl@S!G2n?Fn0ufS^3Xzqw$8207u>a7W&SN%?XVnV+ z2Yuf6=RXF&FK%Nj7nXOQ*;rn8Q}O0E+~Qj3p1D?b=-H6?4#5*&Nn|NOCuEfuw1pGT z41T%BQ}H;o{k?kVd}#r$o|4v|RZ4gEXgT=sfls@S=I72)ht~Wih6CSf*u}d+Vf?o1W@gCy*;DO6DYq^Qj2QqDG)rMXSPcUaQ<2 zEvFmZdMqNwNRU{aV0fKUIW`<)^gK9zY;GL-_hBXHK?eC1r}GXojT~Rr)P!;(KHEPl&5lMzr_0xU%^kHxg^zofq5RAex7? zZ4w!sBxQo8X`Kkrq`+e5M)#K8e|+$FU%cWoNizb$i6qZ*Vt|Ip$*WR3)$i&~}^M=CaZRlacx4hVDhm{tfI%I5m(s(+lqY+jq8XwK;?tE+q;i$M7O6 z`ofN)&EFDc^}gUmg}b$5^>uAe?=FAzF2RqvLa%)1Y0~$w0bR)b>r~ zMP4EqniU0YE3+^A%j`bMr4`N>TI`BZekVp*e)K}N7u|8cuFBJ!HVr&(p@XF4I8@&T zKyd<1vMeDUNjI%Fn3Ws**9HJ`luO3oIkhqc z&feGF;^C0Rw9Wl|`gdph#zT%KHxP$_#xF}-hfVFR*{xc(%{45~zI{^VU5ih0@$i)S z9!R0~{6yqAgo(w(_igUYPvkzde0LK(Ct8#waJ(RqqNE8FtD>9Q%<|!u)^_)=6}k^v zx~*YomekKzk%UAmzKxV-UKxC(>B9fCsO~lQZ#(a6*kwiaAc7dXHpS|oeWnm3O)Ip* zoNqkrfeA&5 z=mVH4kb*=pI>pJ_F^Onh^Z*9SN{X<9JKU8{wkSW~V#AQfFG-bhT~ZCw&{Lq(A&Hgj%vjn)}Mgl@J5IN$&JWP|Tgia&hCHJ_GJy*5v?<27|qH3Z_6AH^w zq~83%t=^)B3=i$~b7t@~TW3aaeyy;&z3zvORbG0kJ;afzK@AxxLZvufVMUE4Y9z0` zH_0+T1xgwZHt#PGjqne74ZIpY5ufUd4Jk-m;d+w!8W!urZ~{d{_ks+ zuWamNnD5y?D@9cPwT8mEdfWZWub(gJbYGa07b`H)t|yCWzx$zDKPmdp@Yq=7R@UKi zNQ%#(Phd$-BpF?nte*@1J#bF83fIwde9%hIz?9o@3K0+dly!lV#J zA#9(;68d^2()>a9-`{O|q}#cWLJvY@7)9n4M%Duyx_r?RHzJ4J_1h>z&VB=r^|#<< zzPv2!96`wf#WNM2dGq-(FMnCSaT#AoMSEeD9O1~87CRQM9NZE(M}K^&Y^F7 zrwvE|o(4s~0rJphG!@CiEpP{hNPggc;Ekj&c6%X`EePywHpMVjMas&OC1p~&kIm5> zD+Vlo`_RF?<)aNtm=Go(Dd06E4!fTl^X0tnZwu{zLDG3%(|AEuDG{Bu3t0Z_h6NBW7DNw73nFOac&p@eY7W|itm_FU1Hj1yQZQ+xc$-v4>mn}7%`rL5 zW*CQ|2hlPQahUH>JhPPY=6piryL6(eAaw>Y!Yt>h&19y zw9o&$EB3g1=Hn}7WKG70QJ@HpAZUscS%x8~fM}$D0Q=BXY!%=eYFUmNBdr+&&vv` zl^HQT@21c6Y`lzuHc4Lxt`5I%H@j-(2!R$9MPyi!p7E$_$GT7EVQqlXpdpjPv}iWw zyKqpYgzw|+7cRIzUiz`{%@ZM_^#~Nf2n5Ls3`>hVN`G%RhudcUa;JUr_VyaDVy&4t zl~q}eXGL96QH!^VrpPE`VRe_>T?YJe^Zu>)07t&)Mg&DNB+rXXV5D#}rpm%9;ysmL ztYjPc7xexSL}}WgDIlh>!V5CT={!$~0UGb|3Aic}JtKe0U0qp(v46id2cd@?ZOzPa zx->Q2kv&4naQ?Dq++W>akQ0Rn(WJDjp4woZ_dxP}K!sVzb63mI?W+=|zBXZ=*D0mt z7OB&-vf~QNd2;u@df^kJH$4y*O%*&TiD2?07?2;e^mX;KXS7`RN*}k!lOmwSCs?~@YRV+d z(MwV!RkLPjuHi0+&7Lu0TCO&r&3%_2F5)O=Xt7V#Ej-oM?z>y9A0|36L9;qXN(xB` z=mt0tQ+5ka>ut@btQT$y6crTO#xbJRsYrHTI6f^~v%8#!x7U2m=LH+e4_YAnV~d31 zPV&tj+Yxu=$(ITY8}Zh&GOaQ^Nok743O9dxJnh(~`%|Eya)Z2*VO^x(M_UhkzqHG* za`V+)m1uo%}63@LCF6(B9_3-Eg`#?+Yf zj9J`hz=^X#PIiUn{H&lVEFqCPC6UnQ0NRC&cw84~&xv1ZyT9lJ!+%b|5jje*j&SnB zHK)^-V;^ppa-EquF00KxS<14hl1uZx-E>4kts%*PRuI5v~s=X zA&;O|Ra3Gghvd_w9;WGq%FKStmYoL^CUdje(usr=L1;hO|?&_4U$49o!2%d!g?9r*(ju~820L{qi5_(KgTb9C3Kt?L8AzU zm1tJgsOleA+h93$xdJQ$wcs^lo=489cJj2XapayZe@EcwMO8&kkp)%fcoFC4<>jl4 zMlWOXFGty~Hf1Ec#a|V4_PqS;rAMS!lLH4{RjjP|USG@e@6W$Ix#_=rN4#h*n+0z% z-Qro_{y?)13z%@{U!z02Y{{iLY*LnOx;8vF%jO!8qexlKm&abZ?=QY0Bm!~G&Bbcf zEuL}@tfpPG?WgJFj0XSN}oC z9!isHLG9{9_!tuTX(Hj%0U%vC?Rug2E%^*&~pUp#89q#w=#I?64_j zb}Rzty@Z>MKg?{Fih0a2(L_D4gKFo;K<4Z0L~Q3d(tcIX`DfpWG=+ za@eM8!>%}D9-(FD<~SsWO>>gFW~Bd0o=6&qX+u{e4~ZEhL^aHz*Oh7x{(Wzd4i0m0 zgG>#)Ic^{27^20a2TnA~Ze8SgwES#1PAD-<-h}iUm%mZLY>HIm)!*||x6?J>k?Y`P zsy;uaBB>C*kS_wSEijK?E7u%aaFG>`$B&P=EyFz(E8gGfP}2fJ8f4p~8Pb|9IVO#? z`}FMm8@PI#&otdxx5n%T_O)-90{wpJDB12mYWb%G)|Hl zN7I_1@SJsulr^d6)Lh5jW6dq2C$vpngA|TNnS$5q@TzYf80vipMR^Pi{Uji*>kCAh z_krhG;6*r?jYQ*&!CBpn)DJH@V1%#Ppd35M&sYmYTA&~(%ZC!xTm)U9*3qY63BzmM zPRM)Xk#cM*fwSQG2}bS!&dckV z@e#~7=KPlRcys~j3h;aqkmXKFj_h{`U0NE~_q1OA4|}!?{vO27fZsuV8$tQUL4D6y zg6GlqA&dgQ|BPW-AYP-tS3cr(cUn;oAY#5wa%c3tz6(wmj`Pm;& z#5--R%PqZ5q-WuV@M`I(UT;tb8Q&VF|2CBMQoB}o7H^8d^#Fz9sIi{j`>W=aw})_N zB@mn-iWEVJJVyoS87~9Gx@}_cD#m*HZ~wyEZ5=d;_%qlMLJ)jmil#^o)j1;5=5)RD zvUP2XO=<8@`fieuC8&$(R^2f9>wP-RrrSMVR(|)u`>#U51~w`XNn6vMu3ok*&1%na zX~T2!9EvtVo9cRESNk^J!3PUM%x7DSDGJ#U(i*%(J;U?erx(BfYYd#7BH9L7s>90j zMMb-)hC$~f+)!kCj!W~8P05Fnh#)avvKiWHbNZyU19Q~8EX_GYa%DE?b=S6{kwm7Y z8q@HTVDt1lbS{6uxR9D3oWhbE$tfz&Noc2j#1=i#^ZQ$uZc0Cv61d!4s+DGGG>E4; z>C-Pg=^-(MWjKjbR8Heq z9mS9{SPXG_CUpJ%o5vo9x;wRki8e{bnw2v}b9B#=oX&w-wyZgvJMLV${LB-hEKqXU z2}x0Cb&ZK$l`xXB2ADhw4Z^IH|N0(pHcj@NJT*6cMJIS^jM0kJZCSp1IGWWcRpCiZ z<%y&gZBj1|I#97~De-Ur7XHt2mCD7Pg)OG3p5d=nBoA$<7f6Ao$OR!htS%?t!zN|e zbDS=lf)b%548~0L9DB9K6PLz=@K>hH1_Aiu%FE5t3Y4OBi6ux~CN*9s&b^ZOS=$P` z%6oz2{$eaOq|n&Btm}!vwSgF(UmJXnr7=1agBZl%-@UKm@~9J^Xn!wh`DLBI*7aZO zd|A)$^_q|LInJ3?X}w?8?YZ8b-{|eX(Vt&$`21gaj-DIBog+$m&bZd+!)W^(J2%B3 z1~Ir%0o@qBm2Gzq|*)CKkV&BcUO5C1jXFVxw8~`f`VnSZj5?Wz6 zh0#b^5+&(mjeCZ#Sid_Ns>s$v4B%?kj&7XICXc7{ADtdNTodb!m=S2or?@L9s^0v- zt=^&Di}_ZrNSnJMX+X zcnSe;@^P1^{(fTJcMntv>}OHu{xrvavQcPTs|Xf-RXs93tgw^!dhT7`<_*~dHSb0R z!(Lv{NP=TTT~t}Kvob`PG&`#;6-!|eL>*Lp6jV} zXMWEHSx_-Mh~wXEk|M3SS1j>e-E1x=st^lJL!QKRz+#&3sl>G&_v|=mMvZgt0POFk*ce)&TiYv#FGh@IEA3i}Gj*D;r=X(yE+nId!r_jSkh_axv z3?&defeznGm2ktiz_VkZrT-%hTVPj1wm@5R9XYx!OY5!KHHYNV2H5PAG)KB4!?|6)m>q_C}Fjo*s8yN9r* zr7^_~-D=Nk&d0a(SR15j-)0s{yByuy%%;d@v2aM&di1_y2cMI$&KPNyq!@-|MM~s( zk`0)&_5R2mJ;udhYFOu)Jho?zq#ckXd>x!oV`P?Mbb=>1^3vkmZz^YvFxNteURgmq zvoS?s2=aV#G3zNDll7jzpZRCq&~bRXBY~z_Mr3$~BUyn881}WR{T_@i&cOxn+Tf|b zxYLdcD&$5pgYD;nBFU1-3bHJaSGdt#1HNi`rh&V_Iqm!iQxhhS-`qEPtgX3K4^D;d zZaxXJ8FG}>=Irj!B$o!_i-Fi;eAT);_mBB_dPT^RNoHdT!&8e$3gLQF#V#a7n>{`L zJaF3&`{8n#8g#knIww;Mt0^R@JoVYvot`<##Y0a+CjpYIM$&ZT_mZ}CvxoX@^rg`e z$1(7QFwl8bCV81d@p-&C+_*gN`EJ7A?@u|JhhUz3_E6mmnV24hUQN*ecyC=!Fr;XG z&wH3IyPo>rPPjl1T7v8}gh?3qf zFbf=#A%WNc%0B|knHGTOYI);<=5b^kj>G)}X&+>KHY4Lz&tmM?9wl9Irg+4*?APTfh-SXE3fGHuk@`2IOJ-5};5u z!8#u>!dPA-Fn&LQ`LBt1ec2DT+dPc@+T0TFQ7?J!pPtdS<{oHal#BP#D3G$il7zw# zqOi_1ec++fBHl^2dHQY0$Zs=LEb+9$t0Jq&3XQs# zZHAm+NY|n)__HiP|G6K(2EXlF~!N+i81gqHYseS0;bIXt5Pu&DbGa@5! zoX^t{62Wqa&9JRZYm1Rn?RvoohuL#{sRRHYE$DoRn2*sF(puw^VMt07Gu!j(O z&jcy(*2#eU;~+oj4&(csDjL~F6T(I=H3k12LK4zGv|lJZWis}6$q-{dO_+9|`Kn>w z`?o_OO?=(6X+uiR&^w{o^erLHrUjPg8H%C_funfKl6S?&{xYLpx_?WIF0xIu??)`| zi4UH~JxAep3=Z;P>iyju8AW*A)8@zL@BOwzUHnzsm9UY1dE`jn5&M;MOCKrfsJR;` z4-LvlwTN{3cYEI4c;Ljs?sWn=ilSl&idHm6Cn-f?Mzo1{7Ijwr0>Zq6^VZ9pRFSoS5mV7BfMzI_+=zI$Y?u%9J@|Y*!ozo}VSEqbC5^PJ4$`g^Odfq76*?A=Ml@@@z^7Mn+1`R7-A=pBn%|lT~ac&pyHCcNQxu9b+OGk z;Q3bm^yKh0_$ynU+5MbT`F`%<(#$vOnj4i6n=buJwf75$`mpD6{>op|e}odhA|W*V(YZ;9 zBxRbFI7(2LH9Nfg^&tu7!SHm|#F(0pGZG35irHK{`XJ1$bzfxK&ZPzy>PD*Pief>y z<2Wir>Jg^A0(u=Kx`m^4SxElFFz?U8Jnt449DN`(DtE2X_+wAkJ!|i#YK2y}VsJ=RZFG{Z*{o zU8FQ#WJN-kH3pTt|6qDY!ILC`y+|g3vfY-|p%+ z?~j}Jr!~0FN&U@Nsa(AB_6bj~x%N|8Gs5V-@siAIv?htFXaJHxZNE!AH>q6y$kbY; z)Ye4IwhVMJzv%xiucw_@RUf?6iLzyNizX@LKT3JwG}4*{CA#3@$FuC zDzO!EW(kbGM|0L8d2*J`nR%O(HA!|K!JpuFkEf_+5@fsav&v&0z=rK^!uJ;D^4c}z5i z9*=x;a3P2Z5uQc+zS;D>r{S4i^F2oz;t5y5)}X1fEHHvbiMqaN;O=!Rrj9O;Q`fx! zF!5Yc$wvAD=KJ@HsbYkFhrg*k>CrZfJvCH+w8hY=(#-LYh?B96pR3tdAS>>TMNTm{Osm@%a(z z)h0*y-UAVeNJ<;b5l_tp&-Qseo@{T$E-8}a1%{>wo)#&A4$!`9m>RBzzdetv`)&2W z#UTy_1Vf30$Wj!|F+7b{aa*!64XolH&vWw1xF3FjI1nVNXf{KOSGMn-Q#B>eWwU1t z$#L3zw5gRa?2%D@zcwc!$XO5J%*;`vy~(>_0Q-$&L)Wkr^h&%NU#OP?W4|1EqNcdg zolaYZ-DeQiw;=qeBROf*QdLuP%O*m|^OXbGvny#Qrj1(iMq1aTw5^O*p( zCxR#{TnR50(0b7FVZE@P%ZU0N%xlo-0+C3=e9vOGZ*2w;>`uhI@Liab31}B;1=qo3 zEVn5Tx^huQqJ!^i*ZKVnUMQC8O*uB>-2Uh51QM1a8$D0bI!7yn#xQK+p|STAwM6e1 zF%c{0{in9DKyO|CT~LMVk)NJ{lm1c z3ewNo7&>eULs#OEcJwuerX#H|c!c7QCB*S482v_#b0EEr7H?yC85}Q1#qhFJ@$$wI zJ5=}Dy2Z_Vu^ZfpCL{z(*=T_&P^fj~C^=a}967m~!!>P~CaGP25v8t=&W~SH!X{R{ z%0rT7y0PF|VOHYN(7(4vQWT&jaE&PE9rrvn!tWAF2_Gz`7{uV(N$4!58s52UzdP7? z2h>ZdR4A==XjY1{W=k$5bI+qkUOd$Nblgp0%C&dM@vql*IYFPJDGJ@oN0!Hx(}D>8 z9JU8nZ?xl9!~1||?p;&%Vl}7*QKUIhQdyo-P;Zen5q>W&tQfVfx8uAxdiRi!P6Y{3 zQ&mlrB}G?*Iu)Fl5?N|81<&IZwCZ`Edw*hw3r~dBBj6RD;dzadXqiIgx*eP2`rgF* zq~*khSt~<3=94Iu=4gh{1Xk%Wc3HL4TdM`wd)_kNY#{R7{NRk;gT+J&v#FJLezV)A z&YBF{IwWXYIZ6;^iq=(9xy|JZrz`D5kBjbCgfkEo)4&!t5w()?~pWY7g zHivOs3Ya5P$>fH({&kD(e>W723Ucl&s`6|yo3CX1&m1?vtcLpTKTmG z>lFr4iBx6&QSiP4>>+dN-I(JrDN2nA=r6583G1DQ5$)l&X57|^iZmhTLnF_Zk2mIN zO!+ONe12)1@%IQrw_wYItkPe_l%ur_)4y=Atd#KT-sYi?P?5r1HuUdtVe%+As+I2g znPQG^Tz9ZB+IubG%c`O2R|O=US4Qo)EzNi|L(>BJOexoUKPD|Hq$2bu^*WvZNae$m zVoN0kF^EA7u4|ZGzw@qv!#~2uU}SZQ<8(C=moj5hBGX)W8i>>-1XOE*|Q{jVPgNe(h31n8H5w@wUiJoweu-*r&}w z`$W+iaRnq+zwj3OeVaG>#8txJS)?H}N*nNc9(}z_D%#hyeK(|=I-OBvNhSn_(=^mg z-5&|j4r@Q}l9NX_&m!@$11Oy!I9?G&nLvFQLF35eh^y=GwOrI+{QW1Wn^iQ@gTX5v zP1DxynUc-k%OPcJXuJn_6H(3-d?8w}4H)e0+_LNBl@6%!uoUALJWFy7^o0&!Wtme5 zK_nSs)iZy5k>As0YKJNOYC}mwz!cXXf~A<{MQiQ@-;WQmevEf^L%sUY`76s8_~$7? zBZ^o|=^Cl&tR~6~)3$xPoh=`pXoh=HIe?c7>jZDo9bm(0UdE2~lN;dhcer=`O$#3R zq$k`!@bOWLn1lO6FKCX*SG2>fXoVdbQ+wS_<%Y(=tz_iUNSE*kZ-akYoO*C&LA>T5 zP6SArvufEkSKlj+1coVnw5(js;aqrk#rzkU|0O~@%*VmhehON#-DJ$0kUUJGxNH^3 zr(sZX6;jSq5n>?v-i51Vs8%@0BfYg7^z4$@DYPblpvsg=lN80UJgNyGF2a?4tM|h* z2bS-v6Cw^QN3skdFbpklBo*LhqTD}VVJ6|K811ci$DhB?9E}glNfV+k&@9c+zR;X# z4VJJ2G}ilg`NuAdEZ+#f4zfv7ux3m4j4Z8tmgIB}zY$ybhy3fbRsrVZsZJQGimt3BEf9B&yAi`O}r46FZR2;(i3>k@%>l zW?+ioCFuV;`(N18uw+XB-ucgEEQUO~=~}_>E9&~|odnwRKER=iKN`PVRn_02VSD`P zwub)nH0bBEQ;g*=2eha*qN?>b5H;02j=WD9akc?aZaBBlIFN82U`jj>#t-S=-vA#L z{Q~AY-S}OA1T5WH9t3!4jkW{A-j1N37)keC7>*K_`vPVjw<9#PG4kCp#`yj{FnzLk zcymvL?SgQ;Wy$Knfm$Q7`f-)hQw&3uN?2 zPSX@wRB1}2@3j4Nto@=zsqhR8L+Ikt>K0=P;XY(Tco=EnFpx5491o^=O^40hnim}A zm&ONcyH4n;KohJ&vbt59n(LTbe#MEE%?_kJgj9G8wciA3S5RYf5M1}eszxEe5a1SR z)f1qjU;5KifMw~|-k*ZRRH>^u=y~=9R8(BgtSl@1gL%K_i2zrW(I;7y>NvKJ9- zPDB(b*MN3sfGzbrc>W5)TA}u1(7p<_kE)Ew+8}9_kn{K-z`$()$U{4@a9*H~fC8F; z55C*)Pf4I~`R!6;Kbl}pN&8cc7__Rla7A~ZnsNLQ&OYn9=nH-dxwn6Zw@(oII+k}uKB^cyLy7!>8KW<+z+y zr7%AH{Lh97UDl;o3Vd~0LT@!0-j(x@K05kDBfLz(p()lWHr17B6(v#?36>)`RYOG! z_$VVbuRLaC<$)vcq6I-@MOF|5mLmy@2{`z7h%cO*;8nQ1t@n03znlpoum+(obe>f- zk=0a=&&Yhz{a9*t3M7T^#u?L}K$pHW%I(yo)ZiyNO`xeO~u^Sr9j zk}he2tLN4g;uK3-?dyD96j!(f2Wg6T%cH%2?R>6L;Eecg5C9qzgmsYQvQ5@TIQb!t z97S_F(QCDW4Zs|!m~5JN;u{Bk?7~DA&_N)S|9R}~T$<?k}d1tXWwU{BNLQL`yBHNRMub9Qa5!tolxA+aV2#j9be4y;>);QrVv}BF}A$N z+xV^@Uu!n0U7*|*40*24{@gV!SF@^;ODZA~9g<^n=Gc2W9662!hkn^tvxpEjQn|0k z`{JOjwdyc61Gg}Tl9glEtPYB%6_I2#k&tL~2}C9hZceMs+-Q((6m;=Jqz3B2HU zW!f!Aoi^>*)4zA!{9YWCfu04d(%CRq%CF({@5*8FnCFincu)=*PZ+}Y_p9u9uW+36 zy^{v)-#mOwJG|H)j5E(z2W0ikwz+(i+jp1LTeJIEpb^p}ZT@G&KFLh2mIl!Wb(q-` zTmMY&7jA3HBd=luXUU8vGot7-|CG?cS$z*0QwZlP1@HD~B+o>8{Zu*l295iiCQLyO zw8a#07Ep2sFuV^`UO_KEWiSuDzyarFVpF0q(^TEin z8jT4(9iSVaLgjjc-+%Sv1zhX10Gho1uwPOE zZ%j6=^Z)bxw?GaAu2`x9Ilecv_anOP%)*a>H;4l7C0KC^qx;dd%?4NlBfL-<-rxtpgiRLbL_X4myhAk?gTB0B*zPkFNi{*%j?TP zo|S?*6F%g1*k1YJm)W%uafv}mOXXCOW@M5hIT}@78jaZ=!sFV*-ad~mGn+Ps78bJ_ zAyJA%=&Y*Eiob2a(XDCbG|bVetV3lMw*3+B_!T`9U#TBbw@qUxf#+$8;|KxOZTkXD zklh?S4Xkp3cOu`%y5v20c0Xtdd3M)CT2X0&Dw+y+fNSL09Iu zxCy8}5fp)vU{8Gow0a=u%nY!{{sMXr!!701MHqhY{;wsRC+N=OR%9?AiZ`zFnIPZu zX8%~iQE3KVfU(WtZF+MclztA8g`++O_8?n36fw0$#tQ=6_`Kk|_MlzQA=C5-`2FFE zf%L-%oxnK%23W2w;Q7;_KN-OJ!3N}esFtt~ym!PLTy6zfA16S)CjiSb6*CV|jpP0x za>|AaHLiCyVLHi z*)@kv>1FS0*K-EzJt{4jvwY6e6`_ZLG>oJ?T4GyTQMSWHNQ_6?7l%pBSGnCWxVBKZ zK2~^JRhz=qEf;e4_QhDGbX^f;nMZf;ePAO1`-W*rf-xUh5m7O{YfB`OGI-g7vnI3!Mhm`>faAecQwGz?%Z)nnekv%xN0I(-P0gRz)UH z_p%LGG&shU z-+GoarkNPA)R*y*p~XgT-~FvR-hN*bXSysv?kbZ;q}<-Rf0F=6Xa~z5*a>}Vikso+~lR7-%Wde=o0qaM$^_oSFCbu_Ke{! zNw#I#T+^&Jd#2{FxtzmYQkGWmdp5Ppla;z~qW_P&0x<4h0fsiJ3kRKi=i zBCgz*yp^}D`uWVh&>Gb`Bhxx3Q;MVq_*OfVh%5V5?~$8l|M-YrCy-PXjg>4*gv@gi zsZjDy^Xu7*UVXg@^aZDaw94{u;U_%i}qTAa#Nwbb+3^ zder{&OBN?XUn(2(DjL!l#2^OIkg7MmIBMGuWbGtj|%iXqvW-}77?yO zLD4IKubTuUNow72%j=-GPanoQIVlp{XJ}cY2~O5Tkw$Sp2|4v2u2eD{O)4XOyGEAM z`3`T+{F(QK_U9J;DK-ftSi$74qQs; zyWTu_`FSuk7?Ue7EF}q|s8fv3q&vfwWB1=|nKsYLziqrT6*#>$4d_rVXoU?&lnC3}O%-@6dcFB)=}vnE&Bh;~lRV$$xahn9n_K z%)fZdnD6?sF;64;VU3OX)(;yGv;RC~%)c=sx_o0KpSj+6)3RW#F@JWAF+X^9==_II z8}m~Z8uLRIMtA#YIc;Q^TLxMMUCG|Ha?P*z?_S)Y94P)P??9Kk!KRJ{@gXe1*kOhz zbcz&InO4;Q-Pib}RHZ}GSmfZ_06E)?gq$j19NGfA$85l=!}Fv7MA!{i`O`;%ONY?_ z!CnG)2tR=LKfB!wSb0F1;SPZ)8GH^7oX|e-dvGA?2h?g?SNG5G`#_rU6(SVrSluvs zriq~4qreRqRM>$GG;GfTWM+PX93=7_>I?&O&w@B{TVc6S1OW69p}<=+8S`-1QOUr0 z=qw!QkGA;&j0jdmcuf!*N<&N+u*O>GSypR0E97Kr5 zDFRIrtVj_m4~1w|8Vx=5(Z*&;aJ8IC37(ov!Sf5uMo}87fmAJ)zb1K$>5TV@6-$17 zyi)BVJBtbRMy6;w!||jba=MIOIKI;*E-1F0@n$ty`%eB-*n0<^p(IgLC{fh{qGpu~ zB6bap+CO?*BsJ-{?rUS;M`TiPd~Z)r(gq-c&{ z8A4OgLHh=)P;%b;s(I8cPrQX4Hi=hBl9xqFQ)G15K7hv|Wb?##L17rl3M>-DX#<62a-Z zAZZHCQ8ONO?O6B8JZu2S_DDXQX+?X?cj2IU3E#(?D_-(G^4Pl%j(r)5FLMLqd1cci zXU)x$?87uA$Dxk2UlCqPxmHCcdTw}U_{w_wQ!>l=IME1YPow<)AIx*y;kXIPbd*v* zDqzY->unt2I7Hf?t2Hm-AT3Ng|Et1^#E2K{W$!B~+wZ<_M@aoYo?|6aR2ZGp)S&u* z+YTg!Oi>bQG5zE1b>BnvhGb%ujaiW+NRi+vfuqI1OP(*&j2M8{@Q-)Avf$v9P9fO> zj#g=fAqj!i1$2k&gr)xG{9)hiYVh2=(Efu7o)Jl2RC$p=wKOl9jj72LJYP6k3HdwR z+aGP*JY;WJQ6y-MBLzv)(cVx3*9YK5%eTUXq6HZd8>70x3mtO#DTpF$fpxOdFLkT#J#CvrTeYJ$Wus7>VH6k}>a z1U&c?PmTNpdxN{IO+%j;DV?GSRTX?h38i|&oVc=U=HJ|DCRepl$UR76XieZXnxeG; zhZ9NIX4lStHNEB6U2lOnTf`j8;WG=?tPY0OIZ>o_nx`q2o_*@*mfZy>!kM4N1x+E0 zLe{kfIY-=VDqh}fs*}I?{{C6jDnYuiHT1ETD5eS z3q=Bl=R00$^VaVL#&bi03j1Y>Wkp34HHJ`HKWHA^ebuAopCr6Elt6qhdv1o!L>LU2;af`6Pg)mECl!*1@yglK}f>{pflP5h**`UjKb~4 z!~7L)N3_)7ccWGspTqv#3l50)S1|VPVZ_pN88{&B0io5BEynWW{$;jiMK|PR!-iZh zzsH}p#OJ>Z$=r(yPqVTja1<}0%ssRNf;Qi&@y0xg_Hm~A`M0;fY4Er41tP43kZYae zXjvu%mXby3{J=WTY-;&x@(Rpe^>2sx2>0w5#2^MSfJC=`e$K(|yB`}A;jMwl6EYb!Fs7xL;-pedP=BwbV}4R>{UxP~zuhG@PYMoypMt}a#NK(!EI zVh11_R|RuuS@>^A9(9^=7Ret+c_Jjg2FX8-{oZ!I$I-923l}gLXo_6lFhF6b)t@Fx21&RryJy-oYmms zwny;F`Yg$D9LI1pDRMLs;G}0#oPVy5V5?}HU;F-x-4d5#t6)fxBY2i#ct#LeD!}9* zEzGj9n40EuO8%)@57Y`=s}A4twArl=nvgl25onqcIrLiZVqEYPt*L4L(FNRwieWBA z37REnQD8KI2nYt0h8?En`Foaru;!PGaPjvE63xjc2DLedXF6>5Nj7`N0L`A^$~?KG z=b^djq6Ixqz~Q4XNSzR)^~e;o&d*f_p6)!V30}5pa8o2Gl&)|*O%g<{J#~7lSid_N z63U7F%q470yZnidPkckXxoIeTAbG3JIbvF_rlvbG@_dqU=P*rD_1$+b%sq0_(k#MS zxJJ(IF9BCk)a5JVHl1@iF|+Hs67W8%8beHu;YMsvMwaP z4TCxvrZt^m36)?ajXUzcjUy7L{!tEIT+RlTnsvxpq?pi~YX-m{ z80`LO;C5&lvh8+i=zmcEHGh6C=+w<%H_(8SuFF8Q)BiSq3cZ^`e~x-FmFMslrycT{ z{7K_(Iucr=NTx(uQ#qeVjT~2*-dWUPa92xYDAFPSo1TYWI{H!whrxoNkTgSb0;dGX zZzZg6xz72SPhIxBbOd`#Oc9jG@)S++zOn-l@6Ypoj2mA$0aHVl{L|un2N%8CBE+&} zXrH@fpZnn+F6Xd3yUUiX4Y$czHhaeE%C{_Ce3FZYi&6sitjDVR)27+}=dZ0XrbiZ) zbWx_~>tA%=HEVl;H;jZ}v#X1`$SEpIGOAYn%dWX?s&y{6GR79fAP6rRjsTfsLluMn z^ckXO{?qoq);1pxbzA!c*(?MCxT^pBP1SOt+*bv1o5y}>6EekD+bci)#R`A*y(Q!Y zsi?Cg!O)^2>MFXyep?0Sk+Dzyf}!6p-uwmBWc?(tM+NQ}1VJYxk>d$Y)4UVA+{J!6 zDz!S&=;^?VyK8Xp=cNT_6H4|zUg@P@{)9slmUcUY)n6tRf@L*Ap;RTn-OSpkQEEW` ztcs^ARF`0|1^N1(ra>7vV4p080LZAEzLX#9ur4^MI)=%EFZOf^Z1@2)cWd$F@ z3K{08hG+<1*&}raM49-ukxxUVmYC}Y}YT$MS z&T4ZG)UsvGF-%i5+homA_tCpvnD;Q92(O>vc%WK$92f>@eAgZsmiA?AAv8Gu>#fTh zeKQPie893Yt7w!e>m<5(_ePE9L-OmK+xtPz+&Xy7@~KaXia@Y}LJ+z*b=kzVhyVJz zJRHEsz<@ji(r)JBuDPN4gR<{_fUE}z$s<@oQY3D+WS6q0P1kxzF0G$4$L?Qy&coyG zdi~4nKFLr?;7hQ3d~Pu^j3{G>I@>ar(yUrc!}9B%e`A0Dy$$igJ3vra+B$4%Z_RF% zvvQP4Uu>JcXn*YiW+=H!0f+aFDZzyT;aV~*|NN!qe=$d)On+(=0c?EHRluVe4L5II z6W_HWs!E4bwLyIwL5#g#Tnes1)G4lz=ps5JshHtJPawGWjS4+SgSlWf9<2c z9;@Gy4M#vp!P?yyC`EJhk`zh3;{9!iBPUmLxNMqpuVy}Nl}{}H+>64omgW5cuzo73f(=4)R@=93%#&)#*%H(9*#B;A8O z%HB&5c=td+pe)(4M9^P&Cj{E0Bq>l3n}jI>DnnU{3Mhi0C?X0fDuTE`ML`fy5d;U~ z1ew22=zDE*$z`-@Y2p3$kL$g8-e+H9ytykOu227v@-UB?hl~}nl>F*VvN*?fSELNVuE+!J zvSJ@(x7h67qmjKI@GQX+G%FJXC&Yg~>Ojvc4I`vTMD5c@z+U%LtTAVc(-YYvOLMh6T`_J?}cbuyZWS4D70g>`vB5F;y$%^b< zaCDd@AtOUkhU8?5wv0zk@9WU|ScQ~okSg$1Ro}t3ON$@YT11|g*CcIc<%P3mm`a5S z7gRsR)RI3xJIES^ED}6N3YaJ>ew%j0=BU9vIk(;X#r2$DfyuWEdh%s3@5LD^BXEp- zZ1a_o&3e+Y@Yc+an%)>04$c?EQO`r+ViDJG;qQW6jhZ{Lhq>3J{7Zu$4JdHY3P(~L zC(0a+J{Z3RA`AuiJq{`?OyW%tBAwANE%)wbudl9oIo}o-j~bmMleb%L;3TVJcZ{`U znQfD-iagAr4E^A>olcGF7YQAk8v#7@Em|2h?NQdtb%+K8LXb`rc#oeP26C z$FK#OKfrsVweP`ncm@i=ci>99iBM~QfhZR^t9L;WNe^|_FPz>TAp~=jngXLynB_vf zHysfN;V)VNriM_zU`*9*{yK_*$%Q;lWAm;N^AX(ILjwbrRN-@m;2nnW_DcBLvXl_! zfy5B;xCrB5{EweA>Y*>TtrJ6;DL&Qz_a^{1%@xwfC{L%-s*oZ*aRp2+8nXe`G{X#c7~Y zZbNrSfv;yK!C=?z_5Cs?X7&?uY}ro7$t6_9dkggfIPC5>h#RLyTr_vCXUnXNHl5l9 z&`dQoMk5%;3`k{wO;&mfRyiYon+UA1j@}-F$ZvM;_+`whDK@k_C(h#>$ul(0@C42K z`RqUcN?v$7a-H$v+%J0MjA_0HEQ}>4`jZxc5oT+)ca?FFV0SjXx8}ePUc4viN(l{2 zu!OQQ%AWD?v6dr32>rv#2&3v$#P4DXspVc&*XRxGsHRJ(yfd{hElT~D9Oiq|BD7M0 z*BchGT%oc9Hh0*G6ZR(sJI##{dukdYA0~vXi9%eM<UY%AXkwUj7imA~&{3L#^TWAa4s1;N zw>^*?0{G*5PlHiZmX|RO=UAy#{XR3Ueo{IAuAVWFAa;LT$&}P+n3r3x?{;p}(*#!)UmBi&||El!KgDr9({U@IN}eSC4e-$^zDb7G5CMw4NF?*B&pyS&@4!1BB_ z@alwW$?$sM5iFyy1jkCeBJ~~h=ctPV_El;dA*VAG^ZoKhu_curYBh0w?h_NIPssWl ztQ1s^K~Wt|nTI$9X?$NL$6>ZuEzUf*7W+VTLhF}i^|o4){ujam1#bHZ0%$ zWh&;FGa44;TJF2^+f}3M_$$SQ<{e39F%`#2Nf9V`KYnb;JRZw^D1Q2Y*QW%(z?h+E zQk6KIqB!YXW8w2Z3a$B%2vdtJ$a8USzio~Bzj+1i=_08zEs#7($$rOipVz$6ac%rB zYwas^`e(N^_rjt^^^Y!$a7!)0c`XeBNimq;%6m>{RA$R_r+n#1iFE|s7lBi>LaMYP zFtp#9bS7jyR^(?f2q9Vr>z9RH4YU z`ebfy<29ew*0Syv zha}i#MW%aTlSbCF_}#gpqhwG`Zvwrab=>m(tkZ|D>R;B8ptrBy-g-aj{dt4uk!n#& z=SI;XC|?Xj7gb7m93+%dd+2npLr3`mFsD3~yQV|y`X9~AKL%0@3mH=iY+^JcD=lA ztRKJgGt#dJ=W&K3XdWX7g1~s*&#y?^#@Ocdz*#(J9)8R{(5V`gZE^GkXeev zRfgiG|8K|6g9m%YwJD*&vJLS(wv_XOe~-Pv%)@ASDfinEe|-1*Tj(1}X_==53YR!R zfOoA{46_;q*}amxtM07d9-IhB4`2F+TmdH8<(K^XgBp2VW`@7zU@jCuq}(1TPg;4x3-RnX9guIk-*?*eM&}x;XDq zn#$xX!7ATlvyXRV36e5M@Sgo-Z8^L4_>8Wv$2UfhPNb(1!#9JNLa+u?n5@DOBAt?& zB?_8(NAByG>{`14EoM;|L*X1oV>CfxgrAs|f~aai7H{RIpSolIoQH!iR8ttk;Ic#z zoFa-{pPyQI(WO!IVy0rk4PbC)%Lffby?aK#v{ zCVnq>>XcK=)H)%Y0R&NHB>^W{N`}S5L_;X?aBuF!hfc;W{X6J!F)}5nJf+Ya%@go( zF$tQ8xI}C7&y0aXIP1N+o4VIOw!ShLrl=MnI$XSd(Za= z7Y?tG23c6bMUxU59#x9?9?HqeWcVoei64He8tZCMq;XO*LyG2zkN^d12 zBU7-Z3mF+Fw~rJm_C1En}Iv)Zm%-BFmG0T~=?AcbO`NaRl{uH+&HUTlG7e`F;J%8!r z+`)%_eI<8oW&a89`k`4??@25wL;d_z&jE!mdwXkop11MX#(ADn$0p_77V~oLIf$v{ zliZgxtn`1``4_(WH-OL0LzIm31cs>`Bh{_6x&6Nr8^qQrp+4dVl`UE^89vQDykm0r zbO4WerZJ%6z}Qs5`*LLe(9BM zd_x^NbQDGA{vwydAFDTNUhv}+1wm3NjHfY$h2>##ldl;kRJD8ZrJ%w9jOje81oc^3N6E{qi^ z9A`0-Q>pcaDg8hHCo7>9lyK`C+_bI3jJGagxobwbyfuSJX1<4f9myR|jaz=~9i+{h zB`{28S&Zc+9=3UZ(IYRTPEBKjS|tkT;&|={-OOTxRw3@p!BvbVSW)6+7PgmF40{y( zm}{9>Z`b4>(S{tB=UIv)XpARuitv+emT8-H(<`E~_$}A4?5T#Y?GL)z$9PYgplC`G zRTW+^YZik_`fTo^K7-_w1DXX(T9Ab-r&*9$nG#rvqH)G-$;`@tbIHU}x%`!@TIN^0 zkiUKH=~RDC;XN}kQ^*>uNH)9d-A#9w)vDNQ9Nd05XW{4Z^Nhi5(y*-=)C1mPu~Z8; z*DIMJ3=a7M2UOsQa=)vE_i4mDOKXv7&-lJahM8`0e}~I8NGq2oHSqI=sln?(8Ulwf zKGJALd?t9iE``G4;_+e{io?Yc5NanY)J53i;j>GDki%*>(ZSaQwnF50yt=P-I73Sa z);8xu?FlZ)PAU(C?gDNfo65p;Pq(>SuDc^d=`FcnB{`o4Ymwd z4Ds9$fgY^p(xbwL$XY9S4{B?6k zT=i9`d77gLobukfAYOkaTkMMDto_@mN!f24PX;p7t6JDU!8ZiYTWj@f8ei1nUdVm+ z!bATJe>BMQ%=N`_o4to1jWvrrPctfk^P;3QX!7yav#wJy@PvUn!?37l8;1K`auL%{ z4%;?!Em!JtYJ}@mAVSv)=sPMA1SyCDCdeFBv+wCeFSOiLp&_CJT1JTubF_58ePbY9 zkWc$Ea+hfV;h9{^Tdql1IzBCkNz}W~Ua;E-Wjn^gR;Rn;!p$hbwe6usRLnm}s}qHj z3_vXB8RHX1-k}v-S$i{F<>4j9HpL`5lcADpZod_~>f74-+wlBP>(FQr z_ukmpJ;&;$7Vp8Gem7)QLS$FT)wy29j#b75_uj`ThT?Gy0YAw$*!d z&6Mtqf{Y#^%j{4T+02rxBnzA<(HQ^DOV7Vy8Rf{oGY4MEX^iaNs;)(~dXC(a4bH_V zX2kx*-jjTR2r?cbWf1RS3jDjl5d3*T&sKBQyj*kTEF&0# zBLcE!QIaG@qGX1^MYAMgkA8gozKa`}7|;x#L-S}SE&iAmU#x{EO}@4V`7V0@1 zj|=lXRJsBpy`su`2B>|ixn4_~f19xv`VbzL6=jT*2m&uLq(ESDx4yTXSUvgP)KJSk--po2zlXwb@qH0F+V^3KEG9!; z*MeWh3?9-L%zpa|&3^d~A1Wi03MJAcBP!M>dJS6hkC`2 zObD>de*FSO-iK|CQJm98h%fwEXqg=V9AMfL3ejX)-(_F%V&A-?w5>Lj3@uPBc(bMiOI`zC&-`XzKcL$SHZt47B4gMbo6$c*jy4g%dPP zCk?gJ88Z-_*3?C`yZ4TI5|Cvsm;&loA}L@PE^!nt&;2sy-UYGlB%mIDs0poKe&d_N ze#|#MQHzgGD_*15gl^FqYx2FMt2VTvRcVXw`)46ta5vN&tDv|D#d|<;c-^@P6t4}% zYe4afQ2qXe;(tQ%A42F1-$L=Pq4*n69CkZ_FMD1A#TP;GN1*saV6I&^d)CCH;kA3f zIzOb8b%TC~$i@KQuv0TcymAOLZ~`Je?c5n+=^Z;R#3OWQqoJAW!%?67F#PubK6PG0 zoW+{YP|aN(PVdb&KU^K^rjy^V2+T6S_kHd<_3rEW@b%Ev)m=NV-Qzabu-5zbR~+9c zxa#ZO+-7EJ8Y3l%rxby6J?4CS{h>ujN5@Ddl%j4`Y4BX}-bZP^x4Ax?o%Q6^hT$Cp zPx3rR$rvLtvO2KWn;-mF=ot9oUZP%Fm#_~F^Y@{}Os{2>W!#owPI5*=YuCY{Cob)+ zfV3)!BF^Iq!>E|-XH|ml6$veRwR3H(X6Si1t3@cmh+w@Y;vSh6@5XMkoMY{4vHIC^ zM<$^1!CZ;29D8V6Zhil`JI}I8_?RdpTV|%k?+W-vwS7Z>PC>Yy{$Ovyw=wnn4Y&nP z|1@sSRP1Av9;&KHPREk9?6f7lFDEAX@wmWo7-S~GggV@P&oH09`HoMZ*Zcn#mTb(pH=bOS__D?1U4&m%N zxSHMj%m+)Q=IG44OTU7k&?1f#EXx#H`t82++0WAoEpWwG^bI>A>E{mD@5f%7_IyS& zv^1Sb;dI`{-2uWx%NW6Fv-elLH7v3sa2?`B}ILNZ2>z$r^R(;tD$=xK8CnSnf1WaXM z?)F+jWc4!|y13?TY%;>VqE-MQMUztOw8W64!m1*ROR$Ji+ukH#=$}(p*B3EgGPMQ) z8k>XOdZIFn$P2inDimHX?e+QIc^ItvjVR`JqEO#Qnm%|x-0woYmr=^3o)PA}8)Ha! z-TkcPiz?fJjQMB)CQ8w~*{*o6^Bm$7yx0DWwKzUqk?``1%T2~qDAiC$lN1*ynK0ee zczyZf4KX0&T~KI1#L;dmy<9&}#wxrsG^l%mD0>&76_FRj!0rhnLxlbVzqjl2 z=lU4NyMP|_VZb$67Lyd36-kklxv^N!^i^XwB!}`b&ek?({P$Fz7I)%OUi^QW&$i{! zz^C|}&?eJ$8oT^Tdwxi}#qpRX(0r~v->7Xkay(Yq=RJS}x450MFwMpK(A-_FmxjK( zMI0E8iBDx{UX?hNl(lMz5}JVi2oj=nEzhv$5abhy3Gwf&!e=JcBj_CyBoafSUyX@)oV^IBfK zra?u@5bVkz!7gMvUVNZ$y4SV_j7jvJfy3yTG(^_{d)GIs)9=dEvqsoVM4eL zsiN!Y{agi)IVTCoGQV5%LU6C%4_4o-D?lw5S4xcL zWvCn+K=Az!YHg6@?T$_Mb<-O3QDuv95z-A0OLxWK;j)R-m}zw1kl? zPH;RW03F&acg}g>E$8objERtT66rcsY}i`1#r5&RW*?Y;sN=8j7a~hek2dknK1b6~ERe-G24`57=1GS0V}P`#-Y!lG zoI#eW#qS#@_xcd+)~oP>KvTGk;eIAcqajj;KEZXw{lRBHcCO-YqAYwuD#gqk9KWux zZSwDg;B!bpgV|lH)IUe`TonBFZB8N?k&`8k7ukjD4!^dz_vJ*e%Z`jP_8&-mz z7nOb6UE^C`{oM8oDCxR@#uP%9SQf)Yg_eme?+q+>LPPC`0DzC-jSVwB zLxcoW-qsq7hKa6^uD$&6eH+n|3rEs4#xNX*V;n~^@W4IZFt2eYy^hLalIv=R+o}Fp zU^oBHf-GdojPtZvw8=RqcYnUFUHhji!0Pghw9;vk9reQd9eyt{e|nkhQui)?@!$Nb zV&?=L(4?XY6hlxd&d?`Mj`-JmeQkv>RZ*)cxE&V!OvFS+29sSU1}xiBt4kH6+08~L*RG;_2szU z9;G~)`fc!2rU+anM4X{0iWA}eewSjx9ZV0n);5-&owhw_^%$8|Ih7X`jPk4A^N_-n zj8LEovV73B+m&`I@zI9CRn6-wTsTR~EG>wPEFRl@Wn{CSbS!*E^o=^DKis3{V~Utw zQOkjrxm>P!XU=2_UBM#D*g$>l6%R>?q8Je;fq+=G@!e+*T)lI2WL@+vnvT`6ZQHhO z+g8Wy*tV07&5DzXZQEwY>Lf4U@80{~xbMGRHR|j!_8B$KS#z$r=CrfR12Nkt-NEq_ z!TNizp_I5=mpA$~b!F4qW=vr#!N^0MAtMVK$jb6#LXN&a=M7fMxwLAkhfz`w&N(mp zH#Nyn1&yK+INv;-@d^9HlI?M%ra($xP9tHK>gvTWnWD9i3mm;x%@Fy4xjS*ixVz^u z;tT$jrAnNoe@|dWZHOX>9(gIAIZ0BPN2x~#{`hTx<3_C%G?(>^w_5F>8{c9vBDlZu}KoA;MP`q{rG3ZJzl+sf@iGiuq9sN zDV#!Dy&{sRwQ@12{i;b~H7~bo%(k0ez1QtCw`VVoU!C4tD?QN#m=qQ0j~m5)NKbn# zRVV?NvzOVhNDn=uEVP98$v_!rQ%(ftY%E;{1e1jdG4R4H9tRT9VW(;^+j=}(*3QPF zgLfK7TtHCQ!3UQB-F<&3HH9z?`VROrXxz`Mi;>x<2aIP|G~2sXB@QmGlFYp3eYWoVN|Z z9Cu-rUa39iUmYtYiudVuw;vDo0qT1+K`~X~&|rz$#(~Ba7wdFKq0-g`?dgzq4mxPg zBIshmeI?S9>8jn96CnFImFVWJ%b!BefudS7vCBWSA>=#80Rql|SOMf5S1FA_acv^3 zs3W6RC8PQegv1dV&8v6G={zxZiIW8BEP4b+C|om5vuHA|x+WH`qG6>|#UjPdGmp10Y-bEXO1q%2e)%toinf$LdxCQHyd6$b;jOnol>+scdDT3ttTNsMAMW zlb?w;7r$=qa#ImSQg?p5^zW(y~LIIA`bX5u@# z3PruD2~pPmM>gSsp4m^PH;3l8`#hcvG^F}!YGVrE6mQRmS1uLh%9VNt z${=qr_k^5cm0E%YF{Y^bWJkUDC_5t4T|`8q=nl}uTG1I6ZQe$ zFAMRu^dt(2HHQMxk(!XCBwg8!YxPat1P8Ttx6|*Rf1ll4vG#v0{^yP`oLY~spU$v5U3rb!ZUZleRNa{lWRV|y+yKP3W1(RO#z zar_v0t021b;g{QUiSvuQC{rN9k{F7XqOEwE%JIV~1g-$ia zfrHy}hjal5OmN}*+FsKzMwkxLOP|zUy0*#(X~*dJhh_<*pl*di>D5F!z;QO&{R^=S zy@UTnEn+s{#okSw$9E9JR($(SY!7^lm2W1QPI9++r$&(%fIIe_sifU?B~#eqw`gOT zFwsJlWr3ad?cFm#1)OLpMHquyA-27ZyHF{DNVU<2@z=x}rb!`Tjv+IJm-`8ZQ82%K zbpH22k$&10)Zsd`Hny48BFAhhbDQ@$PsVoXVoq7p3!|xdGu+}YOOf@=J9*zy!pp=Z^E&(J2CBq2|g>6 zY{Pfw_&Ja>jN5xdRe0xlssBj{&&|i4#q39vI6{b`-n0L|hrRU1zDf5{VEOij9e#`4 zcTc2ia-Xcm2=rsOZJOXM8+ojL#-ehWDu5_Y5-vRYRp2deen#(o>qCJ|4$a?H&;Dce&2QiLLfBDCWW`>lUfLP-W;IHQb=dYG!5qV{{SXH(m z7Bb-^yaP_rADF4Iqk&$%fcMYO^bKD)mtEi0bk%yTDR=EbrmzHbg@i@IS_HR+u^UZr zc^(I+-C4o1I=yG^6FaLRc6gfQg%B^LmJDO(7Q1&lJS~%eUHjdm zxn$4V>E7-qxJ@*@D3VPk)3Xu-#w!X3jL{6WQuq0OmtHH78|ZOE>VZmB!ux_+zB|fZ zd-u{tCOtVw1{rmA=#}m1cJdU-@km)wR94bo4s3YNeyeW%O>UY$to@aUjKx#2Wz?{A z2Ce^fuo-@gloo4-#2_+n;r3%d#dRWVc0Q&ar!V|FGU+_*Ze6n*VUGNxmZ=7=_d9eS zdG@La0w&*uzlr^G%Hy(T)}tWi8Vo8Qr>`WbN|K4Q7!p((|~f?#lBpF7eez z3`Dou;z6BVIDmumEy|!vf_19-q;Z(igOtNK@J3h)?U&M@x`wX>x3j{ITz3IfwM13z zr~)#C0~k4&@=@{-=skkh1@@B(#`5M792{S`qhC!7!a1sBy zRVmw#x{Gm{x`vw0v6N29z91oc(Q+7ist_a0uoIMFjPA!_#TUjy?SUdu zXpA~q5mIw`W1|RkZAvg${rb&Npg=@4&U^H)=q8_Qo*on>DNSY_}qbuQ&pEaYtN zH^5>gE20Jn>UWlveV~927mik$?n&q^PHPrOr0Pe3$U0zMMW-w5NgvMW!(p$=2OFT_ z%-VByxs=jKuLPxjQN{Q(^)P}3J85&_$&a7(!dFR_CqC0WnsmZeL>=!;L_*>k7s>-2 zBlL6jEi=&dDl5V_dGlFQ*VX9#lIK)Uy-HifN*qNx>wu|UC%WmlztT}qZA*t#jP>PM<-mulB4|h z*L6J1cmQ+K0CS{;R3h56h1^ToaM7(aDnL=SU(K2(eYPiDZLn<-ncy5U0cS&I))tGa|qgVMhl=M@}#5dab!vh=~KV{sj)8t%1WO=B3uDp>r$WlVuwd_ z_H>;Il_9Fi2qKDVA}<9n^Bhx0){+=BS1uqYF^x4I)FkXgD&%S+2fazUfG=;iXlVzn>!xtQxC&GEMul zNI*o%E~xzrF!-_l8F}Ml(}=&_QahX&%v(+ioajxP2#gTF}5_&g5e1@2}U z@GZ*KoWKSjC>(3lfbm~=EC{3ll_RW9_p@Sx@^EbmeSe25m|hPxfq0{#9f$ews2%UbzR;2r@w^tVSw;Rixd@! zuoQ`?k-sUfV5Ce3u=6X%9QgQ777z`#-W_--mZ~A1RK$J0dQ3~2{a9>jT(=N9AG;Uq z)I${KO#n+9Wp!tO+>-j>@VwsoCO^ywzGF!}PBT!!CvWn}a5Ry)AtwI(Ql($3BTeRI zl0zfIH&9Mj+uy0Kps*lOls1#G94I~(^qu_6h-z@N(MpK)l7D@{_*a!HpYkuF^E&-9 zvuGm=okCsoyk>ISiA|3n4MkNBH}5g3S_+W%?1VfN*RB7)#=G$$Hf;OaylM+0?bh34 zu5()vnkl6)85;9kU$qt_J6PGkS}x@5boNB~KeU+RUG~2~WZ{hEf4J#?kktQgA-UGy zPX*LmbRGCP0lP0#&fhN{PvQ^t4#N*0%9-^^fPf4YA?XWblH*}TOYV`$W_Wa?#}EAb zmtIBjgG~NEi0UiyKiKsD-af7|IA}u~?Rh4gH{elN4vTWIf^6Uxyex@pM$Pv;SX07* zKMOV=X(g0qE2{T3kn5Zqw(4B6jL(Uu9CaVA+x6v zoNnmagv5ZSN0WYj^@{+3UWo5HQhoY=Wj38jR|Fg#?QGZEb$7t}w7h{5^-r000yG?? zTdN`)<{e4GRm)$NU$0Y_ldQWhGl;ma040hkBd@Z}N9Wb*?Sm^ZVVnTMdGho=>1*|}_mk^i}K_ln)QZalMR{~h|s-dBUF?6KM@_(HQVCQY`? z!%txGqC&I`^_v5@EfgJ@3IU}=E_)x^%nk7J|M9G3qI3ESkvv!#RyG=zHhM9mL9r`C z;O^bb$%E)z=Nin=8lNJSfwQIm#3!XGjWxluEUPL$nifJg|#? z#J*IKF`1B z`UOWlf-1z$fJ6~Ng71Out&EZGHSRK{|?$X#f zonCaK!(5yf0EykWHF*H^JhFA`=RWs+u>nTbKUsoT!nQz$5W*e1BKaV{#$=@faS z^0oY6FfakbR%?_*v@oA(h*$S;^I&RH@~`yo&z}*V%K71nn}{9xjs=t1OC8+-(XT3y zQ-6>B4JZkR9R92FmiwYqu@($R$5961&2RVm^iCe7DU=E*MJ#eI(#FWK!xwN~lFjyf zLa9SBsa1MC=PpO<<#~SHYz|oCYN&y-VFeieoy~`|?11n$2tEU{ujE~_L7wvoV>ISi z$Y6+PTB=2sVKtGn(wcs69;uyval9ad@F%PAnklUNyujG@cO675z3FX+3r_x-17DEW z5Iz`|(v6VV2+Ow!E~eKgipTOXTJWQxPy&`qs4PDLv&AM8lj8!3>sryHX;X3V^ZjJs zwU@2!w_LC~xB@b`!67;I=e;tkjlGsK44MG{q%i-)mfcGe+*b*$&8-M#`b1LZfL4=L z@~@}=gLn#R_jP~`<#gsPo$L9vwcU&>!cAy5A!EkylFn=tO(>K6a>2307D?e-g5xzj zcz$r)$-7uXw`9`D;EKC^@&l}q%p8o-?6-kUo~v7LXY%b|98jc>yk9bA`)KCV zqxI4sbQ(j6-mY)70V59;AYGbYzsJ9z{#<=R&$R?#$w0-zwg*6VWUg9eoaAkltOE@> z;I~`xNM-4WO=6_}It@5hrC|z06z5BBC7%h`9D)@%LJ22x?XgD2E9Zo-4_Z!j41Pxe zyvDkZyi~2nPU@DuShz7!puq?p=K)_k(`*Sr4eDw@Z(&ppxlu?x4N}mZ&iH+G-Nel> z;I^82R-4XGjZ?}+LWnEzDvxmAP8wJFcdy&)hL1ZN5Jfs0?n#21;Kiw6CEa(gwHm*v z=2Z)&+lv(By)Fb2gR+RDEga2em)@PikMew{87AP@CSrHi8!G#0#Dz-g+BJIBB&8!_lz$fx|7p=(Tt<4~wcZJW!Sd2R*$I??iF)SoAV`PfZcr#u7_*!(p zwEeAw2}%YX4f_4|UC^IuryckAsI@SdA9$bm3dVAsc6$8~iNx9ns+r8D`JP6`E*2_w z#LN?L>Tp$7GvV0E_ro&Jt+C5p9-8<8f&(1N6M?@>VwMSt zeg;0$eueqYcLSf-L=C9{|Aw$T(?jSrnsUit6pER`G_lUYt3YFPXn zs(NuQ!h5AiJ~HyPE<}xbw|wn-&qwH9SG+{m+wB(*CG`q4an=qCJiz{YHbX$q@zobqP89ZL=Ntf^WxI1?LJQ_71Bo@I|*d6?G7m;s5^qLREC| z3*=^%Ycik}d+-04qnEi_r3Cj2zHOAJJ(Z>x&?IG&h+YZl*|B>7m`yH==)m1DzeEui zCcw@;hCpAWA_et`KANr>cCKCuJnnzgEdjyg2n$9E&Vq@(0@a}tG1`ZxHa>FLJ#9mY zOY|&~g(a2OFEkumL3MWazaL12gwqP5HbN{F^=!i|#dK7Ex1Ci7*Se4W-5-2r0z9ch zGF1<$qlT>l2c)!Eq7gGkIG#VgkggmyFgC5^q`{^4X^xiUjf$MNF#D_G$LhB}2HWd6 z{pJgz0=_*7=`^AjCST|s^Hv6jXrXI-I{bbG{E?y#!Z9Jn;SU-Q4>k`k}+)68?mPorG)+saTrY=FP3B1 z1x-T&F8&_h;qBDNQb1dGWT!>R(?yLua9=w}gBhEHW{~`k#I?`$5xUKT);4~(P+t?c zqRTgYK$~vN*-7$up*sE>_?}8S<8ZErpP*PpbU5&fPSgpna5kuMV9j#wyZu@TV%J~3 zjVO-ZVBDj*Oa_`v%y-9xx%>zhh`a;^;7udxO?r6$Lf%5PlmGE+fs@UiN>XCX%-&b zL#iFwBK{^B<&lTkvBro!eVPZ=QiPd@|2DkW2qtgWWh0qLt0;j-A;V;uiD@CcGcdn^ zysd5}=gs@9c@={U)N@VW(JwdxNk6BkVAf2Bd8RO`2B@{jhf1?3lR>rMD5ZV@V9V&fM)@z@v;9sH;-p`P`wZ=tZql; zo;qLS94R%u__>%9!KKGy250#~`vxC`^~$4s_YBgPsE$Q6Set*tTA5_-< z%V9R+3En9Do1WXq%Tf`W5EFr(fa+K@>>%9ExI65<3GxdUZrKN6^83!jvz5A}$ikI6xxe@;6JZCqp42(b>yLoQZS&PL% zZ4rLXeU?Dp8krH&(3N;;5>l&^aK#)t3zIF80&8@3to3Lz#N%_B-a-lwWAgcXoOj{1 z7L?d7eqE(>`AQZHAq#!;WK19(A9`~7CwVbDJ($u04CkKMbuPu6&DiXN4N z3aklCt@0?DV;+BX6V$Y4Zfatu4mm$B8A@7yuH)oF{Mw>Lw8V-#rUDIbs<-mjZKqJf z5d6+Zbvmn3keJ$-cjDO&h(P7KHs4ktS8d>d^0HKT7aN)rB`D$|;5A3`;eW0?!@ar> zU<>e+xriS;?7g6E!w@J&x2u`cOz!-i84P(LjTrc(A*j?@?CgmCXp?1- z`T6fXSDb7>-8cywl~8Z}_dOUYfOYaASrq<&LCIgJ9S>an?=wEvBs)K~>m zYMGp==SLEVHUdL9HNQvLLMTctd_$kT&Wj28j?1Pgyq+T1;x>q6KiX57u&=8b^O}B} zioHG+!tgt!hrv{-wE@SMwX2^nt|mLcIjfT0PG3G(W?77`zIIJgcT_L`a>5Wp;qizP zjm@FYqt^nlCt&gLeVgsdi@&CSS7+$Z%y4X^CbXNa|4ePE6%2O}YMDfcH^}V{Fx>kO zi45wWU}aX2Yz{MV=}{ZfL1o*5g#+?L>TgejpUMuXB6`%_XP+THJhxk{`e~qnb+Z`c z|C@c8h9eV2iu$ulXZU|)19hb%k54~My+uKh&cb+?9viP#6J;9Sh7lJGpJkx$^N{OdCTr`Y>GnW&ddDwxya8BHTWrJ zFquT$aU|ARBt}VBMMvtmPn4{G8u?BPK0iMzK75L7#a+OMzh}(n*@W->8=r48vql%u zQ-=tznfcGxB`+cQ)72A(+N3Rl1}&GP*RIb!`!e9EvjI^?=RYwRspbu@kpp0uRjVM@ zAO;iA#P`T#QOM+zoN732gQgK9=;XqHTP-)54hBu^5lhG%%jT=#+$@ z{5P+gzdksBR2)8Zfw;qku~@=#|6viT->^?65Nr%U*dU&2u9mqC8v?EQY_;Azcb?L1 zv&{f%eg~XrW5^3tn83gBsd>*~JSd%_9s69d#oiP$ZIG&T!DGWNvqvIyb}C=7@>~xu zVjjXR=nse)dV{;Z&>uSCOM)vrN3-|c+m~7^%_+<%$$YF7>l%Db!6)}fhPNx8YnSI| zv(p&5Zml24u25A2g&*)3EQBG6QG*zcUQ0yNK_>d~7=1|a7-Jofy1SUT>kKV0S%Iy@ z@(*=wr}L<=aM9)3D+u@9QVVrI=0=1D`xl^4`-|cikzNb^;}MNLW88hhKt~BZ!+nu8 zJ1$_TA|UZYnYA}=QKAg|Lr>q9P;jKDv4 z?7#P5<}wXM_wq>mJ*u|3H8%alr&w#bf)Vh^DMn(J&t&rhqWgfocm43EV76HbqH|(i zxi`lCtTi!rxWwFlJ(-Blk!$_yihTE+#RA+J&AID~UoT-}3(0nf+V>>l>kI)%bO%@T zq@C^BQwY{eX}pB=M=o3WdMzWC<^H-DhCKJ?#Z=dE8&YE+lM?|}S96HB?>eWi@lQ$A zie3;{@z0@H@k6SiiVy}(*BhQB9-u?OD8kb!#Mt2aT+1rr4>J-BwY`A-X@<4s1$|C zrGp&*F>lyT7LSx)>PR2N=Q)?|FT}V0;`CXwR4eFCtNliAzlmA6-L9L=J8k=E5?ttv z7|#n*&s^=!ZJj>81DPLRB}tt>l=V3eh&Z?|gdSl}c-9b|@d^KXU~AR}Ksu*0#xH@h zjGGI;<|J)dSPXknq5=T|&Ldn!x;5v^hb1Ki7_F=0RCqqpx7J+=D%etS3VU88GHw7d z@e15pRNGb3Yc@Bb9eFw=Aij556gyH8&!uFku?VXWYl)id%kTAcdzj#v1&2*aHu3-@ z)N^<$V50IqkSc}K%|&12^*mkab9gE=$(oWQ#NWAT7h!6)nVy*E3e1$$B-gP_iRW-TRa~ylGUR|>VEmw_>wKV7X4TNnC*^e)!PM--MG;y6+93QxnUmrrEPxH6&iD0z zV-gOk&%a_1DFBOr7^p>fxw= z%zq$1%&FvgOp$s}ME;z@0St9kj5ygs^%Z48BxE4|edR@^@&pB5m}g13MM>}T6=mP6(p#IO6YWSqyYIiu@0>U_GJxt(<4#Z8Ru^;e!*I*w{Z%tv zxm{0CqtHw_7|4D4Bw>0)f86@4?|b&3+59>;buMUF6|r-6U1|=-Io{>h5wg7F)-hjL zbXu-9JrTeo&6plHdMkx|l5Q1`Y06C)zcNmh8~oV6e&vY3Z43}FbA)Z@QF*_J&oZv; zy3CGP43Lprljv~3TH5O-;vVCSS;-cY#Ja37ZaHXR0URXU| z)jH1I&+`Y&&u?#eFtm8S5Xy)WfpbfScs??1tNl)%ruF}(EWfGJekiURs7qU1mBA1D zb!QXEqP?Kb{n9nWl@~z8Q*kQ>4_Q-r)>JagdX7C{wc4U{A8j~8UD*gi`cO;en1O=8 zT~CTT4`jJ=#@cwNcof!{<==_H!itxdAEpyLkA@O-_(H85Q!<^R37y1WwAckU@eCRO z_PXEU^H^`~ramvoi>>J{-RCdMF2Bngn&?DqB(k_$zP?#9c@?bbUhIUiLY(ve2|9$yBZY=HB;>C*ZBdg?+V%iL{- zk$Qlo1!w>_gsTO5K$S+J74wHGFHHOUp}Rd>g@wx%jwOmelb!DFrfDcrg^4zGPo|(m zCdtBfQwG$cGDL@+{u5_8MLA=4)&pY3L{&-z7W>{jQEHB@$`o2`73M4`xL*{Y!Jk=f zLrBME^8oX$l|mN4tT;GxFQdNQ;LLiP;0~N5Ym<_V3UMZrH2r^xE}>O!*oH(6WQ-xR z@(X51zsHg~{~zg)>a|yzSW7m;URfkuDoEzS?6k?bQA_Q20nd2bw@=%HJjw!;#G4EL zI%3qo4cUt4M{jYcqaj;tM$1iU#=5xq-~ROWP7e#&Lg4}m6Z*WTS0$q1aZ)E<1%kKG z?HH1LK>WAQC#F|rxeOPHwwuD93so`yH6l5GobzV^WPeOdV<0+uDT=?Kp4f$2gjMF_ z_i>b;oQhUFO&%F$ox$cRXR#va*+>)~wyg!+%~p#?cao>GaXK@+%HcRMdvwr#tDs9+l`!k%&gYN1LKC{9XB05`Rn_ zevT1^$0k>jCe_}&It>_V2dnh%FBS$BgV8HEV^PwC#89KueJb5dtdp+i#z@mzLPbcsnhrWfCWQcJl4VOpKQT%{ zMt=CF-ULBMP}=VKYUT+6>I@C6oL^zJ!aLqE+#8NJSi4i|N)8M5j2BI8Q@ASxyOO-^ zv!03;-Q15cNvETU2yCs+t?&1D4E3wx-CYZ)=PwlcRPf@QWAcjzF4mQ7gHBsZ^Hx_k zDdy9A{p>>osV(>6?WZXs;mI{U-}Cw`bu>W~{#vG{)b}e5E-Wro%v7#^boP4kVEXdi z2GNb-kv3vY*N}VX63jL#t7JT<;Kq^Sz2N_61&;k|gUYHhcxJn240)qBKNzEJ2pK$3 znvA##rPSd0i}!!n_s}+Lxo=UKE9bLXN7cCG%@w$ws|yGZv9-#53nmN8cY|eTNnn^R z(u@~Q`ZBiw&tUW2#RQg&xv>BAwge-ux%+^F+J^osX=6mHQosFLl4_P~Y&>Kg1_3@^? zrhLJRcl+5`s4h7vmcFEFp@|=y1R#$;ONI;cFu#*tD364?i^5kyP@IVY3P#RyC|qH~ z<^Qq6%-soMc$kfq_I$UjzYce9Dh}$nF%`ZN)%fLLVj@v!uQ4dBx2qp%Du{6^hIS>I zQj%zHYD75Ei3Kq1HyowQ47X2viq}aY3V%sncw|f~9hk;qEdLhK5(m2}5jJK~i^G|{ zVKUMcg+&_{Hik9Xj*CoSOlkGw8Y4bJ+r0PRAjPjEuqx-zG=(?nB*kt@Dl~fnXdV4n z%QR4xy9(duJ)W=Au!){Umkgy^ou{H!FPz6NjcumU$(h|}MZOuh3A`Do(X`IKlY^XB zZMQ+Omj%(d=Kc|5!xoo~uaK>u7*3O%Yf#kg>X&{d+zLZ~JC;o;sY zST1h|6|K4bqy)VZ{2-^davPow$$6hX>NlmK(dW=A&r*dFJ4Z&21Rp$beD1>$LgKxQ z`h1JBvF2lg&eDp0O03Xl!oEQsgQlLPA*Evy(8jYUPYE%8-9Fs%2EOSnMCA7ttg<$+=36J*&_IkJNCo;>8Wj2zwJ->XpPR zE6qvWL|vPGB5J_8^(fDm%pr%W6!9K*TT{npj4D398?q~8WHiBr%sV2P-hD}DYMLnqGzFS`)$Ffu+)RdKuEQ)thqgJFe*bm_k%>=B)Jnt(%1<9}Dz+ zhKwG$l18>MhIPljPL_7WLhS{H7_$!?#uuFRz*oNKuaQIL;}xB~a9=5Z04xjO=C=U2 zSY(HZXB$S`BF=r6ggtl4T-oN&f*Z7mL~12JkYO~{cu?DCa5g95jM}LIT@tjmUX)(K z^|c*A-%QM&im8neGWyyQAKaioy`WJa^`b5(eyphVXaFEiN@#U7+9t|f1pgjt@n$Ya z60Qm0Uq<8%TN?iYeKeUA1c=xPEP)n=%eFc&&0NlJH>Lpxu)UojS;DRg7-0pv^;bRK z_mg(s33M$BGUim|;&Z6;lGX-{bSdTS%K}x|7~c4BGinz`-r{x+^nFKsu77#mratv zrzqP7ywyYS4lEJ*t4^O6UOoi%!JnWvn!mRdL$~XGb|aQKhHS?NZr4F~BbucWQqIUq z<=2pnh&m^Hdhlw>Z<%{F5>q)U&g2c`_jmwMUV3KFf9F_#53L!%M+n2RcJslttD^cW z!7*s6pbfKf7428{_q&u-IXDGYq@T1~3cUU8J0c2Cm;Y2DA7lRhuSr^@v6f{s9jE8X z;x2F6)pBW-{rg*u#cAEG6JCBY&vdcV5i+h>0C;$+{$R2+T8h`TQ3P$TS0=Ma>#|5L zY#~K$C3S*-FLBvra=BL^oFTp98Qg589wjbElTF~8 zzUPZq#e&imWdT%l$PV>tKiuk7aDLP@;QpR4Pd@}n&KPCS# z2y4W2stD`>r(ZHwdvgbS0WCM(I$r=c;LaP59kcuW&=pw#F!Epz!o} zMtFO#b=wIL2dKf`BaK)Cs(8Y(G5rHzWhSF^65dbU+70$qfLF|n!B>FPo<0p)G%cF! z0z4&6ET!S;eDR3XfsPo(V~`^zE%OR9>7Tp3j=0c^s1zh)FOiaf>W~$O7~|y;wf@)5 z@&}oVHUw4Xi*zNCqgt#IcY52F5^%BCetgAYsz;CF+6oA{mw-GAQl1aJy-%VQMkKNk zPeknztGE*F)#eweZIjp^@rXI0V@6MO0SBX;xN*UBc^nWKGSCR=Sg@+xNhxJ)7cSQT zpadlX&CzyB)Qf&A3XP(+;GtyDIU#D$x>7lcanD?m#TiK)PSZ(#VsNE!DNKI0J)%RF>AeGS1(sk{-@0^`~qDi{h^&Di$o?)TyHgT*+I#Jb^>y3Pbb& z;vT1f`pKitQhVD37Y{jjk#g0Or;#r3w|KF(9TC+ohXY=pz zvbNVUi3;BeV~>!f^Z zz^(geK*bqjO*!LD3~19WC)EZ`_U~aY)kMA@ni}cv9Mz?H*SQs1M_H0brlP%Vz{otu zm8B2&(vxHR=`yj8^Q$RYlt~(NSZtSI20K<2YlWql%Md{%2Dcwz;iNu^DgN6;ragcH ziv-NVkTjZ6K=;$FALbmfBTUPFdYjtdi>`@L-8PwK++a4##+{73Qf|MVa49XfVh7P& zWc*R#Q)u9dz0cRb=(s+-r75*hR_S-7Wsg2oo2hTrnf|cxVXBR9V3RN*o4I8{{PiId zG5b5BD02!dFWK#Vg)h92AA~EYUn|H=I*md*Du`PcZc5kX$Gp(dldr4IFkzloK{#q~ zwyCEav2&CPOyKS{>2|;;qILeBbchlBGh4l%wFl*^mu>L#0urb*37KYd;IDvI;AhBGdRF-0V@+!O2P|rZINXYd*Gmb=3#9RmiGth z&s_@bj7V?A6%_J3H1imCcttz?hMPIERl_?g_S{{ty*sq-pAO)#5q@DO?%shuk>KuG zHE_G^QVVU|6Sbol(oQs@_FHx>@Z$9dJpc9+r^-0Ww)MrpthAfJE(GHLN7!_rA>_pm zEB+1H%=D6tYs+-<eH+w=C8Gp=V2 zMgc6sgIZb1;s~f-o;7rO(W-%yJ-zob@r&K09Kmh{BDUtQEW87mM|f2U-^wHlYWF@L zu|cG0Rp)OzN}I4}D!+n{R_4&`3lhbP=&v~34UFb}vwRIQ7>kJq>F7p{Ybj8F-8#`c zeaE`EYOpWXu#LWG{uPcKzE$I&>y1$dT;CDhYYR~i13%+-jZ1N; z-{yknb5f*j>$0$FrwzMl^W;5NY!h8=FPj1VWmlTF&7?dEWCw+jBd1R9#L&^`uqcL*l1i@em_h9hGcB$md{q$ zuJfyUa~ccnV=}#7GM&CN{Aq#c>76HKh#X?a7+~d0_E^IG}#)Z;Iu=VT22B zI;iN1U*`}+hEX#cpzHd|p_lNMp9EUOq^6Zidt40{Rj~I14?02iZ7|5obelJn=IIPS zFF2`E>)bn+?v{SY@8SfM*Kmf$p@_*a1EYT;hfO#}jeD4o$_a7|KF=Ko6yC))^YH{M zf9newX3oB4h9`tPuHFlUqGg=_GsE_}^R?!K@VB(`CfOBpJPgB(ajlDANv)K~XIyDu z0@t2M-X;lDUCa)Jl7tI8>TVzqe(2|&CK5V?cvw_KP*4yoXwaX=hp*6{4GzwBFGg#f zsIL3RcZZt^ACEkrYwnXgABD#sPDsGEmknaeY=0MWt}sbRWEDrF`lQKKvalp*l&8qt zYUR|m=H|AAa7j*1d~gK$#9<_Gd0nUt^flX=bjk5{_w)tRN7(i?Zr*r~u5DEu!&1SN zbBKq&*QotVVBOVL9_WCwfR$H;2aB|cl}eITqQxQ0joGu{oUy1ybTam?8M_gJe5W-V z4ai#U>GL?Cqb>aFnVD6aT`ADo=cWEQ8O;K)mYcMd$vduoigDB)Zu9)v^1gz-JDTmF z$AV(GLMNSUKvl8p#~$!T|4XXlIyK4lt}%&CQ+@?4gyYv>T&1vC(^=|%=ZIY5#&x7a zH_05hLPRqiEFYq7oo{7lt!A`g_j6=2=O)S&L(H(Y?JREQ;zX`)1|x5Gp0bySaGSnX z(zFhJ=}MGpSvA?0K2~>lPQM8owGjzv$l8gP>+++{Z6d#-qyPPP7Ke+4yD%*;dYM-L zx+K-))O{Q}UZ{&(1-_P5}f|+NDV#s15V^~Hh*ji{P#JKk+pMzLx zZReH}jhLCahx^?9d~Zkp{qCNEtIny!bq7*Al9GB@hKaj!Kg)Ws77v>rnrJ<^GV}3j z^H0_5(5tnsNCj5sy8so{UeT@`6kV^8#hslM?Q2)b!<*iFZpHNCcA9|t13hVP8(V2_ zsoZLT%KPz=^Du!g2jkOU2`7v70(A=1(q$E-{}OG`y?arzpn$o<*i8QU!N*g_3>s4UrxQ8iseGQklr>k+_R z6*w#s=Mi3BeWxV#q}@=q`>G*XVX>zYC+ec>90cQn*0{mIb=f+mF_)U}_}tH+1Dt}F z9~klVaqjG$b7qtwiaU5t9MQ|8>5Q%hCq?QSJjjVH@IgF(8JIsiu(eqO5D=C&stx@4 z^7qxUJ2;J5a3J_ZVE^2R*0le6AAL3ibN#l$#X}LF*U&B8B7St{4$5xcI=Z}Zc}5-V z`c(L*bzh-tAebp2L5T|&ToSZ1q^lr)QB}Kxjf9(*YKTcqoV^pbj}g7Iez&HMrcfPj-cag9=R_rt)k?>8K8ep$XK!gx9_1)G7vw&BiYnD<%(W7LiHoRv`ULwV03 za~}l_i*y8sbl`KLn%Nu>BmVs~n`9i`ie#Tg1V-M>jw5q~IkSw};q%dOnLYrskns;@ zH`{S>-S9JdD2BQkgANjxK^L%KZZ%xe6(OHrw?a~~uIf;*l}@K@jodLWHAyTp>PTQU75-?SSetEGNL$tO^uw7iVY z?h5x)?z=iKG}s1woJiL{jcbRcX2K(3k>7Jfp)qYkUe;#G`8Xms*odQ)^E{T$E}BK< ztTfiz=)Y~O?Q<&Tj|sm2CNQMo6NhCnJ#daJH8P~xLV9AnV!Jfy_1K~r`+or4Kq9}5 zcxGDFl#AA1ntWgGo~TUf))nH>!gb&zW9{X33q`}GIc2ve&t70v?6N}&x(7OT|HiLq z(wPCDi0)M#v;2T)=DW>8^Pp)zn4*7IRnP$%HW*QVDE-asTld7gPdDFDY7bH;Y#4(< zC^>}}Wr~n(m~`Eoz#ngn8oI<-5B_8%%cGY7+^rPrdyU6^Lp#C zR8o5lFu(u<{;N=`|MO*TxqHNcd!k1uC1h3+1&(A05rgg3y3a|CT(ZgHyY`j>^3w&t zv~g}(+2tZw^o4LAVnl^XdvF?Jq)AFUrt@dXNd+_`uHh{IMs zuBNIRRegw)gms|POA@0TF0ZHTYAPIP6TV=k75@~iCa&WWh$SM@3 z;DUrv^6#fi*{M0h(}9loNvR}#7)s3*#VJ#LpMO;HoZ&S<+a^V9n}oBBLUT5Tk(tw$ zk$?W$Vn$Xf)Yn+sf*!91z60}CqMU zw9&1F@6ajxj6v(8Uf*LCkz#NO$8b)9ukW2~s9p(HO>Q4huK?%vjXYIJrk1`FBR4%z z`(2H5m+dMIHqODnRwu%=^eyhRSUPp{70~2| zn84CRiIeRc}^eK+jCnrZxTz3|miJ}NwgGu{84abUyRyR%B|bk(JN z-khM_z|_|F)T{1(o(J_D%Q1{(6N9m}{aq(;H6`?6SbQTlmyXVEaOOM`(jEX`s9Lw-H!H_sj5hM#oMLAwG`kLT; zqqnT#-!lQ~YrNeX8;MkK8shih7cAKQ>~C!S9N`1?t-&=dynymJE-j`a3@ z>d*i7L~fJtc9f40qSyo?xam`p^wLxEZSM|rrhqoAPpT+&xWV`1((JDmFGUVE&fy$^ zGdRXl9L`Z;gPo-w?mCfG4Dp@ryY6UU3OEJ+I0D@zcr19C!sV8RyIoUdr{EOjVN;4~ z{QBCOIZJAQbMtyo3I=SFD>Th8MbB3?OKvd@^%d@X_tgWhv`3Q~5V?l8_I9|$DZPWY z_hfev^}I0I*)u$HJwtuXAG_z^=l^SE3SGaUaWq(~Q*gUo(}KP<@(0L{0@-Z~eD1iD zeyw&>r~|DLcUHk^^qzX5smT;MuNQj7x$8Br@Gd#bM~%ySGOb@5G?JEg-6IhStq2M( z;S|MOm))YqontZ^CTL>|fWfa~EKM?v@J)aC>bZwEbw>*zDwgt?wU5K)l>6n|z1|>} z$_^!XQRqUfu8-yG-nnDg_AJ;IqOL|h)*Wet??Y|zxiE^_U|@Pk8jh?ZeVI>xd}zd!s5cC0StLcw#?S&U!vth7avhBJy|kY{xa_Vj zXeL>u*eqoo>;Us0G~*lS8f>=(kpR9r*Z(O>7?pdQTLS3!36IXXr z=Q0$S=zGL7`kCt&H4di&DBGt7P#V89eY5Xw`I=eV zCIDF`CyMzPL1CE0O9CYd!rB{d@4ju(BWcYLGDvC#2QRgfCM6?5-=RlT@I9D0Cx6e6 z^}_WN628;5KHp)-gIK-(}OyNldVwI8%;6RQKG9r?&TC5 zMIL*p_6??ENyxespK$B2m%sZPNK@lNyk0S6iPNjG{p&tsxh^Fgs2~48^kth`(c-ko z&nKz_{)?L5Sv&N1RGOqpDos=bdOs!fdo5@JrJ&q-sgc{MRZilFh}}1)+qVV6mw=y| zl@^tsVgv@GNshA7iqLz|#vFF!8|GjCql)?)jk5{^3^2feN&&M58)|^o=I!t;_-)p- z*Uv|HOF~ej$Ws!ADG~=Clz1EKtt;Pm_TRtfz0LzhVO(S(UUqujcG+X)37nKUFYqnUIf)3Nm5oJ-)5Jc-Z4%|R_sn$>hBy?ly7r6hAG&ijoGW}-k#JEs$rEsatfe! zEptc;V1++6HJ3%F`V2VkLAesT#UHu6s)8g<*~RPI;@JGC>&fg0a*`O-x>?;kiL)3h zFpP|cDR-`~g(`Tuukh^iIZw7|f>u*0POB_QIDzpPM-l|=mi)9uONDcrulR*k-FBo! zJ@8gUoRcI;l6h8!54@XSQ8F@_8Rm#7T;*-P30K)x7WhG@R!8oXNXnAT2m(pUw{`At z;*RdqYSlw*5GZf67Q{wlsdR!o%XiLOl+BC+IDK7Yy+@Exk`!*EWs;G38Q=HH`3Y_N z)5)!ME}g1EeXXUSw|(*I)XX{+%x2SFzBO$hm^SYq`Ye)US(e6WhUIadCBqbwi!3Dt z_NGvC>-UXj7WoE!)-imwhiY6&hO`cni-x%+**(5t`5o|SuXP3q2i&+7+M8w|%sqRl2%8N(e*sEM z-$(Espns{VKyfql{mqaD7kqaVv4?`^+0ge|^AElQ>yTF0|3E+$6PSw%dCWSL57YyC)K|^=d^KsDL-74C!=L!!Mjw$>u z^^k!*1|$G}$u!0w5de(zu2IvPRlH@va#zcXgh!Jrxt`T=e%Gaf<9V^+ngp>#Hz3OG zsirG06MBHwEdN*?PTj`^gmzOLZZwVrB>)@%_=q z_DI{CH10ge1ji7x#IU#ktCwKCOIg{E`(B<+bKd@-RS}Mj${gnNHr~c^7|$zoF!<%} zyvzn!Njg_VS(WpBryWh1J^g{MX+U_D`7}lYxAyr3u0p3Y+BI5m=gQs@>v|3eDz?B` z&4d_NVp%g5`0jbs9DY1*DOI2<3iu` zr;65gdM83C)*!Omou$qLIcw0?zpq>%J0;mE+GP(MsIAwz5+8)lsqI42aPqmxx31H) zzeb;nel96-GRHHt%rUG4pG)@Pv`F+~-@vEGcc1eph2AMZ*I4Vw3s;f)J6zNHI0TPJ z_E_zn{z2!6L*$?p|M%%N2AAJB?p7&EVT_|~F)j1uZEx|#O$+LT_uQ!txDfQw2;ZVx zv9ZsE^!o=pMl5-m@ALP@bs7H+@`Q!QBwFNYK_qdp{kE|)%X1aKsiYJ%>O;-_Z-uX~ zJ1@6&zeeZ@%qJMD$02y~WDh)f%~xubztT5hRLlICQpnccKo%{Tg$ob4cmj)p& z!-!Pu>X^8Y4P&F|G^Jha*g^fN6? z@#QNzPEVyqPh%aISlbWkf!m2{9{>fh1}oa$SV;>C;C{j5UR^*&=nGq18WcWmtDdJ) zt9QR-(|-g0Ye8Ek81oVWhtsO3UK&83YCaDWQ=+Gd20=Y=pqfQ~4xqD!O+?q)k=t3* z96iI9()JN1?9oN!}Y`as@{1vBiaMEV?Q+Sv4U2)mCYAi z>icwM@5`5K@VScapVyiUFu(u<45$hye*~TsvVXnmn~>o?{4)(SJNKfmg>f=Xut8@% zEQvMEe(S;0L$}oiHzgNV@B*9G_pz1_-q-t`)_eP_c*6EanZQu*J8OKeuWw#lzXdqF zDQ9~e>L(W4f294?za4v0z?s|`6%)>1WG<%cfaG1@0WA0D)qMamauH08X@<8-Mb7?$ z!(kJ|DWi)DpzDG~)*g7+cvrH1lIg(~*f+8@Shwn9?I$x6RONzXwzvmmcKX2Ww1Eabb zwGoocNFq-VG!GkCwnj1wANU&Ie$$lEeM=qBC515y7o0X=yeKjX#S>EURZHK#-F#Ke zn-VmxdiTAIN-jTMXjx71wTs5{8qoN(G?utd2Kyu5k~8`J8$JV;8r{iRG_9OtgKd<_PtJ=F80?sR0I557ge`OKbM!?ddg;9y&OlpfR3d2!^5< zBFxLme${o)M~E6MrtQ8zo;5wasbBQl0-}OZ0;MntON7PxcnVSWcHf6f@UeAX$_Ou6 z5`$id;ey&GqZ+x>=j;2)D69c;7#NnIc$~vAj^imdY(qb%irj|~P2K5xaaH$SPdk7; z>Kao-eUajF_Y=fCt7H=}5vO>Dv{6@lI|H*$*_8zQOV}IdKBx7amR>{eJrT2Mmroqm z;-NQ3>GKjV3ald9c$}t48|)<>gHjaykc7hXuY5z7KK!D`2Rx!*7v2_dC)?V~;~naf zWNWVMwN57mtKBKOFWk>|xzp3eJ!3EDGV9x#p{dq_vzYe!iavgORPs5EYhV;(qb1xX z@hk~raZ^Oudwo3*oImh=Z&32Mh$WL4C(#<*3Sr`+MJr zO)Do9?`jjiV@3|@fQdLM%Os79kKKQC_RoVxWsUfc(AuAk?dCtacOLMi@BFRhZJm&I z@QOl-B#ki`OVh9&yk2r!R^5-jbKOoYyf-^~&^>|>2u5Lff)u6sX2-(E2AoUJ)VYr1 z)x4Eaw}ZY9x_Z8Sj;6v=WN#^O95wvld34Nc)H4*H(hdid@?3p=5SoO-*1 zpsgciMRvqDP9{o*@bIR5u%>VOGcgPuT|rfn>T3nVRZpEUbQ`k?lXbR4R9k{o@7 z3UcL&o0D{w58l&yU+ed*Pb=2miUu1+wF5ojzxrmh-Qe3h7inquUAVIuJX`F1Tv;*n z%9a@*)ZoDpC2hF^+Wt`CpHTS}Q}qNeWXF63Pu{q2-2AR+IZqumlCcgiv^ngayc-3_ z6xr>uiowC_?UIU)b?9;LhaX6(K;rX2`07*B&(##=4zzO>D8|bRT*rK$^%&P^7?S6z8Pcsm())8h?xtPG z9ZP}x@ZAx1R+1#GipwF5@(Q^@b*c>?=X&|yCc7ro%tkO%@U?1K-G_?%fbzzl9f2n> z>G0NXfj0PTD22!4obS2!U+-G)$>@qs*(OQ2$TI|wDLL%vd56}&X$C!q4e7NE>rohZ z&No-y@@?JKdZHpyl5AsmTB2!IfukaR2a#DKy<(X3U3%qUXuRdZ-=PJje|;|w3^e-c ztd4jK3@*zej>|MjVW@ZuKU}1%lyJQuk9w}j-^6*?1Am7mrDp%4qAOqctUFqHMFm+E zmS;%}qd7WEbGP8Ceiurd+PxKmiY5C8?L086>mOjXtWJ+W#c007Wn5pUr;lo^`m1+j z_K(|>)9M+A=LuzQ3^2ykfJ%RSKPb)5*DA(pP# zm=0?J?s@RNi1mEb*%Fe5*Prgc_1)gr=N(2WggMN{N+dzD1j)e);rhw3b~nR+%^H5_ z*p5+x2r?o?-~@^BvKZzYmq>|Ru*H<=&nJI;?7O0t;r%EJDZ$`YTaj1x43Gs$cK34F z1r|KtgIeRNr0q7_oXIFgn*#XTW;!N_vQ3e6FzNoT&SE%p7mdsYvgdA11} zkAr6Un9NFoDBz@x5#H&yW%16liDx-SEqOFfibF;b`Z`&jD%?#A4}0N007cx3yZe5KjW2Ah$jN#ban z;c-f_2{zbUY!`K(08w>c*IZ34{M&|182b9q$QH9WO~AxxVMA0=E&aP{?YZKw!%?rO za4bd%GDXr{xDW6LKC&W;Zsq^$_usO%HAmZ3NepF^8QO-^I6NpOgi;edC~f>}d-Pp* zb`sdGv1o^B1WU^jB~TK>b32c{vnq4p%2X)ot7*A9!!KGQKL=^7W;g^?4uEA4wSX_q z+z!1D>+j*ixm1LH)}*2T)ykVq?flEzW<5J-0Mg8w*yc4&gQ-I?lx#NFMssO+iawbBoy2x3C9-?I&{;cM`{7>>Rr%r>pHa& z=T(CgUVZ+sx`I`E6Vv3YOHxK%YQ=;9n*xB@)Y<>ju+)c;vL8;y7?cez5SifeC;E!nd2DjTjhMaca+Km3PESu{iYYV$AA0nY2n7tGtXUOs#|y!QZag(g-vg+ z(D|Jfwb0tt66FxLubP6OkN4{Qu3n7eda)oc&hL6BxE-}(6bw*!l2lFCQQl=fzg1OV zqVecb#Y?Su|M4=LM*}p*O&#DblVf`?)vOP{7+LmZFCnDH`iMmabS8J1^nG{H znGlWN+a|a^c$}uH>CHO7*HQ)um&u}Nli>1z-d;3^GGlt-jyRQUiJEsSly7p0&TBK% zo#6S-LX1as z+efv=C01y1iS&PE1?k?!K76)RQ$2(w&hbRSrPeP43@`uXQyxVNF+U0a?mA*wHWF?)A!UtIg$hyQT>? zhdi{<;RvQTx`l#$r&6dto_;rN3DkU)nOq9Q`KLoE08AzM^Cp#~{mr463ZPwvK29eG zLNl-Wfug2e{GsIljtrprmuaCp=T+*9zzgv;YT5%j2ZUYxc0jZb*kl%K^kaY}a({bh zNN51lYpMTuRs378o)15ywi|?ofCGnuW8}TibG@3qcs2$t|34SR?=Rn*@5)bu$($J7o?PK}Ni_ayjwQ*~XRw^Y&ktSY=o z)V$eGm84gzqSq|7T`#GsmU*Vov~SbU{O>iQbQLg@o5jL z={bb`OHU}@#T%K-n1B=>2l(GC+L`wKyi&)c;o(4nxA+N?AO)EtIN6pq_|&E+mTgM8 z29k*24L8w-kKkT?Lxdz|LP$6UR1TEC&sv^YFq>}hZ+hp4*Js}WaEdcU!YM(*Sb@MP zisgxxjT-H3USmWOn6nj7T|5$hhx!jU9Pv~~XX#0rCBhQHS)O7^ic|!Q1xv&#EoD2` zUVALHGzP~RbM#a%!0!hgL6pDvn}ZPLjeOPS&8A`g7rvahpxzolS_)Fa5H+7F&)zfS+#^ZU4wsmm|16$^_>&uYjxdaVStPW`LuYHUSy5GeRC#&I>^ z;>;5a-4b+M{=AC7ouT$@uVDKN5Ld1e1^6XC?`8hAM)+&bTYKoxsg8j6D7&gqKE4*t zkfKD0Jfm2}JXxGFQWjlqDd_DW?cvjRkLhtV<8gJZzN~sDdJU=yh|4GI#@FPFWzgC| zr`T`O*}seJSTI><0~W_xu9`kwNkU4(`d`WI?2w>x^lsf?ojD;j<}Ir1`3OP61q@F<&y2BIl)jWV!~A z51dCWy9~s5#EVz(_w%6?gCj0CkCz~gEPZi9pEXbi-IKd7FkTn~M} z6*~7ASVef45$Aa7jT>95iq{u#{s+RlOb29w6eDetR&GgZxa;`wz5B{rnjo; zC=FrXY#QxJo@WYELco8`aj>b;*Z~^ z0@uh0fBcHw5wwOUS(ajC3KIp9O1rX4=9!_}YIjYL&|g(b#+o{RuNh}~HLIxID3|;< z`tQE$(UD2rK*ZVz(Ob#_pO4BD3gnS;uHEBx7g?v-y?Onndu692OGAZx+2fng>aHJh z_SMu99Gk~!2U>>nWpNLn$n5>ardr1>8;Z& zd~kvROmHGJfQjhJ&$oMX^PX7Qr`!Mu=Fl{J4y^u3r|7Hp%b>Nx!)ZZ2Nzztlp(7ag z{d(CcyX|6cyHm0|bH_OC-eJnR|9iGs8uh0+)_YCKF+Ts=AKUwv8k9|n&97KjzF4bW z$MC%eTFwM*4MvgcK`XA_t)6) z29kj+ZvhK4kayP8$5@nA=knk4cwmm}d4MoE(IUc-94`M zUN0B3pnMXP!eexq+Y>cp5`!xQLn$&tax83M-7XDPwA;U( zx@F8)6VU7*MsOTPlLU*g97oeI`)AQy6dwOwFAc6$M~*I4@th&Z<#?boJf zfbv~`l?-SjwbO7KD}D0%Tm10x|CHWs!UsRn)+b54B#^8~kQfe6!#mAdYYa`H|JZX= zGNrXG(Un~=dlm}3R#C)*6;T`~$uK+1-y533H5K|hTs31r%5#k)klZqpXN99F)Dl})E@|(FYDTE{zWhB+qC3ybO>^S#W9Ryc%G(tn!@1G z`zI|lg{!#D|4qTX`>#u?S5D6nx&ZA?yVpuG%;CYQ)ZJ&<+Ta4yF;#>)Z;p2hyWM|= zSa|>G;wEU64lW;_lx!4@3A9AlUNn1IO3rX_I8|!?o>4);zpPEO{CNw9jCh&Swa>{6 zF5x_3<9V9^?{keRNhgtL4}lp8`VPI%-R0kK)0!P)JF#dg8OcpTFnCfgk4JWU2Rc0k zvgjRbw*`ff9TOTaFMnlWUb^174ft=KxOM2D2cm8^0!Py#CEzwvQQ*zyxVZ+R=)3*z ze?R%XJKt{}zKqI9Lok3FhcO%{N{ZFva7}CQV;ger#PdmDNu4%_rZ9l#cOkl%Y`Vwa z`P}1savq7I^I&icV;CAEaGWB;!pXh8yQGefuZ~>BJ^oEAUO)F;{haX0bO&j<-D!18 z7(>$}Bat%49r$PTU3BLAwZLovY5}L=^=#do^yZd~7U8ru$lU_@6u0&X+M5P-OF^d^ z9#rVV(`J20a*zL5egQN45hPblVFbsLHl7s(9*&F4#Y}A;^shBL#8=m4hwG2rUbi61 zR=2FjzCYBL71uza;lNL+#HLN#pHa+Cuy}j)N-CBaJxeggF zzcYkYkX!{`ReJB^aw_&*c4yD6zu<$9q)vb?!lEK5USd(w3@{)jIBUMR^?>KGrWO6Z zx~E#>Sl!3rVz*1H{!0b_w7~oyPYp1@*l!ImzyJdbFu(u~<)ie@UlZxbn;#NlgcZ+8tXy6pPs ze1Fm4?rakYTuY01V^yMAlHmke;%Vu^wX|Qx-@hk&#iH8DF|s6}-ueF4qkh}I`EL-4 zcXh}6mOvYIJ-lZ#lIIOFJjQ zgXgnHhu=a7+`=Q5*h6?z<^5+8bfKk0OX_=i2XTU zN3fZQw4&u!HMG&0ujuot=J(>||B?}DYBfty`+`h#b}{{|IXdfV zF+J};Jnze04<)rJ*EqF`X|^&4#2%O1J4i0FGK4^~gvjw2OTcykPhA_ilIQ&oU4OE< zZ+}xYA0cW@K8;gWyJu9PO$@pWAEt~TXWFmI-&7OC;rd4{41}Pnrv?gr)LoJ)j*?S_ zm(}-xqtFRMONL_O+>%y4`;9r|J1yScY+B}@{X*Y2W|sy&D0k%P@d~-!3T48^GZLxD zvLx1;vuWkt`~m5}YqDd6B_sv#ftv19A3M|!UT#p59#(lDr}|#TxRP|0suTXm71~Uq zc1e2Zj!;$yge8ATt%p_19izVALdDH{klkP5KhgPEttPEX9aN`;Paa8=R)=ev>>l9t z7WB%Mo!+7Hw7UjuyruQR`l(N;AFfabkbt=FG@ac4{~Crqw`g#Q$qAAgEEW7d(cEwH zCHlM8b4wMCqgFdi7iqo?_-{dZ2lc5TyZTE1z9*JWn^@SY>=8^-%Y6PTHXB390wV~V za^Io@?=9H=L0S_;m0)Ad*7!YEKz(;4K|^&U%FvlZhHjPr@SLBXul+(ackqN_lUR&q zI6)y{ckt^ik<)O#Xtm$k!T;~pPXOyRzs#u6qZ;>gE5w3vJt&(@ayE9$E61+h=l>*m znEHXYQD|y_0aXbAl~?;8IJV@_gm(bZn4T0yZ4{@KV=+pQDU#+m>fB#lM*ozuC>d~O zzcsgpl>GM#CHY0m^tGfzQmAQi4s^g$^X5<+y<=~MT0Z=_GIVdgeL!e_+Q86N%=Hh= zKiMxdzqe;-p1mqGe>)YLA4G=c|LO+4-##>d>x@vT>!*>S`RnS1=C4T$&F}p#bbQ7@ zvPz*8n*RcdK=|wM(EQUwL-ViGq4|%Y{o)o#Q92RIA500&pNCkd@1guOD4zx8k3j}u z??L(XQ2qrd?}qYqp#0wuk^dFS{{ZC=Xyms+`DIZ4Q7A8|`7P#uhCKN8m(w>xLU|)I z*xp|8Z|(Bs#yt!4M3#~`UZ7}Bma{R1}Cx$oPDJAs)Gr*-)R!(E8=GwOnA>?qlx^l>>wAsG9| zp6g8Q&NqGS$WY7eRZOji85>qya3!FjP}&E|n;?b}HSX~5Fe`mcjPq63n4)S0b&+K1 zZU4O+-X1&Y{-}4CXjztI2FD~x!6M#WO4mrk1&ww7n@RE6E_=}-v~ZH;coySnn#CA~ zghOa$K`C6tI{&IqSGB$IU%&v)jku{`G|w}l%oBn_PkOxY*)O*iCj-UxXHdLlpU!CF z>zak8@v^XH(+B>4h{qTC_T(T338OHE!ElZsNSp`{OE^zm-WF61ANrdN{c_b8CxOw< z@W?`WzEEK0C`lk_k`!?fUpV>KVQ)5CklYs0g?bwKL;BvLG7!dTg2b&U)yog z%Og$$bbTfwy4nOzkZ6J+Xo|c=66-wj5SdyBTH{)@x1mNpQ4*NhwB6s%*X8+!Uw}iF z-Qj#m`S7Hiz*3w{Vi+rPGY+m_;+S;IJWc)dMrb4f*?2=`%5PLh^ENeIsQ$i5O+idG zz$52)C}J?CB}Yfxv;+-0Tn-#l4w8++SY#NoG=5)f zj;=0-5&sJfeXSYof<@b4#{RFFO+Y7nKJ+=HNVzfxTP^NcFE~lMByv{mKtY% zxpsfl@V*L3u_CW96oyl9c;92{T%S=<@Qe0Y_6{k?n78Jf4TUbyBifZ`SJ8K7VH36WbnadWHIATqMa7r>&wZKVNowN6XW_pNYcx?u|F5fVF$5 zbxCb2)wBmxS+g7U65jp(KPH;Lyyknbs~1FU41Yl&9A*oKjqK%cxWq$&L!&$V>`H+vpekGqTqf~Q0SHWDT*w5m%hC1+T8iK zW?7*geP2%ehz2NeZZf0_*E=IEVjTH?Df&bf__Y+{E~o)fR`LL$z{utQSK!Q<7ti~ zX->h~ZyP)F&Oeql4_9}Vp*U+31eRoROq7WW zdfb_oFIsnccX>MC_|K(=rZB+ILH=G#^!GYIdM`6V3^kih`LF8s{m7r@fq7XL$pA%C zXiSiKMzD$aiRG_9OtgKd<_PtJ=i7v)1{hEs_8_zAFaHC#TBddFS1JTXRPho7rmzfY z<0TQ>;wtLevL{)iL4veC$uYiP@jn0U&-VI<)qD{gC~hdX^LpKaC|lhe8?+?D1qs8c z+`K1ls$27S3m_+d9=3#}diT|V?LsL`=eD8w(U5-f3rOYh95fORDE~R6(U=Y8+d%oI zP`)mdw?O%G5IsE!<-djUuR{5iQ2t3M{{WPq0_7(|`Ib<=CX`RLfVnvBrxvZ*aBMQL zMVK3Ypp;KzwAD4uIo$2?y1YdN@<`budq;T%uN+j3HCciekL%5qs@O}%Og)$5c9o<( zR2BW{NeGp)YCkkY%|DIsbqJob2CYIY;qu(hS@E-x&usti3x6 zd<*oZ=go;~i#cHXp~bA@`_gS_!sJlBfA{6v@2)Ul& zUU%w_iT<1wi)tr9W2N2GoWSvS=i&#*CTQKY;l31BP(uA3DWKDxo#}K_! zLQ|6|az1_}Y!Z07$D_qP2Ll!}g0jE-!KNS)ID<1HkMYztrFV}ljR%{x4*s;fUW7O5 zioi|(Y|BnN-C2F|TO<^r7^@Q0vJG`f@(`C)=#YmLdIhiDa zt=fZ~tRKpz|5r$x#ndYB^2`HQI!n)WXN8frAWKGbHinfjj+AZho!a@rN6n9w&If7< zFRb9nF3M7%RbbVR{M%<|0@RxxQO=VwhL=Q2=7J7?fh*GcpXjh5v&G2_(6X^)_%^B3=TDV6-b0R|XgfB^;=P<;Ru#AY^j#eIQsKY!h( zRFLWrI5E?oJpOGUu5L(;Ag+=&UcoS0AQc?j>%>|fU$Q=VCnQRK2Z{3csp)&FA@poj z$7fS-8i5mtzG@tb&8kBE_cu$@&8kQ}NB!=!`hK73mJrPW5SvFp2FkaoeFVOArmF_Z zNovCm-BKd8&ite#4@5vyHC>>l zz%~;ozQK7XIYZU3srf=ReHIcyu7JwlqK?N()c}3fqLPMu|3*pv#p}!={TK*|z@R@1 zplkPUsP8pX8(4F-Dw}tz98|8VTmLWDjQkv&wRh3@KI0N04FIn|cWWiMe3T|-fm+d` z7NIiL*MEe*SNE?H%p3iD$%)!4$t5K|@^uNKe)n}rzRoan2p!3SI`Wd5E{4_z{2p+t z^iz|Pl@9v^Ig%Cn`@elk+VQcvKps=Iquta6`XZ-KH2f+hbX)-B0(^w*JF{*2z)^%Gx>ChH@5&3es$O#zE$u?1;rAPOS&pNG3^2d|0}L?000Rv8k3yMFn+)u` zJ+0^3^Wg9}g5JEmjXKFv9L0$ofiq!V46-#VqXK8vyl zCx=b5;Oi6e5Z9A}5?`J@SD`XV;c`!Jt)- zEsM>1*`D43-zTqS*QklCj%gEhZATY5`xh+5WDDWTidXHb=^HLYBR1%6Qm*1fYmn3MsLaFt@L zc1d=6?TTG?dwLfQ74qeuNB%_2op?Se1=;~EF^-1CG(2FL(et4{8%GOdBuT7IWJv}W zaRv@#WKW5l!Zi&K$nwz-2mXzR*8_P~V|eS}U<^OmT@uT0CTmjbula?jx( zb3imsgj03J?g-v;B_v9gDNYasR&l&^oZ8rb9nukB%jcs%NBy2?7&0XKbNF}E`}+pR zS3mq6%6w1?+%#)F<1h89&SgZBjtPaZ5uA-CL{k3W%vG7z{-=`LT;g|(=O(C}4m#g! zX)>TP=OIz^RMnstFS=H8JGx#X_+qT1P%2)lioUM}u?h2_b=4BWqplU4ua-LJy`{$c zu|C)P9?G+*md83gX{ zR!yWXNeL|Y?1o)Gzt=Kcm@buIg#zmoxyWO+3A9aMDT-roZueuKTSxxVDGSKX#VU_K z91tYsC;x~n~e}D zoaaQEf^|bzAo`VLni`Pzd%l{)fYz7ua3BHmyDfP%Gg>wcpMp$L1;&O{}zv%_; z-mXHYB)E(I{NTmxBSTl3@2MF|jTs32e^JvtDt@#cTarGlhIs^5G8223q~FyjDXEL< zB4MUlegpM8;5qQou9gr^8kkv3fR^vBx}kcGA;O#XPAG;SxIf%W{cwQl!{OkS#tv1L z(4d0fKX^@i-0YG#MbTtq%Kj7ZCX}Cpc#q1AUQXZt_&8-%@cWr*)tfF{tp28IaPyU= z$*C&(S6Ti0EIl+eAQa%RqB#}B^F-6hM7PhNpO+UMtLpj^1(}spUZU4iWv|bz|B)l0 z%qpr!%U)dzhL|sIzYQ>UGRWH6QiLw4A#uOOvAAlt>K7ZpMedX4Tvpw$D^f`$f zQN{Zx*oU@QqRZ>5XJ6B7^@B6&v1dc|7yd)o(EJux;E6F+HuOYBGf(+@!nTD`w7|yx3jj+ zeLoXwmk9BB7HGfHE6xbuJ4@B@U!%;?%TLkRhle2M4Xoz&i0|9PNT&pi_h8%drW)fI zD_^k$QI7$UAsxZT>xK3@--|UK@{%~^3BCAsvG`q6C6B}9hWB+V_`Ri?^{2YDCsumU zLQU&I&ttWqZe303W53H}J%RiB;O^o}F%q`naq4M0Y1A`O<9Dr4uRt#g+jX#r`QDD$ z-;#B%Q}8>`|B^cQ53|NPH0!LlhQ@m}HNFS+f@8`ii1l6FA+?I@s}JD~j3;dop;Jon zTupnbDG*1;BI>KBfiHa#@8wxS`E?M|L5;9e8iE2ym*5U_x+Qe|j)Z=n2(2Tq-n>x1 zuSVcqCe+@hi1`Hc=?cU+|E*IVYhFPwa^cM!hDD zdMgXeh<#lzpAFXKJ?e|i5byQSIS+MWY=1+Ib(5@tkBNe+KEJMR@|YGrqwo!tPxN?d z(fc&0e(-)voqVF!eCTzdur#S+{aj|Q8oWc@N=ZTdZh`hIiw0kBj*-u>=$wC;A3){J z7ipcP>Z~_iXE}P;1;EdE^Y>so?#mi^M+aA=qwIIH0 zifGqd=y^@3fAD>l7Lc5ksQn2wUpHvH2h+1Q8t-fIi?Pxh!jjnaXV!^dEHsYIAifxG zKWg9X6=VC-HRxo6D(0_2y_?OU^aG~5xjOk(Ek329?d^cjCp6Hx@1*LK3sL<4aH!w` zh4Jxeb96EU8)F>jSoH((t}jak`HNWDL%n=-N+q?UzRq#E)Z`q!a78t5U#iaeghPXC z@tGB!Ht?{jsAbQlif_fzs zeQO)%xLY*v1vL&@?afAv7nrvkjqhih_0iQ>`HEIL<>KTx=UeOj+HuamDZzLj@A{}L zzRLF>;nNJz2i-0c*MEBHD~$I)$5~&eO6mu^4__>L1OsfRG+QUVjTJKEe6Ik3=QDK5 zCBP{m;MX5S%tH!7FQerJRGnjRCgIwxV|!w2V%xTD+qP}nwkNhHnb3`i_)zyExpQqM+t+n6;Vy!y;u!qN1R8p=(MvLo4I$23xuvdE<718E!FtbS8^TZhM z=+(1H?wcwjUhQ-RIfqpSuB)*7T5ep~L`XyO0Eqo`fmNP`_q+-nGF;b`R)tTBss2m zHS%0bsIj3x`_XB76SyS!EH(R%tim2Skrwq$K~BG|wjo9|(ep~c5kM#6TyP7X+ zU{ppdmL%(Gvb?A`|Gssn#Gawf)?W!SgSHaye*5b&ig7$)HNrtK7V#<>|L60M5G{Pi zOc7(RCH4b{q7}UdLFm!pv4U^p6I*>tud>S(B-)mEvdgr$cLlH3Gi1`i;mkUzg%9wy z%knJZfCKYzz8+SwA36LVdObU@SCNS>OUJZ)m?`m}PqdS9{@f1RV-rd+9Se${`IiWL z6?eh1Z;hWIPy$9Jff@Wq*A;)G-=3?2FrZW95@!i-lIt~v3)@E?G$bb>v)~E%v%Tog zcdPJ;ra`+_FrrO0(at~ua8G`i20b3jnP_OU+|S~Up*0%$O_qDfO2`Iuz)r&Oay~j8 zn}=}Y8JtzMmqfY#|6$||ZM`J5UXN$xa4Pifbv}S{^RIfwW*DBL78<04Ndw~015y$R z5M0&~K{2{j8!Ci|>rJiaA5K1cL^G(ELn*-%Jv`cv6;idCe)p=Wg7X$~mR1BR6rUy= zfEu&U00VfT|6X;lXQ1(h5&3Mu{4;2C2N}ovx@}IMXBS-8HEZe4SvwGL(xIk!jGEVk z+%7@o@Aoni(r4|H9%e+D)OKP!d?6t2JVL*TSAdL1qlhDT3ujD%C56g(w`MrgVzpV9 ziM828@x|VZ;?QTBl;u~BO;}O|cBCf3fCE>0*}F%@kS=5MZm%PP!IoFA)OQvDJMx7 z|A{_v!K>f7#Zd2Hc+`@J@`GoaO0q%(NYjCN-?z!qZg)Js2TDY16q=w>W9K*U zZ74HQ1t@a%cM=C3o6cU(NcbRM5)&;liQ*uQO_bX3ca)|bb)oxy%{{T#pZ zwsq2yb2w{6IW5(Z#Vl&?+6Bo0s<9$r2UxwX3C`84+p_Xl?-4z^-LqBZv;>td#%K`S z-s{&t&%vNs-XsSck<=L?)It9g)aH&J{#{jKJUIYxrb02y@MOeKSpmLT$VqCbPDrgN zW(x~}l6@bHyC=Z2El!5(cuIw-cK4wjf(mJJ!WCq+tOwdjW((gSh!20dmra2@=I3?v z07A*%W_m`D)!EB;){)Y=c`^5{n)Bn;~V@d?aHm)nPB<}g9)}Z)a#p~nY zOP!DvmVH0UvV({xpM$toerPU4i-0>b0f3^Km%lHBzIS`C5gMVr{xl1-gJ(DEYkY`( z(gJIHW42(9BV6-+BR8nHPuEQ%&pW;D<8(MGj8#A#qw@I6J3NRxEn4@mr#jG2=bxgjoGSdBsbA zSnfLqt{v1Ko!Puf?8ni13omAc^3RVf`tjouJXfKUh3I6PKVC0Ch6*>9AhRM%b429;sXC9rFKpjNt+|Od?6ah06ZNW6YL4 zx|xy%V&*z9cSW?+|BCBuk=&o?7EeBNdGYoVUhS_2>*aKLyYv4@pTGtJl|5`L! zm$D=9b?$!rc=j@(GeKZwC|bU;;&o(wzCg(Da@56+*O93OfA^v98|L=;aAix#|L6~Z z<^KHoL{8AZ(p@|lm0RIQNcks;*!pTYD^m9&#_I2K21&5P`&xYZxcDK?dFij&G3O3< z_~X{`|C+lx%G^CO|NO4aZehNEE$v$J*?F(4+4;IRy#}q}={wHU)!y0Y!z|eE46wfL zMUORarev2(go>Wnl1I9)X(V-Ek&jIJ2ZL9)?sQMBY*!EOtt$l0jGlY_8+$TU1;NX- z9&wXX-}7YGTv45knwdrniA~PnFoOCWn<5mL$39SJ4F`nV>*ID9ZxrXJf(*Z2yLU-S z0e6TIH(n7eZ~@o0a?8_Y2l|~^(ETRt_xEFcaMk{2KP~;WzkfE~?#v8wyGat+&%Apw#xq!OfOZ-e2gbbSB%ldckNNJA=LXmb`juPJYWYkcc8P*_H>KAl853 zx6_<9sJixB2mV*Bb;u>Lhz^nFq?aI9SHT#S6U?jIpZYCjBomW1{x=r>jvT6z$f$?n zrH|vEq3cVA9U@YmvaZr_+@4ph(r__Db?xlc&3f*CC!LVT810+eM0Az2M12P*??c6) z)2UR#Tot1v21$NEFhP?F_$?h-kOO;-OCNGuCk-Sw*T-XcZ9bsrc96~`Wy~1E8~@Aw z*zGOB+dC5IBtuW9HrvUTjAZnw03Df@+61^m{9DQAFj!9xF7jrH+=CNUVu@1(8yYgP zFSo)FAS<+bAx`k}c*+n3V24{%PW+)u5&9D|3=J|?`v2%$id)!dJ)2jihO<>&S>;|U zOgSDNz5iG*MI=F}$8bfVbll>fgvnEj31A7VekBDp?L#_kSg5?a5&~)g+nb4-41-MW z;|n!*X{3@k6NZ?g%H>+|K7<;y8Ck1iOL_ePGm&lD1QyX|5cib0)N>C{PF+S6T!=&9B9=#n2!i!i_KkW2n)} z3~2mdG9d+l_Zf;BPqw>DKn<8$IpQr-mYePn&gkC(!@+ zyyW|>i?0fV%8)~DnKuM28bty0WgC|lw(>5#du)Esduh%JZLT;D9RNeL07kBqG=)A= zT;sD9L-9V^FJt<;w*B1-Z9YJfHG4}O##m!8q`?UYCi6}IkKyfKKL#F^7mFuD!(V4M z=fOt{z8n*axRT0U0H!?A0k z!(}mdt={IFrz6j>!|;EC>LdkGQbw@5hyPSU6={%mOJ<2-4kw9CY4si6*j_* zb|xJ&X5daO;ac5DLFtIf2epm2^_XhIrLYJSSBnC`f~zTxfJq`@#Sv$QCi~+(nH7VN z*&e9;BZX0TRB8Sy0aLD#hUJm`<;SEv|qyQNhV;cFzmJL)**yxfpz?8VR z(myfr;d`Bhtcz>txh*CUBC^*A7EJ=5U`hagQMIu<;^RGZ(}m`3|%EdR_BA#dCRIt$E5M)}a zXDEtfB=Qj0`@v(q2Nq}ieM0x7L38Yp&2dMxo4p;+ zDxBCEowNnakOJ!`a3we&K?T3<2W!_^TSP$-<=kw=8%a_#LMyf2Pec&<-4F{a9+G7{ z@m_xaOJQ3Z%%bqM59!1K=1daJL;?+7*_xeGD=BYeMyEf>x$@t1J8$rl11^#6SDreb zHK}9}X=V<=1I%~-Z5;g9>Vq^I29zcw6=CYZb(+{~Y8X{jx;Gn+$18w+#g6U2SL$NQ zPR+-s)yL2v?WEGA`+1x#xBBkS?=|i#j`Aq8r1MccaeJ6~K_ri!2#M5f?uZceYmkn^ z$)f;aQ6w4S0t0YCQ6z9+exy>0s8xz0)YFIp4Io%qg_Qkg*yks@HIw_KbA#jjsl%Q& zQ#W_FpTq3W{QH++>^*(_De$ko?>)Z0HPL%-6w{hY=-$I)pbBOKnj3~p&?!(DDIw1R71s~XnP$%i~u77{7+j}RX zpiECNQ6>;aI>XQq;Vm%rG20}mI?0xC6ECX?tN{!K!Mn>v4L>c+k4+f27gY&eaCoaQwaNCjNyw!k#@} zoVhZHMPr;x2z-P96)y^fS^%S}^FIBp>^`RKu}dgB^VAYUsxiK~;iX^p_q2s^QpIN5 z8$N22b(GB0-*nXTt@#G8ySnQvw!ErNGChlPG~(YLbuwl>Qjye8yc^;U=H>Qu5_oZQ>v>^j6Bh?}cyF|nBa_%ZQ?y};&9~v!&3Jh-^&mBjqK#|Sxs~$@Wv{6b z5^KOrEp6uRZ!rT8t^R5@UgO?|BCoF?Yinf9M8+^LI-A|odV*CFQ>f zb6^FjU9Y;Hsj`m^XJdi91 z2&5i&fD&cKImhRDbQtxzRE!Y*qjJggxM~n4{!tgtTW|Ao%9OIdmF{L{gDu9{lXXcd_mU8WO~$tS;I$cO+{oa4#qM?3g0iG~jj$pRGLU zs>dwHe6Wh+YCZE|eUZH_-OdaA)pQ?ZfDoMN7%-nT*% z#qs%HSJu>}2Cc0eGQYZx&h|LB@UA=+_ zJ(k_SoeJEaOCm%+cR{cR;RC}OeeEx?8QcJ0x@ZAHO8_v&olv}K!**gS6zXcFXMZ{0 z`}LL(%vH`Hy$WSs3|H{i2DVDY+au;z)pAd=%CVS+d{jl92@dWC<1YteJ~J$%hX zksi-$p}*MHPS-p=yldf9U=W=9Dd(sv;;LPy|&ZwSx^ zYP=-lQh)d-)3EaGYxjoRkFEUQ1^d+~3Q=D8lm)%X(UD3{8OgI2767TMFgkqEpJ_V+c)P4-`8}#W3(KJW32Xy94g}< zvqw@tja8%n?da!03}L@dkJ=Yyh69TW&R)>KH>Xr5GB+_ zh%F-A1njy|5Mhs@7d`bsdPeD2x`1sy1XEw!0!E%8rp7^NIM*RM!$wQ}A@uAt@}Tva zs*`*OhNq}6Snz&~?4Ul3tE?4{Z%xRRleN+E&rQ|QfY3zI$P$f670@fYzr++KEG}*> z-Y**^bg=k^fG<^ z_Cxzh3$u@IT_ys4Ww#y6gR`wx;)B=IYr@KfN1&<&L^(sG`mVKGPr+h9KP<)k&R_j4 z3y-f)F(Pk>QOOu}&%lDxp!0lJGN0}M4{OmczrgpFic5lBE0w_7M@MCY-~WC?BBy$c z)2!nmyoX%y3904d0%BL8K!4h%W7Kg|S7OhdjxRzEr$=_HRp>=FgVcw*|7|kYIV{|E zRm?O0?-wFcs3Q;`=?_vM!I$WQ`VmGou zM4x}(Y`;t41xT%Yp8M7{WJYlmLv+pSZ>(C}fE2LY-u7M;x1um;mPv?`Iv#IbHc|O6 z4y9e&ds?Y}$Bh1dDc@5+Y|{iyr)eipIqWh(+k;L`Qi(b+iPO1xrYAT=5%GytIu`xw z>ksa0#WDFf`NbOl<%Yf4A{Y{;I6@t#!#w2xCyLZ9U}#b*1=wM_w#Zj_CNsV-GkQai zomZqYGOX8$1GZX62;^eAW;l@eL0SQe>(+rn{mT!d^@q$J%oQgo9 zRu208OLM&xxCh@@mo51{K1y$wYtr#q5SYLB>%<+?aX=r14TqM(yiL^9j+Vk-1^|B- z>HntstAZii6ZQB|rz!Y(-A^gVT>r(m<%A36brSUbp>X+m%V{Td;l57Miv8cLWBg$V zbgvo0?9AH-G6nn`k>`;g@uCnM!?I!RQr%K=?QEK@&AWts#g{;~ebpcrH0MeAfUX(r7PcDg3H&!qWolvF0S4ZGQ9RfG?*WU9jH>rZR4 zjEL&m`ZDjity#>8|)c7zBWRKaoc0Aj|A+>8XjF<&KUU}0Zqaj_S7uQ;#=qCMk&7~B) zv%bpwRbA4cPyL{*+DVxD@$GuJO#F@#&`Y)&lykctpGCg0z|A)4}IyZ7e}qbaVv7 z(6U7@$jrwh#^@hguf4@T9b_rWo3bsl&SwR5F((qic!ZHdBaB)knMm>xHm_RQMxNt_ zQw^NG?Ht$4$ax6qE1{YS8iA`OsZ#AG8QCyV*=J9%-tLKdI|uL}4-@&2$)cD^KM|Ji zs&u=9d22W3&d8u>?2UWB;wP+-9-=)i1oHTNh<{mr$5tGTTse(=I(10GI*_wRw4{_F z12FI5#esF5Y=z21LB58kiW0VW4BgjrJ2a)-|7MkP=xO-)=|5jjlWi&vW|Ml@sKhHo zRH%(dcWtHenn+KPEZwJ8fHyyhEs#1)vged>>N?UHF2>j6ucmACWmqY7Se^jnLo5L| zGs8FA+|X;YPmvC?nfF0??I}l{Zt|g`m0K{ec)0OCcmvo!t~b9NgiRZk`FqK_2@FV-uwA5@OyT7H_=C9Cc)IWp=p7u45Bw zCji`l5zY@>5tnRFc1(bQ=>p-yD$%*J0sdP64crosq87FXQ5qd$*!||&Q9%n;IkNSRhWt*rW;;jVmFt4GdC)}RLuE0v+g>{j3e@h*);ub;9Y~Hf(j5?B^MSHLo zG!}NuytqxU7%4RA*Lh4>B%Pzr<1!_zu+$b4^+J?D0vzq1hDBeXMAsOi6;`9Y5JO3N z)_@cmLHo7aZi%_1s6Es`-aos;qx4}8)0{Oy{{P)$PRP>YR}H-u@W>r?vOTVI+ z%%280S)Dm#Stb%P;2uz-kNV~K8JHzY0p_uHjEWz%D5t`7R){a&@_wbwMiB=+y=57r zW~G2>D7svCjTugNQPOQ39>JE$RUvDnsn3Ng`$h3(J>b@<8w$wCWNh>VIhA!T!~3?} zK@KH2l1YXYN`#p^h7EXPlN%hl2iYOhtt1Zh^^AOi`?UOqKv3UlFC>+81QCs-ijPc> zxy3Q-_>uwE%9U@HALwyo0KJ-5a+vuJdD>n{U_z-VyAHYoef0tMB|tp#9#K*AS-oVk zWB2V-0r|Yw-Cc!6uhz>T)X32rx__@pbBVT4UCz;eSh{mFR(?Q;Fc^c$$p|#gQg3c% zZ9W3?pdnrJ1uZ=@p~O!vm^+Q;gOck8D!3(a_0EOa%aPw%i_@CGvLZmJv4vKU0_Z%Fi$FL zh#P#58tmvWgg@U+pQDY@9f6@!7QB@?jwalZG@z08S0%?(H7ZnhjHUvE2c8BHXx{aZ z!`o$6AhDOw{ikCJA;^q0DM>Zzm#0R7gx^R+28pvSy#JT!ZKWw!oxpF^V+*|m2feU+ zd>f&uzI5G>I9<+jIg(2G8;)rsFkxdb={ZtxWrBZ#u8GP%h1TEAP4DqK5KH>DFlxR! zhtR;~P)Y+!a^9WOnJycBm7vJU``2AXFohH;<(I?6E9n?-?73G0%#3kH3@9WFNrjdO zhz3TrtXLh?jS(w4^>Mh(#}{0r-{n8xpJZAoRt}*91l^ub)Hck0SoOkIxf%bR(IX*s zfNZ9L8wK1@RHM*+(zuQlhr@9O_4Uzc()R`?*3`CxE{Cf3`F<2pI!@l6M$#}vlEPxa zgshj%2}?qZv~C-of9dq8pDkgefcS0#SoPBR8+BKoQ8I0-%h{!GY)>17_$dnpdQFlm z>6$Uu4rqqfKO)$4?Vg03=H?lAYP8$;4Z=uv&Ox_=e_%Uiz9Wb9>OpsL!qua_H1?s^$2~hd*KcT3cOGz~3@GLrlhlG>P$l#Uo|dX4RS~LnQ`E7_==}Jk zA06?V!ci;(k{o}fm~8tPhR%y{b2^u)UC!0IDmE^*WIylccGiA}Ah#x9%V&{e z1(I0Alz`A+ofDEzXsx`KXGva?t3z)ZzOFN&V8Y@s=dG}ndX2=84SA^-s}u07c?jeR z=r;^L6Y!B#{dIW2^nC)pS%@JJ-F`E?sc-gQzh8%|e5Ukz#C~ zOu(GDuJBg)qOp}HMlj`8Fo%MtF-&7%7<`V>+Bplm16~rI{Nwt`3=@aCDWpvvrw0) zkGgV7scKqNRB~X;bQ7@wg&$oIpn>;Rj>^Q3szPDPHJsXXtFdW7RruD%u(=Mj++*JI z*wQ-PxeB&#PX_kgi&N-Q0Uj%^^4))5PT9=OB6$Bt6ze*VWa}~Y*;8YCava4Dr89N; zg%OL-4XbRanm-wuvMd@^Y&U}fS!I`U|EP079+}^*w)WBC6ZkRF8#^aKHSS0=M?q7g z&7L7-yA<*Yy4OK*GaOX*9tR9v-GDt_k$v$DuV?zO+x)eNFjm{qVjy7xEe;Sq>pUzb zCRA(~KC#e2oq7c2*%06eH#0#>KZZi^U|^`zH*0Co!L7SP+WTPB=O!S_fLKeGAen{K z4G>6**}CoWo*JAB`lA}+Ok5aqtQR!>ymzeK5_7Dz#IOWJHbl3wv*6CX9S|6h=t7sV0W8wugjj;sUr4zZ~ z_-9~x7Dife7+Mtx96TMQ2H!~0#;xsqTJ%{SZxP&9T6mvD8i{7+08AP@XSua2FTFJi zQE$*r{Y~B1xe_sPGwnb!)C4Oo#S!_qwSH7|{O+l}%*L z4pIkR?fdmU4c$T;4Z#>X3E+@b;t(^s>!!t$-^Nd}?%ALEDqXBA!`nZ|Yk?j&MpLd- zBhdu+t!eHvwd3nv+CpRIDy@Z_f9{)(ZrE#jWJygaljhs+>iH$FAw_;zLn0!H!l*@T zA{S`BWoqofAgddGYBV+h3iWr)Q*6F|E z(GKbGI}6?dt6J8^I({H5UzGm zuRBGOXOO6yS%zhxF+)&-rXrEob1c6b;4|1L@MEPYqVqGT1wg(Mmn310z0fnDIdtFx8`EZ> zjwBsupc#Ti<$4^ncXqrIBitp(RHqam+58<3y37AhABi!s%ost%42j_IZENb zn-R8ld60+alWwL5grP|?pF@tycUJi@XY#!{SwNB#Q#ZzpS2tH6-9XoVxhCoBOj18) zU+|%<%VZ5P$PhE8Az|Kj(K?$gdTi(@UW4%|UE4#K?OL(5!+*k73I~YRq{0jL@wewx zoaWINeYyN$dk7QyR9ZCNmxJJ}xf=0+ zjXWa!`ui*eBf4f3YbDW;zQiK}DSgf%3jme%n=_{%B4EOTIAu_D7?ZAjq|M(G;H|%m zQ2Ll6DriEXXDAxB{<$gSck#LQ8nsNF(6E6=^btf%-_G`Xil56-Kw=#wMC1l+EUEn; zWU1}O+!rO>?sutsq1EkG2Lyq4Yj{m4VN|owJX5fw$E=n{^?Nx(!1tWH4qYdo#yXX0 zH4s<;d{}`t+0s?Jd_s3n6Ga0lJ!1;hmfM5jB+r$A%n@N%%wWK_dn$=f-1=n(T)K|6 zj?h6WxzG*{8m1PKN0^}mE;qXMy5Or#0Kv>O&YmT!uzam_o$2bkb2@?y4DOaOc?NVA zkGlOJBzpcF9F6mv5M`G7$ElOvTR;`FKC={?2{>I}){I~n`|}eQmNKqcbZyf@)=C#g zsS{J7K|3i<_M_E%6yP)j8_C~aQJH@#Oo)D9X73@6iR)X(?BV)HeC{MjN>h=*krKv5 z?%YLp2*B6S-#{^?5M3OWRd|k<6L#6s57|F zrx`p$&^f3MwXuK+MpiLsa2)>GKANacYAoK1Qx+#qxv;sU&;V24`Og#=d~Od~GZD1~ z4l1^}XObCo>QD&c_4(LOpA~NOEpcfXD?kmx8_W@3?(+#Cummp2?oI~x9a(^pUSjg-$>gID{S7-)rz z8f?m-Vc;NPu8`(Pn&baXltwA|0#PBwAlV!AgYDT$1s9yqk)^EuF; znhxi2CD&9_5X~&n5DSstqQ;VmP$eAstTQ|*>y0n4XK=Z(8Ie7HU~p>bEbq}w zb^7NGNEd0o=b9rK=AjJHuAGhA=j(9(olh&`ngEz*?Djtq9)hmbot4b@3nO1atrhp0 z=6lJ(84Tn8?L&m7F#$Z!F6F!W%y?ecFyk!nE_UN6q@s!&>t>SoS>Co?NSZ7_OX_@u_I+&wHxl|2RMJPQJ zNpLr-^qUk$3z&43oh{exep=%zAe?z;=s+AKl$F7{hhL*qNlnIXt-rUHhsdaZL7GBo zj$e_BEx&rFQ=i_+%6&A#JYfFcd6keSh<=NA%ueUZ?AF<749qBb!8OS(u6P1zup!M1 zIR0ypHy3%r#(>yWYWaJNvmMwI8z3n-;y0Y3n;FLi=P4E_+SpYBX60uIJu04L+ z=4@Ze3V^$qgbapHKx6_Iwn%%DVpHlqclI@ZHj}^NIm$b4SiFk{Mv;OQ^NK`nhruBN z9<|LExKkU=08QCoer}T222j@qhzI$SbH@P*HH|8`ck$RR2KR0y)JheU6U2tZJ%3Uz zU+1fJyzK84F<9a$+n1$;wgOhsZl#l73!8IhN8{uq}16<=1fd)70Znm6v3x&1M|tyu^vs3!K4My8Xo zUOCuZ5*VYrV>H^d!>f|iI9m!zYW_(<-vnMlyjW4h_>zU%bV>dbrFPXKYqq0zi(pWW zHvAsBqQ`u#)cs>~50)k-QaBX$KIZi2%FV^CqiL~Fh|$O9=#X%a{`?q3h@Mj)(+DfK zh#F#C$JwLlha8uqt(S~wO}eU%)>$CPAc(t%OiYnbLMK%;63(U;-18_2_^uRZ zj0nUdY0>)|E3fz50icKLm$D(_Pz>NJIGxL;KJnbH6*c__G85rzK+a`eJb-VPqcVJ^ zagSL9S0Y^540`ViP^EMi}gCrbR50{?HZ z2WtF(PIkOJw4a+hV6Bt3`qNGnoMvegOA@MND5!*$PFZbqtsiny-Js{I=|b5a-7Pj^ zN7k1=;xX>#2QFJ;Tb}en$2?C{);-_5@~rRYU2Lr%7puA-s?-$n=R92@)xQD|8$1(= z;~=sqUvss1$*giVlwpDh#?4eV@uvt>7T?>=%e~$cmyX=!YDlKky>#i<4!TRhW4aOm~_*ScfBXwF6yRE;PH`u?rlG1t_cs1KI> z=Kp}${%uMApQBdSyr8e9KnM0M%lZ15K>$#H$Uc7v!E1EnFT?_T7Q4rS<_p+|oSMAQ z6AaL)Z1=*C?J$QIh+7jpAL0;h!fuQZ_mzR`z?)X2X#o!$DEnIGZ$n%JuN6#21UM)^ z=zL>-@I85<7hxnm{DF9d#|~Y{K3bvfGz5hAgy5Oey{;5Wm50#KJ2iO4I@7Mp7w`xFPq5D}fRSf*x-@NxZEcgj$Z$%SeUae^9CKS;!-ag9m=c zvb~o3%2XtAb*wDsy0S^yA!#TTd|QbTqs5_l1Qu5Qh`Kx}i8{2b5Q1i)jgGlnTj8oU z>upXNq9{p|b0E?cG`0Pr2pa@;iMpHGHHZkOKCGy`oG>IF$m$}2@CD|n>jMk@Nu8ZD zwBF(!JIP><^t+z%9`5TQ=1qEUgwVsi@2FJ|(uR$vW>8N-WJ2uEftmAt>0kk3&(8Lq7u8uJO$mN>9C73lN~BpTxp>V|-sp=b&N4MWGeLXywx`S#!RK-5@br{Mbu8R?i z!rS8!uJj+jP76QHlQRaXav~^+e-C~&5maUuF`I^U<<4~bt541|j5f%OgVFC1liz#X zMR`U$oB2oJ4lN-EBS*EQ*#|u<@Cru?2^NcYvHP*G0#=0-48$DJe~;6MG3OK{v48B? zUHNvu+Pm}Qz+9~QAuzKNIy6UX4B**&_y=yHQn1rOxLIon1;kIn{P7jdv` z{tQ_roCx~OXwdNFHAS_WxjAAB|5z)hvy>~k&OF1_@v&5y%18+UO6^ETClf3^lmGkBgTf-*zC)Xk|Q6n#vsbouSYnI z0&`iMVq~4@iYBvV!%Y_-Hfh0(m>)QP&Cnw-ydA|0Q@-G{0l;S}M_Di9$$raY0`doU z2f$)yu#tNqM|muOd-Ora&5<6y2#38LJO$qMgo~e{W51{i91(FD^TaMX zC)QEh3@S0Us@uwXzen5}AaoEqAsDEFotp(3P^w){2Ds^`Z%gbJdkb_uE31&tBiK5^ z1X(^x60UcOm!LSh!HwUvDdhecN3;gn&LidWwHMfmg$p=0VyH~dkbPgr8a|zRTy}}A zM=KkBjM80DsMl|OA|3U!vvHkRI6)BIL9@e6#4+q-m%wu&@!^it_YmX&0zuIli1Mp_ zS47|Iy2kppDp=p`gZ(*SS#e(quhBy=H8YL?`! z(Urt1A?5}Y7RHsA^~fWcbh<1CF8%l2te z_~xATa(&+=uFL>a+BomFYEHLg{;t;-s7LIl`MWpCdcbr4Dx5vCoZrm~Eb}S_MX+)6 z4OEY+-nyu|wK2le*&?`9B2b${lL;La)Z$QERb-KIb>(DJ`dPf zu>6SRD&x+bkN+wkD_#w0b`kqN?$m3jzb8W=W|hsn>C6C3_pcmdLubPxaIdXl&oV9Ks40h9`YKkrB{0bI)4>gh~4Z?+h?>>8}G}nwTTJI zItVP~-!NxWN}@4N`9DvRv=q)bV^S>>=q6Gl<65KeJzwsl_@a2K7mRJtq=a1ng@Cn_ z?G?TD^1BzWK7R?BdO|2od1ld19kQjQnj|`cYjl*LE`xv5rucC)>Yrc~(_9VR~+L8*9mIO2OeekPG^|{<6AQV+@(9($EzX zJV?Q-A=MV(d#PY#*SzBYN&r+j{{`ub2-!_UJgiO35(8uaZrTLp_%Da9A3=CV4h*5L z+`O$2!?v2s2*X6cA#b$sauyY82ev04e(#bfCk%rj_iHX%t_;H$Zp2zdB7T3Q`eZlt z9?&+#>wfhd-IuRTu268NsjQ+?DymtADz;}(X{D#TNX4|>^A!8mqO}QLfpbX1mTaKW zlw|4n)WDhQ@|V#f=r5%yZtm^%a#k(>41t;|l1WImx~RBgox6deiU8#Zu;fVBvkK1r z$IMJP596UDzPY2Cp<^n7myrhZBq}TC+@xPoSG|)fMH5)h#V@##Ce$%z+^JoU0NqhJ z*(QD>h~JigP7HpxXZ{H@mVh@1t(Vl>D3TQ#Dww*P=7N2i$Kjw~1bvwMao+01|99MH zttNww%R%6BE#t(WX%yrA=5O%~$|9x!7J1Jis?34j@VteDk%NOS(&)ST34cHIc%N@6 z#WchTYSdCLxts%7{ZqZfw-*-g^LwL4#)XQFv#WZmP-EVf{ma&`bDaI19sVaU@)E9A z_E#Zl4*Vn(v#2aDhg@-ah|cP(TXOx)O~d<;Rxl)O3(%!Jl0Sd%fQ4z4k|bI=m{5`S zv(mIYr;SW)7{z?)eP4Bg?5Y0nvm(ip;d3A-BqUO%D#&QH7+%jmr0 zFCV3Px+`=dV|{59X(QCSe{eWR?{CA^Un8RYJw@g=EL&6GOn`goC7f_2x}(b=QMojU zv;wHK!pXEr8fu#E?iFy=sL`Hxyp{1~ZtT!AO(`NrCNz{oXnkgjQiFsV)UC-kTx8SQ6E*0;5mv5)rYbo~~0;(8VQAM*ktMn1q z#)*#4#mXT%x<`0p*jo!E7Me2X2^ZQ?+cMwG5*iQAU($N0LGP5kUU4&z1|0WJS)$?c%O$8kd>826-&( zp?p^f9x_EHVx^*$qd%y-@-u%*mF_&VN$)N_N1yH8&S&k!Vl!D5S#2hp=fDd${A~q6 zb1J~mme|bl?W|+{{u=VNelEHhxXZh{PexJx-7a>7W}r4152qNQSL%Fm-|YP@KjpWf zS}jOnMnjcUG9MDc$P#vU&VnTc4~07wmGA0~-<-<3gm{#7Wrp3r>VUzLa)uGH~IFLlE=6TMv7adTDFy_Nfd*G4T`_Vl{JT&poPPVKx2 zXXixC&_(Jj+t4nq*uh!I@zL1w^+OJ(>5I{HGcQAEen;cqoiWT>-<+pTYxSbeGaHFH z_C3jJvF%Cpd@e(GN88F){XT=V+g~C|UK_dhkxN|^_h**!%cbK`Z5+s<7Fi?vOFD{n zV(X=875q7x);R50LtjW86qdP=+_ClCMUPyc1=gu?->e4}b3u=zK-c=nrvrS8oS-8l zor*ag)VL=bzwleXE!9^-K&7T1!wo&bVlLoh~ERK|=#+o=N|EczfS; zix7!Lz12GC2L6?&>u_>bw0R}?@OYqjvXrY2Ij>S10DIY^JhyFLJur;TPvI|#44%pdEjpUAkIjwe1`(Y*~raQujAihMriPs9PuznPwY`5#ex?X5=r2__z;0!>R1i0bP zs#IX$`Ph5G{ez?PNzXV+79H8fH3jCOb&H)a& zz)&YYS;V+8%k{e`8yKOu;VnrY8*Mqa_J|b#+pPM;-`EHQE^9=#)HaPt`f&sP-3zlE z{`>>^h@o8wJMcX-A)4|)4a@rSb#UqNBZ7~ZVF@d7ny~CnB>2w>9bn6iHm$U}E-w$| zWd6NiFLUKG9tU$^x8;_zZ$Jn>k8hRTLYB1zGFbHlrAyOi3fzkCA%h?^`2n8x5eapy zi~EJtR(v9R%OD=U-^~%{+)?^Zy`m@glE|mZzlh2~{($O?d?Q>8No)9K0o@AkD%nbh zydDZY)&kweS0?c7NgBVVaF2K5yHbag#|N+EmI$4#kYST708rCy&+2od$W{s-k}roQ z!=aQBEkq_UfIeY4`N&THbs6~i)RoRypSup7?Ol=p_2EUVx+8_%{2!jqfvwW$?czGd$Pc_g?pJtu=yIO}#*0OW-=kk2Ys? z*Mx&jF>a$c4H0;H+lU?p&Dp+b)bZ~;?(CD@C>EkKmPyw!SGh6$eCvy7JWvmY4xE(fhCrIo@}oQ3`NX2RdO&oa(rb)9vU4SB<32KS&E3kZo3f&tdraftkILew z0u;YT1Tp5^Zol6A%W}=OhWXK(Qm;_9ZJ2NMz-*)Ro4_VafMk&dkPUhNNum9|K%XMV z-#|8O93vvnB?MBAx(xdLF#Cb8%jYc`#qNBjxZS7qTK6gNzBAj^lqLURH&8vAMf-7h z_O0%Gd&e``F7bMjEtYy5G+^e>4!r=N5cV>XOmdi+eaVRf0 zhj>4sK9yyM@Yf&tQsi)AN+$fnP@i$`NDEQym*bV+_3q9Cp-P$U$_kqCG6?zG_0r4vQ zC7`Siy{lFo8(;<1@*3**Mm&8QUyhwDm83sFUHgJR79+tw6TySMmyKXEkwAG@i^B@c zr7v;@l5g4vcwsl|~_#({$wBJ1%qj z5kPpKtAxS@6z>O{nE6rCCz~Sj#~BOlOx2<1HPk>(2Gan^0$b9m5I~#=W+aAjA?5$l z{r`>f{}1RdRSZtH&bpJhI?7&Eo8}RjBXBWs@wL9D1`9^s*?iBe zuY*GAZ8ILK$_s`NGnm=uFbi5E`v#cz|PhM zm*h@ArEkx7L&{}!V}s}2{^rIJ*{4yGy)C~wZPUb{8)RXF?nuSem0xWrkm97Sriq;- z6`QZ4^UHXIAW3jN;II>Oc4SMO&zPZnb>hl|ouEme3oYdGEDm{tYOgX2~@>i_T~fCBjpIoaoSe~yCf(ejZ6ogN8dy`)w~`qbXXTJA6LuZrY?XSEk%3K3C~AU` z44v9A$rHXF@15_rdRdG~1gsw)bqisA!ALrl!KT zhi}tTJ^h{A{^#SV=#RKfc>NIdx--c8@fz!vZ#{!oIFUPc07d>&Nunj`gvVeASmvx z0ta%eS);hfSShI|Foz#xx9%c1)v+%OxOIE$q0DMMIKQH;{73oO<0o@VG{LF4Vuwz_ z)IYn9W)Cr0K9R0q%$q;FR~7oeE!`Z|YyabN07C!4feRhNBgXI~zq(uly8in$kU#mA z7RX^@3L@%vTKuGGOGjOE`E9^ZEc>A#JJ%CDA>Q~2+MLPR;;72QN?%|^K=etLGK$t} z*35}4>J?33Ox#g@iE%;7RerZTUOo60iV2NyPK{bR`58%@w})J-r2Sm*!DJ&C&HSay z_)ZMIUTwR)2&}oAjvanW>J~o2OCde@o%6|YISP$b3%MI8?!O~{0lGUY^3#THIC;|& z&tzN+X1PGN2s2E2<-O2~9L(5Y=G{oNpHQ{^_PP_7#`qw!D`J9}wK)OZN_=yEoS&l- zB;{Nv7LOUafGJTSvX9luelEz0+;*Uiy`LvR22aJ!&e;y=d|GN=a1()kE!I*SGSe?n zjSUO-BVI~=C~J~|!rA|lyrDW#Ow0NG!J_o<451pEWDQxOSi5L~f7&{(PWAtC+jj>g zOZI#m1wlfJF??_P9l@F{!csX|o~ENnJqlVw7nFG2`@u*Af0zTy4>Jqljhyd*=2HQa zT-c=N#4}?;v6wh4Y!!z-KV5K!hm%uIR^UK(N<^D)x^j6{JCo!4eEmFf!p;G7{P&%U z@Da{DG7(k^ODj9Y(o-e5nqKU})ga3QbU0%0d+hI&sF;1Xld7Z=3t9OY%yjO|hubku zg<-Ih2U&%q2U;%*=V#*dN-L+FKX1a;713Vm#aM__lrXJrLbCkn5kbc@-!AhM`R9-q=-Af1D0M?PtrRUp(|h&iyjGzf ztPGRP;&5pk5rnfH6E{zqEo{$1=p5j6^)VOwR?asSOh<_&(GMwY3I#=Bq}75aqA76T z?)NbHT?I%NI61Ro7(#l2GaBQ68@1jY^ZOh2GRyZmH_6RT>yHli9bPghcc>(*PzS$Hx+C!n~_0{DO8 z%yT2X5lM$dvHPA2%sI`JJop6ax{d!%t-{3H#|%|5%`OupcIx-H-&3)w>zr`GAAMpL zc>1J*LP?K&6o?c7bR56m8OlRIG3b}!L(k2ytj_~P)otaYtd$5sa+A(Y0#EBWk_X*b z;a2pY4qu^=zbM2d84vQY;|G4*H2J=~gKT&($C)`Pw6A%wV&ar9XN)wz$rsiP8E@kY#J zJ-9xsDv#gf_d5zoxkFE8gREykh3^F3YU%9;Ua{Ryx;)QE%P(n3^oZM}M%1u`x$FVfK@faL3sIO$>j5)*elpqnaQWt>2<*Ur zKkCNP5)ASdoSzqD4_QP0t)mVdkO%K#czDM-7WxaZX}ibbmI;`~y}9f^&Z`iheXmJIs*RKdW{8%=vdBJh3Qu znn#UM1j|)Rv`U%I;%{yPm^4M`UZG7hxj&#V`nAyuuDPm>RJSL_IbBCP5Gg>4Ve45# z`>rl_w--vzHP-^0*&zR)4eF7n7g+VmpWzL`4YyoLZ3TW@HIZj*9I+YeNEm!di47&< zQMU`mdH-(jca7s4yFbHser?bB+Xxx}b!ukMNblM@$HwjR$|TGGOi7CJD_=Jj89XOb zwa+RE%rPFx*@Xc@r+txZc&}S+hjBI+9Za?hB!0P;di7s1*he^=+SKJ1; zyS1oEaM=zYlDyHc$=Y94c6q+mcJS4UBbchiebOYJCo#Ner8j6a@19N_+?}NBmW1OC-%W> zY)|w$snAYhevg^EF6wMHvU{bv$xj2fXWZxev&$vvj47u`DBRES!k8qg@*h}=IGy*L z?~uGXAShkR#(liF-K3?lmVZWMjV}RnGQ*vFv=UKOb+}-al8V{rsj8=^R`F6)?Wp*5 zB4D{$KM5%;h`NprS)4_oZfqKn^=~{IEqWV`i*&Fz$yJ7am9n_w@>*3gwc9s#g?3I7 z?dIY68hOL;=WRoDOSOK-+;}edo`%)YB#)pbtX_Ugys}M)kMyUNjoQUWyyO$UufgA>lfjK^fm+HIRpRyBSy%WxYb%Px zeP0Q-l&tvvPI3C(che?~*3gd~gQuvJ!xyC7;*?NoXn6YhQZuI}=Ysz{%(0?=AqhlJ zrG3jJ<%LxJh0ece`83si3}Q-E&sb7@J=QG|dK5yF%}p`V=(lc;P%p80L=9i^88kc> z{R{K<;42}^sN$*={lDWg8T=u{izH3jm%tm&`2D%8?y#1I;$=A&x&D6)vjdEfJ@6I= zdeL*&ov%4V+QBTH!8#cNbQXE9V|;&HxZ+$2e?iFAYSsKdMU1~h@#o{z`Q&9SLCm$} zP479!P4I4B10}C2&=q|0mTe>na-@NB%J7@KqQBhugah+|VUbX;+uZvZpiIHP5nT&h zArVntnQc@FGZ3cS@s;`@#c?`>Za1-0ttzinbz%kG;wf2iXHa&8SsNQJP~$0yemRC< zd!Lu$8sC}WI}^|<;MwMq4*LeVW9~Z5XMV}(FY?-Nct1DcrMFFEZQBN|Loww;P1ysr zkC~fgc9KV(98KE!LZ4$buUqp^1XPj_rtgKoLAyHjpybB($KJDO}3Fi0Vb0O!r=kC9b078h39`li3>lnsVt@#1WOhHm-{ zh1;>3Cdk0aNWKwQw98`YB9jO|y^nkkZoN0;_d>EXGZz^a1Nms2#_Qp<+yutAm5HY{ z!I;306^JMP?%-i7Lu1d6tCvCgthg08(-nb*z$7XqQq?ben{R>lu3qgciQ?T=Wx(`L zeNBHNAh)2`$s1@MA98te<&;`Z3Viw(i^R9RfpgZ2A;=yxL|)E03O(D8M$?%Ts^09? zG_Z(P26o&VO}sR$TZ!v0ZsEOOlwSw2rmiwR3h%7Z=gSHD61~@PvvBhr4J>Nn7VWxj zQMr<{&FlW#VyuK>K$-9iNzhpoJ_$Q25Gz%`0=BFCUYhZEzbfp@3QiLck546xS-8&# zKX-#2N)u8d5(P>MR^c;1e!I4fseJZ%6$(h~5wP%5iZ{(wlG5H`nm#M8el?LbmNHOK zpg(%+0lL|^S_hudE!|y6UDnFnn6^Kd0*D0`*HP#$Fdp!>aQ_}}_e|mtNI{tUI4f79iTyfy!5jZ7<$$3QBW&l-;P)A@^YpN6!$|} zvH5O{v8kv+Lv&(`mp1gD(e~;**jCwH^k94SU}`bQkk4H!#UnIAujx?r@dkEH#GmS@wP51dKBak# z#l(rU2x7P6Lo_LrJhPFJNn$gD#%y~O^~wbAPm(Hw%T9K%$S-P($>*7bVI=2POw0Gy z1bzM$mLQ2r$|?U0OQVwBg~fN%ZR-78Sl+^+mK z47gNKrCqzUQ0~L3Vqa(?z@18DX9nWw)jm($i(en z4L_IpQeWEp=%-=C1ga6g1Ee=w5+xHX;U74@s0Yg(Ja*7aq^jfn!^Fv~4A`44@I5iC z*hu^p`GNmUa7&=!4J8DR2y+^TNVF{W_*&Xc@A2v^kYZ)G8+6pC2BiY0tPUC0vFNxn z$^3Owk~HmZ&oHLbw0hP2UF9a_M2qkwm{r2ClvNhfSAz>Hb6@1A2}o_BMhj;VD5rQK zVlYym+&x0srnmM3C1cFcIr7i$pS#bj6KiAeXo8OlfllGNVbogt&hBjO?RA`y{Wvus1nFq`)zHJst+dfNl;`Ey(87|~5GwKH`WsmIAb-x>S@prk?=jr#^yFgTbqO)~9)DPMa zrjYA2vNt?f4-rr75FOI&;A?f%ZP)=D0%3#2JK0)6U@izd6t<#r>&%SS#o_!vI=%;y z3Jc?Of9&RTkjnUPjmf}{s#~JmGNJ{Tag+G8Ww^*axS%1g=r2|SF`}hg_&k8VFoKuw z>;OxUupRn7BnzqFAq0F{ikvDbJ;IH<&SvnWY`wT2{G69Z)EAE&n^V1@>A?nxKglKc z#c{i;kL@C!?Bv#IkWF>~3%^K9ybsRpcGZWZ2n9tUOx$C!`*hXVKCt48wvZi=4J*yn zn>?26;DkYR(9!-GMvY2Yn^--L%8>T*++yC``H|sS5pN^4pd9<))5wEC&!nh(Z=))2 z!k1-V?~K^08IbZgSD`C^@~O*hqWihzPjl)2?Yu}SoFw?U;@G96{EoLd{k5p81|l>x zrF5@ErAwZhlUFQ<`Rg_Om3XlRHXFwF;QItLc{sm-7#$VkPkZ!;#e($|Yt~)4 zn_|qucDJhhYCzNGy2w{9t%fD8jSiVxNO>`t(A{o_^M($AJS}K{p7$6ge|NQ%nw%{b zKqw#BirM`sL$KqllqQ-+lbeiTqx=C?%XxQTyRj5fh2k38({b~*Afu~{7me); z!+0?I@{}GODkgy;62hREzjeyp{B$UH!G7;&n_A$NNadAE$;GlGz)`WUykKd$Z`FN1 z&cpXv=o5cE+4CZ$G>n;D04JT2jA>1a4eqVF1YL!M8)@~~*^C+OtLB4(c&fnO z!LTB~-Dr=XZ^adGP2@>34H7=_i4Q<3*MD(=zw5ePI?!Vc3ybZ(V!txgQb)%N3RM^i zC#{(=&V^##K1~`57r?@%$;E<0SD_6Gb=wR^^462}ew60U`0~DmAqmtN`R#Y!t*!~A z*u1_3y{jeF4fkIMPAS;fdCCJOP4k z#ZLx9*-o<*;>~%Qn)}yCI%9i_T4*-7*KF6Zbm}c|)fq#$m~l0q@=2C}oxKwNrapYe z<=6r%mvZ`4+O#~mXm8j*F0My*v|b~78k%h3%{JGMr;RqnQ|oIZDI-Z=E1qv}8fjfc zHn#!HuaV&)fxOI>KJ|I#;b=J&zg~T8lJ#scNqm9*#)IeDksR;mM;Y70Y*O#+l!Mf; z_!LyRqo&zmcpv4OpNodDu`t+MTdc+W8#j{{9wEdU`X+R-NnSwtpuJx=|1TA|Pvg;= zu@de$e~W!FK#9dto`VkvclTJFSg7t_-wO6e^qPVJb=Z{s%H3tgPbHp>@-&!Pp0Uv7 za)yU>>okD~;Sn!@&EA2{M!P8mC~RCyn$?iw(q~h-!W-(_3TJh7W@|UUU8d>>R;T`i z{lqFL2`ZGb`u-flO znD2Q@oaM?|G5|f)H4m_mw%H8~+yVY2gKS*gQACKgL$|n=Pdt5U)D^$eq4*`Z<}h+LAS6R55KeCql^hwE_rm?drLp9b=Z!YfoHCY|?> zpx=MlK6?#0P|mD%IxOz~RQvE~6k!|i{2E!=rNbR}2ZHwgQLFLB_LSv*etOSEZd;xA z#=)G^U!>H=rRpOz3OJ5!b)ubfUjO4GSPuw#=RAv*3yS5Nl_|?pdiIcr4%fgS*P+%B z)i(nKK}H5^i~8VG=s3tJb{g@Ehbzy=;iBG0b^d#jafVx23BmPPm6M=GJTqAAs>)|Y zmW^RcVNV;w9aMP7dt^dI_jo(NrPY=jTSM70E&ncwz`NX3~YdOuk^0mCtBdhpQAZ(_YPziZqwo*%=&(*}S-a?aI=^Yc$pSHh+^%h$y*J}qE|+uSZk z`cyEN@9D08 zHG6mM`t~kBSG<$C4=$S2E0c;D+JYuEkQT2)S=Zj`y=(6kPO)AXg^{zG4jz+-GWQzxma+Uzp`wJLXtYhY zor_dlZ~00u&1U2VWl{fL#qIP__zVfue1N)iJXKQBr1~r5@Z5jmC#X7`f zg#esk>8R+TJ@W7Sbc7GA zo6y*pX_sCvF@AR(2H&(#mjb)9rS{XgH)y#S=MBvY8>7hA%!ZxvTXcl>=@0l(94D7B=#+ivnV<&lYpc!)1z;OJag43=aYG<*xwBOR=D(;ceoAQyI;Tl|1mOH~FY>ZD+7 z7)mqmBc1J*H+5P^v&K0#i;eT683Xja#Jc@~;W)d<2afjFm(i*)y7m))v+|Di2)hgi z7XT8js0cZdGh&pNoxAzWTZfYc-4g?bIlR9+o(u2^>BaxqkJd6P-^MOm`_9FD0=zu-6s<3UVj&m+DDz%XPL^$rfgd4h_CA z|F{(BAgPJCd_CL%sOc`o10_3rGY>ov({HZ$6+ez0!@gss8gr@Hci5a9Abfe0t0wXm z@K=zv2WE%6PfpiO#B+WS=SQBRj_opVlH&&ODY8@0a#AD}&Xt{{)?Y7BTLh{_e-Ty(gVbnG@jCO43$7R3DZr~gjSdr8oM ze=KM`?kk{bg15^Ic3WSyjWx-rplm*?mI*M2#-Hbu{oN_n&?&a`U!P+EW>%U&C|9@U ziI?zI^JRd?lJJZ<@b6D(74B&DpXeq#TrH(P*Yf|>jWxO*=MOg`hg&1c+~Q6s5{lMw zA#jGR$l?(TVb+h`z1gt>iTFgymx*}1}Jp*HD!(oYKS2~g3N9>UF5h7!4|r|}sI zTaZcYTjWw4TRLY~sMTBMv*C5K8Sr9nF>sA9u)^akk2p$kFDkGz(R zl6K$x^Sm#<7EJZqx_jK4V=NbQT-HRunqpL8-X|PMsQg6(In}jtSHRJU&6fpMzTHw0 z@W*qZP!8B4ClhApJ6rDABPyw;d$uzkc#3Nop^8EhDRB^Vo5f=)VPAmOw3| znJlwhGmWzz);7l2AJQ8s4M)P#rG_rr#nX6^Z1PT?4k2z0#h^NFm}7HVABkms{TYk1 zkx;!*PX6#7+$`k>AsVOXeM{{UnxK*P-xN9P5T7&KPNUpvY)QJo^!M9YY_zpF0;l4N zJh0MHwcAiN86kA;Loz{yzu|)pRa7C-Y$J??8E7JYN6vqxl@}Hk{-$|(7unw4%K8f0 z?JRmWyNc`f@^YBCxVrfIWDnT7im}9N=-7|UF(s?sNWt~AGMzj4!iF68b{^jWr3~^C z2p1-*<%3}X$}MC+kzBdE#FRaF6}f%`bd0a5m%LZ}@7}xWgdC_a+|87#q|&Hj(0@R% zAf*>~&>IlN*IF-RCr_gj_W$FNJQa0#F$?HLDr=nG_BxDcU=O%`jLi8%vZ>kvx<+_& z5ocnoU_Y7(wM5KqFew%N8UNO|0|k3|VZNSK6*HEwnTa2H#9AlB?ek}L;eBJ~*ufsa zDGJFx&awD|b20WW)EaK>^~tMTn7*YIJ8P^VGwP*7C4P6;@42g)c+1VjY;sTdB$Ssc z%!mpuk7Dl7YdItr_+7H-hhk)jk3Lfu0sAQTR4hk|)AmBd_8H)4b85+V|5X;bxh^p$ z$Qjg-*i@^svw*tGm;2IQbqT&>6rTf3m}^jr;qJ(=VEu+V7NdagRx%xb5hRS6S1y`Z zVkz#Af##zai6fBVVJS|evxdRUgE{%?LJB-)g%o0$G-i?2OTw|YJzq+F?`m$1cJ}Q& zg1Cl4_Hj3Jr+pjt{NDrY0~`6);%h!RG{~={0##v-!4^}!xq^=szpg8P%1wQTILb|D z{ED+k30f9mhW6(1C6iO$M0_pPcYYHBp05Hh+?h@jaESsg6Wh$o!VtorU)5+Z~YhV+YvhON-+WFF^yhK_>er!0ZxYt(VjK2N7 zj(rL;HhaA{F@2N3D5it$O_x(Nkh5aPo4tOxqfFDF$u3}e*)H%T90pyokwt1Di0<0) z$bDvLYO^)!FA77{9ORmYJ)*pUhj^MTXdS|?d0^Pt>G?M z-6yzd=O56JR$)PZr}KCwSG^*4xVp;~MQEF`qkcR7&8J34MClV9Il`7pOFvCYZ4r;Z ze2n469zahJ$+^i+n6O~nf=nnAsJL!)fw*UNIHWSeoa>yhuRih&_hNwivSCLMl}WWh z)J^=S^i0`Po?0_;S4;hOWzB|zY-D)3eKiqYt<84sGln?w6T3wO~hm z+N>=``>KyQK~9`$rprUBb?P#~9?`;GC?VR#&_3@=Tm6$8^j46#Q54!l+54AUajm<+ zh)Nd{t7IciIL z00GAJdZ+mf!6mV*4`LOIAj~Q>He9^NC~Q)v|SGGu@RW(q2l^ zpAX=2XZU{{rQm)EfuqO&hSY@bMc&9QY<3e^cLIn|l1OyK(qr+IK{DZbW-~C}hWwx- ztWbLN!2!P2eF*W+>mW`kaFl%2Bx;qdMC+#Uiz7KNek3)Jr$eON`V=TuPyS4tY+z}E zi{8RMo+woCR?u}@nLyi=htU&Bie963jXemAgji3kdyN!W^_R>kTfk32e~e)%9k*0D zboGKhfbvl3b}9Ks%y?lvCgC%*3Bvk2=Knl><#jo5DAM&Fb~W$3gqG*2EA zW3_g=i1}mOhp#yvaPqM=4e0l#f2=sSZQqKywT@9SDWkV3Lg%7|$*rEKDV{p8NlXbW zH}T6~?E4mLoEiu(`sS?TN(ky6d~{PfdZYL_4>!gN180KAq`#&dWR9i@ zn|js0oH=r1W4!fz>9F)cctv`qCrGxodTPp73sm}*s$mvrCq|_E@G>^h7(bXIXHx{a zA|Bko$i?NK*}G)wr5*QwJu7ZH%NfA?p=ft2ykrjGOXDopgPoTeKUaybeV71h?1W(_ zxEUV}NJe7IJ>@&@$0MujMi=Ms^KbVm%p=lMykT6#|0(EGV0 zb;>tU!6u$$n~pRuB(rhZ8^!Y%@N$_!X34FmSJ?K5+Yv0E1Q&UrrJN+5h#e`Z`LaGOt!Ff(>AA;qonw#P&NBv7JsUY7)P}|wWy!BsC zXRW8(TEn-T!4-E>wuWNXlRe$kWGO#4J(VtEi@7k50oum%PP|!Hwb5d( zbfdsgg!a|2vZt3^r)b}^UQ4+T(n2cL_;E~nN4|e^z-b${$K`IZ`2&{a5(jQMAMX^E z7%wmGUJ}jeo-vwZ(D8T>!U_|Y_x=gj&uRCv4RlAcRM<|&C6GZ$|GNrMxU0F&+ZZCyn0Rq3v=kXOPdvq-+_s$p5wv>cl zv1siPrajZ-NYpNRfR(#Pzjhn44`cA*1aRScNCmiPn#B0YQACrS+Mk>c=Pw66Rro}g zq1Xe&vo_*5+{XZ~-2iaZUST6(EY zE;QXs5wIRh69sG=CsjVt6#sgogP`SL6EL*6Da_VIQ15HFaill!9@^GcKm4pW4bRtE zTpKN8`%eX#f0=Kpw7B7+N+-~8ES9(09=!gZtCB(5g|l0RFcc<>0lk3L+fpRb#eg{T zf%GZ1^D}3^Vi0Whquskqi&5}t$zO5Dd=S466SN@otcXG}+li1&KX2JvhZCJhEBWxW z%HQ98st7W%`sbOP91lqq09#|D-(C?grh1D&9&gwkx^unSTX65gpP_Ci(hS~?>5+VE=WOiP zc?oVdL(#B;z(vd9B~KygPj%-n<@n#XGR9sk5)w-B+`Oo_s@ro65mSGR>}Z>v3*Yd@6eZ|mfc~miv9C15kNKW>RXVit8|HK>Vg>(u^!e4 zLw3`(+quEv& zP&MLzA%#(FGO!GzLJFibRlUZ1R5trrfTOt44Pb1~4XRlJj;B)5;ut~P#Kqi#&Zu{Plo)|HC>f)D#m?ft#r0LY_{S8NOH9D(<&amcCOHjy)k=BwKf{K+tK(%^Oxl; z!SIJuekxzdH(+i3wVE9v)XvQXr&QhN!eqH?aPK+uDYdl)wEKCgNk0=7XWyFYt!%z; zLU~zq8-aMDkkf7(o6Y?Ey-J#N{@sS5G8)Mf`2m#A=2Gp7vgMU28R>&W;F62`7NlG~ z@Y2?$bJoOpbE-q|)DGXb?9VT%68KNmTl{XI8TagLGSJxNcX1QV+Aln=#$O!mg;TJZ z(eEVU_Y8TC+KKLRDW-jGot8QE)3MF^%F=OZPk9CC_Zw1+iY_{6gUrMesG-7+hkqW9 z=m%#Z_SZ4bgOs)<^AeDh${yorh-iZ3X(Rg@vTF_v-WM_q!JJi}SA7OXNi(-l9KvRG$Je8P{UpGQH5(Y5P&Gkm)da#y}#K^eUGlO^~CY=IhThEL1m*{h>q8p36B!sJxU~| z*#6=n&&s(kc2TT?RUnfsQdK$&|5Gyt0DNXzy@1(YU?t0RS?M7mOq*Bq^`p}=^&#Pd zq<9*E!>2AyKw>;obKQ=N0$D+05Xa6T5_{+YwI}OoH*U8)zrC;Kk|yE8Z=4Ezq30L^ z12yLJ(F~r1yT)$<1m{nGZDh0akbGf0YdJkh-aizreczlVv+odY;7&kL^ZQ;10t=Pd zcepnx$haS&CyG_k&l-%!zJEh@VQ8IVql79mYV+tlE29C7qkOdvvcG?n3nq@Me^5A$ zN8PjpH}9Aj&DJcedr9Lj4dlN^vqGdoWO!2mfK`s9Si*HSO|_ z@il!>l77SCOeTbIy=gB)dOBld9PnG27aqsW#Kz1ZV-pYW&%{~%JAQKZ`V^&O?w|Lk z+Asi_spy`6-0%Q85l1bPbnH7LNA2_JvP0sI(`g)TR5HoL2{_j>(4V150^UDd(CqTE z(_%N)0h>QUm1q^hpr3nwFm9HQSVQm7jd><6h3zY;*qhxBXi}#22sEyOP_CFX%P{lxR(Kiq({LO<;2A z90Z^FQ6EcIK8w%0bx1cz{thQ?g2W@l_epFrk%Ti5dzag;F8ziGa`{3Eb_YWb+UHp4!L)+B<>ybp)R=WGykIE^NNjw zG4+pKwws@?Bex?okYdIZEctOAqD#&6cTzuZcR77VTbT5FX1Ola%(3}L(&m7(lSr3y zsJa_$^8=#eeAJc=#c+#TCh}JbfJZ>JpHrp(gm*Et-*5kTZH^`F4)LC>{_rtbnu^8v z#uT1JF;Ofn|D3wfv|6NtD4*EiG=>w@+KdH=_Y1VL?;(Vw^#Z@^@6Prjwgr>Ey+^L_ zvfqz5Oo={JeCL&=4azJN0oH>udjZ{BM$&%`vCB_R+mA4h;;LGg+9WRpR1J?{+ywq~ zNy|<(kFo_|77d!4_&X#Te-Ix9t21#d=;~K55Fif*#fjc`;i<%8<$Gs0t>SeXs)8N_ zZf-KtqQSQOn1MyUNnk|Z4Pl)bOS7SJL|s=Kx9sU~N?_wpwXllv#%*Au4f*>5$fm4# z@?okq#DmqiEs59d5>DfSo0?7z$u!+`4nQ?Err1`be0FgTgcL75hH;xDX8d~-eApy8 zXgkJ>-;K^(+Uu{MVzZpYjxqA}Xad-#$rxCFOp}4cdIMivA@i1GBYXS@UkYEk%*6$R zr%1}}Yd{B0WBdK^vmhG3j_8H5k{i9erm^R}9mvuyv-EU&tMrdNKz}y*3k8oyWBF)s z%gMu4y7@H9I8}Q}Vcd-7x0H>A!XAXZNgVE`R~~lf_rEW2OE49B@pw;u*q5p?H!oxz zD^~X&Efn6bWLrw`=R~zdt+&Kl0FT(^lGu=hZXN)JTxxGO9#GPiG%rH~+uv z!jCnz=|d3R&tAoD7%g9z!`0evdJXiN!uU-5^myRw7|^kI`&&47Z;!<;tXKA}Pec`B zJsjf!0JIIydadhV7o_rBZss1r<2P0M>UZ_~^x!YoC*aj-9%Ds*$irIa!smRQJdGDd z1=;t@dt+oMpWSOv?){zg+y6aNzYa>cClDA7wg7(Gau+UW>NxVRjiFmGZ$Qq^`~`>|@N1 zE&ZDI;2=H+F)iu3CCEArKyzjH?1a6Rv^zSE3j!+oPYLm68m&W3I1p33JN9z6sxc(5 zPU!*fj;lSdmwe`uLaFLyyDF0`G#tG5p8Xm*s8P6Qs?_;)z|m3d4y1dmc}u4^9$lPv zj+Bk_jgsQJ9G#}Op)z%xunu7KJgjGVKAZZlj7%F@HKR1vTAf}hwl#8u?pT7{dURAo zdU~^ienchGjPp^1h5nQpKoFAE=XxFYhvd_5w;ylM&YD-sc9PWXn;DjCAx+E;is6Or{Ng{Yf~ zSf*$pH}BAmPkX3|aed_Srsx{bYB>*H?}O<}5MMmt~U9m`V=# z2(X>C85;hb`_Z~QlTyW1`>RcqAhp9bh))I`b2y4nbUeAz z{Fslc#{1}IZ^H?NFV|YyuEe|TD-W+MMf5VksnaeIxbHzwc9S2#jW%@+;)UiEP2d~+j zQ*o}}o?g+hk6YR{6o?M9nR!Jo4|45W<1(J;IacgevMeK5u)U)8q2QmUa$+ zdrn^M;9B+@1TsIGWWg7XE?^y?9+BzTzI)f~w!&v7Vr?1VROWp!UJ3(hS?FTlUM?e? za=(wKnt;K=m)YRU6t)Ax8(90%0LZ6Ju34YkD~mT!z2`S&(t1JXbq zI#2-`N{%I$dqD_taRI!3GdQZrAK&_=DSA7?L^D3|WB+E2c%Fj$NiP2|;BN`sD2(b8 zag9l+5jw=LKxzD)4>XE2Oi%dAZ?Kr3dfo$jndsWGuYr;E!N-{SmOGLD=JcUnoeHQw z)V25m!0zBrCwf?}Is0FN&cmH>y-c!03Bh0a)$7JNmz~M-$oAiIt9RVh>dOHshm&Pp zu93~TSZckHlsf=KPsu11zbO9B@5J08lTuZ9)Q}}{0yGION7%AUn-p7#d*d@#ITVM- zHtoS|4)THPI^$D^D1z@>YJ>Qcz{c{Cd94E1zG|_w{~TXmg4+lG@1c!~9{$G8JVITH z)TaZwf=GPEU&+!c>No#(TTYM>UNzhNqOp{d#z6ufl>=;mN-}_;Tbm3+a)H_(L)Z*O zv&{NJ*^Ny;2? zublK6bk!io;7X0-NQ}cTO^+rvg#_JMEJmhCHpYjuQ1>dS+6cgOu?14 zys6!2Y<;$_-Y7`8#OGL=c!qR}Ubt%TK_RTeCVL`#VDF-Zz>fevP2ukfc@XC+I%rou z&Enu*#Lh9W((IV;$rh-LB)vtNK7WmF{z4aU;C1^QouYMk!d9iIO9k(aNeZ74T}F}F8MxBhX#=R{a{MALuo2@MGL zDBMe!Dm^^HRM@o_xHHYwEU(^B-CZ{?eY{;Bf;OV|T{H%Vj?HnURcN(IpQZ^ch;?2R z@5asCY}JZ_09EC{y|q^!Sq2a>ASY#%J)n#OzlFpV;TW9|xL9g=bxP6YwY@D|WLmiY zy1#w#;(YsKzo@B7gy-BTOY})DaQ4l{?!-TjeOrbZalxkxLz;Wv@1UmTD0)_~r>0jmpOrr=PfrhtGmp^F*`E{TBejKs~<=mit~(^zs+>3zg5W3*|p+sd-&R z<)ePSqj8d{-)`7w?pIWA+Q_fR@sKi?92Qy)-@8A>G>*PBuFWF4B6_}KIGi2Y+!3)| zuN&=}XOuh82m;^o!w~)}8^Rwx6{5>YLzEKV3#G_E)#i^w8Pz`=D*xV3L(dD0@-v%+ zUYuY&|L*lr`mRy_3M1y^??(Q4#&>q(yRKxo^pk)DB;ecv;xC9#K4&;4Mtlpw5x!(@ z23cMG`t|G8uUD@@y?XV6|JDno^;@)PQLjw~Rk{PQhl z{I9po_;i3LftbTxmzeR9z%gAzfRFN;@m~wgcz&K4-!ajQUjgv%TxPsKaH#lGwizFz zoAE0F-as?soxoY3-#9YgwDheR^KTt4K!2iBZZdngPWs#YLXB2tTy0{;kENOMAAwEL z$DaWDg8u8X613-8GyXm3j{^Yz0rbywfakl-`0*RecrAce2l#5x-*a-!_>z%kd{a*| z-o3LK-`>>RANxUnoT^jPgtrEcA#ZpR;0MeZ5F5<+y35UYyZUDQL?ttBKMdOUp&363 zVr9P!aQ-nce!<=EzK@vkjt`shEHFRoEH>j$gFVj#fbY29jL!y+C_e^wRgRJ7{-_RA zGKWuqL+SBmdZlbI|2zT&aQYq8PXqOP6F8n83h+CDgXGLx%y_p^X8cPq9)}M%?$dJpYq#ygueD7K@QgYO+?`MPKm7@u#M@x^bN zap&V^`~;ZaYb8KGC*TKTz4=~ys8DBSx!9tJmc`@&jMic{VtdvwSjLR63hn`98+Bn@aq6R5X=W2 z;2(qW_Zq-ovA|yj_=^C4!2#tJGrkJoD=lz%c5njFcP_wzIA&XQgPDOm34FNi1@cc8 za9*N<{fi9n5dg2>-Hi7H{JT5Albc6h|4sw^JAfYo_&y8#jX3d_0q$=M-vQ2ZUIj0ChCj-1Tz*Vqd z{R8Bkmov=kbq3(aX>XbPg9P*A6Tr|4Kn&9pV17Ia=0klT{~rPTW)%<^GJt2aLU6U@ z?pk2K3FzGt;PU{zYXE#MpyviaPX*v-fT_w+AWx10^QA4oo25pdA5DS&;MXg_{000Y z1GGm2^=%IDpMik#sf7TO3j~bOfWQ6*+P?s_7shQ90sb@KC))wO4DgpCfKLH@Yw*r8Q=#%`$qtr1b8!m zXF&M|tgr6?yf?rFfZql5HFp8}{|M;04B$@#d^Mo&RDh2McuRoa3FtlAV!WOK^nD%R z62O}Qdp}eo(1r&fSzvwd>Fu60=%ON7!&wBv=J)oZ( z;FAE}7vNn1-UZ+<0QxMopl33`e+2Z|4Dba2cL97Hzy|^R2B1%ZZsnf@{m~ut#~~oE z-vs!(3i?9>^W$;Q9}fb&D!{J+^d$g(5{&0N0lo>)ceMrmp9l0>4e(n4J_+Dg0lYiF zn*e-@MSm0leMm1DcNyT&Wj(w~Rse8tyc6cOA3AD=@g#d&;9z@MY^ED4-VNYgEpVWo z4!as`g(Gzh>W^lL{K1#1^6QX zp9}Dt06rGrLjXP);C%qz0^nHyKLF?pUF2*B{qbJ|pRj+f?Xe{;J32l`=M$*^`}~k` z^nkBMvARwAW%5f;wq%>2?mN1+#Y?Gek*IM_QIC#aCxyk3dV_RuIqU{M?*jOi#P~>zkAKnl_z2)I{xft11J6rgs0|=ahn<%~Kk3yh zaCnn1%K|U&`D%s*ZgpM?Lz}?!Q+P}74IqI07gXqu%F*`&^8mgV$ot&@UIXA&E%Xn6 z1AYKq_k9EKF9ALQ;EMr2cmUw{0{m8hy8%7{;B^3A9pL{3`;qo`^ZY~RUrX=(eO0|f z9UpCN;2l1zwNYsIMKf^xW_u0v-~FKf9t8a-f&sAs(EDM4UkmWg0B>MnU-BoQ|IYyb z8sKXI&H%g$z@a_LOpE@SXwiSeE&6juoc#y)n}Yy-VBiA|;8g*>2IL3K&uzdw&NdI= zz`WBo1K`sE4((ZTE%-GsAGZN@hfT5Ak6jz*ehco0!11b0w4gtKp6NgH5~qK!3rv4t zo@-lc(I4~Tq<6jry}@-0o2#7k2j-Htp%(P+QBL{;^XRa^T{SG^2P-_;0(bnM;r0am zFDm-a1rz)v*boc``g8%{JHZBq0UfXa$iq&6Ujy)8KnJ}G@HgRkCctL^JR9H>06qrb znHKWf8X#{i1Na>PuLv(3}%0H1vUxW`>%#wt{$qXxTx;WsAzXj>dXOhrGmL|4}g(hWuzexqsFAtauJ6N9wRt;(es_8s1vMCNE9c_3w93|x-G9<94u{idh{V;x83Kv z`r%o#`8HVBI3z*v3@Zu*EpRl$vd}foW2;V|t^1j(xFQz#`gX1~eN=CB4`NzL5v2kb zGOEhz3eN>&0u~1@+`qrp_wC+XUa2xz%dCQd){F5u-doWkU-cGz%W3V};AuZO-&K%* z*1u1-lVxO;)KpnjDIpln5Bl3lvRQS+L%u*@*GqF>!gmDEG9t-}6eI8yEr_rqwitcg z23y2p-_vQIZ+3UYyFsFfpd%HAVi}fa2?`d`k99;;@st#?#CKKwf*1EJiWYZKVkwdr zSb`y04thYU`Kdi(8eY;PzH4%NU;p2A(If*eV3(BfxbL>y)yr<5 zjreCFo_~tGtcZ%Ls)9-^x6K%|{rAE&$lzOFShf>ZJ`QhP#(FoZ;QBU-ake^aPx#tx z9q<%c1?fWCCWm!Y9z`%tEzjlcqvdfwuESb`&Hnh*(sAYcjKR<@E~R{G|*qfd{2qc%_@1y}xjZK~5P zXrx4OA}`6Z)_Uuxn}1zV8y>}2O6DcSz3iJ=;O9Om4m2Gx8n&oOBqfR}r?KMMK-237 z?r-};)0tUmfNNW&I4+NuA7AxV+4$aLi^&*72aO;Ffo5e!(RiHb(Eb8XllC#i+A(+3I$yGW!-#5&(LidJ6hU;LM5oKsH?Pn;eW1&$xux94Up+eh!Qn#X>(3Fr ztDa+ahoG{>giEQ2e z^E~%c34?yyw|5pd{Ej9~BCH3+44M{on$vV%Q5oVagZ}P1|L(dg78jq{ud_(Q%iZ9! z|901*Gdj{X6h7CcU_2&cp2sTerdW(~N1Wp#q4P+<`2_r?^F!az;ZxS%)V^i7+=paTL-=A? z=h<)(L)``50;M3=Q}o2M-~O_qb_N>c_n!*+S4mo7-rH?+SV@3ed^JbU-E_J~^HO3g zkO1A9;+*PIy*W;epjbxWbW+xc1-pmu%t^16g$k_QJXWD|tM6pZL9?#(Mmrv26;cxw zRiso!fyYBuC%YvjZTGF}QLE33k3=g;l}VD2B!(lju#)P;T+*jLb&fadt712MM7X9W zk6{I;E3dG??H#Up-O~pZsFQOw&ysD=?l`l?lYwqT9V@3C4R8KzEhEE2-@z0Iu-@fu z2X5XZ*>?JTKRs9yCcfUHRSuBP!+BeX4y?Q0KGEW9NC-{cqdk+b^+l z5=xc?Bp@+Q5)fbiJI?X=uqVcGqJ0TSKmrnwfdAvI{N{~gDph_B>86(1insEm&hj;# zlsStCIng)%t4%q+1$Jbtq7_03s4Kmr)C=4L@_R|y{Uy)#G4<2?t{7N+v2MvGhbgsz=iw!P=^Og- zD@C=p7yII(!%EI$c*2<*wEGz9)^yjjYw{)c^v0V{&D_@EtF&>(oy3+GNM?jF1ky26 z0lojyC2~~UH^%o`U7-3^spt%2?3e$>Z*qa^mwtixd82px1QzL`s(ChbXn)I@FMdVf^*7~UAbzX>G}O! zxn9lPFMq+#%IxCKf25;;!^sxu`6i~r_O-7Qxwy*Xx z>m#_<;g)pJ2W~15676vck*FSD`)+@0)6TZ9p;{b>-~pUrD5vh$v>B3|tBssqsClR< zPtKXu_qWt(7%FC1h$PTiSBLA&>Gl|@37yL(m+?CUNM-+vksbm#u04O1Q&zVU)ouWE zZG(AlkVSnV{1RimlH(L9?O>Eu)K(|4B|2ZM{##$;vErZ=T~Wx7h2eZx&v)uN&u|Jy zF|1DZeRr_qioEwTtHwE2>YwwreDAw<%m>$ue5)mX4Kal?(*&9p2}0)-;@F=0Uw)a_ zzf!dds#J1?H}0tK;TI=wUw=9Bz_>7StPTC_y6EIYkx@ln(K%5|Ytte9^x&;k(fQJI z4a!%i@_rvHJX1He->u%m<<~IIi$Ood$dzEexX6bN+cDpw%eI~RX99%=YbNE9!l-T^TQxN5=bIf z8u`>rHNXFgLE)y2Bi9T4eoIYrii69+n5bdke*p&YI;I>5tK*y_e_`@zRXx6Z(3BpD zv2Du496~+@+D};E5Z*~LzsDrj=8FpYgL7#6**}|2>3LV)C)b{okq|JdTVPF}{2u!e z-x>I*Yb?8V)MqGiCt+YQb`Z|Z!!N$K?@@13 z$D^H1#K@~UrRb_kt8y?FN~9oJ$uXw51->oVquDX<+ifR&ZC?2HrQ-t-DTbEp#7ca0 zD4fRfq$~&;N8R@HwVQr_^x;an@xj4#bDDqzBp@DH`lN4Mpyj8}Zi>o{Bwo;DLZK8@ z3k!3&s0S|VKYSUF|Mb=81CZdarmR?y; zHLYSjmKV-fzIdtmwC{`Y-P(5I&^B+aE%HvNz};6;a-6(KP@K*RoJPqF_s%4olRi&^ z(oy#^npj$W*DN`{-*HaiPy255jeGxxwZ)+n>``~N49yCXtdoSqaMyZ2INJZfv9!w& z{R{~&!(m>SmK#~3CH%|?bJaYcgFZLjpK0W?%=iv1!*jt3C>m*R5Vi22k$#Gi6c46c z99+X;v-|y>-kG=mrUv1HMk!T^OsXuU%8D#Y#H-)dt#<9;UN#iRt_Z}CSk}n={Kn?- z)eRea!++!C-?u+ACwka$QQ~Au3tXy|C{5u^-^#-3=M961dysj|O))mP^J^mIOr~9|x-nd@RfoPX{ zeK?`=SeA6^lZ|NFLtTYht}9>j*!tYPGNs4+0C z?6yq*%in)n{knVcjv5I;pgDq|7?BfMGR#rqJ+ngToLRW(UBmy_k1pY_xoCP1jmV=_ zR#XX=BXvm=r8oL+T0VYDRVY)~<7lScBuO>&r{-jR)A5aHJiK)DQpm$^G}e44NMUOm z`m27u?$3Ssh@26_q{;$Csw_hilA>-~@TfR(tS1dM^%WyUTE4mf4<~P9|CMiYcTR1L z$a`z##ag6fUgdS3qxAKIvX(S!a(yz|Ro`v2k~5~uGb!fh6*20}0kfL;JNjPe&^|Rn zpwtpFX^c)Nnk>nb#=|gKZyM`Zfswa<%X?tGe!VUqu>E3T+roG$lAkdiW8V z$KxuN*u2U45{n1!aE-QNSybl9qOp#)SbrteykAhg&muPjw)72j(Nnk_D@;&_J1De zgl1V$#92lu6ekk|O%n={df7Km=vPch$pU5cHqsf%=555}%*gL`DYp`!qrXnAk2ald z*C3qvm8!eIQkuXjG$RON>h`g->aAE@H7Q2=mTVm;|D)~3&ZT!XkEWw6J+o1~<{Hh1 zefQbQ%hJ#cx0=xqw~E?4xN_vRgz}&0{HWF@zjg$Uov%%Gx(QJwC`wRyUE*4A9d+}s zD{5yTdQ33V|HRxEjQ{2l4Q7wc!>+d^L6Iy;@-)lQ49#sMX>(#akF0} zXIBp2MW}gBl4szqp$oRQod46SmaH05JaT$O967qcvYf)}ny4u?cbsxaf4*8wp)p2g z&!@#Yf_Q(8`v3fz{R~nM4UNp@@}xp1!z#MSa+0Em{6iCe7_z3$qGX(pkFA5F;}r$C za>`qNeg#>Iz8>3VF&sZ{wwx=^0E7c=q!>JbV{Z;T4psJN{Cj+C?#ce_b>1D z=CeXkR9RGzRZV9Uo>l}{l5;|8#5BC59{$1I#!=76&BJA#;%IaX<8UQz+Rx+e%o^cM$?VIQF24&OS0!ba^ z9qQJ8XgV;<)8Czma?AwNCdppD8aixO``=vFedAvn@e3|bv82E<0xbx9gkT3-!8Cipn9#hl$$F{%A~%!t|?xR)2(?5a;Ipcq}<6`Cl^f(`{u=+ z$7_G?NZ17?;9`KpjjKfeCg}0+slOvm1&iY%Ymeg`<$tmFdx^v~%)f5T&ySo)ZVcDG?CLaLMH_g?t&U)^B(Sh_|=D7bh{~f-nz(%x~Ne+p(rr#Sv7EZkMMuEWOK@D z70F>gXGGY%$*RCGl&ok1!Mw2LzRpWN9GE%79$A=1xRn`5k_8^?d*|BRBmoKde}=@o zNm-n46 z&)~^@@j4=H~5TL;`w9q=}BSN1D`MF2J9OD<5jElaaQwxDu}Szn9hIzT>xJmGHE{F&xi}6vL1# z&xo)HI^AktEcqwj{o{ycqoSYS=>(&Tx<)b_FGC{-rz5r@kYtnnv!*Y*u}^a2@V=NT zqXQ3?b}CZ-Ajzxb^vx^uPKW(b8I!&2fB46pH@`;%?HpSnbe^XvM$q}Nb8HTihBv&>Po^Jx;7DgQw@aNe=BjQd zNt4bz$*Z|tQm$);=FHZ-xBQ(&y+?ot?|0dlVOUoQR8qKyZ!sFs@r%_cRa5oIiBG-O5`}67x*wX-p%vj^+l7I_7<_OU>(}TUvlNHT z>wm1)psUy17hS>1F)YKWG|5pK4aW`c6x!>5`??k#lAE-|%TA@O8F*P26^0U7jU>}s zr>z2k%&Wyt+^& z86z>`B*P^j>=)Nh@_Z!j>9(7pWIIWc=Ts@V|{^V1&&}vLF8$kWgu&4YFLt-z!WjlKYdg7 zs1tvpQFL!OW5{Dk#;N2=9?wvBfw#ary-*u0KtFiIL>tXA!e zlYx20@4bM(352i2*cfZZ^S6x@x@LiRc8oT`=)gj}L@h4xS37RaGqIo<8?tTSsd#yJ+T{qx>J++)G3aZ;}{x!LSA0PG# za+!#^YVIk>)111hvJ5X1vZ9l(d#{>b`ldNf6t|lC=lYb`exCBh^NKtDH~i;|=fABL zRl6YY3M23gMUe^t<&vkc!scxMquYi&wCj}y;Tn@tQ-PN#Qqy@|;dye{x~_wQ9wE^> ze?@ZHhK=2Jm%qzfHkN(>yFRlrr_mD4s-mogtUisjr;nP zKv9Iq2s9zHO-h(`6hpv3~be8OJ*O4$mU}{>KZteA!b#_h0Tc zZ6A%BB7&@GI;V?_LMvyz+1%{-tVR1bmsd&$on&>YAFq{wm6w~6(98k<;hWbiS&@bv zV~P-Tc1jWoCrR)E35!t%;m6ow|HEIsx5&Ez$c4jD=6gYtp1d=33nuW4+3#07IF-1J-@37DzelWfrUIA$=pPc9%STI#{YeC^F zUmf9(5_0sz=Yav7{6fo)a`)B{huA-8Nn^WjLT`B3bfOCuG6QWTDoReHkEwa?$w zd1@8(K_kwbUh04b-%I|LN&nn5pi|3e>yj$Vs;F?9Ebzsn+o?XmUtaaqpHhn#Ck>B-|WMu7E|v= zKaf#HLM2s(r*%n#2QsNKbEI|tLsxWIQM-3^4WRC)XRqM)+5aWn(r$apzah}KsIYmnaKWlrAf-gcsZEm<@7aq^LnXJA z=UK3OxX^8R-~EV4-zX;QFPpSBq2E6TCPi*qV?CL!l7=>!%yVDs#d$6LWs&Xu? zs;t6tVLR$O?6EPwlHxw`FPiXs!~43n#&@}~<)LZkK$q^-)P8O$PjeOq$C#=<(nW{O`Px z_xmfJsJb$OBvp-;C6!~s{4G3>?V7#*dD;f&P=5>uvoxtmqDIm*$H=&YS>FX(9DEqu zSH}7^D`P#H#mvR``LlAWu?PC1KHV9qPbHa=bcWYOism=XoN(>K`$lF$kNm@oe5)8~ zvcaT}+srtECT_pR z2aSv|^!wh5-3&S`ez#Zfb4Q%xBG%2HJ;rugohLdfXxzr?RxL3f5)jL3ebmqPUfKBC zr;zfm8BQzj_Bu65rdU>V@p-&>iEQ(ufA1Gh-4=BfT7>NG3p^&yod5XfT`bPAXAE%e9(JnC^j_8N9Un08tK&lZwH>k z?=O7~>CgV(*T1mV_GC-E!lkrrt}2V9#eb59uLi!fJ`D}`kw$*w+L>zr z@$R68RWW43ov>Do@q1et-$U)F8smE-FOuPi*I^v@5A5D^tDJ=F_i}K$(o)ywhPn$T zYKqtC)^3=rdAuX0=g9@R9wWcq=H*ILq!iHs}Ov4J!&nh20r?dw;F2Cgy zjzbjK7Euw$s1nJkDk(CeGI%EU=e)UeYMnCHEwh~8r2=8H{G&4|FzUlr*B#u6LTpUK zt`-#G96UKUH#h~a35qEmrzb}$)I4I&@`r*B@H2{)ie`uxkG1X&TUuaxqwh!iylCp* znjV3HP<%*v+0QdjQdP|zbiMgT$tm&i^HVgfJ zu~Jq%Z`!8xK+W&PFIWOKx0Fu~bhn>WX;GnQg=YvkJLkc1)iaM5D>4VCno|T;VSMmM zy%~XnUmYswwJ54tmZ}r7Dv*jy>KZi5>W7WXSt~GmfSuZN1J)#l(+Qps7)jDK8X7&u zdU>>Dds92mt-+qlx8)&yaVg`%Qnqax!3qSW%eqqP213_g#(y)qR2=BwE39`%!x-99OP|}13Q{P@ptz40W==UgTPHncx_(Hq6 zYVand3{D<%!2T2Z?O6@*1puE1@Yw*rJx+Wkz&!wW1H1s>Fzzb6*`NX(np;D=rI7&d z2k_ni2Z6$iUF$e(bpmhQ_xIi3T#jbuvXZmGlB-6nBC-S{lfih-zbt=g9@T1Y<}l-f zWyC_>XLM-o9Bc@9Sr+!e6Y3Z_VAIp4=qq+Z~AXV>|i z8zNnErHEJ4bebnQT2**bR!92o-YnJVk~Y4KZK)^+vi}xqu#44>0Oy*(Ve$UIJv3B_ z+}$uRch-K}GlS4|h-!|A+$4A%g6CODBs5jmSt0HC{Nq=i?3jKvw zvVHbB#wl$fYL^I0>d+$LP>$xRV zL}$aL?VC@9F43Gaw&M3334PJ2W~fSUZkErcK6l8t_|EqtUx0)8qwmoBFke(YvUElH zqk5ovSueP41CH0-)Bb!NsT)h(P*U@pEPK`n`S{Ai`D?#hS{WJ0{_cpJK7~o~#diqY z@^bI3w1Up{4@BNQXj3%zbSFpBJk3g+Oi)CxtJ{6`#I)-&t;{oKA*-S4(SM&6XQ2B< zWt7Pl#Tbm)ZtE0q-t_87&4qZ*+HlfD3bd-_xxBq{3zSKVdw=w@(wxph4eM!WiYoyj z>96)+f9%+gF^?uG=h?6pUd=Ny$6YWvJLgI*KZqB&_uOf}zwv_2S)DJ^oo`dj z^Clp*#rR&V-{JB(e?3bDu6pm+t6FzP(#Cp6RB1z|NkI}+UZyER4%E*3=d$@JBNJ+r z1YDAAjSh^SH*?EJw=}{J{yduFoq3u!r$8O9c_!z2Kh^K+b?3q3RYoQV`F{x&b>;bd z>p)6IJrnr)KUD|aK0CV0Z&H>CjwUFP(510g4*2cml!0j|kH0FJYda&xI@}JvYYSv) z$hl_}*I5XB(Q(%s>xW)ac7H9mP;@ z>e-|0?o^7GRheWsnv>TJp7s65i|41IAT9R-YnYi?X)Slu4R8z#Ebphf2yMU#-qFz8rlji8)8pB;GjWHVeG0X}slK z>`;0rl)j*xWa}0fu&sah)DN2C_Y&x=MCTMq3m%n7n%eZoyIy#7f@DX!<`-fV^Ij;Y zI9S&2(RaA-W6ihi1?s@IG3q_K1zO&HV?)<%(Y>b0Ee!|4Hx9L{c~;9)7{c5!LDr$ zaBxvE%uuwp1rB{Xr(Ql-DS_JWx@Gy z&nuOYw9rd+uyP^Erh%U++MsAb{dhh6zDual;^cAdYd z`)etGr>Bjsiq!>h$=J9$@W#M9cD5N@BYcx+Dw7o9p76J{$Yqw~Jut9~=-l~nyq{Q_ zrzldO8Jgu;l4MBe0OF)Ee#lf@5rYCFj;8f&QYD6Xx`IlJ3`3A2B`~;nx`)fygNG|< zBpk-zhvO`-qSU$e7@3&eHaPIr=&XS+tZa@~kOVtos!R3eIBAhrD2A69LJ{;Am)!a3 z6`dW>-e+K(ySM8>U0YcEUc$771ctR*zhm-y&GFK>TT_gx>#U+u8liCttuNR;d}mI2 zt*m%sx(o??yCu2HQ`MrnHx@ZkZ!iu%wIN{1OYLeMROVOaOZ!vpKz zJ#qJ6k3_eX;x&@dS%#BnR)Mxs8Mb&?%!t6odCeU!T^qHzj2MjUMk)$JhIwPIN7WFqo3DM<{;Fg(9)!K32Dv7R(k;2(B#YO_VgONQ>)z<7T43&reV%akLr$BDB06|fG)>e*o~Hy!Kl@B|xi!W6 zea$i5z905r<{-@Qv%0cSxs2c8R3GtvpH$B8%L_QyIh8TLr&@fMR6+H)RG?%S>wyuI ze@?CXCYq>6BB`<{LE;pWp$S$J&Q8?z1NXQ6q3O)x5b3XVk1e&?BI8zCt-FD5u2jH1 zPlEShJnydn{xQJc1^DX#hmPG}1Nci8_$q)yKS|30{v^Pku)rS&_+tQH4DkB^elNiN z#eI<-sBxgCv}8C6V;;#j9ZN7ymuI9aPjd&)Me|%9%`+h1tGTC0xj`>ee+-*V%%4lA zB3nx|)}YEW>w?dQ!a$GqBRhW46m4=Ngs};kDdkIUcfnLABN7zL3N){X)SX8@+g|UZ zN3w9O5Yo<9D{cskoW1#V>KjDoW|7W83Er9QE|^@XseN;`JT2e5=&~(C8h$<|vss*L zM!ajy`MSbb@pPRMXh?i`)BeYi3U(-NbQnf#Bqh+g%Id1f)_-omH?MrRv@-6@p;DZc zaoDB>oL&F3<-@wI@mugbk`$bIQhs)>)~g^mUS0Xwquo+rq2`8%V;f?Who=QrcOhJB z7e@;JTF2)H6NSxjKCe; zNB+@sI1=PmN183P)2(?5a;IpcT$14 z9yz~vKD;OD2v#MArx}(PRg#N1f-@}A@N#bnbbVo5PNRBJgYGZ{!|I%%NFv8Y47!sJ zQV6);OX@Kzkp0}T^vmnCh_3!MuHh*vFKU9ya;m81K6i}S&~F_!)M=W<^R)BS*YVZ^ zTdxYTvBPVC?+nyjd*r!k-!#Xc{gko*<26F&G?}9m!MFI(+l!*Oti?s`v{^j2g5(OO zlvO@B(m8B_z-7zctMqJlv=$deUyJqNMOL2TL{S!d4JD7=cw{Yh4zUN#t5J(O<>NVl zfWRyqKX44%)RekJ=FV1hx28GW8ZD3n%aR<+3D86U+G^R$NHn``PTSKMD2t*Yl&_{_Fm%VM#Q`z*9qp27eq3osd>L z6#9OPVaB<3O9(%{CX^mE(mKzD(upfW>D(pp-8#h6LGjMFM~`j&YNg6(DgVJ?P9Z>}*7X+5un~6J%nKYHc zjGMVEp!R+)&P^ctzM{T+$q`vzL$J$kR{Ws%-p9P(45KJ||DuHr^aY`RW&G;68Q_vk&8%HSp$ zdJokf$FFehvl>tEy9>=9&UN_-{+EEndVTS(TNg^sPr(0&oRi@H35W!x56DV8Ls%5} zpQK`t$94Ig8U&b*WkwNMUDs4Ws#EKDr`!+bSB83kFT-Hg}z zse}PM8@Tz<((zO0L^VUzd7h+nh13{LfLv?k)LNLVp9^dlttjh0wcv;>K?$lTFp8+d zyk}smy(-Xbby4P%-ssk|il7l9q3Hw>R_#=5*3Sn{KQr$|$C{=#b03=A7f_5-p6tq1 zM|j=6b0oK?U;ffD>2jxCa~usUNMBaJSr_BzHeX|WA+Y1lcW!jjmm5!hwqe#1iVQli z?lV3Atn=zoE|1G=7;)zv%XoRuvF=&Ou&gRbk?{jO6Nk^??^bw4Lq-&$m7c3F1V*{; zo_F$yMZ5uplt`APNFwZJ@(}DWemOAW@a*^8#Q`ydW3`0pOLW$IU_t-Om> zw!kZu6HhHCo@~)xtM*uVOG~l9WA&ysW$Bn%XYNiNZ{>-yL`GZ_*wF5#hi@5&tbJDF zR!){-ooAgp40RWH3zULfkMn=*9d~>bMfdK~APJ#&=^X{O7qL;L6Pi-gW6LZ#Nbcg@ z1&Amjh=94f&}^WHh@S$YU_}L_3W`V(DHe(ff?YrbME!o3>?FClo7+pFsQdoNTOXXqm1FLtHs-1q^Vh;D(n6-Em*132AVxcwA?y52`0boFFG&EUeWNdSg_N zOSmjDrFfotGjBJ$q;}7-(Swl)5+zeK5M_!Me{Lr2`K3gJ!d(MOOY^O~TP9Dx|GT)R z;VbGAG$mEa_S$(()M!Ey7@i1=el*<{nub$)C$I54kJb40duWJ=aQrfhWeB_1Ejc~e zk{dX^=>>o5-0}O$zdo#188Z2IM+(y^RYOxke@=W6nvUBYikH2EHs*u%dN7WN$sz8zBfke`%o;VR3_-Je1d@9qkXj0U;a3oKXk{~NI#i8*f;xQ>o zX=`53b2}ffx}kQE5m6r{DM1hjz%vRV+CAPZ@38m=OMQzw+F;m<{rX6{LGQOSdcP@1 zcMdM&`%qb_W(}QGCdJ40duajY2al^>kHH_l%;9XIz$)!cgcYTHq4jNxSxZXBXRJN=W zfz`4tZ}+OsiT5n27p{I4hJ<8TiWO;wCuy3dMGlP!d0XYkvZ;7d+n$%Ys`1aWx->z= zQpmADMz`S=DAc#QrR%zVzEsCpJ3pcdLOxrXeOjqwjMKjGWU9f~>-N!PKbzAoMUew+g>ULy_U~ zh4kwn${+guIjgaypge$vR6Ae6q4LgIV-o_~^ZpvN^h_2XRn3$XS`tKw=h*PzY2RYU z{Eoa&k~^L`U9S;-6;UuEGk~N5cMqr9(Lqyx&Y8N&cIwYGn2n+zb9}u?X!&lf9KHVU zRl*f(XI^fD+v~qMv&c#sd2u6A!D8_=C6gUqxPHcy1Me!j8`J1s3L|KX>=s$E`vJT2 zY7BoQBmGyb*d$4jAq0k?8IGb!0u`G_^Z*TU@sV3&ci!IbMr=IYI)*U19Lq|YAQqU7 z7mWRWV5Bg*5ZW8y6$Lmt<)vSzg34DX%1mqx(V+L{T{E;-(yV#WH_iYf%Ob<~{M(+w0r>2y;FdF=t*-1dUS!USo*nZ;Y6|h+1od2aSJcLQ@n<`+S#f zsd`Q?;$8&5RQk@|yfZ29elnbZF>-Lh=N(S9w^IcZ?KH`%97E6=BPgTiiwB~GpXcEG zTFSz5nCA`4SowxAvYgfOao&gX7S?RI9J1P!c#H*-qB(ndj-2W6WZW)gjstE_!HvP+ z4+E`!eWgz7z54ef5z45Et~SO-Y(c)Cq!$xT8|ldOaT2j-8ZB2pM)FTYaNORI(Z(X~ zc_pO277X3dNc{#I)C1-R&mi}#AR%QmdtI#|%}ls5FcZ10D$ZiFSWC#3t_J2I6u;Bp z_ez*?V72Va>;Gf@M{nATy=Q=A7((PJfnf-NquDU^h)y=LIv>b;>5oSr-}-h`^_Z4u zUSj|WfEu=2-!=i4^^v^E&;5P8-&tr|{uwhJ>35-Uq9BPXr)rc&G;Y4*jUT6+h(p(0 zZcLZC%h=Vie#ewCeZ?5e!Hm6SSRK*QE|}nM!QGwU?!n#NEo|J`_zn^X?ry=| zHP}W11b26LHtsT<^WC|3W`51n{q*XpUec?(fAp$)>#d9iAoqX?gZL!W-~|*52c<+k zR=_=rE zA?HAEjlP3Ff+Xb>Z6eN|H_p^v(VeNnVq>4ehA-XeT6+D_5klKUxH$w+7L;<~7@A?m zK)^+9XyooGW!yMeb3ZH58k#-KJN44u4wQ~#{6);Vj~L9Ab$--Qo&v_Y0(NgLcv{a^ zk*M$Tg_=t#?WFTmD08_j*yr3tjYo&YxgcBuk3);2<9`}^f1G7BOo?I|_Y3KueD7jH zrF~C-vA)Vhq)js&{xiY~QDl3Z+!g(z_{Q0@E1LNJAx^-26#o_fqx~Q8L;L@%^na`W zQ~pQa^`El6A{FwV2IC)^w<PH0`4I4(ng64U{|@{=JsZ8bf^9vwzzOef3~b}4y&#$wjeV2+E9>B@%c*_) zko(!-NTpbko~|$6b7@Pn9a?F4=mu?U4DKkwVED@3RQZ0=5kEJs97KI24lO;HmI6+H z|7SOAak{WT1}GP$=#8x5KTBY=@;#%3#g0hU3MDUOCy4D_bl9!4%zS(EkR?|ZW?1=J+EGp)QS6NE;&q!#WpHP?lSxIOLh5ZbfGLl+-FhJH z+!bXM9-vcG8*gTT7dVG?(B3jM%hIrm`;_R`j|%Y&RwVcSv4#pDmt+2|`z`s5@xp7Z z+OXYcSGLM&rOLz@5v@Qb@tZ~@4Mz<0P3K@$-p}f=4lrpv-2xRDD0Y#8V7!5~E=!a5 zHE4mS+m6x;6{aR;b8a|XhlM65bW;mM2!~UkPsXku(hE6s;Hbut50|J8d!rM5EP&-Y3{I_|LdGRYp0RRt^7uss z9+{V=#y&WRZAx`l`?krB7kpCNa`JQLW2&2uf=Zms6w@dQ>L_Cx&E*SdXU5-V-(1}& zqV=M8*syJ%mp3?VcLe8Zv0+jyVdJg!A}k3qq!=cLq>a=k?&X&Uq~M*R*OkkuhnZ`B zHRjg=1ZjvGHH0^MLkh@B>_fk3bpD(9IhYK48X?BOF{pseitKcoK5Mhk>n#_31Mwjz zxw7Tg`Uu@f{ce1eqRYOKoCR-BaV_S*ao1c0@!cCpNtv{vRF6c$q8rJVmPL_R!b$h@ z-3&}Y_H>f7{lU=Ba)vZx1_w^Xb>8q_Xg1d;8H4AI9MB_tns#i8e!9GWYGs!Sr)Hg| zRMMdCQO6EOD?hmT^=U&pX7mEpAHEY*BwB~gb7^i+NFl{kUsy+pAr%fB6S5I)EH_XW zP%qO@8}40lg|;``z1Q}M&KB4sz5K~AyO=@=nTSO1H|yuy?X1dk9p->akFGd*CvK6> zY5FA}Z#~3ksa-X1pkL$o z!GYF#7BO;k+2)2_GxB=B0eZi2aecScUGpMx{O+*bo;RG*vT=$=aQM?a$Tr1j^R!-J z%>R6ACR)I{Gd1GamX}Zw<>RZ<-QetvtZ=JR@l~$^L_nhbD#KC2DP|<#d0TWR8-i!$FcI z@)LFu1{Vf{w%FisG)lF#&4%F8ksFzywx|$fyOKdq3nrRQDq(7}QgC3a{=1*37NMZu zA<1G{0xm)VF1|XpcaS9kR5qhSr~=e{3nSQJVacSW`y^Mdac%2p`|=Yoj?7na)Dt>( zaLYA6C2|h6l7c}#>egW_6L{QnQow^}pevkh zMUb(}pRz9*gT63BqG{$MDoyFy)pxueE6x0C60={*njzy0jB*0k*U@r|j%Y1O-_Kb1 zK{l)(ar~h1kH#9Ir}hvlpp2)nmG+0&i~|TVfz}%@6Vo3=eDF%NoC;U_AK!#p{dM=aC$SpyLR>~q zQm{>dk;DwH%<#H$R0Ebmo-((yoKh8wbjGE`2?Mr^8P|pYB?IfQZ6(35E-=azyQqZz zt`*EijBdgcrNAx6U#v#_9*E3^FUE`iU*|5=9w6n&vlc=%g_j`AMig^`02N;}Gh#Dk zXul6aPVo-!BQdi0`%A!B0Z|^eOfg>TS;#0S7V;;Scn&AuCkdBxvibdYbq1FyefEv5 zvq|3P;^OhQAb!Lm71qCn_*snKlp^j?Q&Y2z)dXQlx_FOQ*;L=R23rpkSjVUP7TUB6 zg2R-zbs!<#>OwSFQ2p0169pP)DSs!iX{P#GTIMQC?(G3Vx$s-(aN=wUe=D-e5no(K zgkSGpGO|N5IDf(Eq!6h-@vR;T6St}}HFp34_`aTQ&5#1P?C0#7Fu%yEt_t?Rv>Q-M z>JMe=cx-Z+}-#K`aR@X56+@nKW2aacggdJlAb9^qBYb z^w&o5WJ?wQ)cOSX=hncL90J8>h4m=+7AB(B&OaX+ZcNtstW=4yZNfxjlb0N?=X}b2 zT%$8Bk?S8*>vJ|{j71fH7F1#E03*(R=Zs@27eI!wo{dV z_sfHa*%5v2Ug6rLEGkQ;#D^BP>RB0*C-sa`v$933LGIPw=9*PLn1h&Q(Ln&+ z)ZTJB;V9s3pziQD#}Kt{hp0%$M_h0oxDh?kT(Q7-e9HI_hIJ^S_fv+X^}@g{azl9$ zj)+`9Q|$zq?*eX67%LH{|4#Y6WBNTU_3O9G@6j@JYwTh9T+-N*a@e~2se3hJMf&?C zXhF@7b6OJW2xLY!6{U#paz84wj|3n7Em?&FrML-fqspNYaIkYMClM>_wj_^k93XFE z8y&fIQ0aLH5#|hmlrTBgy+C!y?DcUV4f_=EC8Z2l<%p_7r9ny9#+5^jt*=24qq7rs z_vqJWfUeau=6tx0pA;x_d&58a9GG@nCMU8Nb@S#!s;{nNruZd2p#_}e0FOe)L=5ef zGg}%m_gLdOQL0FuKGFIOh>>?5&etME#XE4L{)TqR^I>3%Y`*eOC2e4_jHn4%v1SxL zuEMO&JoZ0x7U=Ml{w-o~yB0=+b1PbvSH^CkbiI~RdvV&4qARRhfc=xs`=_}#>Rd1# zo%B9P=nuSqG_Xs#iGDd_DCT!A90`z_&eS#^KmMI>+hgUPD#_rk@-3$Iuh`_PD!O)(cnm( z+IDZDTCOW9cX$W6mn@`S{RDIiN^nP?i@4$O9r*Q8)*Jd|A@ZLC+`PMLMp&sbjd=;i z<0~k~<0&TrYO}gpyN7Q?-$}k-<<=9P4HzC}LL$;{T{dFgDs(2wu|2$?f6^kVVgF#9 z!3?`EaQX01)?I}_CI!sh8_=eBmw!k@2hn`$XEYJhE^p79Ji)}$RvAUJK$V9my!t7}H97F10ikT%11YmSf2~gmoc1U%xP8iKR{Nq-Nr%{06 z?9Bea!DC}<&+>T>6GNW%;17P@3Weaz9}-9Sf}|J@!mhev7kYE=vTZIGY759S)?yN= zQ`8aR*q9@i&v5HMVjj0z7J)ug4ekf?xLaQ^PlJg|`t};(hhpkxI^+R_3IOaf?hIjO zHO5pcpK#U&>vo9s8jRGy_Fn|I(C(#V#i5;}vl}m(5-2#^!5GL>r^uXuLyHXwdryd~ zL?wLgc0AXmiMmtf5EQLucPm5AV#<>MnmFt#-|Cmk=C=*>Za`yc(t!=B1266&;#r3zW1okayghTHkB7aw9_zE_1Hv0AkWt2V@QW6ckT8# zfy&pSm|qKsCU?T=uBYeAZuyRk35Z+D_t{G%gp`|F6=KyTU7sOVw<2# z7S2AA3Q`~j4U}UImG%}2$NI+RUQtUVn{N;Q>wjUVY4c;o#k2Eg67_%6Jb9DS{`;#0 zmnM2kH#$Yuc((2Qi2pe!6r@97Z~B9+C#0e$qGO(S6tt7l6`5~8e<4OL6_|>sRHT}2 zA3iv=d)Ae4Qs|jX_Sb3N8D?-Qi=GXIYSMl2`xwy^hJN6jw#Wrb^>g1T|HPLq>bNn& z&|37ktTjVMRd{%+V{E~4ST)U5Z>4wZFDX}O8@_f$NlCoQj(xuniUMe6>(_paEC#0jaGS@O>2pHGvy_x`(+ zYNO9FV3@^S#=WfZeao2qptg1E3$BDiM}RCGwT8yT0}`HNa}4oTU~U6_tJ6;eQf9p&dHetsd^OIwyj@4yK7Vwf)9F=N<3af8*&v^ zNb8yEg3v= zKR(1dHnOuM_9yH8CM?DKr-J+?^374IO9tGosZl&vKhsh*HUO-w=26p*rzS9zxzi4u zIl0*PbhG*nMbnyaitIWi*T|SMX}nPr`+B>XMB!}q!H<)XZhF>TX4xhrc9yZiIjAT0 z-EuB#uSeCBt!7x>jJ0G#H;qRGfGDT1g1|FEC*mLnfAmlzWf10)wv@U};0qol)rE1Pw@6-;Jjuy9GGUCjz|M*8w~=!Zt6b$)$!F#r1c(c5_oH% zDUL5(u?wZIw(mB4f4=%07_61otoyEQWPPZfrY!%XUXSA&3py;~xw z`YDwUL=}m;++)P*!XLHlYaR8t6nCYq30nT@B8cBJ?%EuQNNb4r_EK_j!Yq0I&3lUE znp8jc{2xU6_LS7(k4T67$_Yp|qFF}j)-6*Z3yerBNod`lvG>_z{*9(!pEZOOMC18e zK7{+XTxdRw77QgKl{{}+tqVnys}wO8%v_91Dhimu^B?dmvT4VJ04b6~NL>w}{{^6x zqLraxre@1U_Ix4Zri{d z0iVB98+-FGDK+35Y%(i_bU?En+H*$rdtCY??$>3<9@rxf-!mZvXE9YTWt#pPven#$ z^5qFFgSU~8INiPJee|$yGn1EJXo4k1VZziU^u+xa9OQf)%(^fkx)ZzodR1jz0;ed|L0G;oshd-LDxl3V=Q8nbJg4E~AtDBTShog&Dc3s!Ykk72yzR;Xjmrl$RpXjyuN?@Q6sYv;x$ASmLj{pb%9HbjV)X`RR4wps zYSV2C-1?eK*LRDt(6Myn%`poOvB6R#PgK4v>s=kgm#gpIIQggU;~E+GN;?IoR<*{w zP2C^41(W7%2WtuGof>Pdwh2ikWc`cF7gKjA!<&G4vj!zlR4ok9_+kKV=bDFjUNNoL2i33O5%6*j672nM@wIHBYGmZXVMs8bD;jykEz4( z%roqG>=2ftA&@8=;-8{>@VwzS!mF2AC<=}KEEUlt2H)aU&q=br!fBZPd+@J)>dFeu z425^7{5gBKpsMd-b>MLl(YT(=fyVdw$~4oY{(ccirb)}*GKp}c-)Oj)d2>u%RSt8Dg4Y^j>|bpGJ+0?91oXQ3=4Y2ti*WeY)hCqU6=HMkBDeUVpFCv>MdC zU9mhld*4&M7Ch~YaLBkec9&ABPDU-5a(cQidzf0P7hRYMdIlN&PB_9bP#6%8XJZ&A zk!0Na^)hlVe0{SBk>LubZIr7%Oo(xBv5PVo{zl_jq^@9J0Rm-S5`h%#BNlcV0Q2W+ zt1^W5U1hTh4weQi*3Z%8(aL7;b=)7(u<;&KH3fl?f(?Qm?&QinA2PeE zE0vn5^F);hM>;>Dw!CjvvdJ2sRj`tTp3@oOeT3%NN%gbByKQe4 z5s8^{yz%Iz{$iwo@(n?bIF2A=hc8FWN*4LF(O_l6s<*PniRO}5RUJjD7G>ztT@@ld zl0#x5yK}%xsZn|+dUw>pZb+val)FqYiwFQEa^yIQyC?c{-@FVv40sMM1ucLJ}NC>*Bx|PvNB#Sv6tAV2^+DQApv>=WjgmGaJ+91-wvH%S8PqYs6aW zG!e-0zm#5^UWkN7LZx+RBuV4A6qpDRz;{ff74lryko4wd@T=}k$H)4UI$cL990srs z&WfqD;J&uwJp=HzDbH?_MoA_T_RRb+6?+W!<+=xVIu12Tkx87npc)TZH=2rp0!;yk zrvP`cHFXaa8ms~){3EPXq;2^}WA72)CVLz`mb!E~hZNiZk5&(Wq09>nf4&$J3~N19 z&ov53asPXyePwShPRoVqIC$`0}7k-4PgBf7)Yy(59(X$tm0B~yD#$5G!+ zj!b~}@b?&8!ezGDu@*-7%OUtyO%agEQ~$@-s>8CyByc5 z57WTh>J#ii(yo=?J^(6RL+OvZ!Gq(4c@%mx4tWgc{EC?t>#Pv*!kD5_>xP`W84U;y zZey(zKy<_TRe?9U^owIBYum41MQ2|O9X${(as)drOSU>=aeq3?SN&GkWodbM2Aw#w zcph_{2o3#1FWosOX>n+6CQ@f*!CPzJwU|7#t>yHtZl-?$G#GYlBO}2nZ(rNWDyPE|W zD0d>@^n^d<6AZ&8%Gf0%BN8MtaHOpU+#h2@M+%}o&JGZwQYG$>r=HNDFW5-X zKg1py>y1BzI6b#tlYU1xoRkt$nSf&{{fdKq{_mE5(LsAC_=Y6D@r9XlQIr)tw2Dq7 z!`@s^*%u$w9Z4--YqnnBa3h8)kwBS` zs!H6%TR{i(If$J;KA(=P0a|2z4e5cgEhC>+wTBTrq!y$ zU}BlH)!&WL0;{>#%2fl2uv6miCveB?Ol zJ67?9n(Cy6EsrPS*3^f)&Kl`-A@OgWKou|E=Za#rWMR4F`c6+R9I1?t^zc($je--W zO`(NbN#{4QJL<=x*W#K^@F{q~K-rNH@`833n>4PhX~4S*iSoN}p~DoZZ~)C`*@WM6 zLG!ZV0v%5i@hraemjJKF)Sc>4p&!#f`Og33y|y?m4F?8;DIvMAvljk%UqSq%qWo6j`fyXs=ow)?c~rT~&6hHMbKw7A z=(u6?5ZNu8g7~!)Hf5JUx=_I>ck~Ku@9T3``%_h5Jc=HX4~{-)xC~3>*I+FAdzHqk z5@M?xT{3DEDk?j~M8lS0|B5w-<9_L69SrBBKa$VTr5Z-S7)^jAWZZc9rS^~Y^PF8< zKy#-&vxry3K)wCszky52<6s@_*J=H6h=LtV+v)&K#hxQ*fbK5bNwcqkSKJj!9NvRF z9w&tD+82V{hDH{4B3bi(zvf~P`hB6hx*I7UQzOdDIdoQp?+-1-z*PMc5HEGh_rJg)x-QoJbFSCUgBvuYe~6gS>Y3VJTjQl*WB6az z49s{~tPE-hGD3J9ffNj39&OW~uaCVyjI;zbF@lY%f2KDD!ROy{=+*6*lLF8moDA5D z2E)J>6X!!6*J^N-XFK$Q&Yhfrb7BlDUEaZfC zCsvG(R-#2Eg-xp%%6p-eC~=beNor$mmpNYiL}ws*0w|T8^+qtLEvh|pcq~vhkh7cV zq<}R8-c(M&^^K2eGA}`5XiG0McxCx8@2BwAlfQW(VjM!-_3F86EcAIE!MW+-bkH?S z_Kb;*+t=$E82o9MLQ;Vbs+`pC*tn@a&7iShWW1{OPYS|<-^v)&8U>AvCylOxg0^u< z->xX>xD>U+ux{fjre;AHMYytVbgH>39~xa$pH7*iW*yJhWJWk6oMDBE)~4!!A6$$t<6w<_r-a`sUf78PxqIee$O#; zZJKEiVyWMemU$F4?u!#kx7z_r>q&2wsB)02D??iP(;ib9s4o&}YUAK(qF89wzuQTb zR3!?dIu7S32-$LF!C25^C7NW%SFfOgA7j(Ie;D6tF5E9S(lRMJ{uN-jd@hh3!#+Mk zbaQWe#Bo;H^Nh#z@Rh#6-^-n`EU-)m2b!4ITBPy-9$IsO{ZOXh#o}i;`<+VVR}&Am zJ<4=c3zXGa{N7r2@* zbVuLA>9iU~k32pX?Y}qalp007j1!tZd7No!qks8|%0<8DWuyNswF=9Hcp+VqlW7zL9b6W_j}FdM&{{imR89F?xXQGLCLTwxQJp4on5y-eyia*0-MCA z3$b2_vQHD=tVZXfoov*C!@@(v8c+t|gCSBVr1P!fHy?;1&H$NPIuf^vUMe@tb2TP! z%b01+_!POEFZ2v-e#a{|{@~Im411?okm(C2CC^P#-dwg7t7CN{qEZ1n`#%-22?{+fM+ z8ri8&PKUz2YgzWP-tLmaTw~A~`Pk`&J;T=|i3hJ-qCJ3q5XHDxIGMNg0sngwQRe(c z7a~MS_JVROky4@Fo-K;FE!N|Fskf2H6ZGT_+HaRrS#K37_idF$&S$g-4IwYSM0> zTKk&{!lE@liv!BLuL{W@!5O!kA!dG4ox{N8;dQgT%=wzfFlFIZzo%fetX!A~Jp4e@ zkA0|i3TRLspXR?NYAJ`a@V8&Jq%`c(!}*V|ti1FjP$9uvot7pXu<{rzs1uv1*@T(E z?`XlnGH}nhititO)k8+$+qV&wEqHK$a`7t7j;%BU!*oSClxQY1aOH9Kq@W1y+)@Gg zy0-7`ds$n@(DCVS;s@@*W)#Rna9YMUa@{IVHmYnw@`$m7;&5P|KMkGWqqoV)OD`gh zNc@8Di)D~bT6l&pUN;r?EY+ID)IChE_$)<@qqZj(-Y8gK&pGw4OxmK3g_5NTzv9gfdj4Jhijl|knS!EkgE=LrOqPwoif&kb3mYBqgh;)BF2 zTiWYdd6&HRQ%zW+5z!v2P=)}1?4BA4t#7*M0_qM^w*F}%2kd(37g*+4Q72@*t;gtG z3&t{EjS=#;IMYE65P>0uGqLFOTt`AYiczpt#4J*-ZB(#m%PZK?);aNX*~iWg-xqY8*jsQvTKw=0U9+jJdn&L7~{;=;CAQB zx3!i|bVzI4X1Zu61;~_Ih~<3~<#J`o091u~l2)}QgHdz0 z*S4BiBRV1`+-5NyWJSgUfJ0~1txPcr@HuvySCQuKbBiFNzeDViz4wfvLo~(>B&4u) zrA!g@e5Ui8H}{iP50UuWFD*lUQAn%yBOcpjzkxO2S|R+|-gpM6w(iZJ^!ID#6r*D- zNPmFDr1K9o!|KzA6(ifvVL8NmNu|09B~>&s&W^y2Cn{R_N?k%@XK&B=eKB1Ht01%I zI|OGb;nM&#u-Yh(TTCsV9+331_8K#v+d^WDjgM_D8a~HVk>A<*6(}P8xE=6WQ!bDIaq2C7E~-Q~1w%H6 zyf2n2ATMnF4UuU0)$P6ZJEutYN9q}0g%*4jJ&PX;BJnAw#(YZ9tj_4Zk83jW#$c!| z9_nZ<(8DSjZ_Ju#J*QBnFB?mxscrR4H4OT}B94x>XACbsqV0f8BK&J?ZlVwQz*}zP z3?aM!TPt_*y)&3-D!fT(&mSTWvJP+xFu~l&+@m)VS(xKoxs?wfJ8^Sc&3H`v@Akrb z{YQNx^J3zMxka~`=&jpv)$M}JOql7$T{}fhidy~y8PW9pw{^hL|Gq4Kjt5a1P zs)WpoiUZj7&Y%tT2hF}qEw47uz)2yG{hQ1}(1oiNLu>za z<#9jo%Z9JW;_bL=%tD$u7#nT-RGe925nguO@heVgaB!aODVjA<)$2R$;F)s_Lq~$g zYEaV&kSs^L>8rW#U)b9o*n4)QC%3Y*vMrX|WYE9h3DUPZMRo+?y>C#-C6;gq zgl*OsVKlC*;G5N*`;K}zAKv*$&-O~$sSnPuDDs5=jkB9P@Nj|U?b}wE?TBPL6zur( z9J0YP^;>@E%p+Zk7eJDSeVx9rt*Cd{|d!C^5hr zVzB}8@Sn{TMF`KQH!h-1FHi%|THh$8y9hWaPY6Q$s&4s$3uZEma-sme-}b5Hp_r*; zGa`^8*Z;FpvH?A@TsQBnVIu=SI7cI}zKns^MwxB0iNG~I8Yul= zh$Sw2yzb4^4vO7=9(_r3`LNaA=PL4H-`~)`5MYV_4svPs&M2U#s3bx3FY!Sv#yWUy zY4U{_mUySJ#VG3H52A6qRJruLKl~&LQ{LN@ROb*g{sk{6F?mPvwWXzn2#>NxCJH57 z$N1}$iBHx2BkV?RI}+7^BAxF43o#cg7^J?B(VvWpv>x|aVThUrF-vwCCmsKGj|Dl0 z*h|OC-3hR@RzxJT=nG^4J!U!zt-2e;XS#-D0)x9A#Abqa-~n8zo3ER(;_1c)T9Ooj zlc?p&0(lw&BCCWM>*0?9rA_t`{U&c&#=GApfdrl!e3s z%^48&S?0yS$t@`Ap^~EV)G+=<1mgQN1f?6y$KEeVbOcXdX2sNRaA|L3k(9UU^tPdE zNK~a4HV{VCS--05Ap{nC$zkaQYP3|fdOtacR{tWywCJ2F(2`5L&-n`|$5jjqv&k>A z{NyzXz}~;t97ztuE}T$jq~EYaVNJrBbc?73y-Ol=?+CCSf%_H_F6S4oI#++>;M5-f ztGktXHXL2&gZ`?hCL|K4QPcC6=vih?NKfAsMtheb$U=3j;m^86*e%>?4t_)!a>Czk zqo!$|EX(|aLIA-h={yE4E!707_|^nm1#hbJMo2T`2;VR zeh|^tRsKsfEW<YL`6ft< zg$ff~4hZMP>FLSs^ta{m3!iaH7hgW%&D=C5a&t+E1DhJ`dxgR#VGEEKdJiR}`Os_$ z@q6Eyo@Kl=OwWF(h;#sSbPoNpFvCf-f=szk|K%5kJ1-d(EdADa)^3&$qQcEt@6Udd z$|WM%k70ePax}f4j&(I|F{5egp6(Ef?<&;KfZWnf3y;p$_tSlWRZr;RupW|9^~B@TiYboig;-h{ zg~+QuLXKLp!pC+HetbPUh1z?R(&4C?2b&&mB?mQHaYEKsvm1CBqQ21NZMrcps=|^^ zB>XkLaGIFTj#*C443=F3FHk&1wfHt!lrOO zTzV+;@nPRDZ$_T=?V5^T&))KflA-b%pCPseLXV*e5eTn3rnXohy zt0j5G&`QF7vVrujvvv)4WbxGojp%-1AVOn2emm2LyD5Ji@#>D4ZRx1^cJpiy?^%Jh z1H@_SuqRWk@I1!=*Qm8RqYi8PA86JBnSKdxoii{bAn8Ne9IlRVn);yP45Roz4S57& zZ=}w0YGiEbWc9;eBdC$vM8x5wqx$F(kIGhS!A5i)-CEErQB2m_Y|9)#sqt(9QVMk= zzU#(YrPY>7al5ZLDV;X6VS4?!9xiECe;{ECr2|A@+C*5;2ciltxeZ^e)9`F6cG}!2 zIY~$B3~EG4%U06X6$VA>06*KIVn7RbEgj*ftO)h`5!Jg%xixmO{v$cm4)pyof-H2{ zdv33OIE*O`^*2ntouamSKW74xv4B`(-AxoA=i5d_1VhzvH+l)<5J(f$j%I)EQ(kz* zjw@%OCbv>g&&)(*anv)e0A5Bw2&ud={w{6 zGnkUUeelFJIH+L+mK0@!_n4#9Zq@Fxi8VgYcGYS(7b2Zc zJYxvRrFy+Y^Tv(Hy|X=HI3`U*|YPJ>uSaijyX1VZKyT zV+(Su5q|n3T3kye!#^)}37g}DO?Ty&7QANnWM$d{3|HKm?=DWZv#T;8TDvr|!jFl= z)=&JL+iI;j@|gi0=V&&~?TcFP^F0#Z8OwTgJ(j>@iA;iWeYZSzPi;PB1g!E~QsHQ$W+rNaVWQiLd~MQ?l_O^YG_+C0`iA#@o!t#xdmoJ=?hrd{pj+F%PvC*} z=9o8d;xHsea|%bvqAGr7dcEu-DW@xW#5fcp3-VbOOKGHZ0>9R$=M8B zL|;uVl?T6JT2~bDN>&;89(S92ebzh(k>+5Nd9ZckZjBU5Ixs4ukYQegxI$ zSmgD_N^yW~o#X&nODGZz*R(Ill4KqIu@gP0z>&@f^92|BTMm6HsC4%iZ%q;~$<1 z4wRQ`1P)|Wi9?$wh;V%4=c_eDg+n*|5F0gqJ+f4ELKs^u20D=L3vIFcql|=sWm!@L zW_BNRL&@w~Ey!45w>?hQ`swzlu)92hnG;*j%U{4p+NNzJ0&{{2S}=nrr=g5ggchkM z5`~FPi0%sJizlPCnP35;spe)GWI6VXty-M#Csw3VLNb3)l58?{NJ#qYN@^i{ud|vp z`s&7IJZ&_VL0@xyzC*Iq7tb04B;)5*DYYMiJ8_)iv8I*Mn~xhiqAL6iJ}R!9)bH5B zsXGG3IM?l%-C`7YjK6|ysS>u-!$g=It@+uS*7K+QO(JZ~cfB&O=~MP##qwzf-j5o# zYJ3b#DnwO!6Nrg6^&3H_u}cp#p+XXzF#VTF-SI*Mp0SH<$kI*2XwJ|g_*%EwrNxNx z)LP<6HYxkReib8D^lT5?0`3UN_~ia{t(uUL(&dtvP5s4`yb!2i9x5yTK!!_L`xW^9 z$L%aV*#0BW{HCLEV~depgza`vP5Atg)|xuBSwNI9D4oCUWf0rb#RPDTOjc^M!BU?S zG}RpBHY7xxhj8;L2lM7DA`+WSZU=t(Ggb97qHvh-t=J$ub}8>x%nz;UvUlq3HF zZ}-HM{eLKMw8gTt00kVJh`sAKe2*5sdvrUuU|88oXA4*r&N((O?%;d!zo}EpIAqtd zX{3N=uOkEGXX)5qg|3tZ)8&le*k389W#%WbQL|pv7^o;|aKK36iwLUQCR)QNtI;8n_9}IX>uL-m4mX02 zthe%VjgpX5XYmvCDBHH8wJcoc98`S!SU zQ(hy!bcGwEd%a2tDnj3j!^9vVERn<>uFe+@r}(6Vg!oK7js$PEP6r?KAq-ML2ryu{%tKgT`3+K=_(Y}h zY1Z_6qXvk?R*-FSL4k;H%`^PE{fF-Sy0kMvPi8*F1Lh0VH|I}5D;R#rAB|P2s7d-Wx5;?}4c!rINJS8(A?7v2PkFN{M5cO21@zdCMY_hah z%b=wV+O${4AjEyIUrmNMMWO?*63egZKXn)#x?FVRp8L~xX6^C5DALnxu8|+WIHs|` zhiPnxll370;oyu^ks-thl7Xf)-Hgn6%FEZS_YVvz9IK0btE?PHiBID&786v4)p9w-`hC7EXm#%zd;N?D*+MZBi2C}UXlvTy4~x_G(QAu3)O@eHn7(2D-d07=zlDtq*qA&$joJv zV)4NxT9fU|%K0Vl8qOqFn!m3j>}0Y^p|W*XLQ~1{kEB#BwJj0#zVP21Zk{){76ebg z=0HfjPVdMH$WFh=g>F-8(Mn0By&suw74IcGI9uD)Qk-;QEH+;^PCAJt&6pI*HegHD zVfVRi0h_@Ef)k4UIrVy^x))FW+B)&Mki&mUg>p%SYSWU*Ku&MNycwa_`a{ApLuVews=3k%h2`e`BwSedhpdfKr2WLZnrUdl$qC*e>NGacRPPz(rWgYASVhr6;c z+OXT;>>0a#Nx~XQsV>gxoHIDW3q}G7w}zfisYpV(w~jPu5dT?y_gTs&KhDsxdI7dm z!_rGI09nbTXy)T%aom-od~rOJ7PPgf+CrMQ(=3XSHE z97s^S72lc-Ege5LI@v)DeDk~T?LwQe(6e(8BV$5k0&`^j1XJ@g&sJt2UCywmupLwv zqMW>`cLa`C0v=+DSf+EF@uVFa^*Rcdm&<9FjP^GcKQAs8LzqhPOB5PgVvpmS3C?Zh z*K{};N5No$DL5~eFExlA!sS!%j=s$z+E4)Hc3Na4DK;S)TS|c&jXFIrj;=;9q|u5v z=@${R8jgQVyw(TbP0C!z`7^NU57T5B<17;vyDbV5Ahpi|zrf>X(~OnICJ!9$`M9lO zOJFjq3kKH>rJ8zUNxPB7s=78_NmBZWi`RFN=WSkaQdA3o$&R8KZXnjnX6WAN5`_gj z1c@BRxX>mZS2eVYX9+NBkG#seYk$T~M9|T!?w$rNS=qY@`c0aiy+fk!T)$y-wXdG^ zVa24LY8D%e1P8PURT)39xyGt-@gj^jc1Qyl>5#MNf6F{ha@j9oG&Dbmn_ljcq7HVg zqNKyee@6X}c{^K8OO5ukDt7e!`iz@^FTjhV+;g)9jjyK zif!ArZQHhuPOjK^^W67&&Z+koocU!{?b^Fm?Nw{nUSrNN$Cw1*OY`dS!7_@ssU>}d zk4fd1QOjCV=f$V{YDtb;rqa`oR|{;pV)oozr<%hF$f}t{fi{v;(zgG|Y-Vr>R|;>y zgRi~oz&9>77zhiE97RPNVFH)^z4ydclct>N?Z;W{#b^gi8_CYxtw7<^`sS%pteT{x z15A%rrddmp)!3ArvphbIPZtTBMS%+ee)PPiTX!R6?R)!URmSa^y;qRLvap2`u=SJq zD5csc)jo(2HrVsV!;5A3*B2tX-3bbq|H!GZdl56U@z_uE-S7|k64`EyG790t1G1Kj z)uUP2*J7-eDDL8(9Mih<@O>{in{v z7A!VKOHxWoGghf+w2F|uF|mrZ?a&?beGk~kjBdu(oHD=80ZsOkTT&itvN~F_q2|h2 zNWh#q%J^!}kko3Jz_#mxc4Zl)WJ;)zGM>6Nkw`RDJZ*ia!&GsZGr^g29SYBM>?#Kf zUBR)Ysn?6hpQ=SCo#{plw+@xDBG5j#%KmV0upa?9rKA;#R$1_eJw5mGHwmYZY(bb* zM9BW54F-^Gn~h$~b_Q`~^TPv?bA!FY zx!023JXvnD>j9b&_Nww#$v7cx2PK~Blxc88H7{53rkMh$t5_9DMfa#Yx4*m5x)Nst zY#@J48nUxsIHI#khF=QSg|YnRu!Qm@vP%>JqdCj}J(9MJ0YR!AGaYVe`F!I>m3Szi z;Te9S;S;=81ynHt#juWFcj;QVeSlCGIddDQ}iMCppI71N3538=jrKhJYAy(oAx z{lHTiIe~+TRD!#qdj-^zbxQn;Ik|V>uQGE}^!a3|Hx@O*=rhuAC7vwPI!%rMqV0g;NF^oPx8NY8iEN zf2Xij){+am1kG3EslfITF3ePIc{IoNS8)B((vR7=Pugjc5O$eAyAoZ7xNc?ec!zr= zT&ItfpZgrVpy>ScBHlpRdTij!3 zM?dST*v_$}OXEu$4uRj_8;!0+P(1MH*N))gWiS1;H;v3KvC!fYXxVBZsjZ|zg>?DXnN;<%s?a3bcr(7|A1 z@7Zv$#K12{bAA}%hbedesmlea9#K$GgF%*FERmMzo}DmV?dBxR_-sTLsidVB+=KO8 zIabe3fVE8OsrYlVcG2i~F0)J8PTFf1oWc&kjJHH$K)nz6uJC!T3lsdI(333oWqswm=Qvs4fV2k0gLL&Pt`cpp^ zZkX(gE39h;8PiwXllbQCqRi;!ni~y^f`%fBMvx(vN83^MC%}73PKcqx$}WgznMkZt z5PL$m$$vtd)pWyH$@^iF4YLI;NLn9tO+mX=9){FU@w@x~#}8d`-W}dg54tv3(;0*< zK!MR_M#GO8j0_EgD5%d$lMhT}eNH6AGL6Jrq?!lLrb!nyk5`a&<|D3Yo5 zbtQvr@!a=oak-8cVNB$o>u0xznOC4{^700>=%GG^Q2ZN(HEQRBr>Yf0tHo|ErSUy- zy6>+fX1lFq*9XbFoKdkBYVqLP8C#~y^|CG68N;piigP~+4~8W?vF~WCIIj_Of9qe# zdp`_-DRx<@sK#n&&9WpVE4-DS$YmPp(tIuJp2bAJHBr?Mi@wetjDxy}8cF&iizq7h zZGa&)>eK?HQRK{v&Wio?L&U$j8hzhv#2~i#gN_3(61849Nmh)%fOA+aINzjqce$Ss z2%$#I1b{ki7_0I*`tB(uexKnCaW$VO9TnwHYE*FXGJ#k$1lFOLIg6~iU>otNp}+Ei z^AC5JS)K(edCRi>DY`=ynAr{7JbhTq-+U$LcJlI_cU$|YX-!~}kUenGRr$Om#4<4H zayehDo26WI;5{VfYDYU_5P*ydDjQoU00%$A3>uZ*?lZSER8tG3h;#I%aCg%laTVne z5=bP;BW}e)L!~vyO&Y!St#1{*7FoW|@pYVX+?|IGalwycU%U^VWMZyo=qJGAJoT8h zj&OiFzWzQku_ilW05S&4-dKKG?N3~ zoe%+S1-7BJUaRyS%B7m1#a;HpZa_NB%Sn-3((iy%6isP!spspgO)yDeC$GL8de`ec z?LB#-X3XLv07M;Bb6=DQ1r}`f$~uJU5zUn$1IT@Iy3GID)GK`z`y#1o5`_#Ne~`$# zKArTyr=tQP1q?@I!+*Ozy5;9N9v{=eb+%P0{J~I9oHs`FPcChN>n*cIOm09D_GPDQ z@$XW2kw`0xI9|Q^`1ZzGQQ5CF`kImKU+%EA|LthbF3Qk*z5aOps4S(~QGV_1*gQ+pJNJ9)KXS_^!Im@T^fwx<)zv=f3TpDk>i}+Zf5Dh8uZMVtIUEO@N&2B< z#hHl&LwDGZ31VYAydCar%U9U%%S2ooXc{&jnh3xZ7NU?Udv-%?#XardrPEUghrQZH zPccJ-SSEYj_IMUZ9=W!irU>1TO5afnGCCc?;ID7mHMc|rt7jmL);P>u-MIUpU7D># zlaiAX%nB&D3n`W|oL7|^;m(cE!=4%Fw2D#EdaK4|j|Zc&_DvPzgOU zpUiK2IWz2(TO-bVZpgQOp929BymmH^U{mkXN&GgO{k-~E(Kz-MO{C{eP^SzoEBu}q z17_Mx5SEs`K8%};Jh7*9K<5`M<({lmdjx6o~06g!J2cVkIs*~tLztbVec`&r79>*)dG5{^4J{1f*ikR6{em%&ue&ods+ z5-p%d*m2OXlrWN3CQp8{G%OFO9Sc-Hb)s2k-K(^9glCygG4i??qY7S7MzCMR8~mp6Nno#>?>ddqGs zVPcFI^q|-ezkfBjZrv!N*?!R{1qSK~LzO8tV+s~g zO8|+^>rL1=CFpOiO4(FIe%@b!vi(#PROwy0Ri=U^rY_Aj6j%8P4vr3oAtJCNKPnfW z(a{!#`Z``j4jo-k7#H&vaZYSW%GjKj@juSnVIr z_gyhvv5Eae_}J|bxTr^iU?#>FJb{-F(7)l>gghGZH-(*{!zxm{1atzOT*NgMp9!wM zDe9C*?uhr?tBVDlR4qD>a#gwp?75?v(-xWIFkF~U7>zki+m5q_=x&`DY^mY8vACD` zueTLQbdq8Hq9E4cVjv{A)G^MA*OusII5md*HOcj!D=GDOD~h_M9H}bUwYD|zcE(3hm)I$3L)~p3ESm(`{yK9J9_~)V zGU1t@712KH=O!8vU)9&{2~USoC1{eG?rG?9xU(V}di16fvC#j^l-jhlwaherJ0NL( zaaZ}aO?U*b2t~xZxJiOS9Ty|aY+!POrHKDs|KfCZE86kUh5Vc+&uXFWlL&Mk;_-q0 z?Jf%P-67WkRlLG8eCr+QzD?SH7osq=NTAKd!5Q{Oi0G5f`((|cN^vf2YBIivp@%#}RA;O!n+x4+zReyn&l?zh`B=G1~Cr#f27e+PF zi#>kk)n5Lm8JPseN<1pP`NnEkDK3P)GcrYV@E5f!$CKIAE-NO#8}&zMJ82yx9hhRY z6Gd-X;yf$vRmZIql6Z93w!6#AN61MqFL`cw`7lg9xCZ&?h^i9Lw1`8%(;{fs+uCO2 zp9`6=sQko2t9bf`NEzt|?)Ued^>kL0Tz6AXD)${5<<2Q6hUdHAKkgobCbehDmKU+Z z*sxx&_h1f+tv7SDDiS}cJL#P%s|SpM|L9W?E11wP2z4+uK%gO>u+33Ch6#vYe?OK# zh4g)&&H7#Y+0_v#?xb9<;CFEnjzG)pZDL-a)olpu+)ce0HhH^y$EYj|oMLK6G;XN2 zF}uC?B%h`Xo>mq*Q=%bRmHE$zkG5Prk<%{?S@T4H?%94ynwN4+wN`jK82LrQd3T7h0ct$o_mn(Cu z%$j;}1spiC6>ss}|M)IoWuMp|a*eE41B_@_$g5>J9sl-2tlZf=eg5_4$=~jX4JnG{ zsbs1rpG6aGh)=tpD{Le4+RTZXcfm5tIQjUHna|pq5qN@$F>ui_t;o)^%+6^tgABQsx15GjxKf;7Eq>MrXxpnB= zTkl9PXj<31ad2Nl@TAwe;~~O?+2Jd6*BD>mabGzh=cNU_j|ZWe#irwVRK;a_D45Y^ zgZ?0Zz3sXiVi)MZSk;?+h##J{VnY5=Bvoa$oH~s)E}``LM}|=Axuh3%vP*&daH+8 zD4hF6k;Huiyz}m8I3R&xScJRUCF^ppi+>)7CAHbMsAHoYh&d}l6&!L0bUono)J1l8 zMLZ0KSi$7=`n|e^Tx6m;45eB53d#-Sfjy08;<7w`H0GzFbprNkpiQ3PNYo6r2rdNuA|vUJrl^Mt67wec!v>_qaYY<#YIq6PGZwvFJRscF5sQELi>$E78^ z0irXTx=9x3HSmHA$+HyQeG=v%O zRx32QewKZp_EUeH-h(|Lp{dKBTJp=Ur$zFDi9(5;3)85x4a_h&Xvirr>ssn9pK8j* zaNV7j0@LE3-i11p8eM7)Vfps(by~c;tfziKe3JRkl#O;%+{SipPj(DQmXlAhx z;W2Ol{lCJ${@45!`u|??kH95+QcBUWXxLD5ceUwZ`*ycA;9UldTaZypgqKxNjA^Ra zhfA6@5Qke?8P=3`zW%Zw6^z?IYAWe`(Cd2K;M3h(Hw*O&LoC@S;Bt<&(gIW(SMAMB#`juH#4R>ajXd&&)c(N-p&(_JQ)n zCUW_j=@=&I`J{eg4ZhpsIN$oh7rI_s%5R^~xu@IvyH)xSEqRt`RWN8|q2w!f^EWYi zPsF(P7Zw(Cx&CdBkX9bQo4wAuYt%(L#`_kj&$p765cSt2xK&KJ1vGC|u@^Ld5>h%^ zimvh5W14fTqw!Kxe-K)Qe9U)bUy4evW$q9sW;Nr}S#x{6 zS-9RcG)w;JyJOGSzXoLvrP^CtTVvb9=>m&2lfQBv-i z*fvFzhPc=CZd_~|^et0CbRC1P<jw@Tzeh^bu5{nFd6vhxhoZR& z!jBQWpy_M-Nm8F$vCToZv0JcP{ zjy#t--amKvmdb9V_>o5w0hbgz=|`IvD=q8iu%i z#X96}y}Z*pe4(zW=B~O%uRy&Nxytmwd#Qc{^Shadliq8flP5bMt#p^Kmktcu%`t$VCp;ES9bglBwD0kkn3xz2`XacbPas}2 zGXnqIyWAVGM58&#N6)Wu&&aa-STf<+R_TXD5r@eny{;4qF#39GwE@$*gI-HhZ*?|5>+Z~+QR_aog1D?L7) zUC#$tW0_STXx&+_?A?h)2QJz_hl zC55O!cWE_92Xc5M(QwhWMEw*ulcbAv6AN@(fW8?<-jA!k{p(vdwzq$})Ou>I))cY& zoPpx0g$zg*bd1VG#g!F5H2Ot<~}ZG{f%-V31a&`4VO6=Aa*$ z;yG2qkFKv}OH*fK&?dw|98yxDjbbLK9u~Gza~*_k4fJ0d7j5|lwPMP%R%Cg`B~z0g zcDg+%>yy2$v%@j_zutv`VbdEncpPx`#K)%M3vrhq^g+y=VJ4(vC(gQ&+I7;?{MO=W z8mA~3jG+%7zrWAi;{STCJ4d@FMLo@1Jnk0K=u zvN16Mfj=}xUu_+kujpqnm{8$S(m5VhgkMi%gH#H->!?b)pwtD5nXn?^3w%6thsF2@ zC(Bq?UQFD-JVOU?M1byGe2wVWF68y-EMIHOlh00MXM}af4Z(PPWoWyR3OODJ z6zx`(0JO)z?O!G2?Sjhj7U_}n7LS{{V`Q=&XG}DIasaMZ@0R*vLKLHJ7EUpQ5>wrU0ft2L7NIP*~kw_{R z=oenkc*6#v-I!nY2CGk1y_d(&Ek>xowGFq+`gNGlA!JO*la84;o7)72M*v6^94Q zmP}6(Bz?$-9`4;DdKqiD&J;y3OSwdOzcss_K`;rf3`Qa9`Rw^M2`*u}vyhIDcG1LiCwCfr^H=flRJ}r%((3yWkgGXD zi=d0jpUz#8((G5DRy()(P+B`j&|f02gu!o>vK>+X{OmcD22Qf}h3?KA0vYC75tOU7 zQeQ>N8D71HrbZFnN?cx-S)YHkZW8WmAz{uYVIvMC6osOZVN+Y3`a(!QMz1F%trPJQ zNHQxWLv=zmd)B(CP_)9?%G(8#x$ezT8-;d~p8GE3;M9Kv+-xC@utzh~By!tTl+jgq z>Uoa7jg5ueo<#duDUe7-msy%VS>RaXzz?M34KzG$tJ1uvOnr>iE*ujXTwpcuAAYR3 ztVv#jKemG>D~tB!x&^sG6fV{ z@yjmjG~aiRh8qdqA0n=!M*Q7rL*l5&W{yVf-S8IreA5VaSvF5>%$`kG!?B~YbI3)` z0dl03qiIHx?u547SC@%x=DWEbdnrv$NhO*f5{ZMqPovL8*@r`{iw?H6kO;UJryxqK zW9X1Lt&KR2)3r04c((rfyx%>*?Qo=-lMwyi5ej2ACMo&9$GrdlM}O#2gP@VFr) zXMA%uJngjLZ*&v#0UA!0lEX53b}R%3m6DM>{ex-e>^JcOw`_JlK^Osom-8cGuY~Dg z3N08$)uwj#EiY3gJp)XD>m;FT50@@3;wfqPW< zB}BTGJQwQ24-?X#r`!K#1z%hl7sZJ2-$-n7Ix2b9g{eQ|-Rk9TlX z(OgOdFqoh*UAZ@bF%56&x{UHIkV|H{XJ5vpphNwKBf(Tv*Gx7$%jfUTt94Ul{)S!! zK38C>Z!7~n{k2bFxZ=oVGujS9R?54*QA6=wd;PFE^G|TUA`Y9Uc=X#u$t&a4h_AX4 zYA0ncZ4`E-t(M%V2V;>^TG8c15imyoa=37iO#=0+%1CTmTBB@TT%?9ieW5?*4MEZD zFs1gA)Qk6CJ8uLJ#a8{xdM3_VC#(rzjnT`WTr`1O<-bsns=d-{HV*i|o`#QjD=C!1 zX#K>UIrbz*sl7I{4cZ{F_U!=EyamQm;9R?;>$6Co+{Z3_4^za5Pb7SY+6whXWdRsD9d6pe!0al=ZO(c4=xOoGJTWs=JKqE6091?P zRbjw|$&+RdskVM0u?0oqUF|GhGACUuLQQR;Ar$FOx!6rrI`|ch8)+1Hi!zOQ?Xb{CGd(VVtjXN z?A#u5m23U4)g(#*(I$tj(aqs~y$-L4HGdj!r+hqJNIuJlTf-12erHP5Wm489Qs&;J zKbFN6Dr@iP-tj-%K2^xtV>3n))pZh$CCZG|PZnq{;yPWl#cm`9kL73C?$;=DZ}G$5 zZe_=MidWDIOXrJ>`JW#in0?HjUN*L~1yb72&9wj*@(xb#=7p%|^k9OHtw9FCXe)?Q1cIUYJK|_sL5@Hg30+Mphvi$ydrFth9(S#azL#@_EV0LkJakmw_ z0R`_j)d{jQ2|18aEMf%@*O*1u&A46up@{HG%&tXOUVPMx0Jr5;%_?u}VWz8b08NI= z48}3N>kg9zyWHoXghaTSlE8SY$YE$2v}>ehjE>2u+ncpS*y@}@GLvx8?vZ}ZylEA) zJo9NU42GMT__;*G7#W!Sc&gDIVmr8^zG*)|S&4-%BlX8ZnN2o9e8@Zs>n&wV>?OPP zFyj->wFO$-PBD=*TS-<8B0b*C1keI*Z}Nin$Ri*#2pQmgT0;4n3=I6pC+Sq^)ks9YKC1+> zzWp)0k$Qv|LXIfwyER{WZ+mb1oJgJ~XuM+hbe{@ZD4MAZX5iS|Ei$X;@r2Wuk!1BA z)+2cy&l)Zh3@Wl@q2Vcrl7ku))4TWodU?I<6zi}x=|XP98*`z{DN#kolSUO4;`F2f z)JXqH%ho~QQV0kA1xGY0IBf0iw!NxyH(P&wM5Z;3ER3{|DDd039CeO%DR;lB)IqYO zkS;~$g12e-?5?JH^#M$jUombx{}hHDqWR-t^XL@vyCG(xXc{Lq!L?dIUnKCz%An5X<9SvjBRCPWI|SC0WgFcHBpbtkvL=FxAsm%z z?@gc)co2(n^yT~)`~=l5i*K53*0nI(P5q8yPr#AoU0`4qk|q^pqK>-34?uB}k?F^p zo~-;!U4;?ZP?pf8+^;*J#)f~111A1d9S~{|v`?yRq@syxf`dpCZa-9+!4B{qs~sq; zmWIX`1AW!&i~l6`}_>;(#IFZ|-Mmz9`eyrzBKgXE`mQ zzagD)@=zbb9%EHbn(A-Ej3Qcae8c+K%lIu^(H-4Fw~GEH!u>7-+$8YTS^1$lqa*?o ziZX*!PHVpf6|i&cX(4axyLZIKI>2wM+~Bl@JD76mm5G9Bi+hPXe~~gwV>1bz9hIn4 zhC&r^EURjyS>_#*lZ0x8i!pV`AT1dxc^LV)HD6*NbmbqK5a+-47`utx3!xK!p2@m- zH0s1bSif^Eb0S%Q8XsFD&3%A>sof#{wf~k-tF8o-=iKzp_(^J*`?p?CJL^2U(Au(! zA}Zzbu7(r~JHpp~d{M`a)O7D*s^1*^U*v3Y(5nv9Bat%G=2J1M4(m#Wi-rqz+f1Bg z)8T98nIF1ECRw7^rUU={#uXg_<|btt(y6D~fQhrOt4|c$Eezkji{i;Jt=Z@}OnGs$ zX}akvtsN4EoY#kG@!EED**i2k2^6~7%*j!~_NAw@AOjROxFy$JpB|tWGhuqHr@i1m z^-gPXc2zl3c(o-zHKR8|VQ!yg&ZTuoqlrlS-%?r7!Jq2gahBS zai~9!o_RStWfUWcLKA5fSqAb!+wx38x)2hE&~_dv*5A|^tWp0aB4Ug{j9_TUB_U{; zCI7>3qM&IKrXGah-(o|!gMXr)htZ#3vtM{5J-kPZl z4OpM>?0Hu6@I^^3YVAhD zgS)ejGz0@2;*f)O zm2TFk5#;eB+}8RnpNp?SlyG#cXfq8zR8l`wN^G)4TC0ak!pOX-BS6Pp-N)pN8wj;D z9!Wz!?OpsNG<9Aw^y?fIZXVXRBQ=EKa0QB=V`0PB2npBQNQC^A|6yk z?B{s+%7OKPJ%W(i(K6|dNmhhcDNZx{}z}T zL3D#t>U|fqem~?QaBUe9WSPAXWTvpWeYlmjOd@?t=VST}E_4ptE8PuW0O>e1oXesrhiWxR;jZSJC{whwPQP2(WF&)*Y};0fwR5q0W)H+cTxm0U zOtX&?eb`67haM~Q6i(h0xxVh^y!Y*4+LBH$viwn?C9(L%@bT3e2}({_bR?j>C(q|T zuQiv|%Gl2^0)mLNbhq#b<^jxeAa%BQ)kQW>Tb+^_4+kj9;(vKfXZesS(;VA3V; zEbmETNEAB$RIj&_*EhGI^f~p%JH?sfE9B<}R@F5_-AA+!u+1Uq!NYTv)z~Zy`Mmc| zle|uOwy!?i+pBFv>G&}axD}L;b~t2?r_m!~shPyq09&-ZxB7fvM!lD|9JFvUd7RfzMGai1~hR7X7O zeGT(nRIn_Q14hYo~TRKu}IoPUk7q$50oXM!ychOgiEOH zJPqUKa(w?AyF!A;*ZtQCt=D@i-;Rl-ytqq9@}x;snH)7J>Z`&RNk5-7J&c4W0Z&9X zn#+I(qs)-pv;OC_*46F`N*#+JX{IlRq4yX-4vh(?TFPyQ)8rjBmv>m!7i~BkVGQ^) zAAOc6`XB>(79YreLhzna@8SA&y1T=yKORtUqb7XAGqT_>a`WS)iab7xr_G-x8SemK zLTBsDS?He()&t$J`gHnguiL^Bl5r&r)K=PTaA@58XJCnlW%G*YVpW-}p{azbYN&cGhmD@0RFXN`A}uk7JL*WdY8tU=xIzS?>c(Zw_Ll9CSc z%7lVuh9>-?$-q%zdWU}IAIBmZ##uYiW)b?UAc+~ z)8UZv<>}?h4sjv|QaLw^X7j7TBoh(jmfE>BigiaYeem5YrMYCP>GGjRM9Z*RhSi*c zfcAGmE-9D3LvUWNa;%FyG5I+8ePzVzwTOir4!89w|L|2wfHDpH6Sz#>9yeD3gm+}# zb#JFvK6`=-C4@*xyd@3!HmAMdjislW0y$iRc~RfcGF|x(hPS12$)NimkIP2{XgE|A zeHM~$?hR3?aU+pK*njp@?)vFj-EqNfkb&^qkl+bN_BFBK(n0>-0Cl-p1hY4xWOQbo z9LA@tM%pfDzhwUk)1GVe?Dvzq{k=8GJZ}lH`AP~);(eBhw;lS>IhmNHf)WZv_v9ai z4mYTI?B6>`uxL~h)E`c9hxA?6j*rAI);AaQ$WV#wM7s0tXW)JasZcoV+0LN1Qj;gX zy5yyd0v8JzX)DnywxCljDvCJ95VO!cLtsm zchK`TCUbB;q<2S_AobDW3Iqpcsuy&Rlt=LYGkqh6gWp74`ErM)ih>t$&{C5wN!AMS z>-r0m&)^L{B2-)WS&V8?&YBUeej|r5*}E|3f*E%mKo9K$WeDS{zZ|JAsK{sU`S49@ z9%%PMu_n<|CiP-p7w`{>A{25573F7F4d}wwjwja~kzt}vH+bYQcnGTQ-AIj>T_&%1`|0{c^4QB~kA#i&AlxgdKpqyT9mqS5KT z2x?648pivzmt^GEMUK_M{~PF$(@B|xi?-Rd{o-+UY3TG(KeugzJ}sqIlF<{dcvX4i zOKC|}8l0vvJ$j!oG`_`iGGj=SZ@KTk!aZv0uO`a0`(9+La-9;0uVpQOLFKe>KP+v> zsOv@yRUnSvQW+QyN}EqfO@_oZ^0d`ilod^xJL3l&JM%qwh$vOqgt-1Pi@}U+#cBXTt)>(DK`N5bq1Uib{d$z?5Hu%j%Q!~%p#17m;JMfmxuVm3 zM6GKiL`!{pf;%twF8%G&^LH2p z?GeZBiTJC{>?dQZ3m6kPlM4j|)|rA?@>o`}5-cgR-wgyhE5LQIGKGb*h`>J1u3f7o zJE*Q|wtoY6!W+E~i#$V&Sc>sZQbxYBoj0!ld4#vx$07yrBzsk&`KP*Kq@9?@4hdjX z<>7)kNkL=l(3;*Yg2(4fKwFO)xb~)0ZTRRYm#svmfHI1u0{~_KkKqN9&XpaiX?+c} zz4YL$$iB$V96KP_;?aT)mN3E9K`)o^d}-x0tmHf@fTaG+kH3kmGmh(^zXvk+=6q zp#i_aFTSXKqnpiM^`L1Y9Anl}%cT%d38k-T5P+6hU)$GG|CxMI{NM*?iZlfE{c2(; ziJ3ZHmW1lZjd;8Fts|4;U?bW}R4Xvs_}vPaHQW{ro@y2@YJj|P`E%PA&XIZO5N=hN)~5&e#EcmA?>??8?@RdSzxP*r&P}yawjMQ`gODoRSp< zDe1RxyBRht%+5=UzWM+4z4o0%a4hl>4nIM-rs&{D2KVY$k1L*L!f!^Fd)*yv-#ax` zLCDhmf1Sf?U(B`WcRnWB&d(MVASTiN;QF(VZ%C`2CH3d3SC)Iji%y((w z9y~k`x>0rlSm|MWC|*Cc$Qw?T!4?13crP%8C<;iB1HM|WEs<3KZ3Ari{m3vhx_`LiiI6TCSMe z`k1xiG{piuZk@MLYfQQI0Ysi9>LNe)x~{Y^xJIV(7Sf>yLH-%?2+_2B8RW`mjiTsTvBHWN^wg<2PgU^ycuv5o8HW3~a?|x1D+=>7cvp;{$m49xj=!Ay=it z-hWsA$e`Zhe+=h5o~DPc3XE`_{%EuH_}a12Zt1kRJ{&txa{kc~k^7z80(0ET{;sgp zRAf8?@bG{*iwN-6WQk?*Td1s5%OiuA&Aipar;c@(Jfw=9ZgTI>V#MuI5qd^GKrf%u)+~JdKnwXr5hiQ9ObQ@WE?0)NXVz- z$>$X~y)OZ!AJ5v$Q7|2Fx=W<8KUu?!j&gNRyZ*HXD*Je?#7(gx%B>ceopAZ>=lfEp zbxPJjq9^_}B*c^ccjwj9UGeUu|DKq2K&oZPDRt z9z2SDGZ^2|{P4svo3(!eH0{VZ8r?1Eh$HA!9;m^DbrA0mlF-IA;&ZlT?tsBZh~5!ap<1dHJm5v?J^iQ+cot{41WN z0<%lKdZBFQu`GBqkj5luB+6N9znfhYPhiA}d?V+(a|L6tQgR9!FWjl}8|Q7kuT^R~ zSoP-n^M6Q4elUW(omOpBzr?*Jvd!OV53G<)2TLFX6}Q?4<`|@%(NjfcL9ChCUI@MJ zwo#ihYW!hEqy8*tMvG9{#3DG&a4a1uWD&eER6+49+Q{YkpVlgcO7>d z*8DG5UkA4|dK%c?%19&%$TP2kK5$eByzF@^?EL-KBJ&rN0_;BD1PVYk6`W zZ3M!bf`cC+?t9a<3my{gNL!~VPIzD-{Ibs@WuQAFuTpOR0WbXdcWc}Bi_f@+c|3DF za?T$kyD>muCTC`9lbY`l|2Ts7}#R!fyz)70oLg&^y_wqn?Mz@1Yi* zoWXlOklY~l^w~e z=E84J-x|4uq@PiUEdW`G!NuY9{4T%_ctd9)lMQZWhJBiJq1fnn;gx@NSV5r=q#mi< z|6RIN(-B}5YjwEuojCY$WyMD8eV$}S*!W>!-%A3F>!|pD;M{*f9dtR|^)y)g`-%z7 zi%-_M4R2gNQ$rgsz4&&U?kyq(<7=zKaSy+fBkc7`AUVXnh{|tL#EIL%@&0QUnYO&5 z!+6yogW&_eU*I?)!_hCH?x5MnXIev-AQLHvGjU(HvZnJV_SDq)rmK$GVyAnW;WZ-(l4n+>>9;oA>;0Qj>3j7-aR$`f7N_O zPz9CsgsAPT-0ORmP!l{axqInYNUp^%If4y;6oBd#tQIMI<9tdLgrM4W3&XQ{c6Rzs zlvKW~(3C`w&pKTr#GQWutPEypCdA5NIE)-sy^npwW4 zV^&z8x|c_DgBgps*`X-WD&YKWQZt9KGlG5W$L6o-9a$lPN zH2I=K4DHdc#;07``PW7JH-ryARS;IJ{5AvN-0wP+IX6w{KS7#0?+l<Elf0?4zCsobY6o>agJ1Mt^HdzOxMvpn|$Z@v>Zxxl;|Lx<+aj*)Da z40x4ViojiHR(TM)m0@IH`q42ysT(QSLr>l^>5LWNg0p?-&&!42$+QM^223o; zVo|AF^SHeK1EfG(zj;YO*|MHc7>ZcY)6W0WJ4-%%+7-VKc1DzFmZlj-<`_{zX`dEi1{+BGj{fH!*?iUd zN25>_S&0^8o;7K}ijfq>N#n!E%L}-C%9{V@<>ZAAO$Xo9o&3!X-PH2+jC#RMZMB2L z>F_8t&!Y}2Pg=9Jvvyn~clGzUuIoeQ`*`0T6fg4>&x@QO^HMM$Z8~dJ2kYCTyZ=FR z&NJUM3BSEblPQztC`#2O4c*?<%oa(1VEk&o|9xEJy_R^9Ua&n+mNnL(SsqY)!96%< zHfK-Wm;OslbWR+t&p>>WvHpc`p6EEDQCiKA(W;Za_l(yWkUR$?_R!9T$D(d@|}ygL+H5=d3Mt!Buh2>EYI*7WAMzuf5+U;r@mPWR_Gsl=rA~^%lxjT zwI;M&)(Jm}%V8<&(0X52u4?G2+tV*gbvi-zFfikimYE+s%wJv;>X6Siv|SV^reIJ1 zT|VbGC+(NuMP=b(5~u2N<<3w9+7u;K;((w9ohBI+@x)j!f89op)!xyfO?c(1@WL)$ z2a>^vs$_Cf-@%=~TRC%LS|qJ^P?Y}$#x804#{H?NeWfK5MnX$$bM^MKo5nuEYtX(uCuErly;w8A)J&b>44#t} zM^0XL{SH6(Samiz9%|YHqb$xqghc3Mta}KO+5#3u;J1DKS3KSGhaKyo53Jsy)c_!H zCad$3AhU`K%y2n8V6-R4^P%_71Ao`tUvzXZr9rHHtG|EJ*Ilk|+o2s|*$VKM!%>CO zsb9f<%|T8N$aFb8vw;54^6_` zrp}9i@-2Pi{)hi`r=jz<)<_r&^9IhFhWb0lPiL5N?k&K?`OglBonR%=;{Z?gsAFq~=^54;Rf72r$M4-&MWkQoPS>DiPS&%@F z=6hd^X2LRhMWJ^3SHBKn|A%3;`1||P@eKMMo-*V7r+J;3ETOSh_ zqr{%*f6l%#O*s|bcY`EJI?w95Ac#S$&dsjGS$ih=jrsXQ`+wLvy!1N9PO2&i5~K4B zV9YnJ-+M&ad1cXs)%Rd3b+C*gPkyiWPxOyCIQC@Q@QV?aV|7NNG*(q9gEiFb|NDj8cGaucOK*OS@ch?G8fOwC zJWq`BeXf)@w4mSNWsTy$`P+R>u2Er^oM{iK7m74X1D28uPIJ9*);)I2JbPywX4T0; zG7^>>F7T6;xVDP3Y5vYPyp;Nh2;0gRQhW+x!3HA<$fm~2*K8j5@suA@(#oihmRB7# z|Id%EJ<|5}lwkXH!NvhL$O0e93Q#Qn4gcmfJKx@T8Gc`dP_Uzg6IfZIWR?oDu{Mt@ zEa_($o4DaWyt@7~u8vT{3FWqlm4tJ9fSs9R)Xae(I}f-z4sH0LJR>f>qMUPNR&&=0 z#bQ6xKdGZ@n)9`=BZ|lYUX^J{xdDwjtwfxqkbGrn7&}2rgfI z(Dw!=yIlX_q^5ridOEsWSa^|^d4m;Xoifl{ShkjNyY$vdy1MAFJ-Yu7+e??;z6z2U z`=Zn=bNzj9xa3TS$zkOHrPD0MNxE)OL9X>$V7Dhs_d5>XIB=>9zl*70AB`Xj6vK;x zNCyo952h7na#I7`ICG1?X{+X+v{{H{eqEFejaPXcXgbRLpCTbNUA=~3{#*R_+`9dW z4R1kx_n{E$94svsxTP56G{DT!c?E7MM(0j*>0t1~6m#j;j!E}M*?iUA`E+V2zatlp z#n$er6-+nc80F$XN%3EDr8(X<$3J&(&TV-;+Xl}9O58eAph>G9hYqb=1-@U==|Q9e zRZx!3@gI8Ec1it(;a5LInGtz|VoZuP(bbO~36XO6eEQ-KdQFbT{A3ussBxOgse&=` zna{r2(Q{%=I6Xi&6_@oAqM8-dx{2lV9e%ZJuK$Z~n|;%Cb&-G6kY4(9lhs*MU?f=* z7EG9vdtbusDe1_3p<9G9q4y=-ba`|d5_$yY9edyep$W3XVo8g@bkS@?B;FN?BcI!v zBJl=D+|p%iQys0=UV6oUjs8Z;`z;dRi7LEEdg;`;a)Q z*(^fh=rz-*)ev2!gRM2!zq#j>2|wg%b&gAFHeis=IGtO13`L`I#vnz7KyoFmt1 z@7?{~%|}nC_Q!~YmKf_AbPL$AoGKOVysd31(<|**fogR1O8H$pM$=C&WhWq-fA!QI zstUjg$0GdE4(YEb|29JUH9CGYi;^Fs?H93bm9AaIbR{#o^V6bIR?7+j!NrJZmF;3B z^dA3@U5@NB*TNAXcq8GtQ-3Tk^!V@Qg9WQL@LSxeztSqiakhp2VP9PK)eRZ&WKM}Q z^d+y@V|Ym!l09C{o(f!UbPueM5ITgIfXwRS;=*fm@wVvxU+C|8mL9QMg_jEb8p?7c zGl~hEIv9PsOaB6_MBw@auvvq$_3HoioSF1n_mH_o!^@+cxt2|&_C!JuVD*VsdIKVR6s&absE zxxhxC_9>OsEmrtfB~x!E_JB5y5hZxH$eD#K(45ThhHOgWImbo~4z*(oeDWtDEEVg; z!^P!T={M+k8wVBLh}Sz*IXH@uL{^hURh7~E=_DkyVdBLDb zKded%-A-3wzUy4c3GHdRaCPa+)I8;8h8NdMu;_tDDraR?oL zK=Q9+8A3m>I%?~b!2EFX{kZl1QcU~zvkJbel>N0crXFyL(Jn%LAtoW;Eya{)u~pul zh>8R2{{cpj!TR^aOaQL2(v_`nTa@&u6+x~?0vkcj!g4J}UPy?se@ANs_#=Y$ZAa+% zPAU5rq)%6?UN2jOoR${dcW8Z4K1+&n;G#=e{sc_9VqKR@D&+wCK}u|VhrVwTQzrod z0Rh2zh)tHhFf4+e4h=nP{B?iq=)JqhtowAZ!K zdES4!_mUTTJPFOop$~->`e4;*nF2J+^P<6_TNsujVQ@|__@$qpx`{qfbo?yjQ6|9z zIwuK~t^qD9>q_Gh@e5nuGSVJ8CwvGop^c6U{T}Z7Xyc_V-w@JGVxm_tk8Sd|IH`6! zIvp-bKNif*aAO~1rAO=6|O14DQ1QMUIMWq5F_%h(Kal0GOJ~x4b-O6wi1VQ{{0_}3)7TVrG2Xg#fqnkq1c zY#O|-qMP4d*kzAE$Q}NlU)Xkf!lv+Ni3MGgC{bX5sk7)=;xo0wH&&eOU4Pz>e|DOC zEo`Jc$-x69D~gIX&5>>NDcG~wFV{I8xZDM`)HQmJyXTc3(Yw=55{XGbKtNETXcXoD zjR>g-$}oj~*Z-rJJ$~ckkdN|0OrU9&RJOe$Fis0i)aD)i+a3w5uH8f2ZMPBD+_3+^dfAwg(EfKd^8R?X2Dkh#9W7*&LZ<^Nk z=962aTm>~)RhJ}&(pk<>rnlAZd4x%?i{X=lR-^r3h4A##u?Rb&(C;#k@2*G;OfT|m z@I(LRQ^VQ}nTL0j$MY;(aPgYN$OU&!qVuH5*3{yT1RMu^?ysBh=cU>)*r`L^%*xZ8 zVp{&4hUDsM_PM)y5+A(p;R%DjwZr}0Ut3lWcyGpIL&6&kaMk|Y|JAKyGmfnZJ3rMp z$>cazXQ%>St|8{9x7x#p!NmUB-*sV9b5{rK`7cwWOq0_ogA%k3AC9}Vq?dJj1WQUV z;rIEad%tS@>eKLUsV4Cu1_@DA4Ar9!DsXt>bUXCndBCMQ4fhIv-ygj%-dY>I;ovbO zf4HUm|Exy1+48S)bzN8ti#4sGU~jh78@B|qYA?k3{&)LA8<4ya_5l88mE*4~ECc6v zg(X-xu`vEPLM8zbeT(W;%~10xUl^iyc+nB)A7q$AWX^-JhQGfl^>gIIR)EVn0a{Qs*1Ag!3o!4KtKm2tHx+#maW(u^VNkLb9 zV+V*s{-4fXGV+K6nhOVpH5VG9uF;Hc3Y3`i(WIAloNk;3hqU~Z(h=NlJL=y&en_KH zx3mc+d9k=Q8G~m{hNo#$g!gG?%y?rN^X%H9V|eh&1N7|&98+`lkoaoSz2 z8wYDQ@CqqFYbK*|jKEVOdVIcfahxDiT#yFAriN#L-~IL_I~t7Y5aGru zo|9S1kPMBM(S^CgNEpmw|M{0wWB$AUg($B)5=36$bqz=+gT31KCXAp)pWjqTyLEw5bNKqw^rpd`!)^83?$b08h@7elst%ZL*~tsOYqBp5YW|IC28NY2ySCfn z^LLM0l$Vnq-bKA6s4T_nA|nMk0rOx zZ~~V*%aNzJa&tYy&3`&}Sog=`;WeQ`J?XN-@TIm$yj@@%0zTi;G4%7TEkgef<4ro+ z&I?G0&wRTTTLtE)9egqEKUj?}mp*)|>*aS{)v>6JlJa-nw7p@*sM^8i6&EO&5-B;V zN6*rZE$DO8(G4;CO+s(oXoGnk%si?2z0zM6REftKk!Dqq6$#B3#&ORhp&~AP3cO#Y zVgB^tSGL$zH|+e_WI0inO@X6mF68_ecgHyHuJMNXi;mqqrT6<`b7cftV+t%1qREHM zwNHdFn9VZsYb}{<{A<8z^48EAy$MW8R861>;<;yr!v%ED4eg%TeEhbrYN0n5w~Npi zRqc|pxcV*f*@=h#_x$koc##4xCOmhwfMN=Q#;ZC<)1py-!KrSwduAln!Dz^3y$&U= zl<(o^N-gu-jr?DOC!fU|yeN(l7+T^vLF8#B$Scl2PZy2~ZIf^sajX2N*}WcUZmS6#5n(Jm4D;yyZ6}N3bWxShZiKp1*QPq(=RtC z&*R7e{nWey*BtICOIi>AGA}*iW*3xk6U)fRoe;Irj>*SvYm@)z{qs`3y9J8JUk8Nh zpPFh&G9_`MB#RB^JYRMcfo z(ehJs+xd;SQcCkkNQ$w&;xF=k87xUk2;qzjCW2^E}f4MGnxu90!A z+Ky|aor!$982Sg{6EcnwoIe;s+Lg#7Qf~qRG7b{CLF!BT3&906->p$e^Iv#AJQ3mR z>dN1d$_t-&x58R6_GcKv^@Aoz9EVy_$}3pD))CT$?Vp6ChrS<=glQ4djbwLEMCuI@ zX?;H_O1`#Z${j7D-Di+c%5fyx{b})0meXEA^s|){9TH0UeYEt6wBHn^OIjJ}9<9rV zvg#9&=$8~_ev#Uh=ouH5o?6-aKcnpreZ+Fw7?Le z*iTZs{2~5tzS_SbtV=*zHw1xVXjL$2)Ft4$8xt`}cgp{A_=9Ul-5B1(rOL3J$ulYq zbOyai=uwR8g5o?n=dYZ7>f7vHFpu6L^N@8@*G$vrX9&~4SxVCQf-EG;S~aO$96cCLGx z3yg5(YCzF7>d()o8f^bgwZ&@2;IETDUS`1%=eT&3x}f3iPLS#Ohmr*aPFZ@PuVH?P_(-ml6Ww@V!lld394MkNYw9z_8Mp zHZgSmoe*gae;;}A&q*)k*Z;eC6`juC{=>K5J)aHz9ffc~m&0(JqN~~2nyOD7Gb;}; zH7C4g_L`npR(F=kF}{}^WBO2H(fPTXCoFvN`UpO{d7!hvz;oD2U6DV$@A74o9R4mT0kE+xAn z)Y)!bw`3a;N=srG#jTnkd)B!)&rlR40gk6wy%xcMy_Ih+oUB8cdXA)jt949{Zk^13E}&V&C9?R(|r){~jtgXj-kFU!i10(}p- z6xFF_&zcQJO>-8wU>=@VaNU`jeeRNnQ9w1^2OHeD@9N)b#=nIz=j2!wX;jXdY(;q^ zx5t>jJ<4*uSigMgvjy29zO;TZN7+Jbq+W{2_dt}QKGOAxNeO}FI*EKx(T+#a{JydJ zL+36~>{JfmB>RXJ_On7b*w^XgxJ7D2h||@}&cg52^~4JzSPI`#oSKII@iC%s}oMKYX6(5%VfI{iPklKcAEy{(JtN zz!n#(xk)V6aqkmLU+<6}T^I@Nk$9v7w>u&Xguh&5JMin2vA)r^Q>^F`rM+fGlyaws z)sI?X{=co9*7r)wKa*ml;{dDPpVbIIFOC)~Fjpy#j39qdx_uvCI{g!!kKpDfQ`&@= zL-)Q^82*S{w)tZPUA~E{KdyZvwKMvEVvOw$;kH6&Ib8(Y42l12g^wZq?_13G2Qc&5 zNaJxu&r6z>Qm>A--$qOS$JdooZdCF30qYmZo^ZC3$JGNEzLO$c8GE%oG@n8wM5#~K zz{pqB?lwQhiC*9HRL{VTFSvPlw97R(^5N`&MmG$GZ;Hz zZj|4@^Ic&D_kU-PN)8{WWVv0@@~pg(R7LiKSkW9~82Gr9>AmAC-+AT3XAwQ|*$Z=C zR$VUE>T5AWEX_B;13vA3di*f$H`x_dychQByZ2*r+7hv(nE@gKF_OlW8}KjbhJmGv*CSx08Q5nN~IWGXV$*X*FRNrf6+;+Gf^7wi+k_* z`~%t4o0EIO69iImo-P;gs1B#%VtHDiX@e3C$vF7$nA`c(H*3Kahn|=)IH!sE6P~-} zuA%wxoL-&8pxWi|oUFJCPSo^uI1Pt0^V1p2&OYw^KK)T7)lSI7d9)R3R(7lD!ceqM zUcejdn_GwDT3CneFSAx+vMq7=3SSsrZM}b4ZedHo+QM^P*C8vhPg^Uv1FiT3OYmn| zCm>qZD{P_L9EI_t)_V5&k_AvAHS{w&#|0B>zug6k-SN`SmLfmegJ}#?+$0ZL#>_1Anw#T=!`eU;Gzh5U@ zKB*9&PL1yWrgs+3fJIC6{<0TNgv9GCLC<0OklwIAbw251xe$-JK71N|A3`m?K24CD zkxU&gH--OyIr2UFUG(&Nyl1p@AU+|daVW!AY`3Z^lt^g|$B-0%$!@jo(dP6+V=h=B z*Y}S(7^8rDUR)2wleubapRpYw^Dzg{d@zEf@}epb8X+`l@Ztk!2Y!$OFNVvjSZXcj z|G4+96;%G^7MPi%WHziO;8~ad%uV`D#fxg;5bTD`NtmprkZ%-WsCC~ zF_8cN21!+2P}P4|5E84ppbDzsitz$y{?)3w{Hojss`i=71J{aEf&VZ2rjcyY{QsOT zRKb5^y;SkR*Ji)s)_1~XE-$W<=uc8P|F7)+H5rPz4nU@RAmIc5nV^)cq^q4POb>o)R~FJP}Yi z7TAcvvEdpa(t<=$vdq&WxoYr$_T8RZ8@INMY}=KFYt1=f&HtzMxZLA(t;}O{DUT5~ z5gcpK?nKNunE#{4nJc}Gm`$0U&%XUQ&i4@38$U;qJS7q|%d&(ZhIr#2s}DhAYzY%R zEAQfN*>pd)1W^=ORunmw6={a2(Guo#FD&9<46cN1&#u4X*7#>(BMs67Bhoa*iwrLk z49lWL{N31l>F26X>tl*=cxE2|W5QZ5R4ObPQosSG*qo#)(yB=Flp^q_n+=^m{`~f$ zD1B0tRHeAMe2>)4(&gD7;m$ZC?Dj5lEJX_nLs39^>ihm9Q}?Y*h8ok;+63b?-92EE zu4dRq=a^sK6uvIM-%#3_G{uv7{R7QBv$0*I5;Ui9G)W7hfOb)AsV8oMXW?^w|BX2d zyJ%I|E@B8pkz|V2C{mkQH{%aqpMT;ZF&C{+;9A!y`b`xKRw&_yr0yz*IC0zv$w_URXr)!Xw z{y6{9z}8I?P;d4ef*~+Ej@BvN>kY%FanCE7P)+m9|Gdx3Tb<2AmpJU1e1|>PVGHQB zXWD?oF$6`@G))0CE@#?Wqp5Yb$9vtn!Bd7|DHcz$1S_&M&G8IN5hyuH*KrSQ3HNw* z&uO>hj`p=fODGuxgB3+I3I;UO#Ye%I}##}qop^Ths4as%B2*_jcQQCDsbuxX6KiIlAHjK=)h zocrtU{e7(lPP2-v>2X%|=yN0M`)IB6%W4&UA!@M!KMP2+nHN{p0jSj?vD?8)~#rp_Ss{wlaGfbm8!eUOX#3v43{o|JCpCd@(pYm!{dX1A81H0|Af{MRGLA z-t;T8tJlk>7ML=8=E%=|mq}8y&*1sKDBpjjjOD))^)~dFr^i1Br{1%xZ7AOt%y^JA zWlMLsX(}T-*X^=pNv@&5o$qv@{ND##&6r_d?KysUL*ju4u;&?uCTNNfM4BK;kqtS| z%+pVc|6nK96Q1_p&1yn6g;Y5!%)ynU1RIc)tbw^I7^L;HJ5-lVk?GMyhSk-_Sv>(^ z6!td?PT$d}d|QXbw9fO_SmKFWJHR>IBup4miX?4$PT&f}fOZW9nP6(VBQM|Wn(+7A z=AZBWqDB*BEUieo$z`H}1v*B9LDWEKWDG`N8i&XMV!|-T;qt}x?mCY+oN)DfAF>vC zVOa}S5LA`o6q;0oX3y{X?&V=OCc_+{U-JrMH@3}9v&r9!8wL^Ka zl80`ZCx7f;jVb9!NB^Z2ElBkzEb(;#HbwX_BJ^k)TPD z3X$Lr)o}`@h)tfa2GEmUP@(X(#ARl{(FsY3B|V+6^qhl@lZIH~(yJF`LpFKdUv=Nn zF6M{_Z9meA@2$ zO{VXSt0<8lZgHg;U{jRovhEzuct*@>b1~)Z$fHV98lVM2)-;tzN42{JH)Edju5|L!=P2=$A&V2t~}w1E4reFd@l?V}ci>#{6iohQ?f3H5^Wu?;X;=NrRNsmQa~a34tY?nOg7W5nxz}mR zRnl+QDwwZuxt6LSsDi5qQYtCmiuP4k1yyh@f~AV?UsZJJYKP>ip|64}sDdh}f-0zj z%LLH|7AdP-sZ9wPNUaF(nN2TwvK;ke3a*1%6eTpLN|>>!ibP6^rZSAQXY}^nANM(& zlvtES9kgpa z)bgF4uTB=s_;q;{p73c^plOY!c#5V`Pxzg>ly4}EzAtV5@cn4NGkR~`Wfs4p<>7Vy zc6n;mKe)8pMyMJPAL?SFq`3e|OMnt6SyTz_*~2>;J>+d2zoLp-mI|s|6IVeMT>DV0 zIKRiE@K4@({4^w@pPM2QQ9x_7BxwPE4(zv$K596!!}nHHPmt4d@24w~&D5#JP5S?j z>ndFE+WCaed+;w6()qCquWYC6@vM*C)h>B^!!UeEkuotQv4Eg~M#~h~?|W37Fve9B z&8_|5!j?BqEyO9h(sQRLtQcP8O;7jVcYMCBIUI#E;khtq$I_xgP=Gl#chR^- zc{P$yP2Kqz255c-iEpFL=aEw8i@uLn4czOQdDHV}TF!}MK*5loyg*ZlEXCLd+Y^Yw|fC=G)uWd!FFiz@+7)4KK z>*Z$aJ`TopFPzx<6AIJN0vWFRSCJrTzv|zQ)&;=#>h(FM3<2K0*3=lKx&c(qC}> zL2o9VAl88Xr^lEfQ7(KT&{BvmSw_WG+=H%-8UeoaJLqMOW>-$t& zx;wkyL5@_^0m8YE8uiaU@Xmb=crdv0Qg`QY6H%I1g~vP=CA@Gsf0kus-!`iNz_j56oOGW6?K1o;JKrfxnFxX)!1c?lAPRr*Xqpk6zdhW#_lyW#>R-NL7@9!YJaGTP}=l(Tj~~hVD=? zSB7$-<-#=Xxe@$T#`~JTqBe*7#M5H)mOVdr>KHnv!4!q1Ia^n!Qz{rV#Q~hIEPI|! z$#uBg&V0q4>qJKy=W)(rI_OzcZD;STcNH18$6^YkDAqOr!%`I_PQE zY2SqYwlFvQ#rz$uF}y^nlq6Ct|91|++c;7-GE>dCXL^}fFk`ES}RgRJC|gs`k6&;*uKMNSsMtM5-=>HlMx zHCh{YGluuCe-7G_*(NU0f;X-nBTNlPhRGm$%>ktMjX(<`ZO3`(xfO@BdJBaFLGZ7|??^ z5IH7v_5|IT3_$X!6=o}r25|9M1|Z{|5M};mq#tUR@_+Qb(q40!qs+&s{PNbL3L?)l zyrSQip4ZMjH0X|tDWQ@w$)$@QB)cPa&VY73!no7N5iE=8nCGZ%(fnyQV?}V3NQnYZ z(E>}eG|zMzns=zdhGi*` zBA=`t8HaMeCp?F8#_awg3ol#bIga5-k`Wk@C20X|*pHJ6@iTRBeRtBc;{0`cb8d_r zrAz~iO3;AO1VKTgl%*s^x3Qe`eAJC87`+oVCf*W8j>%Jmz^b%LaEx%^%Qd$T`!hEd zQm7p@V5P&)h&#m6&D` zX&tq}9sbOoJBZ|4diRejS_eOSx~>76>D0)RR2BuGiWE(-AY_tGvP3sY&8A;H@tfY} z2ac^DDw++*!PNqXjkZmevh#sW&Yg-%V3%2O`Tp+7R>j31=SB{OLlY#;3k;)aK$}1K zw_$CL=UQtXwwFC1=*=oA|$Kw>mbqzOrwe}2p)(R1F6 zUR7}I%GUO>euo2Z{qSkra};o!V||0;NVLIE{`7n@dEn=r7eMJ#m?&4pb|t(-C_K-y zoJNV-bIb4hs8dG^N@O0POV>f29t=*uX^#AV@q+uy)9IV|oLTd42;Dr3T@DJgE!*w} zPAR+RWZ-c30|jU-BB*B84STn8)|0gRwRcao!Mc4Fc$VjAfutxw5NHwIr0u$B`%_E_ zXFY>o=-WXo+MY_deRb*FM~8es$s(uH8UU&?Zt%7(cXgan1AdS_#1e$BbtoR=bDqJm zf2~TL4_n$N9Aj3}-e}J1r+@fuL%k#@&Hqr}nh zqdEmyqf>_Y`T^;%0efxoPU`%0n|30k0D0lMnCA&eBL!L@frOJkw?nWAlNJzCgwEL} z7&gRgvUuCqShVxzhtopY^pakH35FD8jujbM9XyPAtmlUQ)nOf5nS=37hSR|6o#UI< z_m_!K<-4XOoJ^dPBvoN}nUpBlJvMbzUXS@tFSVEQl13xTpmBv{B#kQaj6~8sku(~!4Yho`1xep*FyT->sF}e9Nn?Zbb!Ft! z&8ArImR+;n{^y^f32X^BfmL1*I8~N7R+23V(;s-St7z#FrSvjdXjMu1p=^l-^W18{ zqYguO*=Sb=ET=w_jzRKmg}m1ak#dek${(&{7%HR#^1c~K4@c_N!GO0f?R(MhV+r0- z?^sT>?gQz2Ryg{uDZB;%CCWSvaQb80EO9{?1@w6vgwCT)A!@+?lDsWe-I4ryJU+Ss zN%IWHQ53}p97|In5pZ>3{ZMQ#RQLLC^e?D+XBr}XMZ_QhqbZfANuFfC|9<3O9UJb7 zO-8mQIFtG!W!;UWS!?i&cvx@$R%>+f^kQ})+3TzM1o6hjsN9lMnoM&t3uKPdjNFnp z#t=A@!E+W8z&dtzP(6P z(e26LlF)QCmlffKr8T?@{^EN#Pj8PG7eEa$-Zm5{xlXlLcJ7q!*^ z*I3H=Kkj`?1(kofeT15$o~L!Z)qWk3{%UN*?E~n?`R>-#@ZaI;Z;G<~DyV|W1fzL} zw@zs6P4modw7?cQR*nY$B`5&xv^ljv&P;wvm1iBdmxkkq#3toGlXrR?pUpuS!skZ{(+BkPq)n$3>va+7N0Wlz zU#p*O3J?+MYW?{f1Yhi93i~~T&r*Uhgxd`iJ)0u$%}%;HXV_4^ep^#;%*zel;cPd1 z{$G$is#zj36tIzeAS9ZSwR_QLTMx84X*J@O%@}cp1%u57JcK36e9)cAf~ik=d931o zYvyhB^u9q;zKradfg?#)QaPGaR8)_2(viZ`(MnyE^UJE>bZC*yz56oiF!e^jK?tiw zONtVs3K}hQntJI5zwXiI^h0AVSc}Y(bazyBFm9>f^Zc2(dbIKW@b|$jFEvlY-=_rJ z52!#?D2~u5nX1$3myT;L#1)BA--=REs}Di|QAy6TNIuoEV%2uuF*TPCKJ^4V;eH+d z4oI^*R2xas1Wzdp;1y}cBKNE7Klfq-N`O)x{{ZGYBj{Jo_wfqRH+p~mcjuDj=}jZ| zOyS8}plODYd65GY_$hDszaJg^vBzwd8Vh(%rU+WOw3c7*|5clSi6K0? zXIP@cCR6w{zUhQ_1l61cnU6n)JUQy8Bjpd0Hra! zP_yQ58*T4)iM#$9V>@Fj=sVnjn)K37ux(!Z0qZnAru^RV$aIT^(>w*J{YbL{R*IwCoC_lR8QMBwb7 zo151?3n&w7SD*07+YS4hi_}|mzBIjp_#WEZZp5)uM+@LK+9v#3apmR!8$s|2tMDYp z2=WehU+>>--${ldvIb?WM}u;HA8r1XoiA?b#F%(*Vx2*M&AmSI$xc;OR#XI8qXiP( zbk<D?R2xD^T(gxUbHNasfCz{a-#6wPvZw&*O-i` z)F9HFMrxWuF|v#*H7wX(h{Ykn1-u?(RB*l8qvUZN;Kn*NT)`5d0#X73AxYsk)^q0Q zu*noYZ6w%>Df&^<JonuHN|QUra81ZKFMc1LDLwp{@MIYghY$9-abu3PN*N?OX5WrI{3 z;eN6V*kz+|*q195P}_2T50}D=2&qgcV}5Yq*Tww57U`ItNPmA(LHz>DZ-trSmm7*H z!)@La?ap=2_KrxKC5;vt2}rC!i70LEr?b*G>T|Y9o$%?9`8&{)QDKfvRk%tXsN@ZZ}H)sF1mK zb@X@frrFc`bJET_FKvZ$KhGR)?#mM*|nAruN_Jl^#QUw>~0%P z(Z3F;)lfRWy@(>7)HP_LRVFB&rRm-QZKvL~ATF|5h(bz&N-z>dt14<1lF>5SvIcr@ zv(DT&{!b|BcrvuCfN6*l(7&+d>7nawB*(-o+%$4S=LyxJ9p^uoX8vQ)eDwda=7VqI zVDIeb`#t(&yR^_DEV;o*8pClar?9L%U{t%80`hSZ?7So8~gh6U48f1Q_xTL@tDw}7*W4;9PG?h^A+G6 z3NpdeLE7@Rw8<(1%0*LWL&h%5m23_r5;j+!GoW~LN%9ZW9t935$FBI_c%6G(2+?(3A^Y29S)*tE`N2H5S8uki89v zB|{onQW1`%n%sDS@IGMkGa^BRC<%iA-mMP=z_(kN-Vz+YfuLFEJDK z0`!Tl?@p{>>z_SB%AN?EE_Aw|K~{J3GH;Z*-j;jby7=1tktNwQD*%DuM2X>1N%rr0 zALEQlaQ4k+lgm4C$<)l%(9>-=K|>D7i8duSI~yqO{*nx`U4xqCu9$tEPl3gY2#%ig zy1h&1{V}ok-lA}x;T+M(l0-8+K`{(Nu1or7;*k$)r1ZJQxTN;`BJTx*F#k8JVlb~T z7n|?>{G&b(bnXnfidB{p#3$6?X9@eQ2={Dguj7z>9}*ff4MVqs0B{o0bo^<)BvjX zk?6){J;R&dbM2nJGb0-qE4(VQqyk7z<_;x4RlVW9mC4Y+cw38Ltmxe0Q+k?dQCUpj zz24q=x1Miv4$lO#0!uO!%P>64QlV<3Yo95^cVfI_-Ru3iA(L@1d*6R6e(<+X@pv9ZXV(PVFc+TzT&_aieo{Vg zxi9@i0tsj&L1+x440*ZpfRZ;Qr6rQZgK@w2(BZp(c;Mz}wDAMcEh(0#IY1a{<6BEf z6|7~X&X#h%kJ~iw_r|<`s?INCBP*PfT0n0O2$UMKb{$q+m(TOo`F+@?n}3RY&nN;J zFcigUfD1W>q&JCfc;|WB4UOIL>qy8MpGgZtC~4kicewI^;_d^o^MLc|CG5wqo_Q*+ zFUE7gK#Y(a_kRZDBcgmBZFvJRX`B!reGgZk3`{-H8I0~vjY_#6HoC%mQWcaBUPSPE zBpoe`yv+5^MKBH$k^0$nJObgH_UiZnB#pyHmr@zXdvG1H$b0uA>j-VvfGDM0F{S*! zV>$CHEi^6X_Z3ZNP!H;rg=5RuuVrm7Zhwd=<$JeXBlK$kCVjQy@{SvtPwO)7rxt^G zS=N2{a_`@bqO6aljQ^XWq`z0XpAwOL&E@RRvd)Ln_O}J8KZJw|?q{QV)r~SgqxUL{ zZdy(MEim$%UQz2K;Tpl$`uJ5r`TDV{KdYb$s-TMQRzVe1K^0tGbQWj+`A~`f?z#%< zZ8pvK-Zu65M~@$ZT5u)2x`k7@1eB)HqRg-w!8hELe(c2)YZ9tqIwM*kNbxP6=?({QqFS_nr$#ROA+NBgbz;hE zk)xmp0wWNVrT~^fRrxNI*=Eq^9ROAzc&%15e4}&a3AQZBHE4>%Mk)$NP%1%FYKQ^U z_SQxn?DyVg?$-00>!DIq2`!RBr6^Jnc!8mKo~OY6uZNxrc=LgWP7c8+G&T^b)W=lI z?={c^Fbufd8zzmL_*%Ln7S71lWn31;gY&RAefr*Y z(>F)F5+?yisgfvZicF(d;?rYsSugQ6{&3xm1^2@%@zmImW@`=`B}f_{aF)+1+_Tn4 z=RW56A^Fi#uky&yACVg2%m_$>0XCAVK!7H6xZ8~`|IuCIS{sLG+XHW}O6!}Z%YSry z%i13(SPjMp{eOjCflfMx@iSl=#xQ`-(Wrw%Dq zA;N}`Qh1t`6@jC8sW^m`bH#>%XD3dLft^vq5{zSYoM;LEe?pXl)oT=Ds#-bI zu~&II<|<8_P0PF z9@GPIo@7E42||-l59*NJx`-^Qi`bx)PkM*`_U(Y-2P1}4wb@+*r5xbOlN6Ailb7ps zXJ##ax_b##@B`1q1YxwGC%waS61JKTMQj-_$%4o;fn!TZ%b=BS6ATXR%=O;Je|rjA zeG(~(F`xyKU|Eu5IRPCu;0cdIOWNRFdGq7smq#L%#Iq_zs65GuvWk|}0)sE2q)N%OLJnMs9@?PFD6VY1wR?4aJlt^`A{d2+25H=L{M6dv&1(PnhK;-67}vCh z8)JbX*#M=IWI-7?t<#-->pYn_u8e&qR?vB6Rg~{djZ!3t9=mQRW4)qPf`P8Z{ud?^ zK_uvSnbs5>Cy1fvkT%GBpOjH+t$794_i`IE7O(wRyd7K4@A1bESgSIIqzF!+d5)rK zN~A+v3v|Q`Z86XCs&@-t?WLtBBOC6}D#ghvOY<5{qlP=?nCP1Gs&~;8aYVi+A#?*& zb8HMj|FQ9?W7~-}2~fOp8iQd#aunnH@ii|YJp37()D+M4=g^d3a{;$II}mTmm7nbn zu%8>Uwmievj7w=@aA>^Jl9HgHYf&UtT4ESl6ag4|IBb`Sinc!JLl?6f;FL5X*M$J3aV8gN$^tffDqgzb6-tcyr*gq%l5R4@^vRde> zW*OS%k{ouoeHysk;RKm>mm4^NdMOBOV1>J$f3;@TO$VD;cI&xRl+)1i!g-9RnYMh-u+`XRY5t{F|NOxl-O^R!+iIK_rWI`(m(Eu zsByw^nyN6QDsn=I#>r1Qqx`4-;EXjq4&}Mt^7e|^*!Ja2sK^n zwO1fIdhdM6sdmY{Yg5N)sQ%|rd{UnNBKqiSqkz<@6--HuY9UsXdIrdAIU!~ z#2&pSU+U50VGwy!BXiiJaUFJ~T`%>>h3|)?{b|(gv^T->80}pwj)w)dW)C0aDdsRHNY*+evEZ2jKzPHj1U{T z4CLhXIp02~-IvKp=#}JK82woEc^4$o*7AoVch6gQRdhEW^uG7yV}IRGlvpxwau<1xpttwEv*QQLyhZUs_aZd; zrE&_X_EwUR>sM>Ve1~(=YcYM{9ei}z4_|U{ovexrw@x$w0=k%rDr;K$?B5O?T$EPC zIpOFWfc*~r{q)ugc&U{C!*=&PUWmu(Zt5G?3x4j^8FBc$ zD6N10m5$fzMx^Z~9PIRDUjEMeBoGz@)$^VW>4gQfAo zbfXqBZgAbg`F|6pog)xB1?gQigbrGe`CY^#ApHXA0-Pr&F#R^!fL{DiVvmmIykNv7 zec`S7dFrXu&CT(gZ%L~rL1r~gQzeODB=nGe!V-)_8M3du>l0RWeJ=|x!mMzT98%FZ zf{{p`1WeeNzcEo_6DEzh9Lhfj8&}-WK`-&gTmA{^Os*_n<|?tNOq`tLsi|(`|j8(+R}Mg@yP-y=u*M z&DS=1?>w%*_jI+Gpw4&d3&O0|qlexrV@_WtJ)BC0;z@SQTj%}OyQy_iD5f$B%g7?D zGL)o!x#hy>7QNUQR5A7U)GN~>{rOgS0*?Z*m-B zua8Grv5zj(9&_#Sy#IRHwsl{+@8C_LWCsasMcQ;zi0+b-1#C*L!{v77EACt;N{Rl# zhEy{9KAZOJfP{IF555n<8aTyyPb%=AMo9YGmGNy8Q|AG*`h0eD>)GE<=nOktg z5{$1jT&P;|Rd5Zgc&qQ}ZRxoa+J&)l%~qW@;ZhWL;IvEG_GuvCJ*nA_QEN&nl;`O= z!59tjCZ5CE@5cJJ_PV$CO+E1DHWV!qEYGu|Ac_Ldqvkd>F;p8C5$kL8^WT3gFh|nJ zR{{ZTIgVx}i9t2;x7IH#=}g^V9MX5D1Yf&<{+_?KZ-l0B6v+z&O^B38iv-$~S_npq zNbudgCqK{EC`>%65<+oJRalNw8CGSW*bFsho!KWC&+8Oi;LJYJaqtPgIlcbv`oelB zrJowELM$usG9}R>0YF$O{mIzqu*noY-3F;0IuoiF{g>!#)xO<+uz zMvCkg&z#)%`-XZ&DkbNv(NXeZwz0Y|_xBr9nL&{wDO0jS@Fc;~lFGlZe@AA_YeH;1 zQg0lN7Vzh<6yL%p=l}K1m^A!hAW%nDVHKGWG)f6^#bv>^zm_lkesSN_Chd(58bOiD z20L>zol;IfmnkpT6%q_9276&t&(}V=eb$FJLd~!EFkCuA(3f=FG9C75pdTpco|}^= zIVE?lGv$$*kFGr0KjxOpOnBGn7+U#kK(HywG=vx=-*pvSPYCe5eIN4wwZiA%mqdNv z3;tKh8qFh$<#<)mIF%429+2o1-Gp5cX};^^?;d=Mj1bD1B?G~IX@+40h7YkUU7=sF zT*RKYulFtP{%)%$Uc`q)FZhbHMW09 znkQ+JWkiM}X_gJCeU5&~`W?3Rt$i&sj^FlW{UXJ;#BelKW@$zhWt!tzs(-h=Up4&w z^~8Zut{%I={|&CP#dhD0IMaU#x*9kuIJLqYkvJQ{X#>rg>Geui-*Gj=fBz;D753%= zQ<>^*G#~9qcRZq+a8i>o*0`yo2RlcHM=K!bph+wc z6CB89$0PNhX9?dwsM{OALBALYCu$k||4=C%v;5b_0fjpH{}1SzP&eupJAdivibuW?xC*#?c-e+mBomBh&O59SO1C#}JB6-Q6^-(?PZ z|FxxvweBy4Dt>+5kGwCu5~bDk;V3WkV0mqH#j&wBKnY4AUXU03|BAiA@4+-2?-u&J zh||a0imYGw9`1j51)OBC?vpUzm3H6#{{rZI4R`cR+w>xB<)pjd7zh$Up9xQD`s;r&xztW$am4s_l^ei>pjf;hu>L|=c^Fx zM&6r+DQ8+K*Qp8fz7>Pf{}&?lt0=G<(4EBy{xnK@2=iTlVmxrDiRV9I=ukFxb-;=k*S2^t*t@xww*6d-2RDo7(5+gJ>Qc& zZ|j;P+kwM96ez$x88`zRHgLLZN^VYGz8m!3l9AE$u~tQ{@Mqj@4#MY#hF>PLb!EV{ z-Gg!6p85f=d+_@zV-9++ntl*{MW-@tb%Dk627*jzsdzUewt9XyVI)Pb=n@J-r=n)P zC$p~$evZO!eKZRYV!^QC-@n@+Z3J0e!!vzm(~Z8Q3t!ul>}`?lJm{8EAZ*!zI9vmP zyQ``?fy`HEeCOO}caL9~*B7lgq(VnaVVs3PH-w_C2hf1i3Dg0(YJN6I5BMa! zq`TsD*<6a`7^Ho4%ZDF)w0%;MBav5i&r_#$*Mw_yyh6w05sX6P8s@0_$!ubL%bq{9 z;;SNUk@3a4buL|ZqC-9jWK9rAiPl85)gv>LwjQ}D5t>2{TW<}*5HhD!k?$&~g3^;N z&NsZ>q(5hzF;Isg7>4F#o@SX4o1Ayso5SYK`8Lgb^YBMSv6RB-!Iv}_CRK^%DV5eJ zN|=0I!{N8&yji`*73R!~DOYFTqu0MtxA|+(JnObFTEP@UUXliObrm?2KxhRp)!pAN z2h2D8OY+-3-EN3gEn`LQN78Y6my1z;7i$hLk~)mZ|9a&4`d{@z;L)1S(X??~%fXN5K-2WI|ioq_x9zJ`2vG&J* zqwzI(jA{obM-AMmL?mq8vvVDwq({qcxw+Y24BGokw|QJl zMN9Hiqm=AaK^0WNwFjwHT~Gy8V7Q;9M)`ee1+BBxs&!ojRZs<0Pz6^ZMC`filGsjtoo46h%&;6f=-1KiIs<>$(L^I4|_NNI`&}M|w$$qyk$exD*(!t1~k{2RPiWla0F#PTsaO z2Ijr)ba?fQE&454Qj|Rf^XubVS+7BhzI&h|aY^@dNkas1(U?9K)hQ;JGqx zGW7Gk*7fntZ!c{W+Tj8FYMu(RuU2w%a_k|IiW}Dq#-XM3_g!3n{NVOYZNgMvH_C+_ zP1t(b9a8osXa6polRdoS{((OKnGZhoHfn~SK)LdSfT8%HDGnPeXgtaA8bhlR>Z5Co z$!Pjy_%cs0m`k_O>J&>CKHU1fKuHj+{ZO5i9#qqrH1+^??x+*@?>i9**>a?)Kf-x)#Ma=wozG{bz0 zU$!pKKZK>Zfa6#VD6+tc6iRcgr6llX(+FSByaS^Di!g2sif0O^0>H8?Nh!R1d{fuG zXOg}u3fA4Dlmx?U8tJ=j&+~t6{s@l!Ga*Ihkc2HKSG8+);2f6QD_hF!V5g~m6RQ23 zx^HDNv`Dv}joeHqG;gg(Lph*(tAv*i|3lt?8WLMQ+k8E(Vm7dWkLW${6t1OXeRXg7 z)RDHXF@DYGkQ^6~MB5#i8H(iS0hH{E7cUN)yM8b<{V1weEOxz*^ZiyY_u#XO;4GUO zt{jrKDS@bhJqjGd?O;kqeqJEBJ5X(kOo`K!JLZ*$5n>SIu-pKvKF;^vsgJCCT1G<# zWT0^h!D}MR@UugE;Zks=3(or~Mv^Ihtf)yo>;@YdMU zRn}@!eG^{o@y{>b2B8bA@OFn|L`q;Oo}~qzph7H^b)}}d7$b4j*RW>69f$KHT30h7 zpea`2XqF1GuHM=;7L)Pu1$q4QhZ90OdBPMMC2;pOvc5c{(-|}D%AaF{akPNG|1;JR zW~Q&bdC}vyzX;s~&k3cWppk#>Ni#WumVY+Sb+O9++FmBHO0Yk}c1fn+Ke3J8;Fvja+3P<0@J;?KT8s<9gVA z7n;@Xkv|_^fPWrlxdtK-@WxaU(0lLLUDIw z`_{LlH~sua1ou89O9g!Ua3sMobcm-SsPHftTZGFuXX}KOy^6F?g`g!w6BHT;+-MUk z&U%XPnLoy+y!99)fqk%t0+zBBMI^iwtrrcFn4>Fx4AY11TfH_ZhLxD=V=vCK3~3ZG zYB3=Ll@e4@;zTBF)Z(+|$T+mz>AwD7txt~G3%5ccoC`;SwAmdhmnq8qXZYFDr_PqbI?Ml2DD=_;2dF{ z#k^lL%KXijjab~{dvI3$%x1O1xaxv4&IuIT6uauqvI(pxOEeDzfslV#eWF*pF&QyN zAuLK?rTcv6)q)#d{wlJQYm$+9h2?36%HUO`w!V)4& zFf>IAqDYA$H1h?W6b{1{G0%6j;n2zFRmfD%!3S|5C7J=6#8UvUyStOmo*no>%8gNS z)6qiha{h1hUF}lld!-Bx<0G63EI~_hzArUy*t`+%7BT15A~NSZNpTvGSd9ac|I^17 zJ&}6E^p5`N6BwPs<9dk$kmt=VQ)kRAB{SF}rDDomh@>BB5zJRa3#GLZOSwuWT}6Wz zv+qO;sC`PiHdXgFRZs;+P_2^lH{yGXxs8RsxYXv3x7)W0WugLtY~AumAZ_UmH%;}- z27wE!fd0EvQrxaq9}J&f(yJ(hgs973nkP^KV|? z)?E!|-gOUTW5b4FlSPqZC5csJNyT}by=$F02>*mQ1cA>@C8Sc3e7epo`tM<1-0RB2 z@lQukhGr;+7de`tS%wW2nEmo%p~xo*Idv8H!eZZKg;(C48I43&q6H4nn#8H1Xe7~n z21$=J7?)=--x?4v#UX!Up!zD1nkxf)PcMkdwddd1Pa?l$-T0?$Pl&j6BAK9JLZK zcNLa0=64-t_2cq)S;K#a!q%AJ_iG~0QPJe>2p7`|bGE!4;p#py2iI3L;Qruy&mrH7 z6->ngE`LiNG3p^Q`xg;1-_#U2UCW?d@Z#gaJD*Nyp1wdrFG3Oxs63jE!}Oa8bM!Y# zTMc$OqkwcqY1BU+8dw-5dr%60&vr_E{H2s_Xn!S@vwti_^<3fmb+^`1yCo{JL~tA< z%7h{ioPv_20a21Ai)p3r&&IzfC!WD?LmbCbydVUE8gir%vJIWqzgUBDalO*F_t?6g zr_Nz_evx1)C19MWh$6~pKv#fi1_!!gGP}kF(toV3aLXi{RKH-=OAdo!I5)8p`02M&Lgp52I=Hm#~OZd~i19#op>bteILq{pd z0X$8jYBx~h>jJh}8-1U?8*}h%t&aFfr{9^dtgXLY18#c`7z#4&xek}j2?7__-E!3e zRK#C5>YZ_m@1BE;h@N}lAdidC(G=3+C8n^nm|=j-u`(%Z zR9wO%&skp+Vj-8)xSVzAgUmZCh8z3!ty;2{GOKMomYaIc_xqv#)9M@yqfS@6U#pTv zvXm-QfRp$89u+5)P(6q?{8e%HJ?|Ty)~4%8qP@{x8i)?rKj16}I0I*JsbEBar|Sc< z^MLcf=!WYXCk?T}miwYE{zc!>WgU+H*q$s+@t1t|3&@{=*93fFSGl1U`q8m=DX@_swvOizX7@mfoQJTXh08Jqnl>iJ!qI_UXl(Tn- z?~A3$F$XSVpLYnPdR+amtP=Ar$d z2jjx>$Y@}$Z}n;Tvnd#duBBbRSNpl&S^GGCqVu#saw5eO0!t7SjZX9o%or4}rQN=_ z&Sxy@IvgL{Mc_%EBALK~5=k<|?bTpi%W5^|e6ictZsw3}HSU168bX**xirbzrhx41 zevaHJj*NmFIXAl>C>W&OefkhHxn}F?%`t;h+W?ykzB5(Odsp8!zSoyBacAzAb_0(< zG~g(Wl6ip(2?tagoA+D3*AK5){`lg^`WOr?(0~#oNhFB7w|};~q#{%wOJp2c=G(qx z!uEIF*6<>7KVFiZ9?*rdk)%M093v{6qKx;6pUpUEPR5-`p}t~i1HgB{Jg-~E;j0_< zCh(50w@QpW`F0yb@Ex?MMAoKAjzFxiY~Th%^Bn=2l#%BIlDhjyOv_Q}Ul(mx^)a~g z6YpK$^&c(B>u_gN~UAZ~HCi1MzYcfNK1T}ratuxt!BNOXk#Ex+W z;>hxXrJV2Mdt|?F(98y_xBP+CC}(JbWJ!i$X^v$`bZe@iUl4qRovk1G)@(fgR&Rf# z7@HCyD;%wAJR#8NR&M>V5G#j*acD`O`tmNkd9d0~hC;Rw&is;6CypR@_SCF^_=3|uKz$j`*?^A7v^bno=rrloMvWSPSpW)+bV6ftmV%SrGsYg1=lxUi(}(gj6k8KgNF|Acv7 zRzSZ$j!YKRYv3FtjjDK|m$vl`m=$yos~e<^F3F6{Qi{bq-}`38O?s}!FAYNvjwRHC zX2HqfAvagYKzO1FJ3G6SKo7OdMa<6gT_BI!cZiVV+!5<6}W+`f3Bqd zZ$tk{osKWD;<_cR(r3mg-y7Dk$tPnYURw%+K#D-%DM=*I^BUeb=QrOfkIMY3-V(NHJi{}pC}|83+JAD+|HOT*61wSMpe;+rkR2$MnpVp1s+aOR+=88kJV$jp z-a;^}FEm^G3wd4xlMl`rX|9e>7%=m*uQ8Dc&OD{$Ekw0Tzs*5Bm&IT_VEVl8*Y762 zwnJ@);RGel_o=tpD2pFxV6=f9KrnAdJH zP^G+dIG0t46cv%CWsci4VAuN9Q%6?jhL78H z_yFXh&V<=;S(33Sx!Ku3aSyjU-T6{>U)KQO&dOC??{F7-y{mMzHmoG3{z#OEI3qLZ zvI4&268-zX@87cDAt>DlV*r-JGQ3TZ@?D^-d&WmE9PYL2Tr%8R?HD;ufjJmoZum@J zGM~hU?ocIJHdJeFI(qsJLy=1YVu~7~^@xkpZYn|5Bc)LqR6K}D{`tordZP9DTG;dU zC1os{S-8K)Cnf3ISM^Fa$BJodivRQvx!<`r-+)NagW|6cy1`lZT*qYKblGzqU8c{O zd4G#flX{kNH>{57lgqs;S2L(b{Zbx(>Q`_*YhvaQPNHGdo|>ks3yL?`n*Q&cTg(YmnnQZLv*sI1?Laj>q6c`^M~)5qAcffuY{WZsa;yW`u5vUA~-(O zild~_ufz)q#VCX*lJe{w-+Z_K;-UB~(|;T0T1{0@rL=q5m!{cN%YS;_xhSKtZ)WgdBW48k3cF8c=Dz~3|Z@YF|t#_Ix!EWz@m5IE)v zA|0Zrv+s*SQu|(7_#ztk+ugBfY{HH3c~GEJ&7B`S={qhg}_bY|eBrF4s^vA_0@BZef;izs8)D4A6W zjn!l|L?`M=tnBLsfBnAJd)~e=qShT9r~V+6)TP5W>3uXyF<=~D+r^!z3$JSPBNB>@@U^INO|4qfdx z`ESX3;7tD|c!4&_5)8)>G{=b?NrmXRch%d`2s^t8|2q@+oik5sj^FTuoQrZaPXmP# zSb_QU;LY`d-1!-d6D*YQC*GUexfTJ7oEo`EhNom&l4Obj^8RsWxRuAbm}q^Xuc|Pv zlJu{i{O_58+ah>X7kC-a0x78k$)F_Q`fj)Zp!_Y!ZDiwbV1^Hczf-U~lsZ za9()1C_^)|kA9vp$`-^0x#6I`hN#xg7?5hTenGBcz1 zx3$;A9JQiEcTxpaRyGG?96Vpn@=_`&-!DvMdmk+aPjp%TlBa)tx~CoRtp@3(L12m`T`pnvvAYM z4V@=cuVpYjalVW!m#ulNmJI5(*n)}V#EMZwaxy->-sa!?@0cWJVOnS@CAl1pRb*Nf zB!ywnGCCS^^gaALdQEtC^MZDvk|M-0Ja5YYIg-O|S1vI<=?*P7td`wr1fu|-55d-{ zr+@J2sV^>R2`A4IyjVvjWm)8a%FC>D!_W6W`{+2y47~=7i!$_ytx5SN#keNr`wsVg zwE2!H=O$z%(vP-WoHn_|)XTqoOTmVg6Y@+xp18 z=kdzZimI_Hr7$8ztL2S0Wij>fZ}j(D@!L&lcwtOnk&+Z4Fq|d`B4K-R`7{6StC@iE zPVgB2$l%H$T7CSlw6IQH{ChKeO>=0Gx8>w26Gu3uyu3>h_}sb9f?tkrZuI`~Z{m`o z6vUO+ma&)=r298oHn$p(f;~#o3@xyPKyfU~&_c*{T?$4-T3kY3KjAtyXZ2ZZ2_z}f zB+aul%`pVaq9xpAEu1UgU_FBk^tV%bjaW-YWz0*`8Y2msB2kKBWXx+dkpGrdo?X%T z8Rdf)VGQ!`lg7S&Ffp>xg(%UC$}=iSDm;3Bx~_k;vaO;1(f_*;ZwC?W{{&K&7=@uU zm0+qJ81r$j3v=VF$ojR`iI8ZW<3fwyutVlj}aw~6=V*ms^^S$Z$F+bwf11O#sMVg=lk|bFo8aJ}z{LjZe=bql$FrE}EP=(IW zi@czt?mQDQy>0zVyZ6+K<}`^O@r=W6W%;xFbvb|H!^o;4G{9(Ge|v^9<47>O;^UCTcIYFC%b zp6Lj9(;qHn=YvA;MyQqqU21~>hkxiTx%aof56k><6fH0`A@Ce6kW7fz-s=rZnB?~) zj~RHhE!?Azhhe%IiVVm=GM!S+P#~$Uo>T4a?zt*B^5pZ&x;2>@-=LKI*D-^%2}91y z8^h2D@Y}=;f0Onzwv5&yuP%;Ifk-G6BP$_Mw=GzjImds03%*OgWlcjxQ6-|z@hm|T zv>4Dq!38mBGCCM5!-I>2&mm+X%|!)0OGgE#$0PsOHz0^GJ;%T7uj>{!x+$WAMw;bV zj-+XYRv6Sl<6GunY%+yUmx#LM@80eAr0MUp!EdMVYEiKD&mJLVPXta^p;p3R04AcF zD{r`t=KC|_&-_fKL=%L8)+mNnD30PaU=)O58IoRTFo&06zB7XA<$NE%b{6>ePM?@L z?iP5ge-d*iZ_joEr;QV6T9OEXVi+O4>722P|J{(>(cn_eg!%tU+u77C=XW)u%(t>U zTeLPsCqOfs9`M(h^=U%JzmSc7E8G%S6p`W>Kv1N{ht=(E9VN!qZz?hes~OB$l%p$c zgoEO;9`uX$**6coJGBI9h3(H&G$#NyrzDB2B#3|{G=P%s<86X*1LR)#IHHwe6n{o4 z`p@IPW9t0=NfY4CH3!2N@+>PeiXido_ur5Ft7F4mvC&>l##Hco#WdCLPhlUK^3$w{ zp7<#sYcwMe3Pn&T>z<`+9Kqh(db(QJbZF6w{THnt+PH@B+959-TO?98#R()g7z!rk z+nr#blmlFOk^%xD<)5)nZ`!QS6De?#LGf@Ct`NW7s%%$7C&uEsYl;8&GfiLr``Z?H zCMAbt0w&ryd6MED3bYJ&t`qdPJAeyiSKh?xKri(l`hw|8+!a;u3Q81Jn&31}WK=YG z#gFe=qnrni`u}>Y^Ji05N9Mj1nw0?uI6)*rxUbK!d9U(MzkBw9_nJgLc#Eo_DxxNn zj6$HiK~0NM{U7tMUnwri>`*)OR2&@Efl_{1KmMJD{mpP%9@kG#rPY`#F8c}p$;EHq z!On}?rKTowBCFD@ph~2XOU*RBS`U_TV@bm_XF(Y^ss&LqBWwMC5B_>_`Yt%!@nOR= z31lEn2}^Q>Dz1I_+daKDwXQLyf;ZQCQNGu(g37Bb=SKfup@V_kEm|4i)du)m(slkl zFPX1Tcr3!9Du=e&UC@hVS49CXSI?<=PT&f-DL^GdOUACUl*B0UTIav-^*$|^KO9ju zD+w$?3JOptiVrKBomC?kqX3@|iN%r7C;jIfH>CJMM1vwp(-@HkBmfE@y_4k){tp-b zG?&hf5FL;cD30Msk>*(@RR90~*gNj{rmAj|?oue5f>QQIe0!iGdyg_@X?U{@q)AOu zXaN-kl;y-G5mcs&p@@heC@4-uSq?y!ih_PJ-3s{qLf>tho7{{JTJW7edU{{pyZfGd z&bjxTvsL?Y7SgBk+5&Sc`uMPhV82cb=G;&bC{9si9Ft@QrCw+cVR-j$52ZhXh;X$i zPk$;qUF>>rUBNRmrgdN72En+4`18xgY0NBGt#(OpDZNz-u*&Kt3pp;u;q1`-VxtYz zEAh>)QEK;B1(UDh`MRY5E7bwr)Ia*Lyk(DLEGVatN z$FlPQu5nP}KTwaZ@f6}MG0~|b!1q`CyBEs%U5=*XYNy@CyVnzZhkh@{|2-H`zaP}t z-gVIL2O)NNU6gugxZQAj8`i2CYaYzu?FC;9eB-lM_J@f|M^hZU!=t=y6)(p33vS{& zYhVqAbT9E;EfO0)~maq%fS%a{TA)A0PWS=5Zrt_;10bD$WlVkM7Qb zNyeqFV`0vIM22EmS&>D7C*Ya$Ts2hQy9&&|p1FJe;4lF^SXISjoaJREV4s`b7`5?y zR?vI%f)~5J5yT3!#96#RaEgNQgeaEqYCX@>c6)M@3rRpB3cC|EAkwJk;nbCYUVK)t zXv*B#4~k78p(&UP29jbJjHG-4l!Z5keARMMh>K_-v~TAyzefwG?~a$#Bs?CmUSn@T zeAa?-Gn#}nU?xaX;&DphF@X$fO81O0G!C4=Ulj~LcxagMd`PDryu^t?+hSu!gUPWj* zQO^0nC4#AH%}*~EL0 z+RgtJ+})_#zYQM@dC1C049`%6h|7!sAF`%E`m;ntIe<|gJ$=#X$&(v zuiU2V=FGQA{alJeaM>L`{o11R-G4q-@JGUpVe|&q8x*tnXA4^7y|H8K4I!*gFp4J` zo+lWLCjvret<`#b4}!ltTQK_gccY2MMS&NL!RDQy@|+?I62?m6@3qf&z2VETDX=ma z96R0)$>$BUS<+q8spP~*_8sbb7+@z5YoN3h&JSAmUuuCxj&##Vtj6dLZQ}oY48$kG z_1UlKbW{z`G?mXBFdEJkT$n$qUAz~_OlcuxCRtV`Lc)287Rb&0r`^1J@xp}0h|R97 z#uydlR64etzC&MTI$yAO_4Fp4@HTE7i*YHJ7 z29p9w9?8%?V=J3$gy8VqG0x3VuF$(%7IfXP|JUD=(*LXR>nmtx=%9lRI#hMKWN$cl@tO#nH*s>1;uble-wPQyifg!=fTa8(ZS?zo@G^1;%T0hDb)R( zaLY4tE91Wk28i!}`B96A;#X2M#;L3?#=I=S@hitPF5L)&?@If9r1@8LzUaGje-|tr z+9&DW6QHLsv_-u{sj4c`zAIot+R3GRmWj_)hdr;n@4UK85H1g-VH^&caiuFC{th*l z|1LOo)097*q3w|coWw~2lNpMTQ1-~D68*8k5Il`uRF?{xjep>lo}VQJu5&rtOj6u0 zYfspG;hMt*ZV%^GBaloQ+CU;Cz?9{8|^)?S&Y}z;2_&)w0h6f3||~t!UqKSvPg4zPX>d+I2_hWV>o~w6sQ=K`+%S#E&CpK4n`uT(w`$SHpe+!f0VW#CTPEJ(DF;ybe}IZjO_Ov-a^vV{~7;$!Si0NO@6<>uT`kO zR*^m0fDg(p#OIp?Ps8s6=%Ur@eMl7yLP-Iy6!7maK;OgPm7e`Hxwm%N()Yw|p>6AA z2@@DzWkp&f!r9h^)9j&Kp&6sDdyLz?(mLj;4G}OP1j(opC5RG3D<}r!?yIbmgv$eI z7>5J3ls*LD@6h{xlKW7N<;|z<3mLbbW<-flX-4D(V&&2;X|E366`O?E_sa_Cs#bH4 zJ$do?ve`urxzTe%5LlU_RaT}H=IE=J#!7(NMCcAPc@LhCHPZ0kWnAf#GGJ;!eCgX!EEVz$eT066GjnoFIITT%@c! ze;)Vm&-Z;@1Ds*SmePS^xO)d}Qp=VbhSO+QEeuYRJVxRaO%Qy5Q~4SQF&&gp z+r91cpKg1)AD|;I7TXaxHBqKhu1cyjCty6u5Y+iq8=fFqKAHmjn0^Ui9dxJ~YP>q` zXHSpbUhPh>TD$tMR*E^7K4q(=ghLp3r};H5-0H0Bp7CnK#~v!WA-pN56%N6Rl!7x9 zE(=0>?lRKa?Tz^171aEx`V6=x0o;L^TE)f@%=NnNj~?;%t+^Lm`#&By)BWN-;k-;q zB8!W>GOgcf<n>^qYus0Q(aghF>blNQTnvqZnJ*RDA%H2IH52m|CFR z+n|QQesxge*(S{IpH=uyQ_uZ+GxPH~w=@al3yW_Y61>@wYjat$l?;b{vPD+p1h3&$ z-wjUqPSc84O+j(^iZpOPysm-Wv>b4KO^Zb>Su*=>kcxIWUplq#3^n;)BP?xKuOu4N zw1VD7kDfVJ8Z!sx6Ux|_~?!M^Q(;Ot;e zFvEG3rzk-cRY_5hUTRaT`(uM4c-j|6prm{VFZ0Ks$@Mt&JM0>69;`D7la`^h1f@ko zaK$qKIAFpcDBT}Q_kz+;1U*AHD1BR)bmuT>_%0}H%Z($_Mnhxw?fa%BR-b<}TG6Mu z6XY@T@GK!m8Kz`foGwSc&lggz!)ckU3{%u8#z~eni%iC9kjNKlG~e>m^t5pAZ!)ys z+?SCy@EopUsv?mBPS9|`16cVOekrwd*TR>j9<3fC+^bKiq}W^zi{dmZiYk+$LMa3% z8XsGH;Nub=k-9<{1znMqY30twlIU$U!MaWhb-K-p5{6-l%;2g#{m8yIKfGm3HE_uL z60{!YmhocDOBHmn=6hrLe1|^GZtZ>>Z#Hf4SYUAUCu)6=9Aoa`i>cEy-zH>Rq@EUM zj!)y{TPAjg++E1*XS28p9SJ*}@!x%W^q)5QH2}XgE<%igXk9o}w2ksO#2jtyj%_wF zrR#jKT!Q%I^^1;-ODrx*yvR%VChDW?BiVfu8wOeSh6-?TA8ilCMwvfaqHp7_ar?n< zsu$hTJ|0w}7eq2e(0cntU2h`l! z`J$nP$-kTw#GHf=P=LAZ?gW3aZnSpr?bXT|2RZ5K1l{of;_-tT{zA*o2rUL{gyM5J zXq3HUY%gE8b^$vu~PnQprR5J_LyGSI%8s!8pE!@ar%|K2ZUF_%AYI%7n@W25F zS)_I8tTSUmC7}5MqL5Q#q{K@-Ny@X0=}(;`{WLXhAPe=W{8Yo}Vg+b9Nz{_WLpL~2 zUXS~N!gv=rLRj^3J1V#E3UEt7*dd?U=`Hss>vr*-SkjVM+IM=t@g|w^J)^S=eB9r< zdj8adcLxO*T$P5s!c1ucg9espB%eK|`HrO5 zRed>)@nuSr(|+a2^Q|c@#}{?0aM(G>KP~dD%XhnpX-(|;BrKYP1|cI$$Qd)UCbb+UtbbqjD5Wp9Jk76BW|IN?nBIpwD+*Xh|#E) zglOxwt4Y#dPOMF>8qvvIf#{r>hk>3^-DJxI13H0yfo*+h|4pM0AsV@b$z%QVe|)FKMC0OY;ErNa=6Z{bNr*7gr-)GA|_Wzmf0_k=I?GW ztQDj3O56yZGXYOkh2$6P1u;*gif8>bzx&=x$jhG|UKW2=M}9?_`~}>vDyYWLF74vX zzxgWYu|*(2)C*BxstvY6xWk1$yh}}C1Uyk!Ld4Bt7W+nl#%JSQk(l{{O8lu3{P@l; z^R<(z%u6yQPH)V`T*$Myy*R~tKzeID^+`krO%}atl6wS|k*L@Td@TImuyKsxUtuf| zd>M|V&K^^Mh8m(yIA+sI&4s!~rWQr%?9DCsc%IvD$MuSevjNZrpzj2=aSEl>C+Rr(w7%Z@>w|AQ!F|425D?;G8v`rUiY@VO{T0Ye@Ju}C9 zYOe#@B>#b;VNhw|etC=*C z?)S3sc$z6SW;))WjXAirDv@~4AR6)=<C;;Qx^Or?*L94!#1GmES_u8jrDuoUO8HF91 zXC`wYWsy` zjJX$U`{mtrx;^XmgT4-lCYGXRAGJQdUIdjq`=-b1jpN%Y3?o@09(x2i(KBpP#yIS7 zIgD2r93Smn7F5Ff5cJLQNx9jRfKYW{{YwkGL6-I4goSJnz|b57r!- zXp(_~3N<2{@OHIB*J3XdQxO;N8@A;Qj6x5j3HRB8#x&ej4g4wOa$RtRr4`l4;d(#* z>HRyOFkL?yfzLmOXq@c_#P^+_r)a*LcBB5-g}M~uu`z}9^CKPBe+I2!DoaLz7qtMX zDn;ZA8^HPugyd|sV0c>okY!&wnFCwz#iMi=IS+Fw+df@4dou0fC$f2$+JmS`{kk_0 zZ5g}n4x;(E|CE|2ZBMl)KfK&Ch0~91+8LLKkpb`f3HQFV=q)g?t%AYMS3I(a#&B0v z;yS&A`Tls=WF@!sp_*7GNjS+DJ|#N-ttrl=c!l*lId&N4O82_@<@_aQxBoHwh44)@ zld{WYVPkEAe699m98}Y%{{E8%VKVZyDVeQId5l*9Kz*cNnmsOlqz=G7MGyvC;nwM0 zq$&mc*3r(7+j*>EN!ap!tJ8j-g2W0_CqvCHroslGfwK?1C?BxXV+$YrD`eSv&hho= zq#{d1EaVK4OdN&ejG}UYoP&!OuNxwnSmweJQvmoSOj0EhE#9hCB3-h-IC4Z3#dxm9D3X-BvSf z*t&hz9T9hJE3l0jp>em8rv*Gy<0MkIe^hMC_p8=C4>BJUoIXpM+Qj8wJN?~6zIMD` zWiJ)}6v=O++1qR^)+C>yf-NsYo=Cd&8kv&P?&d^;+}6h;p|dXWN@J8(jSC2B!U`oM zCLPEe@=n{oxI(nKLu|euTsNn`uE+qbbP&X^yEUgJ#?wB-&ph(A9zf6^_KxfFEruqh zt3VwdkA{ACe6Xm|Y#_NRUb`~fT85qe>_mD%ITQ{i6Uijf6f|rrcVjrYAA^Y_*NawQ z0d|AaX$G|rz6AY*-}u?s{D>6v2>AGn{8Es2@Vx{wtfi2RUxdrDtm2GwG3u^U3lW^9 zRW$fD{o!_Lq!l{OUi1-SbNU#OMP+$MU8r{^a9%ebz+lLfv~-1v3jd!oqivKaEfGwc_O@*w$l-uP>BXu3E=i3$&Kt5I3WM zk<7kcs`O)Q(h0T(_2 z47KhM{CU=HDH`*~5@<|T)j{}?FYX>jj}Fq}uG}`$gA1oh8s@ANQpC4TE9C%ON`hg@ zoRxQYszktLePL?DjBsJnF-$yHd@G(8hdoRJXj=R9p?L{pLsHCf_l2V)Q&@{Vho;d3 zwb;U`w{07y7hY~KXC?pWpEw>1u(o+--5^vQ(;HK46dhq(*L6wmoRf*phQeH9(p{~T z{l)6D#R^1B;U1KqZ86dBauWpR+6WbhYwwV~XCU1hK*0&=kWT-R)z8{yyKBnXqrdT} z@4M^tn+lO>;_}U0;@#eP&FAoSYHeNm2y;sUpi)zrgoZ|ev?~#e6=}vV*@qrKKQ=o1 zG)19$qe4PW(kYFJX%3Q$p{?~aJ4e@65`cB+|e(@CVm1nL+OIJcb&O_=0RUsT> zGrlZV!Ojp?T)Jkl!x3oT3zFCIfAD@K(yC$>R)=jJyKp&?r48B9L@tEZ<%)=Jc#Th` zzuE9B9M%`w&oWVf>KTe={KWcVbsV(_@*zVWYipHD-WWX(Yr18(c?pej5jFsm)3A>1 z1p;$SY=fpevRv7__T+Kv0|BT#`qYpl_mcA~h)ctvPq-R+{(pg46g~D75ga5O8U`Fk zvNdE7f<%ebb9u`>u=&`4W4)7KvK}+WkY~z?6W!vSm$TDB|LvSkrMGK0_jQP&`@9Oq z@iwLtc{I4tJ(R?OejNx{Y=GLWq2pz3I0+m4ieyN4<`tb#GM-k@$tzc&eMAzG+{lA-;Q=tsO( zgjbOXeYCS)PK@Dw57~C%j7^phO2q<8lk;J+;L7O&3C>~Rsl-Db$7a(ogyxf9?tEX0 z!3NXrN369R zbG=^ku}jZ4NGXjar&&oX{q3Os^~R~AA0L9b^F7Xq(WtJyxz?%YciieIQIb$BnN8uC zNz52ZB!O;*9bg>+DuSfFiRTKp_2H|S9nB^9F{8+8>3X=XlWKDm`NASqcT6p;lIQUC zqa8tW3x?>Lah4~Nb z^7&JtN^E7ff=xL(v~^2-NmH{h+<(^Gx^z+0q8x~sDRab3pr>$r@weFT#fNSuhPP^`#|n$sar2~l~zGqW0E!sTJp9@ z;(%5ZIHJt8v4G#Vr$to=m(|$t=aU*Jhlo`F2mhRjb8YiDV3q%HQ&^NU2HNLG^c?A9!Xp7F`N&!zIO4+B2{EgcVeEwV@>}z{l(_EC@^llOjg@gwG5PN4EV$^K$NC z{^Bj@R0f3BHDXl~YS;pg;)SU*RqSsNtx`e_*Q|H z(Be|Ga!O4i`dol_$H^{XI5h@2u$Zd_W>$FG=X9@g@d>@wTDk_w*ofWP(@2oyKB1QV zNhH$0UNu8!4XXlvtiSnQcj=TFhW_OaDiO-yEx$cO4$27Z;FTk3O2;`jB=)>WI~*RdaZSKK*7QVD)l4)Jn{OoSsne41qOV}vTEIxIqgo`HjB zKVg5qUNkY<{LSt8ZJD%3;WpBVOn*oqO5lYI8->CuItwetr~>r7&@N157qVOH>jdEQ zda*edH5hdcthNi~0B~M7)f97HaXtLW(=QW0SSKmo%3||YN?L}nIQ^^D^QUR4Q@v6h zKY4~k7}^o;@^gcgcbd?aVq>e;J@d_=>uCmGRwrlZEe+y^_0mV-I${0(7`ulpni9J7 zU}Dq=1nANxt{D8=8Rr7(k3JsWU0ZHHnqaYeWS<1EkIuJoHvjgI`JvcOa8Q^uq8xdA zIdioQa=!9rNAJ1qL@LIDJIsh1>&OW*v+)~J5%ghV#tzWp>l`-?%44>2NNA-P$tJBC z$ts!9$<^R>^g7+?6MffdSxI-@y+Tp$m5gkZEeO$__wcnMm?xjY^}YYY(`uE=Z~pmh zzh85IyK?&Q2t~HkOs4uOHo*0Ko2JG;|8N=a2I{efF(#Kn8d0i<{5EdlXTu08UldgJ zx$pZ2OxbW@aej4G-dZ04vsp9{2ssKmovs=s zi*JzzdM=SQWQTLrz#S~}cUzyAfi2B=KcF(%MFN*NpB}Tw^s6{jz8Dhwfs-Cb{`gdxKRBfYu5R`sSlQVqS47 zH%NbOK_zZ%F1$7vD2#v6*=x}`1T0|Ms*!J0C(*S`+QEl8gk(2+P}vWd+ujs+)--^G z>JCUhzYEXaYd_-Y{Zyp0|COb)f?>=Rl2W-Q6z+FBYag!w`{1T!3a|)McQqpuosjBB zlmZz=P6he+3(b!Tp29ctUF@R(fdYCQ^EMhGuh0QzO$Vw0=PL9Sv|rn&TQ9$#?n#nR z_7ujLkc>cxV*+RF9!5<6r1>xBE#AuTH(Vw}(fKF0Q>8OONoEY`M5*HEE^J(#kV$BE z+cHdRCdM0{+Xo{=4oLE;OEU|#=XYq;)K$zQ({i25gm&Z-_|<0g_!&sVWXvRHpfgFL zQz9>?FU2_}EK}H`gRZ@fD@L=g{fF*Mb42L51t$cHjVL4trR8p%j3!a&)VVLJt z9_D+N`$iDLvsNS?OQfbZCJh}J(E?)&GIm#&d}jSTn`|G91`pUs5pGzw-tH^jn4n+vtnWei-mmu@;N(4OGA!~g;cM727v6b z`a7h#6IkqjZ&l9T-qR_(p$_Wl2d}OTDXatk6&E=5RQQc1l^2lth@Gql^H)D`UONAw zQQ-}r(uX7Y3Wt0PM5;At{xRPr5Gqa4W zFFt+Wk@KKNd+5!&S$12wG@IsdpDI_yx~CR6#phVtHtO#6S-C!`s=nL3IQdJvlXJu?eQ$cxSA z_AG7Bwjf-{MuHmU)rlt(QA_UvN$)u2x4aJu9%=4|ix)Xm;FHs)L41zF?>;yW0zMw@ z3*1@64oyY>I_MNjrgKE;#EE-Nd&YKsz9Kfcp(P%12AWd^_ES>NMhgfs<_?KE7O=bKAXnj*mQJX8ZkrAQVzD!{ffd>M&ikL&Hcy?kO(#q4HIop25GpCDT?(JDuR+Hm|X% z6VOHcQ;kA0(j}o;!mB^>oNB;ae7%QFyvNB%t&k;((#ZN!qqSIP^=j#qRC4gKxoRdF zXGjrNHWnx6mqn-JfH4a3rX6t}^%jrKMq3NRli2SS5|5B$6&Z2~=%AeQZZ(FEeVI`F z+%P+zvv=P-fBbYrZ~MAY4`Ez0s5FVlVbyGsTLd1Gju%|DfnyVAt(SxOClLe$uXtal z;jGhujlaE>I@u+Y1G<5%V}LgZK6lNktxz48(&gTD9RuT!g#f&T1(pKsXP-;gFv3;N z2qe~!J=!T2PNZstrQF=Pq)@oEKgg$WfxA@GD+{cB#;7>)-)#&qOB1~{qOyPWa^L*c ze{X&pHqKfl^{-9s@rcghN>-*PZvHtMYVPEd35D9{g_?^wGAwg@#uEP4(|P2}C7E%~ zP#yu*Ods;XnurJ#GUn_Xg&kLvM+Ppk;P#Y1Kj&)oId9a(CLW0*qs%p?3@mX7Gza!R zV_u);Y8J0kv`rH7Z;IoyQM*$`&50-9O^X=b2DEh;EH zt!1(KE#w9Q1%23q`ml3ig+YZtxd{+UpxL>Yhgx+MS3MwTR)$x6o^xPDUHK(#l0DM%lwc|X?iAqw zEBn%fMjN-lBiWZ^R$zAbL826necgUZ!1Yie#B zxRY}(o9u-=Pcwl@uMjj78SH9Ic%Y5GGBFZ23_%AumJNjmj!(bKW2Dodu5%kxA$91u z0mEaO6dDf(ZcGt3aN_EONTa%tP*$tu=x{ySSLy?V2^_BmSY@SC;MyMV!Ex!oU-a*S zAo!J=4kpo;oF4c;ZRmgyKKokw!L(bi{2CeRgJ)GAJ=NM`k1Unpuca09rIXT%2K$jS z;rqOF`#oZhlY4>Eq;J!nzZHQ!^xs)P1Q3Nn9gw;1T#b4iBoDfBA%hyJ6ikWgAb~5H z32t1bGrjh{YtJ&$4*xouIPEo=QAmx^e(zF#N+q%WNL##z+2*M6lp$})TEjrWrc)+C-val9+WFQ7U$v9C!92;Pl_p7pjs{RCCM;Y?krT#OBB zPr>a=58~>|>LZ?Azyp&@nSf|K7*Z0b4qAN=i=FQ2Nk;noEj1HxJzBE7O*&vNzfc$= zG?Ew4JCB!(gpmFDH2aI*A>noBkVJFbn~dHLp>yig=}->}cy8fm4X2}o{${3MoO*kL z{hOCzv(a_n3*lcOHzH;lTUKGakQ+@9Xw>CaQ?WBZX@@nsig;75S&EuNXJy6TLDf>c znYra$D=bl(XxpBuji>j4;34X{bv?jFVaz?3Q@W2mFn@-gh|a0hUE%}=%l*l3+CIR zwek0=jnw&DZ=ud9PT*{HHaZS-il04@=U<+D|3MhEcVPQd+j<;VxGHu|n0ncXaQ1 zGc-ZR{BJ`Q@!MtID_@Q}!x|Y$hW0A@$7tv!qEIZGg?mJ!^*u9?_$J7GWBf@V(TWN&8szojc2Ws zTxJV#G)X{`oU83kOYJ-Nw!TR|pRTP4s2R*;O9iNF(e+jB+AS9CMjBL9sbi4?<_6`Q z*(HA4JG(4-5Q&MX2mC!{4YHvbxw8Cu?*_{p91!z%F6t6$qIHkc&5~OtL?dM3YG6GS zxtJxp8M$$>wY?uhZEpcXTBc!Z=6GpzZKz>Kkt9s+(!>V?Y}UD@*I&>0(huxHF#dcn z7^20N0mG6qdtvyUVSeWKK}&+mlpr}0VSjSXxqBx%dPKdNfGbvSjQtb&2Z?B6`&`rZ zS+(ithln%`Ll#LQ%Ai;3n>cGod8|BO@Y-=THm!H8}_($KMR)Ne)~V8lT;O zoEI?rJ{%~yT*E3$3#sA5h`I4A77BN68=>f^BV-5^v2~PXbQ}TvXYGDlL+>T_FuYMr znkH01gtB&WkU3)9fu55|dN<<{!q*s7O2&ap0mGV7YG8uH8s*bZ zcyP+M&)x_6w=+wACL!323cL+xk$|zv{r4(I}HsvNPg5dbZM@$NN+KVWtJ0`?^G&QDs@6 zOUo)@l_4%a=_r1&r=4qznN%7FpT_>Jax(%;Si8}=7n*^z{vxKQiN27j!<{Vqz(bL9 zrN60iPxpFeGDbYATp%-X9ap6i9dt}ZMw=Ufko6OH~Ek?V)G?bO+4VG z?Z3u`Sy+6-19+RG#ZiCvB&WnYbhiAR?6nOp++4_Qua+arT}#KOv_T{An@ye^1*VXO zu1U{Pdru}9ndDZP=eR8SUEoM~-n(d=_xV|+_^0pmFUJcaS_d;3l&YzuCYmF01lEVU zj5!-t4=FGC7y$@pGV({ag99bm@2QZvy3Kp75mN09PQsR_kRdfmawySV)ojFI@EmP~ zr!6dC&$?c_#}R>odt3s~8XPzp9?L)S4jB~wq|;z&gO-uGytH3KiQ7s2cuMGLw}ljj z`If`2wPCIj<4imZNnNDUJmW;C#YCnH2v~3o9(+3=66@(p1&?o-HBQtc`?N#@o}WWW z=fqmC4d%iV__gx7E2#6wV7v-x4v`8X(8eZ5Q$+gBI!%D?V6pJwVD16hT9)aCV+?wN zhy1SnwT|zQd1a;uF_@j)BNFhR`3CE*03Ljxb8%ky>Ve+v|u=J>n zGl*6;w6c?R2zRQWi^S|&OGpP+oKJEU2@-3~S)Tr?VPM*d_jRS4$O* z=o6{Gq~pH!{xjdSIIM828NT58;<2pSpjv^W&-9vqrIrC}?_oFUy!5dh)MXemkuK#_ zOl^d^@E^AAG3=Ea!!VHiGCc8nE|EJ!cDa2Z0hM8&1>b;BAai#JG5TBE)ph_KfjLiSZ!^g~pg+#s= zA^KuMR~-z?C72=TKDgPNJ=O=Ma7d@5sgOuwJn!;iq`r>h!SdrLnqQPX>1tlUg=cww z8WOs&Rjg`Q$S!Z2^e8rP)hYoO(am;6sc4JiO|!>kaaA#j-!S|dLZUUrx42BPb!i?!)Ab)_d% zE;58tLbHfXREZcy?KzFozqoBD0H65%PT!9(Fw5Cq$K7egQgU3^mcJCvba9&=Uhj9* z1@Y9`Ww4-sOe&ecm|dt^wW?>BTYC9)NRE*x@b`&`5&8*i&M0~FFa}cf6F=_Lr0A02 z!6rBrSS2eX*;3}%J=Z`K-|Wk;7Q=J*+1<)U#s|<1u~8^JYsP7JmGD<02~`|Ha9nA! zVJO2G0hZ<7T;_(Z%ANhTW*CUC$o&*+#`3!O5MP!v3~MJK=#}x z*I7U3&3qb=58)j6luwCFOg|S$>3q134SF(x7@r}LcV=^ukv&z6hYO5LlIS0TlaoqoMN!MR31v8Gp%DW0zRWnd3T=g3~=i za7~f$y^7q2`>u)e`BI2CO5iIeLgfeVMQhHnoz+_Jv%4Am0<;!+hb(_J0?R(OOg)kyC(Qqt41@0MnyM1w(X;mH zas_kZnLqr}o{4ADe#Tc%fKg8H2vl&CQ&I&_%}*q}`YW~?;?a+y?WZ!sja^8WNtwAz znOMr132^vLI4S8J)IZ(bif_f)`<$KbHgf920Mc_kBd{@u>^mCPLCrebf5c8XBv-Sb@4)7k2=nl%M{l!%-hzK^Eb z2bqhi@~?T|ZY&m`64_z?-e4n#Cbt?seCTPPxQ6qU@aHfX?p69c&Pr3O)fOcmrWWx; zyOY^VF1>H~GY+wV1CH;8rY~(m)kvyQJ@8X-X>xbJ$8bx(h^5Ex#r;|(UR2My8@%y( z{$lO$$R|88{}ge~tPI6>&58qY$ANqOZz6#W>50(U^kxxfCF21{-fJWx`?o#w*^<+IvU2nbnnIw4fq$ugXs zH|B!0Huv(>>EkdSs6!lwhMQ}+SZ6px~`i_sQd1(`4V9^tRV<7 z+>S*C+o}e2w?nZQ1m)aebEYYZ<+9Zd!aVhMN8XPCFd$N$^A|0pDjbmCWl?my3stbN zmz##R-6hqsSLF1y0b36Lb)y}8-dWMfWaFEn!epwt6%4`~nGU~#@^MFCXqrFJ)YRNA>h7~$v4_7o`=#0w zX&w-y_1z4eM1a_o7dV~XLUMJ{1da09Y%2Thq`pj>cN>9Tqv|%qA`6aJLCAw}N>_?U zrORhRb<|<87we?Qfj}WLywjbe*5jnobf;!H^0#4y`(%d9a?YohHoB+ik&V8NePEn} zKVQFNdD7ure`CO6^#=qNCOboT@Td-gw~%1d!RvtqQ&rVRFO`ouho+9g8?0RYT}vr3 zU%Kjt?f=1}at;0eN&hv!dTP2}&_7><{<8u0dVjs~GT_yKONc-4y)Xz>Y=gah75>nP zCkg?xA7$GRzmxG@y3^oOeyMLD1mgFIgjGVp#l9URVAp{0LYzjEd~qUq##*|Ug)&&z z)5Qs4c0tU0xxO6$*t&EE>JgX!$7|SI58Uer`gtAWrdHpp7yj(Kd{_lUs}A52#LS%r zL7(9A^#Qvauy{@?0DhxYe$GvUk$O!vxDx4uj%D_BZu8vSotKP}oc4`EKmLqT>CV5S zH14?^6R*dwH~p?mT-YO=rFQWZa4y}TL{uHQT-oq~#z>&OrOo#DrKwTXyk?x@lAqPLncv^@BQf0 z;dJEnKL*S9?~`|hrBI7Us}8)}pApR$UfPy_d`JE7O7qtrI~9`t>pwo>|K0g-!2e%P zI=zUtxRn*hVI8abTU;|i)uk&RyMM*jlVIVv$r{Ttr#wJy)ojfYja0Ht$pVwp3r4Q? zerz6tNB1&Y$G=rUV-9`JW=H&R-w6TFE>&&ta%sZt8#23?@^hpkvz)T^xaLxqjD>(z z`^12ZF|+Cm3`9|#o9t}=R=Z^!)Y;ZHpfKTj>Of^CZue+%Qs5%W2mqi;!};#x%yUT~ z3x63_HPq^d40ohy@bi0&Cii3eVMh8D9w4vu`}Pa#wo})K- zeT>X_Ff+5HxmG#UXGyKJ<#zt2FoIHBa+~RVCZFZDEdJ-~_tsWk7rcPeQ;S|8fLZ>F8iV}X;Q$+@M1LYvzW5WJYB0I}qmn}6`}DN~kh*8C zJC&b6O;pK=I>s|Z+}SY6IfTS2n3PV8D36crN=Vag_m~bTDdU*%?H-*+ue(lGp7La( zYmlJ+WrD)_U%8cG&jR2?Ej3C?CbIhY2y(>W)69d^VRGYmJMi=`3?-U6!fP)oX0>=I z>cZgVwZs0}>hX~USpp?178FEx(lJX&qSVS4RZAV3q@BscCu;(0jQPU6T8+xp<(+x{ zZTyROwmG=vUx^G8H)H|@8aRe#Dv6&ppUqdWAG zzU|@gQpfD)>C|VeD?hO6!6FBE0A=VPRH}(PzS{oMY{`wi1g~KihvUk}R@lB)fus$) zWTRQ7PvrhdQJbfh%2MHjYSpq$A9}CbW>wQtl(I2#GLT^X?e-H6{qmJwF`+F^@>v}) zxe@Y^GAqN^`*pARGX(;5CiGNA!CO`4a{}6!KDi5!M;v@6$IM+iwFK-~MC6M5inLKs zfn(X{Yc-=~Q%!X!huW*rjulsU>>@SyB(3!{x=!Q(u?C$onsdp1hk4V>dq8jY_pTvUxh2*!ZgWimPNl8 z5DTVrAvw%xt<(mMmSDVH@mAOLS_wb3sEO__A3nr{yb$h&HZ%UwTz~Jy{Okl$xW+Vg zIFk*kk$BesJjVQX`qk>yl)R}SqvJBa`;T$4rVwZw%v|_Xyuc+4(j5m6c+X4uMAs21 z*)9+B?elUL|JH`R{84yfv- z&2UU*jB@4j^VB-0=;2qJ0CncSl6s2s5SLw4Q`qSA$aNEn=6-N2mSKq=Wq}?>YPeCW z&8fWlU_~o|qC%<~GH?>JwRaj}!SZ+xS-UpXA+!upA(QcceqRz>Ec@#LFtK^5bAygY zii0@5H5I1R z<(2W{fGfg|aOKmrX9E2*TR`hVt3_#+hwX7`d$&5t8WH&GY{C zR*w@`%o@gQ=aIr)Oh&xLJnqRxUHZlv8BfBFjqO#sMe&X5gUuX?i3Y;!0TQWd&F)wcA+kz zXEg}H%J~|EL(kpwq{yJpPKZGR9K%G6%p@AUT^`$ytT(NL11a`vg-51uil)tQs>r*x z>aki?6uc>wh`D6P!eVdB_qH1$^8*S}R6({T(y*{9k$4K~C6^-Y24#c`1t=-`!GX_%azv-;m;T~)BYbmt_bmdn z1@5?vN{2;a4Ir^f88h-0e70KmpHi17tn+d!@VcaNgoo&Z#eC{Q`}(vzsj{*yqGf`k z5C-O#YYoubDg>e!dk226$Qf=!cs`f}!IKp@f*on|jz3=-whqW`uY$*Z$=86;+=~W# zL=BxO;hH7xIf|+}WaO{;P8~-->*(p?Ib3pVqIb~WT<>kyT3`;RTyify-#9yGo!t3x z3;CdAGh~(zbkU*Efn0mxj{emw@n@PQcbvDWACua4sN701L?b1euuDOM_!h`rY53A< z_WgTt)Swjl;`GIbl@N?`bvmR!k??Gn?`*MxfK5?llI=g<^u7U*@(nuAiF+m>tgu zB>r8WER5b`o>HQg9ZRyd^St$TbL(%T=G&vG>^cYAfBd@Frm1?a{j7YJ=`HF`{TfBA zQ`+S|togtd#V;uf0!>T)%gF-A6+!a;VO&+@!zIxvYb>tNT`k#{_g$md$W6Vf!3Sq} zJm9&*Q+H%i%32rjzy?02JBC>x(e=+6HYUS#=+tTTe#&Wl^N0rU&B%&&g89Y%cF@r8 zEqwApedfVlh*v^+ZwZq_`!8<@{b*zB{Z9FLJ3epXmuWC;do3GLm^WRo{IU?2$o+0=5{OFEF zb+>SnG|EO3pw^lNdBI{yP(l!_H!Kif6l6?G)o^VvG_IRjQyup7jv1r&6{Hcq%M~pR zbl9O0!jYbCA< zX^2_a{`wr@%Xgv`#!8S{q10sg%&cBh$1F8aHr!VhaykIRd~0OG9Doerhoa2PrE|p6)yNE=^|bLTE(;wedSpmb&JZ(#)Iqb#ZEy8fZNXlu zUH*MvOxfa*40f(3E>I#yexkBV;yej*?~b1TiW(4?$<0U`X1Chj{J?DM3SyK3`k4z$ zMfLBx(&&Tf;|p)r=bRLj8xel@S&hgJy8noRXd6M@&MDmq7OADEViS_4glwSe4}rF# zaiBnez6>OlznS~JK%Pg+KBF{Bj=Jlkpexr(g-h$INfe!Pc!lrt>8fnweNuq$Q=pk( zW@!XET)z&FO`VXswGKk!-#Ny0ujgeLkh4Kn=uHsm3kBGi#`M9ubke<_vTpc~W{L@L z=k8W3>VDCqqG|LkpCJ#ISUPyqn)EqF%AONsA~37pPh+hiTK07AD?8B!@naLGv-dvD zrJhHGBcCBCI@bHaClhDB4j86@@y#>w5O^8xVuVcFwfqu|S<{(LyM7M%?zpZs0X>uh zm&5O)-}35}hb}tlP36(2!OSHG6I^)eLksms*>bQ+a&6NKDZG9zZ))w9-QSO2EART( ze*f1+t<39h^5`y`Xy)p;+Mb>t7V!Pssx&6laa)$6Yb*>O1mc9udl*$;dS6AZgi*6V zMT)8?beTiI6wL2A@;|^4Czh+~F#Tm~2{XKzG~ryJZZU z1B|j$+8q>hMZuw;hiqbBDHH&iD3s(3U!snK4I=fB7!u2BznXOwX2_O1N9Zv^8xi-q z7|0%A<`MnG6;7f7UnB|6H{0vxOvJZJ*H$uX65l<4sqJQ;p?e6&c%u|!#o~-QR+dgp zO1+PjDxd;E|y_L|+yRa+o{{cNVALsOL==A*5t``rv z)ydp|T4V%y{QA@l>FCKexI{vD{dvRN&`lm)cj`Yr5qf~OEz=66{&`Gp4p_BPbvMw_ z=N%>Uir2?9aO$)N{%MnrC+~LftG37BlNw$Leu}~z^hE|)DlYsm$3QTNeSV)}y1$YJ zR)4<(-pG`7j@Hbe)o=z&W7crcB&XrMSmUCXZgFb(bY^kR-OfZU?|r+gY@O&NdF->l zzJ^5~D#0Gm1k(N$B%?>9Yy7fnXDK&~DsmWV_^+x80 zH22qG5s1Cu!`c#oc-?%zuyKXKDT-*3sQ%5;zn`A7 zw^&1b(b<`)1Jw+`L z#{~ctr6jS%jWjIBey8^!qN#{VVB;fA?!l)?$plyN>C_%;Fsmp=CXNnK zdE(X?tF&v@!qI4ojw?hJ{l|Q`lH~MbY%}f2t)SS60cgdjVb`P~Dmk^oHic0TfOnbsZEvogbnNG$pK>mZyA=q~woQ1R zKr{(IWZ5dG_GSuL#iM^(bZXd{yXc7!=*p}@%CsfvFvJp8|k;y`lca1B| zaA3sqpVEc8#FCoy&oN^?=^uRW_fE?-5!hfiQM?1zF=62s(d)Jd8-3^h7XZCLLcc5A zee8KIIg$vQ49&{CD5@$Y;;?xYVQ7ZsFoLE?j%8R3mV6ip z!$lN$$2{3((V|AcqIE{FJ%+~%V+D54wOCyi+m)>*!xtgdG1+3v9BGpk)nZfRQMVji zown=Q>L_Jn%?Op2rDr@m-n0o{ZM%6$k-QSXEU7j#At;_`u+Lh+0za2BH}Ng`NTxe}zW?uizpk7)q)m|xo;!vLPMNvN6g zl(%`aVRQ061$#nPu+>d)W<`*)2HRz2h&sSxlbvI+;W5s>juepZU7Bl=yyNjf)1@7& zr@W>fh8s3C4ni18IAvf+9#a?$<1v}GUzk^tt!h;2N%FM!t?Ui?-M&23VYUj@b8!ZZC;}Vno%n0%tny~Q6zXmVMMk=m!%`- zJxx>x>{9h8S!CFi@?WOllRkQ~vCRAZ^Y0JXy%R)R=!z0*anjt)>GIK3A6H?{BL%Zd z$c3>=xkr>Hyyv)N_ zpFVprIVcV+X@$3KwWFPGOazY3+A@68_fO^2LSq-QU4K zZGh;YMSFg%gFpQP(I{ys-^JS8+J*>2+VjS$t`l16?SE zV>ti_rxq$M?s;z>_3(XCt04X<1ZS~-3Q`amoKr=NA>!r?YG0BIMU_=t#tYtypR7OE zc^tAyjLb8%sPYUcVXb$My4M#{88m6(Y69gBdzH7|ao1B{cLl1R2|=7;`<&!Vbar!O=4LB4 zm$P@SO>$Z6Hs2v#n;nnLUXr;hzHgZ7)YsPP9_TXF9jT$GLWh*#zfVr`e||LH=!cuf z2LG-HbdjUU%?2H+G!0$jtv1W_&C1IW1X9HbOkoL%AxTbv1F6oBaG}jFEmRUlFC0-b*9^i!v&I_UIN2OSU)8|C~r8ea9j)4R*IX{#c-zNIpfL=qSy zF&uxbT;ICwD&>p5PO;hB>=6Gdo`81bNKqt3;T%meJWJwO^o}dGc$r4uA2|NMBDYF` z7x8_zl>)~~IHgD!%jJxD`qNGa7ns1!&H0nG@z)B)HOG!B)TF01yhVF(Uc<>yrPgVz z*v|(4d>wOxx8)wHu=6ssau+}A|DDO`Uyq;a_@}{ppuUY-`sY7HvjTez@!o;yg?PKx zb9ZakCufxtOX<55RdRa3uFa}LJ{!hDzjILfCMew=O1Fa2Eub{)BBBYDP7jlYZ}#30 zWg0d*X&5G5KTNt_m~>qPXKOTU_0C*qUo|lqIAP2PP61Vm)#o9EmK0o~1OjJh-1_oa z>fPR33@s63KFApQ`Jk(Gjpe>D&+m)y{pU(a;-3paD*a>kzU}>N-nzxyW-wcWxNW&I z7IsH3LCP}ABu$GrMv<~0&E7cm`JDV-adi-tXp zbi%p7PAF;fp;8pf2rN(YxIFdl6CeFHIX$*fDQC*GYm9$48s7Jwe{b8V#suiBoZ`+Z zHWDNyio*yQlSM)*c2?abfBIXlvyF@(GnAEBIA{>@$bR5G*c6Zbt50ZGyRxF-7)JB7 zAY<`+$IVJxy*L#dOiW6Ov}1O9=RExFtDmI+Lb)!;wQ-za%&shleTq|jueNYwtpd|9 zD8J^2`~hx?c&|udqE&o$fB$WRhuYB&K|^YU>HAZlbs9Zi80XIj!`Zfm(jOxv!ARxs z(#q?HOZgpeLsd zaZM?wg>N$K@(vmQ@$i-QP?lSwAW(!%svIc@u;tdPnT2%qK?uVo?eaGKV{V-bZ-X_v zC+M0bd6vRd5~o;N-90hxbDyW=(gxXnA~w3`6;EV8^|p4ryXd9=wG5PCKp#jxi#4<0 zbSe&)S>{xRr5K4;LKKt$m834Pl(n`~a%hEg!|&t_lV+jDkyW_ET)<5_`|Spq1Wx_9MjaY~tMfYbFK zYxO*|YFvw{a)05XuV4LVQOgc`evv5*sbm-{$hCPc+{_yaa%JFh=h*TF0ec>B9hHP< z>HpOzxnhY@|G&&tGgJEuKcCy_!xLsWPh}i6pw1L{fnXSd=9GZrZ(DAj9rf98*o?!5 zj|$D}h86Z)1Xo1!q;M;OloWxY8IqA1;2tThoz6w{b;9zROsb;BXaXD7*U9fm!6^#x`NCrR$jl@q>D?|(84w!GDCwWCT)^lxPv6JQT47E6$9Fav36Rpmb~+`Rp(Yjfwe zLIfI&urFzZ%rmN}08+^6v-xFlx+SwA2D>Jf(>e-`6I5M<{Lc&j^T%fkM`c0B6J?Gl z%KB|&IKg}U2brMlbBR#732(8Vjc;JcIzL=UzH_4Ku9r_b4V_ibGnsb7?q_2 zn&(yV{k}?;W2IdXo#K|VhI@$vsRj{kRsCtl86?A;5uyT8J?U*q`Vv-^N6kiI!KyJ}Tr zvR}VHQe2%16ASbYck1a4`oySu`YeueUt8RQIeLD%&VYWXYglSyzOx|pz(Y(C^FB_C z+X~UVPt^t16B91ZzS*maHE+$+!;d#Se7EWX#(>=7^cVef^;`XaczEpD7_6m+PWzJ) zZEPbX#c8#Zka>aYa1Qc)M?{Z+YWy;hdF_ShA0ENTFDfz*D1Pq5v>R<7`d%Z0e02rf zIxTwwRrY}6yF%BC*DdeYvQDTTw=5^kat!KU6~{@Ja+F64C+!^#FH zY(a{KCJCbrX5E6UQaFIkk^dnPfl&g{D0ww7<)V4i=ouHuYBffC*cK`8F!Fp1(wyk22EPszz!78V26jMdb?awf%_r{g8mu1{%2vcSK2cZZ${d z@h=vXHK0rgW8sh(&tHtjS$}Ks+H0%VWu}!8BEgUT=JSXxBr;-|~g-bU`Aj7^OiTMM{(iL`mai+I;ukH-@!qo`#bsUNXS?Xa&!$ zNv2B*r3CEv!lY&2jY)VFay#4b-XMXjF)CnqP2s5buK7fLqt?DuvYZ2Ou_g1wa=usD z_Ewi0gVB1YHJkn{d}YC_eIMQqQG8tJ>Iis?1XRFXB*hUDC^S!8C)XdD1abC*_L19y z{*Q0V--Tm(4E>~PeW@;^ln|eQ2XLWxXcfj!|0k= zg`;V}YcfxkG5W64V&swi8{4En+Vh7JobDbTTq%4Wjtw=3VM9A0y6n1$F73~e!~fR!$XYmL;X--1=#jL+^J~7{ zc%d$Dy!DrDU?IVcpQN9$taERhmG6Eq={aay)$H31=brj?-6DO2ZS9+H&-dKAV4mL3 zl|9?fldpZcja#p5!0g^zYMvR~3(D8M`mA1k&&TJp2I~1bFVsA8de6!}XY}^`Jlo`b zq}lU(Z`Jc>$fNfpu6^S;)CwQp`pAsg-H(qNspr?%mo}V!;_p8P>-lp(On;N=`N9gQ z1zNxM#*D9;EWQm6`xfTInvd;tJydG3Rogpu{c~@AvkiYgRbUB`pgBSy1&$C{)TH}K zJub*Y3AmZA<9+0j^&RSOkEk$F6p7YInq(A}Llq_iut?O&c~#HmPj^!(<{dlRFib4|TN&c3yJX@@9` zc)xh{UT@=%pyv0r(nhI@m1HQZ>{JVOP5JF?heMee=+)WeYIZ>JH+G6!c?Wd=PkN`R zkV(r6H~2JTbvd1$VNx#m``V_zJ^Uk=2vtu&Pw&xjib#z0{@%tb4bFLa)c5rTEvh9^tF#;hl3}Mosf|%|jK{!K{Kq zwGy1jP@E_eq$1Q^e7Z}m9*vUGBa~i9T2UBN!T0bRX-{u!=TB|@zhh&7sVXlkJk5~` z$)RD*9@NJ(CF+&m%e$Y-n>cz@qlhAd#wa3?Xi))@goZU+XECZ{pm$W);~yQH6Vbke zk!haRNR^-o616X(kkp9XDMid7-m$lJShjLs`_R!ej8~W^d2;HuJ#H-+UCOmI zhIl>Ke0JBL{jjqML|Wq|P1ZCaWH!y^rsKoBCtk@ox#=k6sPBr*QEM8*5iBQ5GSh$Y zp3UGBlcs2{A= zTlMy1Po;E0sGQjVVQ^qB=kuz9>T2s(I*B)VhZB?|aXA)KX@+76g5U{KU^t!);mW@@ z2dAM`jPq__+>M-t5mz_I@H{~gf=coPy1HwyxT42IO{Js@Fz8!!Eoj;!+S~R$^3M z6#+$~OCc9q@T_-syl`S|TBvGu%5*D9lc*xKPQ7UHX0n&xJo1f2nXT|z+n_7V6eF^t zswpfG_ubNPebbDQ39!;MB*j-c#e4h6{O{U}u+nXjE7f>jB~*>0HBx;4eD9~eJ6zjR zw}N{K3LUC!h1$Jeuiate+d|u_LCIk@RGKfQW&@nAT*=c{vfCecbl^`XvzLUNt(La| z$p{C4p73?8;HHLqzd4e|YO^hG`>w%!P}S~bhLq5!^8Mebl1zsgQx6KUjuM2mRE;>1s#`PV0PH>i4OCI651{UI?Ls+tvCqE{-am+~sNc?Wkt zx%I??NY;QN&1s~jDHJ2is5M|xD>7^vT9eD`f7z_2PQ+ z65T35>phti`MD7=6;#Nh-{A$->E5>rzd!m+ugGWLq`*@uqw;_hcvNr`+u~NzZ11F} zU)w)uU7b)>UeK*=ihA|cdzLOZ!`Fh0x{?1Q-1E}9@A8hJxBQ_^Zy(w(CiqNFJl9b&W4 z>v*u>ZA@Ms@TRR{TD^R4t5DrFL?ov?#R?oAmkqeB5~UF`M^b^KwGZ3UYcsd`5~KB& z_>Vc-My>Rc$9)Hf+>bxx;{=|i1&$*LjuuD;T|nd0gJ&hQiif=X_fIEzzHSj_e=bo} zE3~FcvZSa4kZ>FGE)G|P&#P?siFmhTdpv6cd2Gsh!6-4??)12=Hungd!vR8!{04Qz z)l}pi@%L+^r#%umJVL;5iB~yUr70QJA1~3<&V~&7YVRbl?6)qSHh2bIb68nLVg*@J z2!#-e{m9r{K=I5Um7MBaf5?pbK}v`cUsrp_t{D6LOMk*K7tEL;4Hz2^yWoM*=9rxA zkz||Q<|(k+9Khx2?V6k)u-|ZZ`QM(qx=)*9hPXdKFKsY_sA$Naj{5)Y&EeH}@cH|g z-n21T2uGq+_cXj@GV0@A>N-v9d3Ey$l4C$hlWh)lh;16U;Avrap4&9$y%j~nC#J#O z+@aHs&nls43EqX*dbp1(Np0ho(2L-uqvB$Vt0*eGo@N64t}_#ymde6 z{fb)Iq(wc5FWW5P*jK@-+T4odQh}NcZp#m_vG1+@R+7J(m7ZNm=HzmPqgCuC&TOy% zqixPn12Hfo%JShn{)m+4MgIRq7u#VdxJCLO$SJ+}J87tyT8iZa4-VR`G!xNY$n@3eNax^`n5;8+%x6G(<4SeoZKR^(8Q z<*_EkMgJeBhG)Fnn>_sGeTiXi?v_gVoPL7Y-;tkN9BN|}kktK0ZM*ExKA*r=axiu| zFYWgbR$ioXFNafwV6P3Ez3a|g|H&XF%vpXZ6H9Uu19(D^NT8Np=U%d^{cGvXPNt<` z4E1)t|SpI33wLd<`#q zYyGeDJ#{^BqBmhI%k7e9sFY{rR8HoBq>>a(P)VDY{a?SFG$P8q;9^4pCZABw_9axX zo(l5}mHaLtYBqA0cYoft+=+ifagPY&i-i%m5ML|-^|4bM4cwk59;(;3-H4@0vMCO6 zD20Q%2hkpS3LTHGj`I7TudtoAOf*dIZ@+GPe@92i$BqcI=2(`}4aD4IT{aIGp69VS z9qxdk*pzQt-uu^|ozm1x)DcTL-?PN1zG&0R_QMk6aj5EXk3)G`+qb;)_NfJ%y249` zd06EsMOcUB+wGEU2V(-~D{iY~w`b=_d3yHnzVkcE-c4Vkv%Jmqbc+5Qw^>3PhSaEu z$=lu@-#+oJ`-8}5+7zwu3aLqeVI@@6yDbq{_B&qo`?kmDox%GYAxN6zI8h`Ck`|bd zv!w=l(F3u~f7d%QY4%pi8Zo36B`^|4Gc?BwTG)_U(H^1W9izYRed3GPQ?BWbKLwx} zo)kHP;YEU^$dIc(w<2jM-@ezI=c$#^Yj9h<;=GLCj6TkQLK&z7@*PTm^rM#F1C~UG zA$z?|S1s7N?88QQ22XQX8Hpu;L=h?j_^1DQcHR^9=GB^Xk=EhLi)62Y&izLQ_1sZK z=3Pe8ZB~ryGwL_m?ILrqq7WXPT~o<+;5<4{(KVI#>-99w2;cr43DK3`;S%g0!9vtN zZ~7~T)}8sRO(+p7BSg_wMarL?QyiRju*2<<910k&9seoo@3J@gZmu+cx!*f?|6`B8 zxil?wO9(iQVMC6G)3HT;;W-Nvw{L-f}=?ZJs?hxQo~(Z-N#i%*}r7fh9#$W`=<_7Aho}N{Z^Gjs4WSwCQ!r|NABW z&fSG0c|M?or36t3IXFI~GX>XUt2pRwGi1U)9h=}cV4fB^L1YA02wXx9(e!?y--@V< z-Qz#^E^GhV7ytPo@+B%-APJTwIhGU9OH^xhELxza@R$ywOml4SzVx=Y-O! zZ><{Lv#gR4LQN#CI1TyHdw+x9dK^3-*;xoJtBfWKl0eDy%7deXp6k;r(R&vB$(!z2 zd3ur;)^Q@NhXRJD7=`5tp5(N*rc6Eh;;>(8jJwQ5k+?^L*g}7V=AZzdry=8CUFHEV zn*Qp2v(K#`(7mEG^ixHO;3P(6C0RjTeL_nS6}uo$di|4JtGhkaJajK8W24VsaBNm= z)|>6jcPU`-WA2-`9a&Qn_IF8z$(VEV9kFQyLV)2Vth4#U&x~xKASMvauO|si& z0<`&b25xBn@rv`<7iWDH;_10*}S81VwTJONsjxMtt$7YX45(E zeFqvm{oxOAq+DYPuPRH9Q43=Yi81;PzgL|1 zuDR;Bw(mX#S6vz3+G?(q&}>yw__gB4?CNn$9K)&yFT+U~jO&a#f+>UOdOjrMiAA!q`Y zY9;%QynEfMqK*;82~A{}fHyb=01_1^8tYq2t;}HAs#a$BDwBP84S8(G!f)Z_qq{r6yGdQTQ=jqI-yKP*|t&w-J#nj^-q?OG|Su8 zG7j2(>wesxb^N}oLM2VNN1EIeOrQGI;h`%XkgNGIELWqck|t5AMiLq~rf&DU{@FLijOx(7OD`_fbtt^B`_I;3 za`D0Y6(eUk2$4Z+Tb3SNE_&%7)g7i_v@RA?P2a1}e%NGNE4U4O8D0*);Oln#&s<3MpmPe-&OQ|oK~@>ulBmfeq6etbr`)&(49V3psanJjzA1A zPv0D%Oa(4fMFXQ_j6~{AH^kgpzIG%2aWuZIQ5n1l#asa>01%q00G>MI{LB7LgF`h+ zt`&7eQXQ^FtSBGRPk>g`3r-)#wk^vys4G3T{~Fk~O<~(6(<~*hB0(skK-B&)DZmHU zLC*Q04u-b`i;2S#-NpE;gO9UNM>sq6AL|>0KlUm=v8>}GpDXxf?Jn%bMUb>i5DLc- ztbpoY=PxU7b#;B6n#>kl%dqS=LyH1Wivqz?q{xNp8}s<$^wBh26<7JX%-&QhaRd}& z!bAcH3IL*znjlfE!1B!hvgP)pe>+~Is+@JrLiU-Q^th%acrqLM@@_hK$g(~n%ciO< z&rmcYtD=IkY&+NBif-)V?ml_6|M*DKI;ANZU=&fMq!7;p=dO;n6W92D{c7%?=TE`w ze}(w#jhsdSNfZfM;fTNJ@uQB^?3)CgAG)ICN?euXv?}^N{0{ehwB@$EL|E4{YB{K> z@3{@ah!l2SW09eQPuQ_ORW!H8*OAN6R!6{%TtJ^V3b>v2X+Rwzd2-Mb znJ*?qM!f`6M_>ILKTLXiR+9*|Q6gc@bJ?6Oo2Ouu7v1AT?E%RF2^V1?T-lr7sOyqxHf(?zs`vsh}AH`W+5kJNoV&QiuQV0Q^<&fccN2 z$)wDPC?^TwY}AU|PQH{)ubn-3tO;Jbci|{uy3Lc5ZFf!&;Omel$Q8eTd~F3fk3*Jo+_-StjYKFKL1`jI1I?-g^p4q{T+cQA-rC*nnbpaw z^u4X7?&-R{UOlC2{WN6DE7H@K^#@;CkS$Vg-X?wPg8^-|#gPZncad%$&P%GE3!6)* zUhR1~erY8ltW6E(#>)$ri$?OD7*R&6Ue23({x{tR-%U||pBN><9#O&e9m2>SEu-vz zEra#4TwmXlbgkpl7}vSSqHHfb0pa!bU9XpavB+C5jHrgj@3`-($mOI}b9{#?$2wJ9 zZ%&fwvVzW+31fG;0gPk*rGD^!6%pTS;}8lNnDv3$YwR({c3m~NOvfuRjxu2(TJnP4 zzDlWJeHUBIsTG%7F2JLg$c1y=N^aDY!T*OZ^LeRiHZLCZB@WR5O3%Ojm-IUqvtJ}v zk=zrl6)LKaOS?knqq@_pE0vF|YCiB{!@rFWd^GvW>Q4f)-fGABx=S+tafm~8fU4rh z;_K+&$#+#({wF1lk8!BRaFVOu5?gh}_v>rNaVHM3hog04Q;x7ZHs##4wK)Av4|CPz z(c<(WS8@8Wex1~*+l@f`ys7%ah4<6j2fK1G>Bf+zMy-R(gT5!x@*oUu5na1W+|ZbU z%Yp5fYCu?^on(~~=MSLg!)?UNQfA3RPb zWS-Cz1^7KI?*={npilUI{jQHoQCe_59W(Am_LQ($P_J4OGta}#!S8>U9Q=Ooy5aE* zeh;6cS4dz#%WG!|@f}TZ{WBr+2iv;>8MuUrf}i91 z_l^EXtn6Wo!GU7E6r839&!8H zR3zURY0w){t|RpSik|Nn(WfMX{^RW8|1ucpT9-)vsc1}@G39)p!sr>sRIuGOs|atW zR*+sL7|>A&Gcb681{qC_@jCzO604yuww}oxB_D9H0HsG8wQ+Y>?DH){vuS865cWQr*|`LwFwued?vzZ!7(dtD_7cswvb94Z1S@gcY@A=>?G zOq|ZdAr93JD!LBi>n{#h@_rSEIK&|iafm}4;t+>8#32rq1)N{Iy@>rK1Zb~iyDGZ! z(R1iU38iOPw;6ofpbFd?O+*w2N0D==vB>+VUqn=oI1C*xB!9g@z6X*=%fw4CaGJ@@Oyu!eD;78ss~bbNt&GjCV66uLlJ)nN5sO+QQ|$$HF&8;KN3+w8F5I z#0w0e`lcKkzV&KvBF=6t)gT`$z|~XP`eSvWF;bM#U=P%s@AkbdvRBqGoZiQo?@%RI!R!}5d87UYe=Rsma~Br>@|{iy zn}*ig-ItkM_s09xD1uy(oFX$CP((qdn*F@+xz!UTGo;IVqErCxsidN?qTj>saNkE; z?zu~3-+|VP4|3JRcgLBfb84T794Vb+H9)BfNy!q6#$M}hYJtP5*d@2yn&)zQoQl(K)g-%JmXxVh%5|;d>k|)8Pl(YSGSk#MF7BY0c6@;E z^Q7C?)E^!(a4#v5K#?R-<|!Ht+*|(8v^V%Vbeb~n!RKK-#8gX|Dm8^>Bt@o0UZAWh zaC=LxL3DFzL9n`jmhT2booc=v3VclJyYO(qL%}eDA!R%zq{D$dYEtMk6*Tz*TT^m7iM%A(&`4DVR^aP9?4@< zMi=CP{xkBNE|2@2+`d<5Zu2EZ%Q5^Db&RaxzV5wh_TKmykka4ah#vW@Wl>^ZjK!Ojgsmw;V$S%muJGVrw8Z?3oP$eJ{W^^auh zt>(k0J@xlKZDNG=BYmBdkFCAu1$eb1D6qAaEN+UDv+ZcxnSl^=C_-5f1jpUffEAAP zz18{4$3A-*8~2W8Nsc5{f>#9URC?(7~fd7uwb{wjqzqL{VKGwH;0{PD4Up2xjXf%ga zP-v0RWQG)Y!g<$_=EE`;l&H{Lh!XagUij3$xH`xAe*SdBb-n6z4&5|Li8aGoC65Q> z=6NnWU?oM<03q`-FIW|skY4Hk?)qE*R|{3-rT?F$LX3~ubc^pz*UsCXd81M2MqOqk z2ufrLQ56Y>WHz<=a@2w|d@ZAUG0osIC@m*%i)j0hA0076pu z!>={yad`SYY0aXX@fwwUuLdSxqncs#1Yg3FZ)8tg2ghX)UhzjLvLcEKO-q9EwXm2O zeRX|PD@^e}Wk$}&3UKvQw*Iq~RPdcON4JbDrb#}n$K$C(?UAE|ODrLigiHcK43Q)t zmsb}v#glyLQ>XQN>W^j-cs*KVtvOCR7zHHN=9oOhrg?H+I(+QoPX;96R!qHe4oN+O z@6wUfjLDlX@rtpSBwr_S?!aEBu|YN|fg%Np=UHB)8Ik2tacp)<@vr_(!&f2s9#GCc z*dY~if|J4#e2TIHJIDnN&sdku15y^=`b~{(zQk0F4A8&Q-3Gn^?`FTg|N4fZD&rR|=Z?mW5SsuCIO?+^4; zuVvbZ1cNQy=Bra*`>^pHupf_x>jx>*6h&&hMhL{?Q@(iF-|cidDns9k!%OCmp?7y)M%2#DGZ~@A|TMIS&J>&;p_FrEvx@o8ByoWFucH#vY;q5i|V{5n}bu6 zDSW=PN|!I|n?;R=KO6b%LL&hq5v<5bs(^NF9L0VounJ47Bnbco zjP|?Z8Ikj`0$e?nt-tE_yL{hGhiC7(XFtTJoUp4;5O{@=WsXp|yH3{_^=*SgHK1>o zn~*eCt0>>s{L{BQM~}4#)q#~Fx+2gVBLR|-cp9Fx)M+vD$o`FOQXpG9!5o~PK`NXO z^;k{!ZT`OnITHkECL@F{1bs8$MC%aebl?Ims|zSjmpUdOX^(YDd4qsG54iT-(r|s# zjFAbDHlB}iP^SA1eRS<@9s0Dw>&DB<^c2t1swe@0B!u*~DO*P!>6i&sgs6)bRAFw8 zn%d3sjo5wGo%I)f|7CfRVR?}t2$2#P65W3f=x3WtQ*c$>?OT1`+iP_e?Dw|F{gxz( z;|Wy;1WCPrzV}n#9j5nzSkHI3}i{sunwrzdO2*NxiGFCa?X7|_} z#fNbN9PZ(o`L@|9uMT!3!G-idLS-j>sqc>cZ3p(~-YS$9mpNPpyv&J`NDCrSdbqq~ zRr}Y{o1H8<0H}kcCQKgp95*3LeY0Bj8cvRamYZdcE>*=!a;#N$ss(>Ols({!L!Mgb zk+c002@8`A>b0Ard%u(*-Q&AQT(z--OyCKzjypVK9b)gFYx8)3%L;OBo)P)7-R8~# z>S*BhbRF$|rz};5JgrzkUbeJ__xUE|*SV$_2a#z8u7#Ynx7!o2`x;=g1FPNU_6$k> zb@jtPa*5G0EuPmcF(G+o(+c13)@|$(8zgy9cnGX^eFb#QXH}9`G(w|TLA(3Oaqs@I z`r%aQA@t4k;4}_#h(mccq5FM(Unlo|M>fMtd%6t)#3D@BA*#k!Tr9& zceFj%K!E%0UEu{j7f|yRU{!ckqC`btIhJjGv_=1Ee;g_?BEQAJn08ZI`JP7$6)pFc z7#aWK%86C^_#=h~d@j=+b?r}L6+;w|HJT9!g(4_aG1UHtVnzuX+$^tQv>AAP2X?<; zn%_OLDpp}-UY13UAOL}?Vrv<0I7Pm@g$4VjJsyRfx=08r%hQ0T6nV`48V`lDQ-8C9 z1~qTS$PfoD2j+3lagY?F_Mqk9aqAIZi=JbjeDBXjcs@L!h|n~iQKf)btKp8&npYUV zSNrBG=T1H*LX|`N@W=qB0at-ld?e!sSy;^Ie0dbxyc-G_PpRGDRTVd@eR%w?C;(w4#RKVuF*9uOKO zMON{ld35}iCxlm_nN4ea&-{1Xjpt@XML8voQB;AD6fu-?;`SY*{U|BQ_bR-RRZ+Qk z%K0B(OKx$|4xtTHK4Z*})dEL$zU7Q>(UACCNMMx!i4gIKq;z1%N+ms z{ymmo?kP%G7h_a0V^9Vi>snFHv60TEg8>!Itm37DgDc&cW@SxvGA_}){M#T}JcI|O zjgk4SgRDz5&A_Z*ON`e=b@!EBN}HedUH8?ZhI?O#tRSEOClCZL3ltTiAbsqjOx&{K+{tN%nk@+2?n5*0c3%-FJn=X`dV= zN1cor&dS2Ni^S076<(uy-rQT@a zO3}|%7dbSp&x`MO)oEO>^zldame4E*J+HZ3^)Xpd(w*}5uVyvr-@3%=fj>#*9JiX7 zeq>$bamuP9d`_z5c*5~X$CR6d!QYw&_PmyGDD@RJE66WqnX31Ar;?nNQz@bUKB-<;HeOw zf+rG-iTF9?_FTX0ou^}XHd*vYrn z!29oyhQDsE+3W$QYaGYX6vwI}sYt8F?e!zIm0M#Tx@u@=xTw!m*Ocy1}@`xmQS zcqc3``WiRf_Vb)~!bDeqJ`iC%9M;;~<&p~e7rWgKgaQNZumO6YJ6xPwUHJS2)(7%S zzFAL=pEUMp6ahIoLZ)~@rA3($(14tqqa3cyl^pKPmHcjVCCeRhi3r`02xu1UPGN`d z{@>PgovT0xGYYNWVZ01$4y(#3Ji{sq#n4RhzVY)KZ1C4gh*DfCb@ukA@4BBi``gaJ z(rTLKI6@$4hNo#p6hgLxHI0fp^1r0;RlMojwXFWKGbLUqe~cW2h?f;nWn_inBxc2& zyPmJVWLN6VRg6OiD@gllq(?XUc^7Fbar{e}n@&|MRmN&-EZtn*@h$nd)`n>kT!m$0 z?3MWaB?L`YM2^#VQd{)I(?9&W;i{5od+a^aE~EuF6(g7V#^UF9HY|SQmm6>d;9cK{ zKEKxgy;bv20#G*Gi7MrJtSm1I48tgbtb|;oH*)*=4?;&};M~T((@B z$2P6lea*1^3z1JdC%Vf9E9!`((U10C-(UBv?J=n{j9tDBNBIMRy^AAx+8mR!J(6s* z+dKuD69)1pSxfM=a2mjoE9qzbNqQiS`=H}ffZRfkheA|L`wGFHvz zP=^9fj#C}(IK1P2G7#Mo*531;;`H}Vi`y|C@s zDv|<)?t4$yG8*GUzMUyU8#MV6>nxLHWKjS-$15PjS>|wkEANC9iyzl-d{4eT@sFm5 zAlEP{>_S#(i3B7A1fa>y_S(NK8=(fm! zMp9#(KH_uyw$;D>6r9|@B2O;M$P7tJK;{%io;;=Vx^~ZGHpW!a&|JP9;m>Mrk6Jot zM}1nYn^T|K)hTq2}^BfQ!0O7viG?B4z){F8fuxc3M&$3@?#VK{UU3%jZ{L z^S9Qv=*B{A(sC>-Q6?bFWZ>xIR0~D{Prl0$==Xuei%tdzajZ_cMwjz>XunSS8s4|b z``?7-VHnaN9Sp@1*3m8i^!NrSU)oro!l?){T#w{KTX4#kSMck?iR+q%**r>EVvZCU zplF;((AovP&yky#9IU^=U*ZzD=cLGKXuW^?ewb4CX61f37q6Ap8;Ebga2MiR?3(i1 z*$#&?Gtk(1ZzWVOLSK}K>*qhdnzqC>Dc41IoyDsPrHCA<$~?F7;3%Qz`ZNn91@{|b zk;%XPtvc?VFJKt}PSzBTCIHYVfHD9L^?2-!qDESa|Jb5$Uw`X``k~v1%j1$1V08(k zMra_w1W_Rlg-2lvPV!%G%__QcYvj#Bp$S@)NKPS0zR)~zaX8o1M(!!`TG2HB+owK0 zeINxpmNF-ElBlv8#n9+jw!sioJU5x?Klj11t6MdOR6MIjC>5s#v3QH>>&(rQTsF7U z5r}M?-o<;o?!M0~ca&2shf-AY7_Wh5muT%ZtY%DKjVP6a)g79d{$0@C6o)vT#^Fi_ z8o?U|q1!mT!tFDSFz+{u5uIs%?7$qlz{IB!Z?1nU@%$ z9KvC3|Ez;M>a@y)WcaDlD$2-)GD*uSC-N$()OzRK2_f-7&00|kWmQ3sU>g#HWX?MN zj^lTAsQ-DGxZh9)Bq^)p_6)H(Kpz*7rlP}kBRw87Z1wzqJ=XP!$rN#WkkZNI30@Q8J-7Rl0#kde`h`JL9QCO z8=6051*etQE!OwH_s=c&9BB?uOuwujMg-@DVx{dZz~$`inmpQ;TkM2fmXxW>dk@-t zr{!ewUzW>*u++Q6_0KEk`{wxgT@tWdq5U$)=i#z~Iq`9h2zBDT^1l);mwAsBy>-T? z{EFD0uJp@^aXhV_<5KVu{xagi{gYwKV;&Y7R4Mw}(=EQj2S9X7@Q%?HfEN6R- z;<`oq+#Fx0afr`rynk_sLmaLcC_N%*;6F9;Xu_P^;UP-s0gdLciY(8_Kvh|V(a`gm z3{24$QxpHNQ~ej$`~{x^)62D zr~LHaP6u~Ka3{hDA}_KeFOUo+hTI=N)ULQshp?9F?fo;vokdt3QM9Fz;O_439^8Tj zx8UyXt`~>kPH=Y(?gR)f7bob&9WL(jc>ScuJ?dRGs;`FUt2$@zwf=QPw<&0V2S|wD z31(Esocc*@`-Hp=4zu=K=>M3bN?2~+#fWO18pISS7*A&85QFA+d&u0V76G*RU=r_( zoDP=Lsz3TmZ8NwvC?pVu&`?1ctHs{nhQ9jD_c46{d{>Jes;88RrwD%+h9RQAyU@V` zMV#?hwkFOLZ5Icm zAz)g5WL6>XH|p*N-jH}YSD2~!$}xA|Z8)?h7azpAqh&4Q>&&o8XnJu_gNUrCyaxG+(PZYqL z{q8&UMY6|4D0%rggS7qX^lyvmvn2(B;ekaso_93<=daP_-LZVW&?!Aoh2p0(PQ~pS z%9sQR!b~}Uwrtk=X(JJAMd-W_AzmNxoqwZ=&%!X# z+jS!#&!&Ym|5qvr9pNIOE&g6yo{>*#Pr^q_Q;Ka@BcU{9C?^YjGvz$9%(b6Z9d3E7 zSNWg)hXj5;p3H$m`D)@Vr^od@!Wcyp$F@`*LrS`t5HUnCRh-*fbCZ}$3q1wsw^PkY zV%K>;m2IqH0xebsHM~?tiPG+O)P!nd@=J53WCPb8fsWnWU08q(t4AXC+AQgj(PrIE zw3f;2|I8>crIN6NdMz5k=N!VQk+TrvH@ey0^?O zAU$|!Kw-nNKBjldJ@!uSxHpPil!L3uQm1G0y`CD{5A$!CH-Ar`(rKgT(_(MozFL3C z)LYv7nc-K9+QbX~p@OjkAsu?uK_*jH{>eIoBD|#MUxDdpoF_d>_LCiCSn2U=84Ft2 z{aV;gz7y?-3a%j~uH1^4#GSyh zEa#Uq$-W_^yjLKle+6VREkgv3O*}0EBX?Fm>YJJcn?gnK&v%1_sr6r*EUvo*V#(cN zNnaFItaq*Ihfrb{KTe+1(H_c-S9S-|S!NlP)ssgPv3uh8gdB_%6hjGL?T~4E9TuV} zp1?kCCf5Vf%z2vO0*&)fk;jnrFJCRt6Mt;EqQO9F^-R_GKF5?1P+^f1$aDVVmCJ^F zC28g<2Jq=IVT&64kxLHY(YmJRA6zY2PMYr44#^3<%QBq9OJ?EX1LzUf4KTca$miyL zI{C)r>aoTvIGR?+IotF20v)$Rdn_DF;*s=7#aOcPo9#gp2`{u z_yS^`-|fHM5kBYN@m;WGxUEy@TYTcbs=WhQarAAwD{R{rydUQ@`X8Z3x>)B> ziSrEYy>c=L!A+)N8M6awZtmXtM6&?2s)F-@PWr1a)Y8K#(IFTjkmm2(?ILlL0Oimp zGLxU@ubS9JC=?;nPWgy=!Q~naxwF$vv#&f72W&{0qVSopH(HyCtU76DGBp!2ebvP= zGpFoG0pC)SuSiB=TNklK68w7Q^BYExS5>J>{r8oJy8DpC(VI8XJY0U)=S6aiV$ zd@yjm#&3|jD|CG|<9$D`0K?Wyk zIoo~U$|}M>IsI-=IRxpi-OVlJQ!En{wa_92Q}88|^XE}oLu%;qipfdZHT=?lALN&x zzPljf$4@1<3)<$(EatYfIEvdSYsL4zI7`RMTQ3#ofztmNR9si;x7I=>)XXGJ$ez5jT?ia_h!Zbq&HTU;BbYtIv{0kUhg{*Q|~`P^)% zM7l7uQk(gb%swtOUPZM#%=3%tG~uo7ijvIzAr>p^Th0ZYVb8AiOIVbqQy4R6>||6d zHIqs0rT}3{p&RF5zW*WIl;I;R!0@jtwjvhP)J@2!I&#dCwbb0!e$g9{_i4irYhzr-_=i1JG9xce+S?|)r!J+a%*RL%AOmv0 zQe)b41E&S0Klh!eJ~R9ItvC7RrQzNyOR}1qj-jzE$gwLj*HIMQf$4c;KwU(|iWaca zYljYrdFO)!;;n*v@OP>m1%qpGIs%KYw_p71G>{{=yk-N9W_?+O1^L$iuc6y?WTMFV zsr**=6`EO+%4s9o&NM8yMtxq-ObJ*eISOw8TGRaB0|!-k=Aif&^mZo~jg0Yf=fBaMEIv~^SP1i3!|rvHzN^I`Px zlOBRGHrNbNtnMp%qRDob?ez5W;@D>DLlb8OqhyIt;LJGABpmS>Zo9Z~3c7Kf&C_b# z;+4vy&a~HihW{i!i`=$caZ=&Hd-qv!fpkQyA!+ZSp!=uY(g(W;HAK7ywa)D^z_oy*qAfcm&LNBlQ)zoy zmxnSsdw%7F20NNbCJh5Uio>$RyT;%2Ssq+d8%Q>YvzHzt0d4!aYq-1v6ClVBE=)Q#ZnW=lc%BG)ME)&y?gCW~`FVsQ zqQcUUmS!5LhAVfe0MnDofl@d=^-|MG>p^W!!;3PO~1rxpMCh2^M;tb`Tv6n z{g^;%2U`wDN#zifu8y1Pg8PP+y%oy$v9H9+Tm(2L27MA$phuqpDz$4 z_`PJdj7=pqt_?)dhU1nb%`3xIi;O${ z$MGf`-S{EPL0De$=>Jy|-h1g|jwB&2z(Phct$})!lg+xZ#O0fI+NCY0E1~Ww#sks7 zLM4V>8bKy>(YQP&O=K+&eo3MKJncts1lreCf9~r=*GyIH2pSw5bPBDBU<$mRmA>8J z2yl)ME;u`m8X=h6Mvh!Ncwb`Aw_>&(PJPJoX2S+xkGNb@7P%p%${BaZ-(?fpT(Wy`^vS&QGPc`HavY&me~ z0#vO?U|6a3AocaSF=94^FW-d?Ie)-+l#&8LuzX22e?-yWZ zL%RK}yBcMc?Bn(HKet{By?o=v-)CT?dvd5AkM3~JIJ^@3JQ&u~{kA)2Mfrq0c@Q&EKE#)R_?-dv$vjkXi9R2{`1N8I(Ccwi>8Hj_!{>G? zF_Lb$$k@o+K4tR_FdjH|F(;vmCDGiVAr-Y8JA)!(8a1=?0 zc9a6ptw)G3;*0B%{Sf`I`gnB$m9$P#sviciAhXe90-ePc%%5fsn%bI4|IikdbQH{% zZcEPinvg!M!z*dc&_BDU#uKxY^}N^jItseX;C;z!-`3~9_v|PM>7DKlp-{3Sjke(H zwW$ACy<5)H%L~(zh6=`pF{I^A`zE#2bxU@sG-%wEB!$%H@{Ak^c8?#b_vx}79(1S|sXf6{ zPo#9$FmzKBR{EzCatGMJbVa;4+Qhz|vc&o7klZSRyg?bDv%g%^Vf3)El9KUsKHo}I zhdaur12%rT*!s_W97gWuIMBAi3uiEtE)q-32@R=Z%0#Ky>6QB)A=IB)0b}U$1Omc*QX^Y#eOfkrfdh{8La|L@ zvsf?#T=FL{5A-je6EZGdPdLjQhTA9Gi-_Md#NBE{Ka!<7w3#RRSe(8FoIAZ9yfKmP zlVb_7rm7vNg#<#uuS?jIZY7^FxH)AUHurqpLo{s`({#0x?f&i^-OBVkdP(NE^&=PH z*fvR}sFnJ~A)(n#m9iauD+Jf4=znLL*M(T@66>SJN84UltaAsV!}>h?JDH*2x0KUH zHX$LHi>sIcxuL9@>musPFv%#gC)}yyOkQ3LEAOlIHEf43TdR##)RrD;V>F$(wT%jYbUtMNh^>jztm&S46^|bAkWoIcxAH^HaQ|BT5dv$G$>MZW7TG!Bx z79ruhE2e9c{DI^w1i^{+MaRP82MYwWygT=wWEB#XZ?kT>qL&_Z@)6UluJZAeHG|jZ zp0cf>9KmDGdM$rE?nHu5Z@&s`y?8$E+)*WO)sQXA`xeMQR$)bxo^Vt%eSGP5S3ht} z&$6TOIS9!(Q3pHty><<@upIsJ*du=!MjRtmwKts&}BqGiYT)7?yK!&PZ=)U%N_U zR`s1pps{rRm(iTypWcX@8hN#$Jdc+>J3?m^&vxNt2}p@xx{=C*%>aD^Enw=>gj$D5 zq;p4@VDk@EVT26Xr{5yXm0HseMEW~Ea?*3v#ec1k%5tU8>wcD0;^>QxrJZ?AF}ZIH zA(A;is%}0t=v%(kpS=>?E!$rLTyY*oOvRLEpMc#8_i%2hx58A?-gfaAhG`jfx{UlH z(!1fOTFK&F>KK)5wIREyQK@*v6Ezg)5~T~0lWtU%twk!Eu5qg=n$uv@o{J)b$g9iY z|5oVA_dgWPbWi4`VtDpdsoq|ijpP%pAy?(4pV}mz{4B0tXypsm zpWeg*E|6xD;+hqI49L>81VBAL=RbKrCfS{Hv|X{C*Z;Vjc(oKWl?#nix#0(o+gE8V zoc{pIJiI|H-f0^+UvAWzGA%%|GT@zcm2N*bnk-qi7Y~&^OEqoUH9j6W;xoIeTW|FD ze?u_Tu+~V&Pg6N%71=1&OpqR{NN~m9^RTB$dapw4jg+aDmWo_u_BXjjL9Zo6iP5Z~ zwhN(8WS(+MpvHaw?K&e`sRP@n!TGJ4Hk)dmyS*&uU>q~7ObYxkaW-b}L@I|q4vY4q zR474X4m-q@Tdl~##dW%>{3X2*iOY!{uC$Pchlwzo$85Kw^05gj$>htYjnR-=J7h<7 z&aEr(_botsZoZKo{9O0(Fz<$uutp!B%8*_rYiKsR`}HXBNK-hRZm@@A*NUTsk+SZ? zOgtl)1|wRonz=P>QMp@tT!7N3b#0wrjKm-jZk@U3XtWT|GwqkZ*Ko=9OygPlQdI?L zHN{9Y8P1IR9cSZtxkqhU$H&wz17?@AU?fawDpWn{*Ni$@)vaT+>a{j05BYa*sdyj;!KF!)bvGUUVA z%+c$gt1T4~>&+kA!tLs_z_-o^RH;BuOEi;FfwBf>XGUAD>HJfi+( zG_4wvBd^C`u>S2zK2PBHT+AoKm_IH>1!`iN2(){K6QS)=^DRfO8dS_}?vY(mFYqpl zr4U|2KZPi&Z#{u1XsJD_EzkOoQDNbkVA)RAS?W5^Q<)4> zJ&=U7eyI|BPz!O z@5@Y&XuxD}ZEc{rvvq2mc}R~&F~h`EjoKk6(qt8i_5_?22zDvfBltiRv#VtgZuuS= z-U8taNPXR~sI#V6On#tpjxi-mLn>}rSYCK-^d4T+Bfx}tB@YjCFrZq%+XIVy>jBMH z|A2{RV}nigIkfb|Y(EhvRQ?CzP%5l;S&B-p$~5j@i`d$w6T7y?Y|Fp_L%NcDxx&^)Ci zlhXimm5CXS0uyRWHm{H;2|u990T`eY!?j98zr=)yC;x}bh*pxWpS$rWi1%$J0cr4Y z$knf8%-}{U%|atxolo41?y#nF_x*OAI|^zLgr z;wK&cHwqfuf+RS&xkt+JwNm^+6 z5#o_(qbrx=ZGg{2Bv|Gpch=7@`^Pz=*y&)GbEZn_qF-R>gWUQPuC)7MwY3mcV|RgE z_piA+w`b7c{Xz(y-0F|UDTgX+dqSxvKrv#vmZ8~{bx*tuneyFI5%1~!IN4E%TUHt- zXL$#6zwS-RAO479zQplx@(iKA-E15;&vMd%W=P=}?N1-Hsk-T6k&kRxEkLluX!~x6 zLO|PwUG$Fql#V227*Tm{;-{z$&fm6M#2q_d|F@B2D{WXW(^R4nx%BeZ1Pr)B--4fo`r{9*32p^Hg|} zdJ)bF)mOx{h$zzdsj7WT-c}?~5EGzNQ#C$x^P6AlgJ97=cmHFh3-z(A3Y=f!*PzAV z#}?iO7kySbYORWqIx^wMMl(mtBO5H>Tv)L4H+W9+4@!K#UvN=7;DSYICVxjPOq5@4CYV`F-Zjp2ooWQfQt?ZekcyT)Ixhl_Ud0!I~ zFPGNEu|LMOl(9=>w0-8c+f0)Ql?c$l{ATg6aF%idK3d;kD2d zV+DSuP(wBeOk?V?A;Y#)oqrLASa2OkLC263+Z-1Emv385V*#EO2M^!FGS!9L|%5_0@il06_h8p$z&TNM0Il1np?;xnvOI&97mzNFx>J zIz}_;4*uU@u0VcyWS3I8%rgybA32H9=@s_zy|E^6%Tr#|eSD$6fCTCocH1AW*7W>K zINWMAYSewVV9`79grFn9KnCu{$f_hVZ}3>j>u=VkT!jqkD$+gDp_u zs+O~lVORHU;(R@UVzs`RA{Z?y3D>y(=!+tWP_Y;LGp}oxZCI{iO*d9I1Y`!6 z5*sY5GI9-`b^=FM(WZ&;-|ykMP}{}BD7MQUR zw12W-FVJVX#<%tma~*w07wU1MNnNd&oUT@5?)IItzdA(GxTa>3RM68NIPz3}7)noj zFB5)V<$^Hn7Bej<3*e;XKHL4MCikiQQJqLa+Qk`ISjIf3uaVBU%!WEr4TwBA)9X;2u&Xzpz8-cs6^i52AegsggmTA%8RK8M$X_ooNQo zB(qBIaoE?yW_Huo0?TV>1QHQKiL2~9Xz6=V5Cs5Ae*LJn-^W+ottEyY;f4{HFzYNh zczuXjcEw3TO8V_8WYWSI`={EOLd3P5h4O5|HV^=Qal<>fU&=?{#*s{862??y_)>|P z^+&g|F}zKbJMbdjKj!^S)r}n`?l?MY#cr~RHeilWO}DR``zKZj;z}{|y6_8dQAdi) z?dGm6ritSBtCNB@N>Cq>j~e-i*~FHh7E__=yfTwrMG5cb;uT-?vnW}9SLcN% zi(M6sW2Gu$q>bo#sZQh(<)DQ;*OK!gM3NJ-oFsdCOt~ooe8wNpqNY1{I`!E?+PHybs!TxBXyfU~mUvopGi^%E3WHLh; z{*Va-;3cscZ;Q^_kR83zMfnH$KP)^^R^-H>KO26zQZdsa5`C;=q1>&t9M+~rem;&z zt9PKA_blP!ImIA7k^7Sr$Fj0PljYFeG`>B%Ky)t>Fr5jqw+?81Cn`4_LOWJRS`%52 zQMo>VCs>L96c1b!aY7jeu>b%Se);nslGEIml5 z7QyFy%o*hpFZo8VnXDn1iL=$<#-(Zk(sgU=@}7FI>;Jb5?P1cTuiHV}#WPkqmsOzh zHLn37tK?IbIH9xA3#>5`@$nWzWyiJ>!-jqJr{JDOB+BVW2jT3jiTTr-&WzZ1_LRLL zH06Czf9mDR_;Vh=Bw;aRZDSiA1XixQ&gGs}gapTN2btLz`yX@u`B83ZXQ%lZ@Bp;X z3bxrVEo9X1pL>_Kx9qU@dSoiM_p)aFz`nY!%6H=TSr<;1c-i#H?} zSABC^NvkBCqVdbAQ?p8wpSAq=EZSoGqCO;Qo=y=ft`5g05ja>d`kE4TO>9d3i;hbH zAO8;dp_4FhthibRHOjISez5v$`VV?(b#Cv{?>fI}q9O>_PgK%}VP|6554TY+$_qR9VKv1JjRm6cs~th#-FUuSd;0cuXi zXusGH6^Q4BC={27gBf-a7EwC}>J%EfnBD{v`dx8lSq!hQbx*bx35;ovty9+`RMb~3 z=L2{y@Yc>ZvY~OI7xdcHd5eb{zXrFsaNCvgCE|NOO;1=An_r4e0kLRBy{APbzF9z= zZcyU92di$yx&Cug|wMpv<_`GK0q%wj_ zNs$I>w9I7-m)dcga2msHST$-ZDC+5GN&JZIksk;Upw0Hmk{2P^;X?7QStr_s;_LEIP$96F$ zQr?|qj|m?ZC_qSg09u63{5OuUz590V4%3kNLPb{v*Usn~!In6H#lGgqufqX83kw9Z zt`jOG@i=o-wi!FPfMwLHTm%dyBKrJr*r!jz%w93ij+ZpM3`3-XrpxfI>$W#A;h?@x;`$BK&{K zkH|JLtqbnQZ;m$Qcg8fka3)%uoQUFc*n>#iEcF~fAXa{6I@*HiNZKsJ1)*Dm^_I(o z9~@jH7s|B|Yxu1xT1f^_>=i1C4Eah2{e$BNX*T)0P}DMcFZPbiw{G~WE{fl8$XEH| zuRQ2Rs8@&I{2Bhvql^B9a^EV=iu>5?7OK|lIpGO-r#9cIgX%AFf;wq){>(~^X3FaUbJqV^!SL-uoaOKSkXv7aXHJ=gMl4?+>+%ZOm^1g z1#LZaxG-+$^T5@D!{2Pj4OCWsXPUPg3z>Cw;%xy@9r_q8k;5wW? zwuxI?bUo6aQ{+G>{%{xtu@Fgi(%ia zNQJ#pTU=x3AX4@FUjO&Gi<@1=6!KZX728I$z~adpa7y=P zI697CrWbGC3qhcMH<_#J(J@;(|C3`QyeH8;^cmt65%|$qSzy>VE~#Ocx}nK)?w|yc7NHTV(iE zAM)6fXfGP(&Ggr3mf{x}_KMSe9TVY7LC~5ScBe1s{?;S(kxduwMy}fUov1>WE%NdH zserk+!sq@yG(MLMxA!122WhL;8;hdblJ{CmY*hRmd|pF#2uZyYN$80 zmS0W@KwTv_FYv)-`6odfSSVd>jT(BwxLfRtrM@tv24H~(t$AT%^+fVl>&GG;MY z?cHCM95%}ERp)W*C0P`7A~1!V%>QiahJId`ZPcJ~D;Rb+zF-H;jV7R&1KT!d@^%N# z8kXeCT&0R#2`RgD1D3PliP97|pv6HE>pYJEikTZ%w=y3gg>f8(8we}=$&7n)vKrbt zOXiuz4!3OyB)Mw<%QnJ>ys^1$q-VZD|5)9-qb+SJ*E=NL6tBD)<(qGmmxYjW zPrN(2yAObHYitasvj@}$F}kOE>7w&g>x(peo7IJnWXn@3sChfq$wz*&K^J{c6Gi-{ zvrkh@qPu$AoDK+LiflD8968Nce%|I?{SKuWtw!%Hhhk=$F696lsT%9~G@Ycyu%)^937>%#w>i6B;m685XZy=%fj zGPi~$c6`~FxcNF^PO$&g0o6foRD+q6W5@&tUjLyqfm62(V}GASg2TJ2+xAI4uR$B? zB#60|H?$6T(uKWJh8QL`Un4d!e?W6!oP~tqkuKH^p(xb9Y+QjZI#WQmL?fMmdLO$5 ziQxT=(`h}L>h$MOsuB`H2iAY%TfZ=mQDFB?&a#pto-W0moT=A8VN9BVa)`Iu$e*87 z0;j2}-v9&mt1;ss!O1(*cYZqQ8NTF(63GMx;iKGdMpJo^p^R&j zQbg=Y*l*?C2-t%vP(+RFusrIhMUQ=1QhN2i+XFJb5L2jA3s*@VrR#P4LMg;9J^}T zvMRSoUfUm_BuuVwpLBmKz}Y~@@ZkFXy^X#PY#GfP3U=53*eGZ%Xm^`xxPL~wYeK}P zL;Nz-(7Gw%Rhx6OQKVS`Uu>Yb@k3LiSd0Fm{^|JgKx!a!G;1J{cnk?@N*!6b2^noU zReiFbdjW#D>Z!K+_9Vi@KmtWTlR_Qr2RE@Q>GdOBJ(I7AjK*dd^+#nt0A z;HEBnQxfNI=nN`p$^GeT$I;XMvu#L`$y8@lyg^w{uMO0EuKjDqDVJ3;nR1uJ`?|lLXl17vV{I zN^BZ&=s+zWRg-2CpC8lZvFa5H0DGv`${?`bvKP^+dk3;}+nY)6P1p>;o=Sf+;vEJ60Uz>$!sJ z8U=E~-4q_=-EMxIjpw{26t*F{Fi&xE4@Gm!Mt>VkyddlWdF#P4A`bgmeqz+zNdQph zeRG|gSNihWS8t9Gm)!_*hU5==<5#Va6Z5??Jl~X$e8z_T!6-esXZKD(gcFLi zG20gXF16p?>k^{@R^9q?&RN(TRa%-{n!*!VqVZ3-G^^Em@(|VcRwDryg2V}K7CBTk z&)u$Ll``Bt*=zi73R0*33Y+_5;pQhc(A1uqg$dC?!b#CjY%G551%dROY=4Sht#`bU z$U#CX57RrHjvxdJCOtw?p*mJ9$d9?gKYyo%_5JDDu3HMj7xL`N@Xx|-*IS>D*8j=5!mie_u!e z{MDgLSgih)QS9(Eb2Xrdm3T|-SYn!7U@NZ|b+H)FW2Ph4$^*zR4+gxCA5Jju;CbISLB{up-APF6$;><@wRW2PMg$@JvmQ#%ftDZK zjq<-}X94HMed4M7(gpUKnz*m0*wZ#zkAN1W@J0$-=4PEu8~{jdzfsDttpHw=s1Wnr zGjHlmqiMk@EWsn5CxvG7hW9+s>b558Tx7_ZvGSU*|B`Y2g6u2YC-*|g2yZ`gb5Pk) z=VM8;V2Eq5ldvR>N=MC-Z`IeIy=ph%?dUmja!625J0rfSdmq@#8!%Lygr!m>Tw9E2`&t{uwfl#g^Y@;x~W-GFy z%a@UP>V5>IbE9(|k42;Kjh$wxisGs{kFOnd5WuVSP4%~3|KInHvuDnYjg8C_ZdLSp zQm2v=8s~yH6ae6XvISo~g%?`}WkJSp0osIhFRS6aL;{?xp+HlYsLl zm~x|si2mQfLKZDo$P%_+|A}Ts*l%LjJ1kH28H!q1ThGi~3IM;)kJD-X6Mpx&8d)Y$ zSQ1KUPozTB*t4!Z2za>O1btlTB&cLuJeqtbCUVD5PPxsGbhnVbhrUBgWSXU0!acqU zh={ElIQ>K<%diQEfB4#&;4jDQLzNp$R?X=ze)d@|=$VAy0bRALqI4~wb>7`!Q4j|R zbN`EMCz)dsYYe?>mX6Rdtx@bjrIEG~N0$0q(DKEIZ<&Wu^zB-535fSL_N>OTj7_I~ z{Bwhd?-7HQv&r?#Z<>(Q1WeV>$vonQOyf!{A6Y{pI*WPqGWl(ifUidj$G*#4*~wXA zYRRyImHHU`O{v23u?=t9Gu>BTz4=;h>Mqzl0HjH_#+5$E0b&}p7Omv6Eanr*fhH=yjbnQTlt0sa4Q6>ndE zUw*=9s+{xN39311A>>0blRFZ(5|>eoW@LK&P_I3MiK*+X_v_MQ*^1?UM0N}k|LjFw z8Q`Y2TcgFmijlyW#&hKSD0J-Un$TlRX#cK_w~hXW7@sUOJ#MpVTI*jW;pRh}wKg>v zd2|z@%5uNS;jW=-!q%X-C2CS+Dk-O?pLI4X*L+ib#_Eu>QMgKCfyG_Ap1hw)6L*q` z)Jk7lg*p=_A7Vxv`+dw<-5o?w*eIa8&3e}>>c5{f5h$6TAvJ@{ga{>ZgRC4)acJ?@ zNLQc(M`uo6RB?Yn7B~jVH^gwsfU~jCi7v&?vRXOvDm2`xy*}I2S4VCwz^W*2*H%g` zdwP(tW*xg=t#tK1!&!}$OV~J0oUm#?v+$Hj01h2Ld~TmZ;3 zb%>lNV=BO+P$S{%Wmdbixd4dYzoCiMx&9T<=9S^iT1jpV4$;9p-lM?+yg~z~hqwbq zZsco2pS0@HW{%Opiw|!6=<>LBH|5zIp)-L@i3wb{AY}#KiDdFYa}I-}x7SS>r+`Na z_l-inh$|? zl+n|a*K&>5>$+{tJJ*{&^1NqH2{@d7X1$B0|CapUOo`p!yqcq|Jf7!>5{(&Ii9QN$ zM8|wUjLYvc-kFE3f03ZOFRYPDsZdK#bFo{mge)UX+=3{*3u^~b7o#n>&9F_}G||)& z^OP*?3Zsz9pkL6OhAQYH(egdnPLH7J`Zo8F=(77Fm-LMVTF9 z4rLZ!)Pugl?sE|DfI~zi^Lf2G+D6a6J-eYB4{XxU`^>|C_Biy5-!eJ_7aB z-DJBJ|K}Jv(UfmpdzT@eqt}$)f8mpp{JP=)*Q??INYo8Lz9J1oecbR(dmJKu7cTAl zPyv9H@M7r%Umse|tNFD>G7e$XAHB8&oz+G(-@4Fl1|s1C0p;&6Ue^N|GJ0;_9d)x$ zyw@QhJh3U5*EhRFGLu`ulwgs98t7MH{Qez8;U7ZS zy;1N0nwMpyol?b62*LqtyA0XKx{(}IQ0e!T46Jh0nSyzmdx z^n(E?*TVC0Rk&9x=QG1R0?C}`Y;sw!WX7j6; zO;IJ;)=O)g)M6WXZ5S^%CdTPBB|taT=Gdfwn~w7F+mYASzw--$w+Z7vW@tM;g4Pf) zccjuadUtD5PXU|^lO6*41lbP60_~uJj&g8BB z39PS|Ht%H=Qt*}s)M_PRzMbtUmKocwSNBb?IlV=!14%1{S6R5l-!ZflaU8;GqGQJm zAEt9g-rCxOi*s@gj0sOi2Rt@s(>8y4fv^s6sg7{r#R>aV)NyUo7k9*t_ z=(f`bY(^OP%AHE{<{U@sUKI^(w{a^IxCR!zk%7^vZM%m9 zdYL!h!^$s-z*u-Fr=vva0DEf&!Po;cp>*U9*@%^0)5?5HMCZn9tU{_*phAaDkV3hWtvs^kTqekl$VZ zo>0YnAE|(XJ>$0|rSOB>&7fwoyz3X2AMAaQzI`a4y{f%i&eS?nXerUty;1Ns2!wR? z1bc3kCq5Jk2tkc+ef%E5|7*&wEDaWUyygQH>nx}4UtI7gdhd--$^REKY|5R zbrJ&u{kdKJnF`_hsA7MOVdoZ#LLgjfhJrhGjtCK(j&Z>(Sb067PSP$Fxpm9hm9bUP zAqsXvY=c@{Ybd+zi)*_f0m8(}!F-H^!{XJ1^()*OdoVj!K)0o*dUjY(oSfOs1Lp$c zG6m$n@27<|AQOI%(n;Kx#1P5zsS-2sGh_N5Ewe?>+V6r zz9frVm<`&65jyF>h#x)*{sSsTBh%j%_6Id&*Z1|o?~}tLvn4_expK?T{1{#A0Gq=O zoW?QF3HFpL2W)#F>RW06u8S{5ZKh~bimDJBE7Ux^Ds$8VW^sXerd;r6nZetY7+c_2 zRtX~Ywcf{WphnN);4O6p7t+8ILH4)~zZwNWo&~80w-DeiPp>g{+T(4lAi5xW@-X1I zsBhir2X=AS&b#iUT62+2O%dX%#A3DbKAbK8o4P(i-TaiaRa7KH&@4jTn8*UKjF^vD<#=R)9k;@>h;mq< zc-Bw2?tqc}4V#7ffN!C7Tv%1c>b2Yy4XOHev|IF?Nvc9;B&v(BPl#n+J+;_3se9H z1<46NrdP4-!RIfM-ptjo-Ae36Xa;gBF{x^D;06dB82`gs@lJ&*2{@KQ}b zOrb2FA$}15Q-yH;_EpF(-WPSvBeqP{C|j=^NYnm7SZgpk@Ql-drh;ctP1Ea~HL(NY z;Apk_(|3+;CREC?$zqtb#(g(gA<~+9f5;F8C%l(AV}F!gOBE?$`8A$|w=rXObtB0J z*Qq-LNL?K;uAC%Nl(k_4du(6#HYS86?RKBPMsUPcl}9Tege_1~0myX>+F#_63Y0P1 zXER8wcn4le%YFQGd1nUzAc;pQ$N`o?=^d2EZ64qdT@d!qo+hK+`&BUNy3x=>=JSU)7`tLeDtqw zX(QXK&p1Hb_uqEJ(ysdzTyfk(G~fB~glr=bSnRZI_|>0e)DK5uh? z{DSM9J6Jj3vyUs1wC4*w0J{Iq9;XRbUbJcJS1f@{_`ONJN~WmUBhdne@PXYXAPLjv z=j;fyHjD+TYk*P~m4a_VS;OYwCJ)e5WMIygzr1kaodO}#21~2@BZ+gZ&|RKrI14S6JIcjGHFK}Vm+<8n z$+cpasE1l|Th2$;O}i3H$)~QR4J^CmA`&Orwl~3_*Xpnm&A-oe8X;dZP*ITSR(73+ zWXA#Bp7g7(^34WC9;6l*HxvR*9*@uQtI800Ybkql1SI&VR2v5X6;IgvNP$esmw;na z8PnRfwd{ZX`Vu2tjn6b~BI+suQj5SE*rhJ>2M|ZdU9fCokHXi~*USA?IFmjF*!xpW z`A<_-QXeK$kO3FnO8xhR$73w`fZ+4V623!7F%DJOyRP3$4?7-SV?)jK=K}u!!PZwt z#nF7*;ub6cf?IHRw-5p(5ZnnGAh^4`1a}C*-Q8ud;O@@g?l#EG@aFsO@4frpdbeh+ z`lGt0y8Bf3%&Aj*@8kK|E*T<;y~C4#ce-<~we!G8RB|ADSkiaTrs>@+Jo2zPoXxOi z7?J@06dWgQS;D}d;7Y-^alxN*Zyb$ILw;6fEb`}v_lolCa9Cfnk$#qQB|Agyo*K`CDo?Hx0YX9~f~ zcM=<5&}W*N^NU8oxwn^{kuycRzwD;cX+h@sO{mAs?6YJgfJ0hs%2{N_?!MtkdS>7N zSM%ksW~E=*An64)6qVO4ujI6?n@Ss6f7(T-f&Jgbx2*EyoOvYOVsDQ{QehkC@mHSyIU`G_Fi)0YkkmmhxY@$V0>zL)(c!>;8gN~+A?u9DyHYgxV|rk2CD z3{5)cwiC{=C-@dMwzKPwMky~mzyoEmtPXHPZO@~tp0*}x4)GZbosa=6L~QR~nMom6 zOUJfGCj~d_#3<7*v4y5G?~&zH`CzQYN;{zWB1ev>C>83spzuU53|P?#Q{v}Fy+_Nr z2Rt3bmswx@yk}sM8YmcxNRw;ASlO;)*(J0Z{V+k7Vwt_Nkg}>h#ar^h`%H1jJSh&m zUAdAOuFJrEzA)_6&N`U=^ZlpEDX*dG*4tfuQpw+M@_q{&tS3sjwPniFE{v*`%dOaO z)A_gpj!&|Y8@RV7@A^y3KeK*sI7Ys7tq{;_DddYt;f&Crfie5gf1BVh{=4t%?7kPw z{v6#ooMFmk`;dLp#|L5uiXxGJ&)xcG#VV9$!9KAc2juBjF%YZMt7tXz{^c+_DEcPi z1_N<_;|#)~^SFuQ-Mcf*W&Wq%e5%NEkNa1xju2_DQ~igrOMT8l$l8<+_si*fyIecZ zM#A8@W_U2x^EZav95Hez8rcF{W2HeDa zkV~LTfXB;6hR(ZbPa`56@hVb9{ez83jA_Gr&?tA8@yxl(qAa{un?bQVCf2mu^v9^V zcK{0C>b0;x*vQ@8FV$Nf{@0iorTt-PTj~DWgfxqA@ zL?dqAD?Yq(4UTA>X|+4zKv;PC^e#aB3}4i!X^~{THV_5sx6FS0R(K1=(Uuculycho zJq-NkmF^`Ll3*kDx91CTZCC#Tdtw*jKDNa`rj4E@XYEG6TaC_I*SCn(BpZJZ$%G5ori|+-l60 z#gy#=?7Z1%s{VJA=bD(ZK|t(A>J07Oe|mYYiYY4wh}Y5=d_AT?kzo!vH@L0$KBw|} z$%v*eiz6Z`gKPGl=)c&PynE&&R?wy7;#7sV0cA9AAnwCk)U0u5go^(#Dnyu=xZor3 z4zG7UU_7UYhNYh|^8Lp+vl|zDuKzUmWdC{VN99ER=5v(&m__Qp*UhWZu{3|K?-uQb zGfX~x-D1is|F14qoi)&P8|Vp1zyc=L*2QzM|!3+tc-C;$xa3s#h0cCxhd`W2WU zM>~3HUG~7KDPqAr@Ds#5&lG0Ub214zi`Zl1Fp89ZW7c7JIC8;ep%gz zg};iT;t}TtGze>5^UGq7dgbup*C}U2%NNESOP%MNO1R{{!|VC?Sco(!J*Vjf4bHy| zrZD%=j(q+9)KS3h71pz8@j@Z2^iH7qvG}KGcgzeg%xq5Q+31OsxxuxN{+^zPn$Tql zqGhP`;y;s!u-3h|DnZ5Z@kJu`+>rO5o0l_sNh5j1S?gZ@XC_j59ysbArZ8vxmZ1Yh z|5zQV+5cCa|3~%!H!mcuqp+iY<|gpmu>TDBEdubs2|GIfpOKf(C^2nbIXP<~)=UpL zfSRMPHm`~0{~4q1$$uGYY-IGZqxY_sy!!+@{O_~z zE0rH7=-hB__MfZEZ(ETdCS*cEk$KbLKCnF1|HT;6+hc*r3%ZXaIxe#(B8q{xIpidf z0&;SaATCcD^A-b)jd)RNvtTCdal>8{ivD{_iXvk|wQ-(_OsLYZ*ioDa_|WN>r$eKE zPMOIbeNg3ooB95(A=bgtQEx*x=|sS=!8r3j=ATod;ULnGbjfSBrajh1 zbLGqCpBb97%0&y_vCnL;)Is_q5V4y4KfI@wLA+yE*~W!U3@_aEYWGXs5$C&7_iHHB ziX)XyW+wn-_=A_u=p}rH=z-%6<;Rc&q^_v@m;F}#!7m8WXE}&bXf()P=6B>5X|NYh zdy2Ex;D?b|zgPIo;{(>cs1_`#R@7#a<~P7)N6f%;5|^+mOX33kS9vDQdLi1#5`0Mw zWNG72gCur;J0NV2@ON$;mJ0sI%=fU2hqh4^`jE@9;5!C+9;uzmTpe8)8g)ibO*DIy zDWlSg9rV|!>XWm_>I48(y+{bQXyaEjZW?%=Ep&L>XgP(TqVB2tV;&Ko^4xjeyVCv` zJqkV)Yl;6-KFxiC;(?hn<&(BrSt4^udAWkAy4U}K0(V?aJi9g|+FkLdtNeV@xE!}V z^V%uLyJt}Qw2QI)HPXD$avzW7i~vXxzge9ec*dhu_^L)uk-^O7?zNs+t|2J4T4yr!;FIa^C60D`uzrOM{te28Zvk`OoM z31j!<7!3`Dtbbt;ar(}e%MPoR!7Y(GzIQo?X5l6Dh&8oon<|zyb1B3Lcm^Vs2{Y5z57S%cGrJ!_2vj4^!hdDp zv#Uy`{1`TNWV=B6xrI^^_Kl#Qb4#0AGMyh>!6++^#pnqoNZGi88OBH%ffcid3P(EFD#UH9uT7(4zRnAV6tVD?&x`7-l`GczIE-lN6qU z{i$P+fS0MQk-KB9pl#Br{e2sYne-nfmZ&}M^VD;?J|@yztFG+tm^9$tm*kg_p&HN~ zl73Y?nm&7#UeZR!Uj0fXhB=;~&feD%{R_RDv6Us8%X&*&QsoATv0vY$b|E8cG*J&1 zzu^b3CDRK0WQb)jzg@&Oq`?r+gCgX1aTRjcg8D;;Y_SmG8r$ABiG2Wm2|Q(Dem@|e z@5Lo^+ni1aBI86tYT|MFP5a_{Sj+$fpQhwAJ&~CS=1;k?wh$lTWz&j6PqP*tCMdx} zOv6E0M1&z?qxbHBq@+6Nav5oMS}X%fXm2QQ0ZZ-&ujzGyw|B8;`o0cwvEyoJN3X$kSK4*klAYx zU7Ycv;VRiXg|{RMQLF=R7Eb{3V)pJ?C~ljwU4z6aFfFteP*BN8f_W_!7E6C|Af1O? zfne|8a{4R8e$tSp9gw)dJlXnv=#ebtVPjJxRz9a4N~LY@!^yi(fXRlGdhkTJFErLf ztdTv+tdyfz!43!}*OwrRA+w2JAk9vlXapuPdZ5AOZ(m=hR5!%>(+ zJhWqd9mY;iaO9Dqg1e~6O;29Z(l)880+jUVom8vFAaJ3hh0?qdPu(A zxI3N^;e>lxaMx1Quq7xEb;n!Er-tY$8yBZS9z^6h{f+Irvha!Ah^b%WXAmNjftX*b-q0>-Phx99b;;AX;Z2CW*I|Rq^aKb{5SX(hGk;=>|$KL_T4!%?{K<&|smPQH`pp7Qj)p<5GpZc;%m zmJOt5LpvOesNB;znl-(DG&uNYSy_wDjb)PwTm(P%v00O0d9WI`^cJ#)i}e2bK5f=}9uN4f5l z=CLX2j*Z)Ed5hTiB`vhZgp=|Ft>s~2xpAuii{&FR?$HETb4D&Axs)5joaNbb1NF3O zQ?qtPJN^evCtNQ_1ee(7%(`-}Q|oSU@C7$}^EWZtLT`iXv)xZlF(#Juxc6J`40IM*BFv(KU8O+jD0d#xRBkgu_}v@*?Q{<)NZbTwl3_o)*rFb_put_zDmTW)QH8qfhpDjI5>2JXeWJRG$iQxU|)GzFzRO+vM>LJ4k zsL!c-9b3cB$iE%sW^9;mbfz-z*0O4XxVF6HN7kQ1R<4*02)=5VR`Qw=py~^|PHV_M z@*6RFc8gbF{M3R@VjRFvH*o0tO8s;nO$e9KtH`&&khUmK!Ivh;R)s>AK0>Dg^N(!O zUw#0O&DYakdZOMkq@c?c>ZH75O2L~>8fEwyQo`7C8AG4aUn6QATPmFz-#um4c7x zEi06!wpiigN>MIy6ui4dWr0`a7@d~6zo5P6OmUO#$1&bA{l@*T&(%J2+#qXGZNwzf z18juEf;hu*w^aG+%0`h0|ITiIzS~_mt31=i}bZXQ+N?z2@iXW=2 zbA7s=6RbKgm3R9x6ZO9DdZ;$)cq%HJs(yFdWJ+r=oE*6fxdO7G>}q2Tq@(!7=_;D? zbBQ;P4ccEi!~epJH43h*|HzF&Q^9JzaVF$?50u)pLw14uVDBUVm&==;qWOhC`${>= zNA^jt(e{G0!(;rR&FzMz{;K8#L^7(G1S$19Ofxofnf1aG7t`yt;ZCa5qj%AFww>p_ z9N2LP)-4AiGC!QZ)YB&=_e7HQ@~lu!zL?I=R5%Cx=4m&d9Ojv-_7xaRyESKUT#1bE zYmiIImwSg{tzlaW6jhm8Qq1b67inz{`{|!~nQtzOpL6{e&V{2jh+4=p?<`y8`0{Xc zf0uC7ZD@~qrO0g+q_Xs|-!PFoC8Dh~^y0O&llh8#WF~=hgjxE5{E?kY(KXC6=rShD zT-|mPd6NHmF50ES(^B_oClWoQsobW#{~&oZD^ibL$TZHkj=!tCue>eRrh6F-0mRkI z7LRBT6|~!ltCEfP8-xdx)^0s%wC~;<*gb+|I;Yk7QPMw*!E5HA1EvG4+h zn^eW~XUf~_#mt+bX;N#Cz`d^tTb~nik;P5)_1Fs3_t?{4{FmE*LjC9fXY-Zvi;#w8KxJiXs;U<&KrgK2ol!W9`9SBHF+3T9? zos17xE1zYgz;wOvwLfc+X7Q(ZG<;K zJtrVN>xkIjT3X^ticVIu&y>USv4O1-wS%XJ0pR0~zU0_x*yiBiu6sDWUEj0heVQ zGXB&{U*sBm8KCa-`u>x@DwSE~Rk%x@hh+Mg=L-&od3iT^e12CKD6zeW>^PLC(4Z&M zS%$sU@ep+VVpm%Nv6LQ4$N&37c+CB3u;%jB)Pe)iTD$%4h06O>FVi>}0r691E@!bC zZYYiLSWEtp);Lo|N0_j?>!@TTe^Ap~?g5>uT6uR3aTrqIaj?tVE^a+#;E3!oZTFV< zs}o7hGTQr1%M5eIq9*_iN7wUBJ$u3~t_`hy3UZ7}CAE88=OX++Vzx4>AoK- zTwMMkf%RokDAeD^_o7^Ln*2j?fi0;O4J{CvWFn3{BvWe3EM=ZCHH9yQ;t~3L4E)`G z%j`IOd`8&*jSmM!TS-y@snf@dL(A;<(%A$;83lr^`p;vz!W>ss-y2=}*ZYsc;yw^Z z79$5y>*!K_a!W>-asMpiVCOa%eK`r|Ew7njtS=5|2{8`cHj}sazg!rp>McFk>|93a z$#slnlVJPX3a*YxXB4sS3a0d@8Q_rKX-as1KZ{Wprk@dVY`|X31)) zHF@|H2JjtS{f-k~hiQX?A;u}i3;T_-wv&Ewuv>}|d-Kx_PWC7^+9FB$LgZzF4`TNy znlWRQU7(}6TNN^RJC)_N9RXUZo;-#RO}C)_Jxja3UMgEL1q|v=7~_jrAGJkMNOMOK4Ir zjokA#cl?be>C_wku%A*WEhu3)F9p2!m&+PlG>HQYo-V$g>+3wyi@4ux3?t zQ{}cj0CwStv)n-E3x>@qR+0b;zp`8=%^XhU5-;AgZd7&MW1!~xvBaU`BdD6t`VH4= z@ibY%`7gI&V%Nw>y1}b=8{yVgrG~zzYonNR`+zszlU=VdHg`{?tPJl&C1K)1PM?RL z=oEvyplL6YRljT9KUpgmy$|Uc(>kz_G2{E=PYE(F^2V{%an(H!hGzg6W@fqNr=vv?!)Mr_~A5d?$qO63NX*A0) z>cE}xRfs{CqE-O~Qq#u>lwDgCt?Et>_rUf_yv@xnLQY&l%Q~R(c=HKxKm>7?Ph2Y$ zjyeOI@5*mnpt29AyBW)P3rZGwWVB!)Orh$9@__9Y{uZTFhfe}bl5eNXbwP`GK9Kw^ zuS{k*5eKFnh!%{3g71XsQE)ht${=GkDz*`Dk8p}3b35RQjy(m{`O(*<0b;3+tQQOK z4u&LZ0}-vbO5J6kQyOU9rk;q9C7eilD^U;6U zezLY*%MeY;Bh%Geh7xdoHt?09}mN2>9xywYi zUwhW!U|(9XaQ#Q-hWV{+>7M?n7&XuHMG5;O|3S`NL*t*s#IebhZsAt$$uJ&l##r?? zr)aj8F}qjYOX6l80k8ME?s}O(91?S*4fs`!lDK2&b=X;|t^LVKKRF;9vOI7Y9V@C( z6+xzS+bQ#_5P>vSsI9d2@-BSJ##I-7P!|85BA{Y|-5k!eM~4&Q;uB|t-e zeuu1f?-{&q^juaQef#zjPgG6sl%F|q&y_=+x4>gWEJ73k zD=0DYVsU&vbC67x$HpY!Z+lYe) zgkYB*>^1e^uGK5%B9m!&0-9Vn!atv~2A(}?LV;h}DXx78ToMp{Y6kvZ_jaPiG;>XZ z+>Z{*rXscu1HyBbP7wdrw2G&{aujlOe*`utKlK4H*9SAvj;*OHIb>s2qYz;Dbr&SCz%)$yVP2vb7@R|d z<VW*ISmQbtmVT3EA_!(Qwni8UDX3S6;9z9$6ErNu4Mg*O8csu3L$K>F`aJ!YN z?>7;xER5Nl6!OYh9?!$YZ+-%0-FYZH^hxu;kcHfI`yHtyJ+pTzya$uA@THnRf&+z= zmMDk0guxSJX4VgxmqF(2XO<_M;_CY+m!E%l9*icQp`KJM8bjg=P^_)liYLT@*6V;1 z`43pPmdrG3_$s(W6(}r@#uJCeOk{IEfuT9oFHh^%l2H;U``?sBX%tvcvqN;x8D~sg z%T=vF7O&B^153b6ev!f80q$=c&wwn@+O#|n@n`rjHo40OA!sWiPxWd+TZvFsin$eg z%LcI^ZQkQ==S-6dG34(G+g)`QvgrX*^QiexDBY)c1#I>)xxSd{4Fo&xrCrH7)9#$n z)t<=mu2pEkrrey`GanYl?j?Op@_h1dByg_%SpQB*`kv%BWVXkxjUIo0 zWgn_Uf{6F$f;ndq3#hwDux40~AbJrbPs1F~*rJ}c`~C*-4-bfYD*T1rD}R*F)Io|T@5sw(31u!U*^bqw6FuMJzY3hGqpnx5MDZre;#_SgEe=GywaoiWT z#IR+S3Q6>xF~&;dqkYT~0cuqrO118UuI65)MVBC%;!%fH`M+A(GS zxOr^sYkh?N!-A5K#0ZZTQhV|bl68pVD^}+B!ZK*}KIZq_Q}4f!V-u(pZ2;jkNWr3# zhevb6bFQTgmL0Xh?sHD_$4F5+hoPD(0^-Dw4*jVUe^VMIZm%_W&@YC2$OD?kO-?(b zy7bQPs7RDeaQEe^&T5IyoFkFv#h3^{+G-);*G5Nf$e=UX#0=&H8WLMb=D14`#k*(I zVvtV_xX$%vtpB5-YZ{%CVTu}f$Qw6#usn&PY~|)k>(w+cO0-3EnUg6qRJp8=mx-=b zHK%|O<3u~9R~d2E(QNMu6<^jbr<@K|poz~INh5K}m*X<~kQFcfaMR+00!HB%3{93d zIo`Kj26i-9GPNi9FN5wDPrdqUMt{309PB}4lBw`J3A1r~6le7GUJZMp zp&T$XLy|7|IytDk+G%|LF!^d?W}GcacKu;hWZM04(y-$bIrE*AfXDs$d--eHktmFs zQkT?4YgtIK?WV1cvE|+Kb#TpYVCak2b(0J)&5s}IqysCA8pW0T(7zf5i<=ymV+$On z0$q5!7T@yYvCv5_T>F+R3$TSOlMYgZXB4^rg&%T;je8Tt8q!J9+iOaLQUw`P*rm9*MJ>1aPH_b~e?zmN{(7~`5 zu;%JU-$vFQbKPNIfN5@)To85r!z>!e;4O7l6ycIryy$5aeKv`D8Bt zlXX$A+37Ix7k1oObsLaWsC-Q939^P6G0P?(wQGAND)qgzh+9fDl3(MlI&H{7bM(ja z#*z*h1RRE(kaoq5K|RdPrdl5p%m8$!N*#N>_=Fuy^%8jh{8kmE)x{c|{Cs^hFe zK;}N{!g|PSH*V!F+}?Ze=~UBi1637mSkN+xPpiokPdmGDRx^gMN~>n$>4u!LGegbB zXy=WI4Z@DC@Vf=zSgcdPip{eGaOG^QN|6*@Q_kj`>_a<7*xOz%4=5j`A*+Z27RKf` zfW@b5r~EInuh+>}a{ew82iO)7najKkJ#|E*tX`cV5qzshVI8+jtcF_tBqps`Cw;@_ zGbNW)BSA!@@bJR=N2`p!Vk_FwM_uU;%Z;Q#R$&l7O_%XRYDCZZgbMeoLb}3YHYn zo9D{&eKRE?$oQt_5(Yf@qLmpU9T2GEJeDR-Dzez>XSt-G-scG z%$4edv|hhb2a&)-?c};X|LG?a&M@owZsq^dV!smeruFzh5r$s4=RW)=G@Y=8HZiJg zsiKT?$JXZ73sVT#;&~IBLBk-?!oS)NkTvjpxo?Vs37&n@chUSq%T}a4auDy4P;5GA zMJN4x9O|%nldoXC7Qp&oV;jp{-7kLMYj2+AO($66k}^(|~8k-!#zL>1rnY`IR1 z@-QbGCx|kMSlp=LxYYht4AtIm1w9D-fftVebL^`d$YsUEuyVphC{0tzby>`M zz-t%k^NIA)<0oM$uiXV+Zk8*+StY=YRqnD}SF2OVoxG7_L&T0~cxARv*!HH?R60p7 zUyg}+V6Jn#d@KyY_gt5>*oa^qT5L`qhJa77sDbDIA<88HbVX7m`gxF(GJDFBDsKP( z|NOytI$U=hsL^pJYwv2A#54BJt7q2oqK*Afl_59BT~ufG#@^ooyiWd<+kemn_>PNp z&U~=px)~?{y*PqV(z+f%)rh{*iT^J;k?#t^lXku~U)EfJg#czaiFNDM-@SOvv0cK_ zKaC*@!PH&$kgFX@j@iL?+~2@~bh@jRAaJT*6)S2WV#w+{EY;t?mg#Mw2vzfj4{O4t z=`q?=6ZwLQKBsKkFt^OO^d3`El#UZ)B6};??LOFUKdN-jxHGcKJ3zOtB6Ab++l{Lh z-S@L}9B<>h1H)Lccp@cdJNIUpO*^C(^eS2|on9YohE|hs5`E_s^x0rcAIZg5$DVbN>CO%ZJ$T_(l#EPmj5IgMaOV>JDlSr}y@F7G3@_qZZr+`3z*lYVS%KdnfK0CsT0I)B`ZB z3WI-22=K8H{X$gUO(BpI=7Z5ISHT!sJNd z5MK?VcjGw<=D&a3LAJy;oRSo3zPtT2&c@n3lkKk;|MSMSdfz3HuKgizaRnfq3Xsvc|vcvrn^tyz7Xb|rVx!-vP;K@mvgg5q}moVKi8 zmSwvr9-T-nN_Bs|IVXh64}MY@w&%aD9j9cExjuI(HmI7ul9Et-IXuKmqDc$1+!5$G zwk+WI0@a@a=@-BfM^a9C%e~qKDE10lQY~?~G9Q!wmAF%YtHJ~)nqm>!#m;ex8qV;Z zzS?3{mef7ml6GvspB@Efr;07>BX}&Y?e105yS+2ejb8;qQ(I&HHE|FA zY?rqI3a(JOXjM zUzVIV`d$h&hm8W=u$%m?gywNK&9E1>;2ap2_oWbc1?0R@YR3G&*!v@wlF10v0wEC*BywGWNW%^!h1ZiEPORLQ=296^&$jph~%fSI|kN zQX>fbIYi^YNDT2=J?M)-{H{kxR+MIJz$6I~V55rjD#`c-X(=i(`sl2Ffz04lJpAU( z1L8$gy&uplb{)qOd1Tw&*#i=IO8wC~D?988F@L~V-W1~@YSU;-)}VYX{M0Gv7Wq&B z@0lWWM)?s!Wfeja0w(;pk8_GQO{2^qMgJ#UJh(htVP0uw_c;MjZ&XT?m)jJn^FecvMPo7iYVV*n5ogtik;}q&_F->@^14IquK#0qP&T3%>h1#-8TL$e zua%Q#GK4KTyB{*=$0ajKcGbnhKj#DJE zxLuRw80$`w#nMOIIrVv%2Qd#+dM$B9VE}wm|LNh`Nm10rJCqKEv~7nX@p&TO&XVaYLNCAaWvwMD);?2obC%NwXQTtpzQrR3 zEjbnoC=Y`jd zJn9+E28d2y|y@>T-wV+*EpOb?=T9?;G*MfKb=)%1|7+IxIVMq`86vNXSoG! z2D=Jxy}PGY_+}8JFVRku4UE)i(@AA%9-4t)Cp2-19XOy>Fm*d41YY``&5Q2ECXm7T z63nj0#(b+Mzevr!(jq9)WS=Sac9g9=-M)h3RrW)@gI*$mO2S$cVxfJ#*k2MmvnOmf zpQo!5cCuDK&&B+^G5>#6GrY!E*|E7(v=4Nz9WG-q0^PmCLYHUNX6Y(qrF zs3|Be!k6e=13Wx3o15TqF7d)z()Rh{QTy8}BJ8vbh0K@IuCDB2;d97oa?Em3ufWWr z)hm-9vu!c=Rz?Fj0`?Lpi@?f0l{Z2A@Npp0bJ@Wg>vZsYDT#n7n(81A^qoT~a2blI zkNAm21SRtI6+q;1c^i+~uM}Q_Xa|+}3Nh~{bP6>jjj88_!<%$j3nQ@`)vHDlY!g9g zLN%2{Wx%_o6H$V-=l?1BCU{LJ8T>5GB%w?phP7HSiSht`V zU*m-#$ThP1E?1MnH&ge*kPZHCFEXtV|CX88x96`9!p|!Gocn=Z!q4BF=&otWAq=xG z@F`7GgQ0BzqFt4SK`9Q2WMA^TaJuo!0vCXQmZP7(>k*gOH{Uz}wpdulYe@?*D1XY~ zTKQP0F7eJ_1wt`N>@l-H6o_ca{aw;^Hqz8Ff%?!PeSe}CqE z1^rZxEk=Jbt(1Z7dlX}yAM1y3DWNc1M;N;>qa<^D!~k)<+7!Maqs;fkEy~G#XaQKe z=d|t>IWlmPLv>VM@&?qWASEr^5%j>_`29I19q9$DEV*eq{~L=in0Q%Ez94=$UyC5Q zigvCb3R88aPg_g;A8#YQU5$yPFllM(FXJIpWnLG=@;n8*^QpL1$rT?d2DUVtrrFzU zo*uSC-OP(mRLRy1UKxSl$;Z~>h^Ds$G@|O$9r9;#p_RWl*t+w)FAm+!vzg%ZTW0;w zkiz@RziE=qRTlP?VSZ(Kmp9!7llx$3|C52nVLg?}`qdwd^ur#A>ZgR$<9_u*o@Ytc$56&u>6<{u!nq5~DG1RWXM8Grh$Bz{Kx|(t9fHwDh!0Z{h!PiIEieIw8g#Pg3 zB(u@|__n_}O93e?G)h+(AwTZ#tyVO3-z2f2W-mZsWY^RUq4(I=U7 z%tj(_!QHRWms{Ci!E#b%fXJo!j2B)8|=HRCGTawS}zcwq3{ay`%c~Q*iS@Rok>(Q z{VrXtN(_oh9}7ae(* zIUiCLM^_$FpugKpbI)J0~eFuX;@traqE*4OIKdY4)D?gxj=zZFY%95y=74|%?O z$3`2H=HphFQu`IbTdl5(j{OS_ zU=}#~D~1yy6Z+Vh|0FBR=s)lG{aZSa_;wQx5wn!pwF8lA?WQsai-JY3`s{C-;HAbJP8@9R0GSud+coJgtbjnOnE*<+bT>pLi}HBeR)0 z07Y?{nQj`h*rDF230FUS(I6enYkDd#dlcQXhc;DV&A`erG*^pZF}N`i*4L(6UO$e{Ww9f zVK4x*o$IJL+O9k!hjbSV+``#ned^Hd^0GeHHRV6*Nx$TC&&2Xv%%ur7B-*u!cD{0q zjl!kuT>W@?VIAy;^~{Dd66U8*-9A4@S?$_2Zn4yqC#%Ir*}i(vqyjwI{=?$(SPcKF zkbXtDNHqL?Rk{ZjdTiwukxlzI}sE*jfUIoFY{Uj7U{rJH6H0E*v115{_3j2B$ zH}-|aj%NhLz%NS@#bHxGjaQWp4F&z04+?Rq7?WyJ)!p?*%U>~}{%ltX6$DnJ^+c9p z1Ow*P1Y%9i)a^t@!$n${vsm7WMN}6Oiv~3G;qY4T%A@pqeMA;({Yi}i;8FD6mRh}N zEzM|Y`RATV+g7wuXpK3Qwq$EPc$SHWPUBQ-iij?*Z;7E0!Ftp818Uxcd4F2?7&(Ki zfnZ4u&{}Y(PuV%t%=@Ya`ps}pp_a~aS?nc5P zUW&$;jf%Zx6cS>DnE@8WVlGEic5Tii)g%Z1zx!zp51HT#Mi)4M8|+_C=`jy@V@!}2X#AoN_MYL zDQO-wj_YL-C%My1X1L(1ajYSs^OmE%1xA%W%ZupxJ-2#iz1QdOSx%oI+pCZ6O0B%NMdw8ukq#tO2$D-w;A(;y`tBqwbiR7h&V$$p zGPSm!IT%ip@rul+xMY7q%F02;ey#H1hJ91_I~RFptd<9nF%lTcE*XJDHIa%*4ohiq zd50QJ6GYXmi-MnS3)*uX({FrDvMx!q>M0U7)38%6Uq)8m?hq!RM8$4agEpG{CTVH@ zy8R4c6bKcWvSt?R13R<*m8ePaRv`(qK(#2n_3O7PH!t0%4Y53IU0kJP^5V?>IWaX_ zeM+Doet*RA-isp2gQA?gAm0s5jce-;VIgOILdYf`|xRn_fH$B0<*h z6=kFu2XQm&C>GuWSgKGB&*N=e0^t|n?pdDJ|9O>q{--Kn815=q_FN*+_Y=bN2Bw(o zIRm}qB_iCRaegl7PIZ}ssY-9NPW;~b910#tD!{XFxUmjT6{f2w)vZ!PUu6~~)8qrr zXvI5K3ewGDNH&y@b5Saaw%6}sbr1E1BZ(5M+u+=HRHk2Lr`dO|lw8}c6D|Xsrw)Rz ze(CA#l4B3hEuU$kG9?N!C5aB=jsB9Sxy-`@%E9+1M0|n&o0dAi+125&G7lkh2pPU+ zV$yn>8Mz>NOC+m;Pgj~~*(u|KF zySudFs`Y}HPd2fZMAQYEOL1?pnkZGPx#$CfWIycLx&KL340y(oKJ1w&Vxk=vEi78n zO0@e_lr*R@U!xT%K5qOV>&l_}6K|iw{Rm3f`U?MaQvC5IMUD%Tp_ZB94c*PcaUBkY zZKIct74P2`l`{Xf#+Q*(L&(jgBh}W3vVU8dX-XdPrZA^}1gVy90Vrxr2m9@8X( zq$vIjLK37?W;Lr9~r9vi^KtiU-~ zg#MSHMBK+ncG}ww3HJVaGkBfwJTA^+k+Sfk7JZ*y|XNUX2R?>gy>l{N7uH+txY~qi-W_DB| z^=3&nFGSr?8wy!pe~6z9uP`ny1C0$=n45L02@=iAhKQ&nxDByeQ;WJ zrprWp+86Zq5j3yhQ1ll&@HFa3w=s~=ng5l-FPo^vUQe=+ORfqnW&lCA8%|!MzLC@~ z>(kL>gN?4%d!8N@Tt>_PEEyWnn^abhgpiG0TG>xx&=x`@*oWH{Caz%SzxF;z(|xWE z4{YFY;vY+4;*+a|gQNWs*T+a*)v;82R=&Xr#Qcdaj{Hyiw{t_bT_5&Xf#e|by4pZP z6WuVU>CK6I@5k!0gWltNC3w79CHDkZhu#21KZm7C0tK&tGTRPpHfU*1VaCFC{-Y2eo!8gN$G1KDqlUr1Xhul0!&xU$Qfe-=kmt+h}y%`C;$YxO()gRS)P$>wj#0=dobII z1^D^Tr->L8!@_$RNHewZ@!*Ly$OCRpK{I@=Vvg}!_*cI}?6Tw7eH9+97Sa+tGdyPe zU83DFm)w znF?CV#(Ebl7UP`!E!R*CyR1F@)GfrMLCtfOrJ_P|N)HL3#b+}j`Hl>4T)Jtv_Q)aJ`y{EoUjhEWKr;p58$ zfrcd&Y!3&&$V4>Ke`VjE?EPBtjWiod%|bacj#vd*J*(Hyd!<=a-LrX1oOHtkAv_=B zg#P%QM%uBp_o`g!4WgpUU&5?D=>2a**-Px zBuN&LDFwnGeR5@8F zM!N~ZuWW{nN*O>qRtViBG=v=v=CkuCkk$N`G!Cm$`ewA4E$+{AI2AjIZX zh{WH$jJ-S$vA#*JN(d2U3Xw{d2J~h#?cAFqPr;6(YS}*XVw(nRIwdHw40+AfYMIWT z&0*T;P&KUGUMw0;HCt0f+&MDXH}Ko>-L9OrwGvJE=IMn_e||0zcsM8iUF*DDjP4#o z*$mvA*>i})cZ(#$$jH&?#^F&+KQLC_CcJe1;&Fa?SD0AMkpUy=G)BXY`i3tA7h^~R zL<9iFE!2zoOBrwLTy1urrdp0|d`h&%mQ)qw_IcFg{(^L@Tf7&**;>V_by+IwMv?~F zjw!FL7mcPN4~Px&WaSJKH;p7>f4CL}+Zl3Op5f_$>-#c2POa*}^ zN7QYVvbT1HXYbF#TL2Jf2C#@p4`o*MyY&e*oMo2Vog7^E4*rocaZ2o=ks2{wc*Zla z4J;Q#_klYuM=&Myi5=!aC-|W|Vu`-^Q-TQ!#ApGOn-ov)w^k&TDL{SPa^orYjqUC9xSOw7i4!#7HeIkEd1x>_Z(3|%?wFjE<*4(;#eWkW4{iJ@8H zdJDuuvhGlp25h^tD75jLh1p(u})*PTkCx3{!oqgX9H2e;#ZZ^VG$J-OG51W+~tXxDxeAv)f#grP}~T4h(7R~?eA%zcHCO$ zZgR?tS4Gb+2Nz&YSYRn-DdT`vF}oFt@`9;a@Q+7pll?BC?j}pmnK<*Rw5$?Zae8TM z7DH@!`tuGJ3OsPaF+BxKbN?t`3>-sm*wCvyD|8+7dkqKA@aoMI=kA#4mmOTXf@CEV zeZSNqhw_O_MQm9SM-Dl_J<#M0#1me=Oc6=&1=mSGT`BckdEfh{y<3a@i0zgwlf7Ua z=viBiSn1d>8q@HdwA>N44Ih2>xipbrn`elI435v>XT=Eq@x@THnh`B@f=q1IIl#e>Y2ZX5Yvq!=9iv9`T6m$fWEc9jkRq+dLX4@S z&;N7L9J!i)BUm%4Fz!JPz8+U)fiB4sbLkoB46D>b{nO}<@y8~^T&oJFGBE!eYB`nk zbwjlbi>U8I-Tj+Z3Q&mLQev3L?C`n%^yK4iXNgI8woD&_bZ#m=OFUJjmI9e3lfF(B z1-utUBxp(JlZ9jc-r(qcYkgV5Rb({1%VL9+-XYAF*4mgu9hWk{`yCp!A9kC(IK?Ag9bicEHRY#o@IY6pmIOtWzzgRk3{q=v{0RC-mE`m|f+HeJ4 zMvhr7mSHO`kv6+@HAoZSSyIV6+PD5HRAyW?O(qecEbt={ij-^`1v?vPVdbCxfJ*?<^?p$Vw>W!F>+cN+`&@VJ|_f zSn)zd9Xgjs+)a~g-_+fV^yy3X0$(ZaUZ$3DC%rD9gLb<2A7L5GWfrFs&Zf0)w`Mea?qE3EG|>RH!?nY1 zXGtvvGF`@VeeEmsgv(pqL*Dxz3}8a4LApK?1h1XDP|DqNt!0>Oh&(e2av2p6sq9FD-QdD0REy684a62L9U;1&3~bGU|gk_Lz7q6JCuKqK{V z`@7yE`>iRq`8#5WHQ%kROfPMqp7aZaHOI$20%8jQb?pgOhkMmN^}$JNW&H$_QMu0# zRTWbw9qJ!iZ~T3hZ_gGVmjcPS7&hv%z?l&6aOJiv4 zCl$}~)4|BBPEbdv`}GYV-H2F>?x5FKmsg22bx1?w$cTl&RUPOmRbf0iwG_RfYpl<6 z-jnBfj`bW#>8Lb=N(?8Z02TSI4S*0qb;ug9>o}Wx#0ohS)$$W9Z=@AVzq)JKf%G53 zuB>e~KkH3;lGN=e;DHV?cOfjfd<*u=p86;y&VJAg%~sWXS!+CwZ?@dswBl(zT6OI~ zq+i5p;g=J>?XtaIanO#yuc2IN)}s};>b)%DX?Wa-8b;fngL=iqPBZ^~5OF4e z75dEhbD>btm*fRE%vQVIMO#{0`l2ClkXJNcuw-jZ=%ryHH-6Dz*B(F96e$Z=|AZFt z@1JWZq$*d`;w*$LOwuaXS2&o8C9+1J^FBnJ%YYBFXnsc(xLr?>>wAA19|nWn8G~H~ zg58`>kK&=vvx_CJOf)o8uY1{}-utYQIljYkZa(Zjt&?lKPJZ1m&U`T;YFTU#$xC22b#?(PP(?SfJu?hRHl+&#ML`BwQEPB53@Ik1F(fi`k{-a$Asv7zqa1(hr zL%^QBvV>92F@?MJ@i4Lb(SyS#+9NlttnlNe zxl5(rS-g60Svh70Dv8JvS-l)1Ull-rqwr+Q8;!>M(>enIH4)gL5j)K&W#uhhlg;w` z!(XD^wPSGz6-aW40+G-Mb9I?FGEse1@3D18m1a%p;Gh!1MIQ6Ym8-X;Zwe+U2Z8m414+ug`S^FkO|o0etpIQ$RY}{ zn1$4Qs~nW99&Rj7*Mn+qkYVKpRB0UJpOPi|ylPYlGj1J6z4ebzs2_0Xx2E^6f-?<$ zW!yn`39q{KLbq$B(7QJv9g@+>DkKF#FS;gYWAibfCjJ!|U4FsH+|7SmPI9BX0=R_b z(ZyymhUv=)=k_~@{&NIDp**MvAIPd!v*-@JKPP)Ho!>?1D#_C~4(saB&kY`nn^bj$ zMJub19ICnRMq5|SOQVR*!RWf8W5`u^E!4}bEGb5?qD4?<{@p2L<9-x(;rh=Z56(f1 z=|cqXi5C}AIXC?_A^uyX|3QfI1Ff@1IKSFrF$L1e`+xOfBaE${M7#In2MzS@7{a&*a(S4?k0VFkqK^=nO|OAb*ZVSX zPhm$(v{)4dRzUzODFcITLekIOU-9rXao-f^25t;cC8dh8PzCK5S3$;zQ(J*f%5-xY z`E1^kXXxS!R-C`H2Ytvy&t*UsI%ltked2!C;Brp~%zu*vG{Ex7Gl*ePS#3LBeIX6L zeHE;tDNi0#cKU^@$7qmbvE4`&YTk7ZcQtRfn_uHuWW1d>7ZW~1pHPiE1V*Pi!Cj$S zBXEaL?<%vl6NEOHXl3gL4CVuX9zGq!sG#RfCefwW@uBcZJ}Z_bwJY8T2bA$8m@#h~ zaC@Xd|6$Yv9a5U2OWe~xSe}_|zUqqvDe9`{nEY_aL^X``W?%+cU zt<`4NZd}`3^G}IE*{kX0Bzp5znyzxE&0)@(wjg3#b6~DD|EWy70NHC~fqG^k+g1~g zCB$T!P;jxoMnu|Q%anw@&oEvgn}z2W7}?H*OkdL1I+BV>0F)y@xa!CLjV$P)Ht(SEvGyD#7edzlkQZr943rl4RF?I`;f+m7D zXzZCz-AlQS^V}$2bEO(58o8sADWg7$`6MwwOMkS!^2=4P{CxUD(+P22!{PfYlFR|s z2r^i~9umEG`!@{M<8#rq1^hLa?||vnV83o{`r+HI6M+yAwE)l6-=6*Q-*2Q(7@!Pj zAe!K_FibYc&K>~}BJe559xkdGYUeM02SMOc$hDLFm)Fl7y_4gSH>n}Rn4_?xC|vGW z?A4!1hcLk;6mj_Mk!BwVY|bW9vjO6AYrR=vSXFN%8YVN2;fI*w3X2LZA#pZNZGaF$ zJ%j^xM@_%o2FVwf;xPsC)?1t!qyWd8#*|=>UFcTXjVDEUPl7NCY`+~q^{DrxyjAf$ zV#n=f^Y^Raq4?Amj-ojXZBYCb83ufkR<%rwa%%s^1%PF|I% zC0j+c^k)o~z*Nn?cOGSPy9%Wxh>8+|d$&q~k146yomF8AFvt`)i613ub^+Cn-wE%l zHgNFJdgxgr%`!_M!Z;J7pihc-SW$NokF(-;Ly2 z6O|GUl`~J0hl}^GiC=?$?n^Fql!1{mM@_pP+Y~t9p1wxs%U;2vZWcW!k*4jkClO%4 zH-UJSig;1+2Q#K`mp)y_*lB&>FgLX4#Br05gH8Mb_Do-5JOGu@d#*B1^G*#O^ktUD zGK>9mDpx*}N_M$K8G_KT%+>)B8ZaCIwXdox^%z{f??uh&^SayiNyV$zlmE z+>;9J!Hn25$QYu6U@0}RdOj3N{FJ$=!ewpP$D9%LeeYTm{f%L*>~iV3hFCO*X_Q}= zGa&t?Mq8!ziiddJ8fN|r8aU4j1T!yGstDsGyW_PQR&IhXA5N#z&q0aaVHS`M!ZdU2DT0>tQfhFMMo8I zd(fv{xaoZp&6^ zsw)#_Xo)mpC(F;csDFF@MsDeA+xg7=48k~tVJJmK`lPw%IS2nD2=oEj;lc-E1=z=V8vRuK80wPCUmcINPbk|*lp_uTV@G%r!S7@t(m zD?IKHz3O87Xq?0*`d!2=;YORi1Rj3Y*TTcX>|=M+(|GN4YjfPbk@BSM3J^ne#J%+B zX!7JW=>f&|t*CR@Q=>CfjCg6Vp+3zNIl+p%5}^+8bffSCa2W|rrp3iF&F&H~H%)(n z7T$0kbWP}f1l);?R5)?lv2A&@AH^c|;VLAQIHu%G!xQB6;Xbqrh8%4>f=0dSAlAS& zf&b7@ahsmIy~x!X+5}T=tne*Q`zMh>JnK(5ygz7%iRSe-C|a+p?E%O+MK(#h;;^Um zlys3RP?gxlp4fpO*ah_Oq-=GnIx+iQU>o{RJsEG*UxZjl1$gp}2L7929-|_Z>Dld# zI4Q}nb2EFGsV7Uz4GQ3nGYVJR8dLK?s2LkozWHlphNmt&4|WThyqlg>MQyqT=&>`P zt3&iW^!9pP;Bxh~Q1#6+rOan+8U9U37GEyy`jgYX3cpI|=Nci*17%VP+#=VkHu1f= zv7DP?^q=;dg9v`*40QG4JdV1x?IN~Lh8|pSsF$mf;bUc!h&iyShJV$3)puTF+lTdW z17Rd#yL%znBlORUy-!|W0vvA|A_3jGIc5I38ie;aquxzDPIfalof9)V8R~Ip!hXgk z0w_E7vNPWXd4ff)nZQ)E?7b)Tb7gk{ITc~~ZcC5ie|D7f;Z>`t{Njc|QtR@Zr}h{R zUr{2aHjQ}z6&ZcraCr*9f-o4<^G%7ZB_Va>dKu)cHze|o);`pCifsEzPzAP0Xe%@5 z5dFODnm6!j=GV&7w{HX!63s{(d@aIB`1U2hzOLgc;6@PGuePy9zzy9;Cin~ig9pB2 z4}91uyuV+9upPNv?OCuC^Zz=Pyh%~9=R^sSiVfie6dNWd5I}|t)r{HDN@p*h563`I zN++>@VXje}Xb7Fo^c#N%O5T?U;CAcrWy0Ymy9E!%mMF8sI{OZWunMgBS!Oj7EK1G# z4n-pDxN+C*^Y!6IepKX*!b4%%ezpSGuL`rOuFOa;thUNB;E?!L6l_{)2MiKLqW(c* z6WK%J&3-kyeqz2GP%g`TK3E?xUygd?QDUzcA)K^sdV0J_5NuA>=9-va^}ggboE2ta z;NCQf&>6%i?z;X7)u%Y?S?_XZRlAv9J*Oqna)3v{7TmP&&8p1!GO~P(9QT&>&n+MiPeIQP z{(~(!OUUHI|0u{O%gX(H2Fdo;NF{O*0Go`V@Fch_u5~M8kfgM7YxFIGr{Fgnw&&jE zkzE{1{TyP2YCeQ)L29aD7tU#1hNfQ|Euow$C!VUs?!Jn`dmOliE!{R02t z>RErunDiFShxx1f1mk=v{O=0uS8V4iKNIf{kIm|$wrUKdKPycsi|%QWN!eL{zVXG> z5{z~HqyD2v@aI%(P{=C^B$apXdAtKYs|+PiIf5|kGNpik-^in$^ha6%BJY*CliDH6 zuWR__J1uJpGF+Gy9XJOCVv5v#49MRdHhdHwPUeyBFe$%Q;c-l~?~M47N+@5+iS{0{ zQuCuaxGtdZ<;I1^ej`)B!ABba29Ra2Y&=?)Rlz2au1G1(&ZggZ*S{w&bG?h7Hnlp- zT(@^_rx~xbvQj>OlhRbpA!}Oaf;NmOg|Js()5XE}?s7-fwxj+%k7e0z!C?Jl4Vev} zTjat89v`uZuz?ZIQYgv)l8AAL8-xf_vo~aFMCH(`5LMyJfV|6d`VhNnUBldooJiYWYDDRr)BWbE6^hbLeR9FG8uHUh=t>?|1}2f6qyLB^?fC9^xP zPFl7%jFn8q?*EJvnQx|9IZ+@GG+$)~306(8R7U5azWJ=&UWD+FeX0Qm@p1FW<^PdL znG_#{Wcj6cJ9d~`T-eFcobiO`qK7BC`Kwt>d#b+lir}~qH?gN9L90|%MQNJ~a*LI# zeIq`%+^#NSy-r`%>N>iyMGW_B-_M5(d7;MJx~&o;<}nKD!tP^_iYs)SOy|KZ$GpF4 zX}W&J5W7e96UXI;i73%kY;MxB=6Z)(Qo3yxhLg&p;c!p4By6>R3^Q4+wQz+*f)zM% zvx}jrpiuEf=D`6NTqVy|$J#yDKu6I%5%0EGd0ztiV{jGnRP}HXIKHOW)0xgcJXF{wo(u^GQ0}6whU-_bRE`;+^1zc(XGik{ydgS@+ zpL_a1g2r$1e7%2)?DQL>C#!7~kt4bfVkh`f8f@ z+@r=vdL+B)vfaCrOK|gz!qJU8QfrrT8y>Tf%6d|ktjL(6rOZ7cD@tZxZT6_$f7$6n zCdAEMU=`v*d%5zD18CbH&1mZ^S=zWc@$+PBxEec3K>53r_j*h$b}4ZUSD3RyI?sky zc5zqV8-_cONVpzn9MataQ0mJ^^oz2$uZKa^CAK-*#W*U-Kgr+O0kdNKI7mRnQd)ifr0LoNFtQ*CuM-^hfN6p<}+084HJ4lN#}6k z={10gvz{6HOR~lZBf$?%V-v(p6|!x1eO<%MQ9&(WB0j5~w8r`L|#bRNvRS#e3T33SXsqw26{9TDQk&L)fQb z)?RoE5=aR!(LDfNJ!OEFjXPv(jf@RQ9S<6hf)=g?qXCY3M{t@yt!_h)Q5^m(dTGhV zCb6jPq8n^B3-L~hqib$*zB44sS@e3*Wg{(3ZjbQgANdCFEk#~ET61}!ib9G=h zVu-Nf4^i%Ih`ymiV5O9)4CtJz4n|Nbw!iYBTa<>_KFmPTn05NxLCWG~ zxhLC~YKRo5|8eQRZ3^GgX;8a}fu%(-F`zH=AsioDI;LCqN|VDXtrJngcElY(v~PON7cm$ zDR`Bs@6(>)FKcoPzx>%Wgk0!#>wO%?*B~03%~|+WKSILr z^WXcCE+zjh<>=Tz;cM){8tg8M3eDk5JfvtirGvoPaIiF@hDTQRmc6BM9GV#(RaQvy z;8^oiQ{yyKG+Z2ScCuP;nrRs+LEHk?&4Ibk7hhz-7R!WO3QZD{L=y&F=Y?Z5wPSrA zZQ34%?lS=sJC6k;S5qZVJH^LPHBA<9g+N@Ud{byKb?9Xs;u)?jUFylUjK#Q?ryvA^ zCyL$dqzKx|D(4Rr-3P^AvpAH`8j2)ib8nl-Ia4yfxTB-f`cajj?cVf!_GmkkKv6Kr z>05~zTY8Y;2I^+5gb*4v+P*H>=To!VcLSQYtKe`gM7>9BVKNfPQ4x`=kOf&*=9Zz=bRI7cFw*ew#D9{K1aU!=$)*q=3&u+1$Wi#zep z8;5>5r;4bq373IC}plEGrjC*hvhZ- z?@OM%_qL9qBY{q2_Ec4Rg>CZGM1yJwr1sBN$>7?5ojj)V?Om!K@mtETcc<4pOF1C}*Z7S1z> z{#psAl(Lq#gAcgwj=EiQLCI;Cb0ou$lyNiut$tDZf( zc9zHO+-(gWOLGP6DrX*Ic~`pN=RolDHk{l~P(o&V4e%j1hkCnKBDzARjjhirWtLf_q@c zCv9t9C(w~Xon~HZTE53>=)Ms|C441pezj$VtWZS4hVM8F;waTh5f~xd-fvGvbS;+dTa%hVli2DO% z+VXCPwOp)f=mGw`ZwALi>ybMZe!J~HK*|RDLz?dO0FUpqX|dGp!3fs-?dG|bIWt4S;R+v zqYV9ag{`=rO8VX)##;hzrt=grP}11s6*l4ts2DCG&f-sD^82!Bfr(SsN#9lwm$Da+ zzNH^pcNT7r{tIu@RZDcfmOO~yq%;n*cTb|4|14W&tW9CJYj65#>*3CwqLB_hNv)}3 zAJ#`6q<`}qFyq6WnH&$1qE$3l$Rn3D-bkWp=|#v`ijI@ofZr2eq;uOiV`2uK@W(8|>YRFL zIXLQCYNR#OrqAUGWA`_7k3E1%%6sZ=%o%e!n^NZ&e)8~nv;AAOq`3X?+^Gs{@38&* zCy=UM#k2MWUxsqY8?9kMXi>a$Bqq)e{3#DFRXOf)zRbJ(@r|ygRGrE^kE3Siq)Mr% z9Fw1K^Vnu-JBh*uq6Wr;`at6kuwe@!%7@oj0 z??ro~=4+a2EW5b&F_hO6*`Lm6oYg?gl2XXZhR$;ZO>aA2KhYG6?jTZb`a70c^a*Qn z>qS_Zb;$O*&PJCE2za;!L_fsggiVYFHz%qK)!oFv+jOwp z2~+)KsyR=?%4l3YZ)I&~q}Pk7j0;73L#?u8p1$b`_AwFw>1Gqmc(YTc|A{8QrRLlU zrA(K@=KJd5T6Be`YmT(rXcCD(T_!z^+*^CyXyPm91z9St^NkQLl34Zht+btgeqg21 z@sEkxd1n*+Rt`4dy%2BhU>2nGo-d9*xB@22K3rgA(U1v{v57|DL@3FNBkpU}g5mzR zhFi9Jl9&DDvp@G-0xDp~+Yc*<0vR0RkR-MNL_HlC^I*$P6q)N@Si3t+IAeqhfL`9F z2s(g;MYE^42f^y^jCc9;=u!b7(Sc=6897KQxJkx|XLWT@xqF-kh+$RgCc)iS=EuJ# zrytTO!90VEVT;*9M@(l|I?J3`hR{^j}PeRkX+bJtdZRlXM4w& zbbJ#&yR0vj;X56`=@16tz?7t!-=PO>@LRUE*F+rfq>V5zdf2p#IAuB}LNdQ3{X$l0 zS5YK^MPtY!MJW^M=Fpm~vW{al9N*@J@WEY6?;vNsK$UG4ooMqhP)Y}H57-|=o$<@(l>(+cm)XnmUS6!zr zhAlS(PbHoqy#0pO_9C&D>7d_|W=lSD9h=`J(&x5ff8are;PJL#rt0Tw)}H?nTn_B1 zA48iALnf)Ebii|WF&GLCk(R@Z-}HsyPndy(QYK1(WFCLoyz6!%OaBRq2AvR5q2`et zGhsp^QFhktn!Q|m@Ts}I7Tti~hBgsfD%Iu6zgSO4U*3DFL0iSE4uaI|H|6Ov;o4ng zE03xCWp62w7&U)esVUm93HPnaZEHdEP2oZIIXdx{$4J$zR3fC2k;mr3K&dVNP8Qu1 zBI`;7JG66A-k$F!4tdJTR_$iG{@h-2-b3#-f)|ZRsfc`cxeHs{i<7=~-oO+1kh7#{ z`F9+Ox=|;_v&CsWx7ZoHiUNLxODTa-NlMmeK8-JK>WH@G&wsu9OTN0QWY6kjIS2B` zX3oaL?t6C}n2W_up8%2}QiT%}z*uAEbVH|n3FG3a7L~uB1O%D_N^Nca2Yx`xsE#mf z86U*%K&|HesTK?Nc+4i}Jy3Ye-cNPwrO;wI$w?6fNg%#?@@i+n4qDDFKX0|w2?x!Wt){z%e8ay@O%Bye z4QG-DM7@>S6CPf^hx>PxUQ?E-ZGhY~W3{7q{c5E6hgsv;+sP23p5@J$^C$PX+7yBM zC?|chUDm{bBDN}pnms2p1+78o>KpWB(L}KtF^Db{Ej>p9F0EPi`G)6nu0g6)&pQoT z%KhztW#H6@jlJ?LsSNlL*Gjv=V9tr&&!3dVa}edjgak40Q$3Cz(4pFG$E?O>Y7(zD zMRzy){bZ8NRH0xpiY+jRdSCy(9mrYb>UM#ATd5{!DlvS>NK}d-Wo2wyGdTl^qs$rh zy;KN5>iU7#{cJ8yHV^pVpAZtHJ2hfedG?@x93ql<*qI-1W6;QAsx$jxtQhtzpp@PY ze|}L2n5afuWw5kGSk9#^uaQJ9tP%bX>OfiKv(4o>*x!s)D?LV zt=}g7dPP8~@k(Ozx+)P%ivn4UDD^{U=!IhriieOeB7_OENyO8hHxM(7sD~}_wDO^U zljNrFLH0TBtuyn)W6xaKZ%UvwFV`M<4UJ!vF1E7o0=^8&ohd7Rhk1#LQOJePTj#z* zzuu?gTTT=BThWaDM56U4-gfPJwEv>XJA?avPjiyf!wovsMg9gn98$SWk=b=2>8%0M z8o(FrY8K^9^?=z){>}|-_uTKWk1@@cY9B>I%mRAfqj}ge*ASu%+#`41$;3w%7DU3o z5Z&vcy|}QuAvHh*W{JP;1_^x#F=X##)Q#E3g4j-^WK@w<+?y!fLk7KuU`NRIo-ksG z_`qR>z)h5M49?CJh2D|0Upw#jw*n(A(QamwDq9*D&?&t}pZDkT#O1_*7C4;n#6%3m zSafLx^se+r?i0L&ja3r4V<@y*-PgDiA-cM z#)O>hl%BYn`IA;A`%}GH!YebruU_V}d346ZWJJSgBUR6?3gb3W0L-H-k8Gp*H95wQ zML%}tB3&DY_tNtr3AB~8?XW&#a|G;&q< zf{e-T4f@u_!@?Qg6;^&@_;dg!{~3KwVsKmdj^b6nmu_9!)>WV?{Mh9MZC2T8=SHOO z4EBITnXrxay;pIs#8Yw=b!WPobs>FvlKT*eOoh}IWXnNEPkDBUAy_#%W54(oF#bVo z>DxND3^MPYWP~q|KltP)+3`?Q_!YY1D^>h``tYWrmbExhdH~MNd$6o+clB{OjQJ(o;(`{uBY@H( z^i>C98l~+F;d--NV#9 zIiwBA%&K@SO7Hp-nf|CLq3Cl>%mi(BM*YUI2%r~oj(lGAp%lduA%lg9=;-%z2Zw3} z(sT}!uBlsTJW599gC)^$j%+Hq`CEK?@8C|S;xd~_+dN1}WF~*0I}oAs{800uIlz2w z-Z(+7YX=|`5-?;$8gUPC+iA4=d{+geE%NamWk9VLjKfwj^EcAUD`gox{(29zoAdC1 zo*_O?p+YZaDr;Su#4|}u|+BoMi4Pdmes2HT>k)d!S7LARRvSsnWK`Y zJtAdq=LnW0d1(7~SFoFRXQ>HsDxAr9?4dh=0292-ExdK%n}w;wDihQIVDla~g>Bk0 zS_W}6Q8=p@APErGCDDhPCp3n-Pg-hpa+RH|P@Edx=Qp9C4%ngYB_|X^t$fRFzN$S4 zljGM$9r$MutupUQPz22cD?r2WA?X|zo>>TObn7AV>wCCT6tCe!#jX)EMHGSkH>g& zor^w~7(^ag9W++Fhjf+NzmJxCTl+%Lg^ot3GZfS!dp0BEoZU-4=dk(6`+AYq5XKwNiige6jcqX1Q6qf10B$mtkR=tA=Ob?|{irgcE{5xBs8`<8LR@gft}fRX?xn zr}KRUzC{{{qK_f}v_}hpee+;4l<BXnE-yRC1X zXx+%Vn}<-O^$4I7PVmu6DZ_Pn_=Yue!Z!PH4n0z=pF@uZ%&X8bhW7ph4q3)9aSyJH z>k{UwqQbvW=LD{L#0uwKjkklJr3_a(oH!s#mw)~s0)(NtMdj9ZWtsvRDzx^5V|uR? z5~V>q=4k9jIGaU#ji@7)L%r!DUb zz|f_#-gYj&B^e?`+QOcy0D{ z0ZpDM1$|$p2(N>KgbdfgjZurSinp#(aM4h(aS_3-`aV}YX6GjX7wabGjgbuUE0da* z6p?oNi+2995PAxh4-sAjRE3@n;=7Rd$gY10z9T$%A>0G~n;>VT%BM;HvNFeqoG{Vs zrL#!dL414`2jeZx@-;qQOjF};UxNch0q0oE{#~A`Awz!o3@4mif*$;WZa5{ne`OZH zAB5H&UN*Vj0(K7llzK+8E9zcIjx=E7UTrQUJROjt*oUd%i;HvSE5>-duMsAfinr zeiZs6{Ms!^#{BH)3}0W1FFc$+zi(Ws&%mFM_EZY-njmML+!{C?bW3 zVtm(>KI*JSCH;ZN4b!sz;v12G{{xbiN~n%`64{^JuwWrcEe6X||9$9#mKO=SZKJY& zr5&pb>OAfXIk10sz7lB@v9DdoEf@j8u1RRdL{d&ZEeFG8bIpD31+jVjIPiPh3q0ie zIkwIX0)8*^r=N2EAF+RFKwU7C2dyJti+MTW9U-NHEm`k00?|#rr9hrIsGd=T<#f^3 z{>B(GV(6Uy!(%j@1`H5aR+4rIS*|5@t%Uzv>(T|_?dZk5l+ z@&r#F&UJ5r9WZ{saIpmZ=AExT+U!?vs}%u0x#kLPdtOH(lS>G$tjTl zvY7aP(Dn_{nMGT-v2EM7ZQEvr728fKwr$(C%?iKRtk{37^xE_qPc$IhBLRYD}l+SJ% zNM?dcMi+eDDc?(lHP9eP_L`QH_6a$*m&Q2$)teM{T;_R9a)z#)2sjVeVDI*mcQFgz zhZ?_v6hEa!*Hp7U< z7e~W(M>pLfZ+s=uU=}OiygS@O-h{v9?06gcsPsI*rG+mB7s*+PyRThbcb;)!^^g1s zj7T(7mq|4fK_lO&l%sdpdY#Aq8g8>bN0RdPfU1Ow(7%s)GGLXDpll&W0MmZceeda_ znAytLZZ)eh%v&`8!_@OhIHGj{34d{nN8{<=x5fa zBj3m<>Zf5dX1{TJ!~VJf0A7#_dD~M?s^caEiZE}+qO$+#Z$1KAZ}W3Dy!XTqq3YQ< zx0#P&Mo{NQaNz5v*4fZt4G_rZ9^hPiN=UMPjh$PABDd)+@%q=dae8Wfb3=u61f%yi zU;L_%yumAX3hDFP=wHb_c|-5#?b8)Tp4&J0@kvcDwa51ku(ALk@C4{vpY64Hm-~a) z5>m=J$!BSf=e1OD^u6@f{nncap$~Bg7XyIyu#|{Y!-zl?dM|!-Ahc`0jASF{93qjZ zk?bh-*(cx}Xo5=gWJwUCs*%Mn`LhFN4MJP>(q;q(kCZ~XcWQmYJ3SCQTem)WfO6-t zR)tQV7beOhv-!74nkMdn-HN7$t%C-sp;4ifcRN%WfAx9`oN1leHGg(GxHdc#hFLY_ zedv`j4^Kt5-8TDXGlY`;L1rWW*BrEPu!!i>Q?a%B?Lbviu4Bo}PH(+k_&8BOEo@J0 zY?Vnw6>AO!hwzY9wC$$_`1K^cmzasc&%e&&9Ey6$3K@GnxtA_?pms+t@o@w&%Mwk{ zqyKL1(XIE<1<|_Tm3%Gj6Y6uOE^rl1B;QJ~F-Yz;)!ojix%`jmxSLi86*YXqOfdx<;KFEkFU?qH5j+1c+xNP9E=fICX6Hf?Dks$P+Xno8D{ z-AlT^%KwNJ?u!dLU(zYt_G+@_Dc%j5SfQ4qb){?8Er5ilZKqu%8NQkRkH$M+)TWkm zqEDii$6>sRd0bL&sBbOjk-C2f^Q3xDgOjIY+}kJRF8^D(`Jza4Mlo_At`i20VG?UD z0*>O%bBOexo$|83dvKdIBY1DQmKu<@I2pIjFjPhv^bQTQ+d2g-L&;BYVVCX4=6hL| zy%a{lr6MuI#l7{vQLq`;W-ERG@pkpyrR;r+waw^sg-%PS4G(bnwes}k(4x==VdF&S z#ft)bmd38;2615ANh(_ZcxE*${80^cudG?8hP^PYMZCr>e|#;d}!BT(c>JpFSQrxFe`N;OsPPb3eubO0!` zJBkUvUQH4r9R+139F)au8|``Mw&$G%X8l27T*r$cgj+bvZ<$_Ef(-v~6UJDyWmTfE zwhs$wKBHhyrHpWrxlO91D zgx;24eQxDc;-*(k5LVr6OYXmW4=4hE$*x<^(|exYvEO@Gr29Ji_uF{ok@F+N@Rt`~TduB(1obCQzf zI{4SwzSgtM0})#lT=SR1V-s2nTY-i+lIR`bb0DyZ6+85p5Dh}BOIOmAo3rFoh_p-O zA`s6`Fo;7kwrGjrTLo)~w}=N1Re##L5x3>ZPo(5%2&WpCG%M1gom}gX5!iIhDic=A zXbFY3ePCaON`^c0YmjU8(#hM}XCe_kkdPqIzn`GKKH1mMOn?A{7N& zwB5`0f|0z2G8QyWWNrysv5DSF>Y(&@Paf5DE>f>lU0_8+Lc=XuH7wUG+| zdw@UIk~G9p!i1Nv=Bebg5q^-e9@q;N;2BB?YZTmGkGXBqlRc~*TQ}< zgj9LJeDa@uW8MW8k3Y=4y4+qL=O=2S!dYd~u#*zp8kmH3m8? z_Nskd`5^a1Z)*67lsNk^w|3-p6skB6V>DbLOrf9 zr!9Wc5&6!e>a^u~icDiv92Ms;53JavnRHHO*sjaE4!ob>&($Qj0*Q zLj1>-7=zkM{G;eLr>_RkaDssmX`xsSZ9;G@G?a~gyor`z#@;Ar9Ir`({a&jszAe~8--w6T>pjuPN1WuNuei&mS|=Op#@mTnO@3f5kok|Z zZogX&Owta%s^+CYL0_;N1|R@yqAwN#KAmx0ko(^_Y~{x7wabQ82EK*UZHn0!>yo5~ zbj(N>NB~z-@Ic6sbVC#pIDbQe5_)c=XFmF&wJJmUy1X8p;Zz3dD!22TKTtz}lhK#% z=ceELqH}iVGXy9L3LXGOp*x;YsXVE=ATy#x;4PNXf66x+=|~U24vnVfo`UXhbkAkW zQKka>J1_vk7@H0s-DDRKH9xd{_he$fW{2k3^v`6`ABthx-lYt-%TPLK)Leyr=DXoI zD*y`AR^;%V3>o)Z=L=4-4#&pRw_EAeW<9Onuft11n*kzJSM+2qR}t$4B` zryvvqfGnKG=$6zh=h>MF>(#uvSAQP?d_jNoiqO{y$$yLPysld?BSFMV4zCKv;Y0Tc z05D9OZntisc{zVNfZa=QhF0CN)h_Xb2+RHZbKHTlp4Sg*0*8O^uTkbVWe%rH5>i;+yl^Tt~x#!Wcfc)8A z5*=T`_l?bJrfPS}#x*xg#S#;c<<0`sqxS{)kD_f|y6~@0WfIb<=s)xmeRItOl=>6|5a*b^Ag8@Tmz3yub z2#@h9bQQo#BUs9b@S#vX5NA!L3F`UTN>PAXD$KRj|!9 zVdx<1pU0qlg`1hG7Dm_&NOAx+HhRObjThs5=x6-ThdMNrh=IyL7wU|0s(m1?59T-r z&Ml*&5e|Xj2abJWn%BWrfgVF)-v z@H~1j4hKttdz`36n{&ueWxc+n&5*CUu3PN@qzK^d!hg=iiZYV8eYL{Hk8>K3Ea|>JPq1fJ9UV1%C!AXR(^ zXM&U@z2R}GfzOzF(*nc|^pOO2ZJTriveMJa#QXUP;)Qb9&8GV!NhD-@nrJRZuyqj3G{<6-LfHdP&zh$H!x} zcOuZHmv7+9=D=jH=#Nz<=wnXnte-xK4*;<8FHWcJMZ4@xp^ z`6aEEB6yS3Q1Y~z6GtO@0(iVH!0xcCT6&zw{Q0K zPsi)}wV%joT)qj7dk~2`F4@NB@x>32xs98+I;(8pzAW2lFO{Cz9;U|~j-zewf)n?Z zDDJWJ`zY`-XQd8#s)oqm>RFWma*PaFe7Cf1bpx6>S7?rk{-P zNn@fb+?@s6R%0*Thvd`EzU+I@3pUyI{suUSJupSA z8^hM;oUJ_xqmVn3a3Q#%F6&{!B&DD|zUS5OzprQf0Z{cc%|_*Du(ik{6&XWNTO?E@ zQAsivZuHl$9d|qVn4Jdap2kq!HO{9@=e#hpWMV(yD=+#EI;&~)@BYxEP>uinmcM#+ zddo8K-`|XdQ8gnB>&6t&1PAn3qfTs(W2Fr@(}c|Q_%Df;8aho4Y$)|5pNow%K4wna z)sSA#{D7Q)%4da<1vNosM18;EV^Pt}i5(?=&HZ-7^0bJL@*3LzLp$)m%EKJN`MI5aPd5O|H>yfDy7l)ms+sw>zkIK#ycY3EK(Rz;NiQQsE_rD z!ydl^T;=o5GH$0=bG58*7ISO43ja16Xr=u z|Ge-&tzWoJ-)yx^v`Poxx}#Nhd~BlA$XYQ!ZXa6X0`%B$CxBlE;@e?G7z51-i@isS zJ>n9@b=^2I_I{p=-KMXto>!`GTMSEYUB+ARR5NfJ)=O(hJ50=SqxuQDJ?cj_2F{^X zR<;xjmE$ZbeR%ZcM0sH!IC3VnJidg4_Kz$F?S|?8X6X9XwE>gO2*)(kHgSC=m~t2| z+$M%^og7)TOf#)C{;hcwiTe}KF`;5W6xKHeTj{-gVCQ}g`_u_+eB=1mX9NW}5aZW~ z@ampF=>3(EqreDyKlNDvgGQxS3YAeH1}ylTJB+uK>!#mYtpHx_2k03Ef&+c&{eJ5A zXtM~3g%rA2Xopt+=07lK3QEcQMC(pzq`B0khBM!D)WOgSYD8<+ceqOQUo{IHv(IIO z5hU3AZrNY^u@L3|I#W8^yV<0WNc-M{0$>K*iyHzA4r));zpg*0`d5bYWh>|;67l5k zx&Abv4Gg-gF!r^e*h@r%7wvhvmoZk@VC2C)*d>cP@Y}J-D0g@ z5xkj(e=oGVq>3)OZ{RY+^wxr&o1!mUkTA)zFEpwCM^!JQ-vw?0UDu!h%;-| zl>E1(#1E`hz~o_Rfhu;OSl|JTV@w}#@k#wa%sfno!jQ+P_R&G@kb4T-QUptqx8Y~4 zbduFM!uhQpFgDeS6Pu=&Q|t;NQPCb683&};f?T@xezypria%FZ`k}jKq_j2Yf16PhZ}wd3M(hui^}Q78qKZm2DLdzOzhCaA3ZK2p4L{W4 z0tx&q!#EM2dnjmlr)ze zo}2pb450)?5wzLnnq_K9nnIJAMf#x)D25NWRXMspHJ9Xw>T+TlNh?zdH@Bvziv$I8 zu)emH5l+y;$SiXc_wFmD06TRI|6HG=ECf8d<}~t;V9o}hFqtz`nG@p0?zaA{tnNYb zd@jG0(&Z0zGKkm;g)9iz>^KNU&t?2A=bT4Xgu700zK)U#G)EWNqrQ90VB{QjGE|fa zd~uPLu{|ix)VS$1oL;Bd;0!B3_6DkqBoY8E$%i@42c37z+CZ*blUa-cN;`KRP-Y@h29%1>bEO zY3=gWla$ykLrzz**C0wEo`cW1>pTX(Lf4)U!Q&s7fUFE25VTo1bPbAVyL(4e#K1(97%z_J)VFPYlYQ;XBMb(6`!-NzAUHZ+ zG7>~3t2s;4!mBsS+(?vh<54o!{~C6=WBrC<{kCAJv44V5DXO+MuVx*HvI#a`c1mHNZHNzRWwCQLu^j2{X_uRbCw5ntk2`t^9viX0n@^n%OA1Itq3Q&S7u$Jn9YfO%CvI&2`W%*w#uuJ#dC{UhQ4N{@F{0M1H)>Z?x-n`i z{#B$pfU`-*iv$Y9stPMr?IHH*h)b5)KUw}=QdH@^_~}Dw{QiAJdV(&!Pfrp{n@&b` z+ntEU?yAs*h}W~fT&^J0<}-{XQub%B7<@=9Y|xUS*v|9IH^DfV*yTmpG(QBQW52@5 z5_%Vv>O%m{4+BOJJhWgRzKQ)u_HLIWy$l*8SMq>B%S8xk*3?cTG|??;Z)s3;}0l|j~e$mqPF&zng-;NdTtDZFnP z7~$H8jkA#buQ*ZvsHBl?!d7!-!aw=4#w?#{t6OjyGe^zrC_{%TySHyP~J zj5LJw9jGO13SzsKC&6MlEiUuIkobi`){oMOCpmrmady6K^kg(bid^HI=PVNy~92Qb%aVTy0`>5*YLzKaL(~-c4S42l3n1(Y@qi_ zBOa(-Q@xk?!%^1chgt(E^FL+_Kk`}%rK|r7Bv9R#yVO@aEj(B8C}A68L)DY^6K#nd zU?phSAgiPWB{g_AosU&LpD%tn5HW2aO>@H;Gqgq(p<&qn;`vaxb|7Vnyjz~CB3{Aq zJXS3A<7NTaD>nuNm9@|D?DCZTc~O+(iyNr@UNT>;Mv|QAiHu6N74#>W zu4nBZtCjccwHn@}md#`3mAiIhVYKGbTj^@Uh*;J_mnR*rs^GRSgj|*%XG*1FrU$J2 zBG_+YvP`XA?f1_+_88WFHi+tIVw;0}CO!5f;2(q52d=I`1>x#tT#0lbG1n-`{a?YE z>HD1-e7lge>k;;1l-i@&Y96dGO{~YuF{?Js1TfH=p!CY4p+}YlyN9< zE}7UOi8hrqT;s}FShgfr+-H%MZ~fdxh+$&(6Uw@r6U!6|-|SR&^I0XX_w_&Bgk}QJ z7!2zv6eezkvw6^#G|G!qcK}yHq>o9Q5qBnqW9qSVCaINHH@9*1G-s3Wsemlg!_(oM zFT+VxT|BuGny?C;gejNgA{Rp1ZLYOn2P?e`bPqrF`i?#%>SEBkJjx6)L3&p1pUDXA zbG{ZoYpq^$4{>Fxz}9*+2#YP4iMCI^n#6B+#%XQ5o(As=13Ed=m1``chCa#?EH>Te$s0xo%PqY zTM2tM3e%r>F|r|<`i0FIcWY=gP`)}hrjI2nEmbvAtlR^K>@lgZy>cSl{hUfD9#&xX ztYdG#10BE2V_*ygLWOFY{v#wY?qu-epc={(=oyI8C)K5o92CRsVvX;T5TR~zVwVqt zbGCY^&+q4CAs>=KCx9YU+}_J}p{-zh%!h~wk;Dk+GKz1@q@(bji{(@J(rA3BNwj!Z(mV(taGq^J=&*Gx!KYis8r0OxeB(;Z4?9tN%MF{q_(4(s{MgE;rZP zb?{2wYlg*Bh+*YlI>76lf8YWb{|D24Pd%)y8R~y6oW+&ecTZJWaowZ9+T58oUx<3) z!>&C(iskt~y#8N^_uFUDaP9l_jq|^__?z>ruCA~1*!@+AF^l`(KU$hA3y5O_1$tJuV^=#J{|Wo zoE-zQUgHoAny$svvB)}j-@z8zAW$X1v3K!abW$}&@RcY;6wVNtYoy=2KhEw(oTmhP zU%TCJG+I-@60h@(?;?-=@*aY#7(1r=jcE=(97m!33aFe#GsidI!X|TXX2^Fo$^7y0 z)2Hha>1aF2S+i}jod?+A8yfzGbcG!8dHLN(EvxQYq_=$56i(O?FU}Y%9I|g+Ah34$ zvq?0~h$Z?KF3ab(br0@#4S7-@HD>7;<$|v2cm40=L`F2ssei4FadTuyIbwP`2G__1v--j5A<-{gF8l`BW2JgQ=zW|+8HQ30ctRA;|j;pHo0)b(8Tf{%Vb+JD1}^a=b7D+TEfc@{2wCgdZa z0BT7;3}tUK{KMC`ZzP>~*#7yX-{mY;#p)xblRiinVn(A#%F@^5cWJ!u(l#Qft`Lkp z)XC(U)$FwtBw6zqrF(KYeP>#%0{fwqqMY$&o4{mIg$>BB)wI)v5OVHvUk?n%y?*g* zID$_ovrX)>i6?LcL)LhwCQo)U%E0Lld(15dNjP(CWriCp_!0)& zG_g-P_4}~V40`rkrXOm5WbF;-N)CF1Bxy^VFcCDx>&4?HeT{chdXHi_Bq@COU~369 z1)X4l6wM?K8R*bxMIJSjjA$q`I82bw9upGY_*)K&X3)kISZFLqR~_kd{kX5r!fuZW ziGVj~o0rl9m0lTZG5t#ZaPnoK>GHP?pUz^>-WY`ZTyT&&sOCM^uy5p}tGyngm@z#x z0aex&AH6hRJlj8_jHB+JWJUB=m-Oy;Te;6F1Wx!Z7p>}D5zav~ezJp_1d(A;zD4pC zFz?VRrbll%y|f&yPcNAm{(q_)UwrYa(b`A2tnG)5chvzqTXiUSH0i}-mNLb6HZ?ab zeUXA}L%L+{qp~~?8>e;Oy3BK%(~jNxGrk(Tm747HiHpy{%Bnt#{#@4fl**0t!kO2R zpe61NWA}A&?}jB?*VqK{F40UQSM>i7Qi-fhM)#C+H3h1`>NR!cT?xg1&(nk`QiI;L^6k19vlX-ylA{0op&)z>G&Ym%5!(B)&2Q8q{ zGiQq%2YmocCd#8Hl1gguoOK{BDDfI)>o@v$a*wBnq9~z8Sk1!kZv_wRik1N55V-51 z*aeObX$tpp`fbMuIGzK1gr1;Oh}SN=HC{ zL!s>1z*jLJ@%xEHcfao-+IO@aXWi|RAH%xtY(uP%noeFnAdYO;EaUm-{68IdPoVP^ zu&>+`NwXYdZ*znr#mjI}FQ39U$=uq-G=`F(1F^ooyj4y;C{YJbFkijY4%g=|&)YQE z#1##|=2ZBG?&0KF(FoZ*rK>A%g>xA<`9A~-XWW|@U>QK<49q#IwtuxkBZT{wY7=4S z7EnbOdcrVVJvZONvtY~c^V-Q-rQNgqolgxD7^Fg$nc)&nqLUr3F08-t?-leH8o{K_ zIX~^?)cHfP`nKjG!6di7o2CM{8l0I^qwsR^a zwfF2IW?Jw7@3P{212eij4ea>%5iffX()>4SNfs)SI}-qAE=`-jrDoP!TQ~% znAcM4Z8fnGo%ndQ91E8_O^J7ypbcKoUk>Wgr@6aZ1j)DB3!!bln%X~PncBy3pEF~K zJ@?_o-Lkgso;VYAZC+u%&AUefQI(g_B6n40$W&2ccVxtx|1K7!VFm3we`zE99r(Ku zGB&{H*mZJEp!s74mVNMKR6s^kP{s>&{^=*XjdWMhze^>~bnBx0&h47e->DtYzZyFY z(WXlr!8Sd-n5stb%wM6<{v8S`rtHRJPTAwJZZXPPR=lik7m=2(GXwAL#bEe8*{!

  • m*OE7LK= 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%0A{?@7M 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