From ec88372ebaea6001bd5a127e67ad3261ea21d14c Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 18:49:55 -0700 Subject: [PATCH 01/70] Defer island modules whose recorded load is deterministic (NI-7b/7d/7e/7f) Node islands stack, part 9 of 11 (scriptc-perf/results/islands-prs/PLAN.md). Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/lower-node-island.ts | 25 +- .../src/node-islands/effect-gate.test.ts | 5 +- .../compiler/src/node-islands/effect-gate.ts | 43 +- .../src/node-islands/probe-recorder.ts | 489 ++++++++++++++++++ .../compiler/src/node-islands/snapshot.ts | 311 +++++------ .../main.ts.json | 6 + .../main.ts.json | 6 + .../main.ts.json | 6 + .../main.ts.json | 6 + .../main.ts.json | 6 + packages/runtime/src/scr_node_island.c | 9 +- packages/runtime/src/scr_node_island.h | 2 + packages/runtime/src/scr_node_island_boot.c | 157 +++++- packages/runtime/src/scr_node_island_handle.c | 13 +- .../runtime/src/tools/scr_node_island_probe.c | 16 +- .../main.ts | 12 + .../node_modules/pkg-a/index.d.ts | 2 + .../node_modules/pkg-a/index.js | 8 + .../node_modules/pkg-a/package.json | 1 + .../node_modules/pkg-b/index.d.ts | 5 + .../node_modules/pkg-b/index.js | 14 + .../node_modules/pkg-b/package.json | 1 + .../package.json | 1 + .../6844-node-island-deferred-env/main.ts | 11 + .../node_modules/env-pkg/index.d.ts | 1 + .../node_modules/env-pkg/index.js | 7 + .../node_modules/env-pkg/package.json | 1 + .../package.json | 1 + .../6845-node-island-deferred-require/main.ts | 16 + .../node_modules/native-wrapper/index.d.ts | 2 + .../node_modules/native-wrapper/index.js | 14 + .../node_modules/native-wrapper/package.json | 1 + .../node_modules/timer-pkg/index.d.ts | 2 + .../node_modules/timer-pkg/index.js | 18 + .../node_modules/timer-pkg/package.json | 1 + .../package.json | 1 + .../main.ts | 14 + .../node_modules/data-pkg/data.txt | 1 + .../node_modules/data-pkg/index.d.ts | 1 + .../node_modules/data-pkg/index.js | 14 + .../node_modules/data-pkg/package.json | 1 + .../package.json | 1 + .../legacy.cjs | 4 + .../6847-node-island-punycode-warning/main.ts | 18 + .../node_modules/puny-pkg/index.d.ts | 1 + .../node_modules/puny-pkg/index.js | 8 + .../node_modules/puny-pkg/package.json | 1 + .../node_modules/warn-pkg/index.d.ts | 1 + .../node_modules/warn-pkg/index.js | 6 + .../node_modules/warn-pkg/package.json | 1 + .../package.json | 1 + tests/harness/differential.test.ts | 22 +- tests/harness/llvm-differential.test.ts | 11 +- 53 files changed, 1136 insertions(+), 190 deletions(-) create mode 100644 packages/compiler/src/node-islands/probe-recorder.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6843-node-island-deferred-per-module/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6844-node-island-deferred-env/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6845-node-island-deferred-require/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6846-node-island-deferred-own-files/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6847-node-island-punycode-warning/main.ts.json create mode 100644 tests/corpus/6843-node-island-deferred-per-module/main.ts create mode 100644 tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-a/index.d.ts create mode 100644 tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-a/index.js create mode 100644 tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-a/package.json create mode 100644 tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-b/index.d.ts create mode 100644 tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-b/index.js create mode 100644 tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-b/package.json create mode 100644 tests/corpus/6843-node-island-deferred-per-module/package.json create mode 100644 tests/corpus/6844-node-island-deferred-env/main.ts create mode 100644 tests/corpus/6844-node-island-deferred-env/node_modules/env-pkg/index.d.ts create mode 100644 tests/corpus/6844-node-island-deferred-env/node_modules/env-pkg/index.js create mode 100644 tests/corpus/6844-node-island-deferred-env/node_modules/env-pkg/package.json create mode 100644 tests/corpus/6844-node-island-deferred-env/package.json create mode 100644 tests/corpus/6845-node-island-deferred-require/main.ts create mode 100644 tests/corpus/6845-node-island-deferred-require/node_modules/native-wrapper/index.d.ts create mode 100644 tests/corpus/6845-node-island-deferred-require/node_modules/native-wrapper/index.js create mode 100644 tests/corpus/6845-node-island-deferred-require/node_modules/native-wrapper/package.json create mode 100644 tests/corpus/6845-node-island-deferred-require/node_modules/timer-pkg/index.d.ts create mode 100644 tests/corpus/6845-node-island-deferred-require/node_modules/timer-pkg/index.js create mode 100644 tests/corpus/6845-node-island-deferred-require/node_modules/timer-pkg/package.json create mode 100644 tests/corpus/6845-node-island-deferred-require/package.json create mode 100644 tests/corpus/6846-node-island-deferred-own-files/main.ts create mode 100644 tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/data.txt create mode 100644 tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/index.d.ts create mode 100644 tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/index.js create mode 100644 tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/package.json create mode 100644 tests/corpus/6846-node-island-deferred-own-files/package.json create mode 100644 tests/corpus/6847-node-island-punycode-warning/legacy.cjs create mode 100644 tests/corpus/6847-node-island-punycode-warning/main.ts create mode 100644 tests/corpus/6847-node-island-punycode-warning/node_modules/puny-pkg/index.d.ts create mode 100644 tests/corpus/6847-node-island-punycode-warning/node_modules/puny-pkg/index.js create mode 100644 tests/corpus/6847-node-island-punycode-warning/node_modules/puny-pkg/package.json create mode 100644 tests/corpus/6847-node-island-punycode-warning/node_modules/warn-pkg/index.d.ts create mode 100644 tests/corpus/6847-node-island-punycode-warning/node_modules/warn-pkg/index.js create mode 100644 tests/corpus/6847-node-island-punycode-warning/node_modules/warn-pkg/package.json create mode 100644 tests/corpus/6847-node-island-punycode-warning/package.json diff --git a/packages/compiler/src/frontend/lowering/lower-node-island.ts b/packages/compiler/src/frontend/lowering/lower-node-island.ts index 40fb46f8f..6e80d991a 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-island.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-island.ts @@ -322,6 +322,17 @@ function deferIslandModule(path: string, loc: SrcLoc): IrExpr { }; } +/** Whether `node` runs while its module evaluates: no enclosing function, + * method, accessor, constructor or class member initializer. */ +function runsAtModuleEvaluation(node: ts.Node): boolean { + for (let n = node.parent; n !== undefined; n = n.parent) { + if (ts.isSourceFile(n)) return true; + if (ts.isFunctionLike(n) || ts.isClassStaticBlockDeclaration(n) || ts.isPropertyDeclaration(n)) + return false; + } + return false; +} + /** A static import (or re-export) of an island package: loads the module * in the importer's %init prelude. Returns false when `spec` is not an * island module. */ @@ -1512,7 +1523,14 @@ function lowerIslandLoadCall( loc, }; } - return fromIsland(lowerer, loadIslandModule(path, loc), expr); + // A require() the module's own evaluation runs (not inside a function or + // class body) loads the island exactly when a static import would: it is + // deferred the same way (scope-B NI-7d), so a native package's top-level + // `const x = require("")` does not start Node at boot. + const load = runsAtModuleEvaluation(expr) + ? deferIslandModule(path, loc) + : loadIslandModule(path, loc); + return fromIsland(lowerer, load, expr); } /* ── native classes extending island classes (scope-B NI-5b) ─────────── @@ -1841,7 +1859,8 @@ function requireSpecOf(decl: ts.Node): string | null { /** Top-level `const x = require("pkg")` statements of island packages in a * compiled module (a native package requiring an island package): load the * island module in the module's %init prelude (shortcut: before the body, - * not at the statement). */ + * not at the statement), deferred exactly like a static import (scope-B + * NI-7d: the build decides from the probe, stubs stand in until first use). */ export function collectIslandRequires(lowerer: Lowerer, sf: ts.SourceFile): void { const islands = lowerer.nodeIslands; if (islands === null) return; @@ -1860,7 +1879,7 @@ export function collectIslandRequires(lowerer: Lowerer, sf: ts.SourceFile): void actions.push({ kind: "assign", localId: binding.global.id, - value: loadIslandModule(path, loc), + value: deferIslandModule(path, loc), loc, }); } diff --git a/packages/compiler/src/node-islands/effect-gate.test.ts b/packages/compiler/src/node-islands/effect-gate.test.ts index e2d753511..4f224cc57 100644 --- a/packages/compiler/src/node-islands/effect-gate.test.ts +++ b/packages/compiler/src/node-islands/effect-gate.test.ts @@ -37,6 +37,9 @@ exports.sep = path.sep; exports.join = join; exports.cache = new Map(); exports.mode = typeof window === "undefined" ? "node" : "browser"; +exports.size = LIMIT * 2 + VERSION.length; +exports.label = "v" + VERSION; +var __importDefault = (this && this.__importDefault) || function (m) { return m; }; `, "lib.js": `exports.x = 1;\n`, }); @@ -69,7 +72,7 @@ exports.mode = typeof window === "undefined" ? "node" : "browser"; ["an unknown global", `const w = window;`], ["a static block", `class A { static { console.log(1); } }`], ["a getter with effects", `Object.defineProperty(exports, "x", { get() { return 1; } });`], - ["a template substitution", "const s = `${{}}`;"], + ["an increment", "let i = 0; const j = i++;"], ])("refuses %s", (_what, text) => { expect(effectGate(pkg({ "index.js": text })).ok).toBe(false); }); diff --git a/packages/compiler/src/node-islands/effect-gate.ts b/packages/compiler/src/node-islands/effect-gate.ts index 74832c5d1..578cfffae 100644 --- a/packages/compiler/src/node-islands/effect-gate.ts +++ b/packages/compiler/src/node-islands/effect-gate.ts @@ -6,10 +6,14 @@ * files), and no throw. The gate is the conservative half of that proof: * it admits a module only when the top-level code of every file it loads * (the closure over literal require()/import specifiers) is built from the - * forms below, which cannot have effects whatever the environment. The - * build-time probe (snapshot.ts) then runs the admitted modules in libnode - * and confirms they load without throwing and without the effects it can - * observe. + * forms below, which cannot have effects whatever the environment. + * + * NI-7b: the gate is the static tier only. Whether a module defers is + * decided by the build-time probe's load recording (probe-recorder.ts): + * a load that reads no run-time input is deterministic, so one recorded + * run without effects proves deferral unobservable whether or not the + * gate admits the code (bundles such as esbuild output never pass the + * gate). The gate's verdict is reported with the decision. * * Admitted top-level statements (per file, CommonJS or ES module): * - directives ("use strict"), empty statements; @@ -36,9 +40,9 @@ * admitted class expressions; object and array literals of pure parts * (no spreads, no accessors); `require("")` of an admitted file or * an allowlisted builtin; member reads on required modules, allowlisted - * globals and same-file bindings; `typeof`, `void`, `!`; `===`, `!==`, - * `&&`, `||`, `??` and the conditional operator over pure operands; - * arithmetic between literals; `Symbol(…)`, `Symbol.for(…)`, + * globals and same-file bindings; unary and binary operators (no + * assignments or increments) and the conditional operator over pure + * operands; CommonJS's top-level `this`; `Symbol(…)`, `Symbol.for(…)`, * `Object.freeze(…)` and `Object.create(…)` over pure arguments; `new` of * Map, Set, WeakMap and WeakSet without arguments. * @@ -290,6 +294,7 @@ function checkFile(parser: typeof ts, file: string, load: (file: string) => void return PURE_GLOBALS.has(e.text) || (!esm && (e.text === "module" || e.text === "exports")); } if (parser.isPropertyAccessExpression(e)) return readable(e.expression); + if (e.kind === SK.ThisKeyword) return !esm; if (parser.isCallExpression(e)) return isRequireCall(e); return false; }; @@ -322,13 +327,14 @@ function checkFile(parser: typeof ts, file: string, load: (file: string) => void case SK.FunctionExpression: case SK.ArrowFunction: return; + case SK.ThisKeyword: + // CommonJS: `exports` (tsc's `this && this.__importDefault`). + if (esm) refuse(e, "`this` in an ES module"); + return; case SK.ClassExpression: return checkClass(e as ts.ClassExpression); case SK.TemplateExpression: - for (const span of (e as ts.TemplateExpression).templateSpans) { - // A substitution converts its value to a string: literals only. - if (!isLiteral(span.expression)) refuse(span.expression, "a template substitution"); - } + for (const span of (e as ts.TemplateExpression).templateSpans) pure(span.expression); return; case SK.ObjectLiteralExpression: for (const p of (e as ts.ObjectLiteralExpression).properties) { @@ -368,9 +374,9 @@ function checkFile(parser: typeof ts, file: string, load: (file: string) => void return pure((e as ts.VoidExpression).expression); case SK.PrefixUnaryExpression: { const u = e as ts.PrefixUnaryExpression; - if (u.operator === SK.ExclamationToken) return pure(u.operand); - if (isLiteral(u.operand)) return; - return refuse(e, "a unary operator on a non-literal"); + if (u.operator === SK.PlusPlusToken || u.operator === SK.MinusMinusToken) + return refuse(e, "an increment"); + return pure(u.operand); } case SK.BinaryExpression: { const b = e as ts.BinaryExpression; @@ -387,8 +393,13 @@ function checkFile(parser: typeof ts, file: string, load: (file: string) => void pure(b.right); return; } - if (isLiteral(b.left) && isLiteral(b.right) && op !== SK.EqualsToken) return; - return refuse(e, "a binary operator on non-literals"); + if (op >= SK.FirstAssignment && op <= SK.LastAssignment) + return refuse(e, "an assignment inside an expression"); + // Coercions of pure operands; a module-defined valueOf/toString + // runs module code, whose effects the probe's recording sees. + pure(b.left); + pure(b.right); + return; } case SK.ConditionalExpression: { const c = e as ts.ConditionalExpression; diff --git a/packages/compiler/src/node-islands/probe-recorder.ts b/packages/compiler/src/node-islands/probe-recorder.ts new file mode 100644 index 000000000..9b85b8713 --- /dev/null +++ b/packages/compiler/src/node-islands/probe-recorder.ts @@ -0,0 +1,489 @@ +/* The build-time load recorder of deferred Node islands (scope-B NI-7b). + * It runs inside libnode in the probe host (snapshot.ts writes it next to + * the host and the bootstrap requires it), loads every candidate island + * module once and records, for each: + * + * - inputs: reads of values that can differ between the build and a run, + * or between the import position and a later evaluation: process.env + * keys (guarded at boot), process.env enumeration, stdio TTY state + * (`isTTY` is guarded at boot; window size and colour depth are not), + * argv, execPath, cwd, uid/gid, the file system, os information, and + * the clock and random numbers (volatile: allowed, but then no + * constants are snapshotted); + * - effects: output, warnings, process listeners and exits, timers, + * immediates, ticks, microtasks and any other async resource created + * during the load (promises included), child processes, the network, + * workers, vm, the inspector, native addons (process.dlopen), writes + * to process.env; + * - mutations of builtin state: globalThis, the builtin prototypes and + * constructors, `process`, and the exports of common builtin modules + * (island-local, since native code has its own builtins, but + * observable by other island modules evaluated out of order); + * - every file the load required (the module's closure), and a throw; + * + * and takes the interface snapshot of the module's value (snapshot.ts). + * Calls made by Node's own internals (the module loader reading files, + * for instance) are not the module's: a record is kept only when the + * nearest caller outside this file is not `node:internal/`. + * + * Determinism argument: a load that read no input runs the same way at + * any time in any run (with the same pack and the same files, which the + * boot guards check), so one recorded run is the whole truth about its + * effects, and evaluating it later is unobservable when that run had + * none. Volatile values (pid, clock, random) do not change between the + * import position and a later evaluation in the same process except the + * clock and random numbers, which no run of Node reproduces either. */ +export const PROBE_RECORDER = String.raw`"use strict"; +const types = require("node:util").types; +const Module = require("node:module"); +const { createRequire } = Module; +const asyncHooks = require("node:async_hooks"); +const SELF = __filename; + +let lastCaller = ""; +// The closure of the module being loaded (files its load resolved), and +// its package directory: code in other files (an earlier module that +// wrapped a builtin, as graceful-fs wraps fs.closeSync) acts for its caller. +let currentFiles = null; +let currentPackageDir = null; +function frameFile(line) { + const m = /\(([^()]+):\d+:\d+\)\s*$/.exec(line) || /at ([^ ()]+):\d+:\d+\s*$/.exec(line); + if (m === null) return null; + let f = m[1]; + if (f.startsWith("file://")) { try { f = require("node:url").fileURLToPath(f); } catch {} } + return f; +} +// Whose call this is: walking out from the recorder's frames, the first +// frame of the module loader (node:internal/modules/) makes it the +// loader's (reading a module's files); the first frame of other code +// (an island module, outside node:) makes it the module's. Other builtin +// frames (console, streams, a public builtin calling its own exports) act +// for their caller. +function internalCaller() { + const limit = Error.stackTraceLimit; + Error.stackTraceLimit = 64; + const lines = new Error().stack.split("\n"); + Error.stackTraceLimit = limit; + for (let i = 1; i < lines.length; i++) { + const line = lines[i]; + if (line.includes(SELF)) continue; + if (line.includes("node:internal/modules/")) return true; + if (/[( ]node:/.test(line)) continue; + const file = frameFile(line); + if (currentFiles !== null && file !== null && !currentFiles.has(file)) continue; + lastCaller = line.trim().replace(/^at /, ""); + return false; + } + return true; +} + +const fnToString = Function.prototype.toString; +const hasOwn = Object.prototype.hasOwnProperty; +function isConstructor(f) { + try { Reflect.construct(String, [], f); return true; } catch { return false; } +} +function plainClass(f) { + let source = ""; + try { source = fnToString.call(f); } catch { return false; } + if (/\[native code\]\s*\}$/.test(source)) return false; + for (let o = f; o !== null && o !== Function.prototype; o = Object.getPrototypeOf(o)) + if (hasOwn.call(o, Symbol.hasInstance)) return false; + return true; +} + +const builtinObjects = new Set(); +for (const name of ["assert", "buffer", "child_process", "constants", "crypto", "dns", "events", "fs", + "fs/promises", "http", "https", "module", "net", "os", "path", "querystring", "readline", "stream", + "stream/promises", "string_decoder", "timers", "tty", "url", "util", "util/types", "zlib"]) { + try { builtinObjects.add(require(name)); } catch {} +} + +exports.probe = function probe(input) { + const objects = {}; + const oids = new Map(); + let next = 1; + const value = (v, depth) => { + if (v === undefined) return { u: 1 }; + if (v === null) return { l: 1 }; + switch (typeof v) { + case "boolean": return { b: v }; + case "number": return { n: Object.is(v, -0) ? "-0" : String(v) }; + case "string": return v.isWellFormed() ? { s: v } : { x: 1 }; + case "symbol": return object(v, 2, depth); + case "function": + if (types.isProxy(v)) return { x: 1 }; + return object(v, 1 | (isConstructor(v) ? 32 : 0), depth); + case "object": + if (types.isProxy(v) || types.isPromise(v) || types.isNativeError(v) || + ArrayBuffer.isView(v) || types.isExternal(v) || builtinObjects.has(v)) return { x: 1 }; + return object(v, Array.isArray(v) ? 8 : 0, depth); + default: return { x: 1 }; + } + }; + const object = (v, flags, depth) => { + if (oids.has(v)) return { o: oids.get(v) }; + const id = next++; + oids.set(v, id); + const entry = { f: flags, p: (flags & 1) !== 0 && plainClass(v), m: [] }; + objects[id] = entry; + if (typeof v === "symbol") return { o: id }; + const keys = depth === 0 || (depth === 1 && typeof v !== "function") + ? Object.getOwnPropertyNames(v) + : typeof v === "function" ? ["name", "length"] : []; + for (const key of keys) { + let d; + try { d = Object.getOwnPropertyDescriptor(v, key); } catch { continue; } + if (d === undefined) continue; + if (!("value" in d)) { + // The module value's getters (esbuild's __export): read once, under + // the recording, as Node reads CommonJS named exports after the + // module evaluated. + if (depth !== 0 || typeof d.get !== "function") continue; + // A getter that reads inputs or has effects (esbuild's lazy __esm + // inits) is left out: its member starts Node, and its work happens + // when the program reads the member, eager or deferred alike (Node + // reads every CommonJS named export at import; scriptc reads the + // ones the program imports). It is not part of the load. + const marks = [inputs.length, volatile.length, envKeys.size, tty.size, effects.length]; + const keysBefore = new Set(envKeys.keys()); + const ttyBefore = new Set(tty.keys()); + let got, ok = true; + recording = true; + try { got = d.get.call(v); } catch { ok = false; } + recording = false; + const readInput = inputs.length > marks[0] || volatile.length > marks[1] || + envKeys.size > marks[2] || tty.size > marks[3] || effects.length > marks[4]; + if (readInput) { + inputs.length = marks[0]; + volatile.length = marks[1]; + effects.length = marks[4]; + for (const k of [...envKeys.keys()]) if (!keysBefore.has(k)) envKeys.delete(k); + for (const k of [...tty.keys()]) if (!ttyBefore.has(k)) tty.delete(k); + } + if (ok && !readInput) entry.m.push([key, value(got, depth + 1)]); + continue; + } + entry.m.push([key, value(d.value, depth + 1)]); + } + return { o: id }; + }; + + // ── the recorder ── + const inputs = [], volatile = [], effects = [], tty = new Map(), envKeys = new Map(); + const files = new Set(); + let recording = false; + const note = (list, what) => { + if (recording && !internalCaller()) list.push(list === volatile ? what : what + " (" + lastCaller + ")"); + }; + const undo = []; + const wrapFn = (owner, key, list, label) => { + let original; + try { original = owner[key]; } catch { return; } + if (typeof original !== "function") return; + const wrapped = function (...a) { + note(list, label); + return new.target ? Reflect.construct(original, a, new.target) : original.apply(this, a); + }; + try { + Object.defineProperty(wrapped, "name", { value: original.name }); + Object.defineProperty(wrapped, "length", { value: original.length }); + for (const k of Reflect.ownKeys(original)) + if (!hasOwn.call(wrapped, k)) Object.defineProperty(wrapped, k, Object.getOwnPropertyDescriptor(original, k)); + owner[key] = wrapped; + } catch { return; } + undo.push(() => { try { owner[key] = original; } catch {} }); + }; + const wrapGetter = (owner, key, label, onRead) => { + const d = Object.getOwnPropertyDescriptor(owner, key); + if (d === undefined || !d.configurable) return; + Object.defineProperty(owner, key, { + configurable: true, + enumerable: d.enumerable, + get() { + const v = d.get ? d.get.call(this) : d.value; + if (recording && !internalCaller()) onRead(v); + return v; + }, + set(v) { + note(effects, "a write of " + label); + if (d.set) d.set.call(this, v); else d.value = v; + }, + }); + undo.push(() => Object.defineProperty(owner, key, d)); + }; + const realEnv = process.env; + const readKey = (t, k) => { + if (recording && typeof k === "string" && !envKeys.has(k) && !internalCaller()) + envKeys.set(k, t[k] === undefined ? null : String(t[k])); + }; + process.env = new Proxy(realEnv, { + get(t, k) { readKey(t, k); return t[k]; }, + has(t, k) { readKey(t, k); return k in t; }, + getOwnPropertyDescriptor(t, k) { readKey(t, k); return Reflect.getOwnPropertyDescriptor(t, k); }, + ownKeys(t) { note(inputs, "an enumeration of process.env"); return Reflect.ownKeys(t); }, + set(t, k, v) { note(effects, "a write of process.env." + String(k)); t[k] = v; return true; }, + defineProperty(t, k, d) { note(effects, "a write of process.env." + String(k)); return Reflect.defineProperty(t, k, d); }, + deleteProperty(t, k) { note(effects, "a delete of process.env." + String(k)); delete t[k]; return true; }, + }); + undo.push(() => { process.env = realEnv; }); + for (const k of ["argv", "execArgv", "execPath", "title", "argv0", "debugPort", "exitCode"]) + wrapGetter(process, k, "process." + k, () => note(inputs, "process." + k)); + for (const k of ["pid", "ppid"]) wrapGetter(process, k, "process." + k, () => note(volatile, "process." + k)); + for (const k of ["cwd", "getuid", "getgid", "geteuid", "getegid", "getgroups", "umask", "getActiveResourcesInfo", "resourceUsage", "availableMemory", "constrainedMemory"]) + wrapFn(process, k, inputs, "process." + k + "()"); + for (const k of ["uptime", "hrtime", "memoryUsage", "cpuUsage"]) wrapFn(process, k, volatile, "process." + k + "()"); + if (process.hrtime) wrapFn(process.hrtime, "bigint", volatile, "process.hrtime.bigint()"); + for (const k of ["exit", "reallyExit", "abort", "kill", "chdir", "setuid", "setgid", "seteuid", "setegid", "setgroups", "dlopen", "binding", "_linkedBinding", "emitWarning", "on", "addListener", "once", "prependListener", "prependOnceListener", "removeListener", "off", "removeAllListeners", "setMaxListeners", "nextTick", "setUncaughtExceptionCaptureCallback", "setSourceMapsEnabled", "send", "disconnect"]) + wrapFn(process, k, effects, "process." + k + "()"); + for (const [fd, s] of [[0, "stdin"], [1, "stdout"], [2, "stderr"]]) { + let stream; + try { stream = process[s]; } catch { continue; } + for (const k of ["write", "on", "once", "addListener", "prependListener", "end", "setRawMode", "resume", "pause", "read", "cork", "uncork", "destroy", "pipe"]) + wrapFn(stream, k, effects, "process." + s + "." + k + "()"); + wrapGetter(stream, "isTTY", "process." + s + ".isTTY", (v) => { if (!tty.has(fd)) tty.set(fd, v === true); }); + for (const k of ["columns", "rows"]) wrapGetter(stream, k, "process." + s + "." + k, () => note(inputs, "process." + s + "." + k)); + for (const k of ["getColorDepth", "hasColors", "getWindowSize"]) wrapFn(stream, k, inputs, "process." + s + "." + k + "()"); + } + for (const k of ["queueMicrotask", "fetch"]) wrapFn(globalThis, k, effects, k + "()"); + // Timers: one the load sets and clears again, and a clear of nothing + // (undefined, null), leave nothing behind (NI-7d). + const pendingTimers = new Map(); + for (const k of ["setTimeout", "setInterval", "setImmediate"]) { + const original = globalThis[k]; + const wrapped = function (...a) { + const handle = original.apply(this, a); + if (recording && !internalCaller()) { + effects.push(k + "() (" + lastCaller + ")"); + pendingTimers.set(handle, effects.length - 1); + } + return handle; + }; + Object.defineProperty(wrapped, "name", { value: original.name }); + globalThis[k] = wrapped; + undo.push(() => { globalThis[k] = original; }); + } + for (const k of ["clearTimeout", "clearInterval", "clearImmediate"]) { + const original = globalThis[k]; + const wrapped = function (handle) { + if (recording && !internalCaller()) { + if (pendingTimers.has(handle)) { + effects[pendingTimers.get(handle)] = null; + pendingTimers.delete(handle); + } else if (handle !== undefined && handle !== null) { + effects.push(k + "() of a timer the load did not set (" + lastCaller + ")"); + } + } + return original.apply(this, arguments); + }; + Object.defineProperty(wrapped, "name", { value: original.name }); + globalThis[k] = wrapped; + undo.push(() => { globalThis[k] = original; }); + } + wrapFn(Date, "now", volatile, "Date.now()"); + wrapFn(Math, "random", volatile, "Math.random()"); + wrapFn(globalThis, "Date", volatile, "new Date()"); + const whole = { + fs: inputs, os: inputs, child_process: effects, net: effects, dgram: effects, dns: effects, + http: effects, https: effects, http2: effects, tls: effects, worker_threads: effects, + cluster: effects, vm: effects, inspector: effects, tty: inputs, readline: effects, timers: effects, + perf_hooks: volatile, crypto: volatile, v8: effects, repl: effects, trace_events: effects, + async_hooks: effects, diagnostics_channel: effects, + }; + // os.homedir() and os.tmpdir() are functions of the environment on POSIX: + // HOME (when set), and TMPDIR, TMP, TEMP. Their keys become env guards. + const osModule = require("node:os"); + for (const [k, keys] of [["homedir", ["HOME"]], ["tmpdir", ["TMPDIR", "TMP", "TEMP"]]]) { + const original = osModule[k]; + osModule[k] = function () { + const result = original.apply(this, arguments); + if (recording && !internalCaller()) { + if (process.platform === "win32" || (k === "homedir" && realEnv.HOME === undefined)) + inputs.push("os." + k + "()"); + else for (const key of keys) if (!envKeys.has(key)) envKeys.set(key, realEnv[key] === undefined ? null : String(realEnv[key])); + } + return result; + }; + undo.push(() => { osModule[k] = original; }); + } + const pureFns = new Set(["os.platform", "os.homedir", "os.tmpdir", "os.type", "os.arch", "os.machine", "os.endianness", + "crypto.createHash", "crypto.createHmac", "crypto.getHashes", "crypto.getCiphers", "crypto.getCurves", + "crypto.createCipheriv", "crypto.createDecipheriv", "vm.isContext", "fs.existsSync__never", + "async_hooks.AsyncLocalStorage", "async_hooks.AsyncResource", "diagnostics_channel.channel", + "diagnostics_channel.tracingChannel", "diagnostics_channel.hasSubscribers"]); + // Synchronous reads of the module's own package (its files are boot + // guards, by size and mtime) are guarded inputs, not inputs. + const fsModule = require("node:fs"); + const guardedFds = new Set(); + const ownPath = (p) => { + if (currentPackageDir === null) return null; + let f = p; + if (f instanceof URL) { try { f = require("node:url").fileURLToPath(f); } catch { return null; } } + if (Buffer.isBuffer(f)) f = f.toString(); + if (typeof f !== "string") return null; + const abs = require("node:path").resolve(f); + return abs === currentPackageDir || abs.startsWith(currentPackageDir + "/") ? abs : null; + }; + const readOnlyFlags = (flags) => flags === undefined || flags === "r" || flags === "rs" || flags === fsModule.constants.O_RDONLY; + for (const key of ["readFileSync", "existsSync", "statSync", "lstatSync", "readdirSync", "realpathSync", "accessSync", "readlinkSync", "openSync"]) { + const original = fsModule[key]; + if (typeof original !== "function") continue; + const wrapped = function (target, flags) { + const result = original.apply(this, arguments); + if (recording && !internalCaller()) { + const own = ownPath(target); + if (own !== null && (key !== "openSync" || readOnlyFlags(flags))) { + files.add(own); + if (key === "openSync") guardedFds.add(result); + } else { + inputs.push("fs." + key + "() (" + lastCaller + ")"); + } + } + return result; + }; + for (const k of Reflect.ownKeys(original)) if (!hasOwn.call(wrapped, k)) { try { Object.defineProperty(wrapped, k, Object.getOwnPropertyDescriptor(original, k)); } catch {} } + fsModule[key] = wrapped; + undo.push(() => { fsModule[key] = original; }); + } + for (const key of ["readSync", "fstatSync", "closeSync"]) { + const original = fsModule[key]; + const wrapped = function (fd) { + const result = original.apply(this, arguments); + if (recording && !internalCaller()) { + if (!guardedFds.has(fd)) inputs.push("fs." + key + "() (" + lastCaller + ")"); + else if (key === "closeSync") guardedFds.delete(fd); + } + return result; + }; + fsModule[key] = wrapped; + undo.push(() => { fsModule[key] = original; }); + } + const guardedFsFns = new Set(["readFileSync", "existsSync", "statSync", "lstatSync", "readdirSync", "realpathSync", "accessSync", "readlinkSync", "openSync", "readSync", "fstatSync", "closeSync"]); + for (const [name, list] of Object.entries(whole)) { + let m; + try { m = require(name); } catch { continue; } + for (const key of Object.keys(m)) { + if (name === "fs" && guardedFsFns.has(key)) continue; + if (pureFns.has(name + "." + key) || key === "promises" || /^[A-Z]/.test(key) && name !== "worker_threads" && name !== "child_process") continue; + wrapFn(m, key, list, name + "." + key + "()"); + } + if (name === "fs" && m.promises) + for (const key of Object.keys(m.promises)) wrapFn(m.promises, key, inputs, "fs.promises." + key + "()"); + } + wrapFn(require("worker_threads"), "Worker", effects, "new Worker()"); + // Async resources the load creates. A promise alone is no work (an async + // function that returns at once creates one); a reaction to a promise + // the load created (then/catch/finally, an await) is. + const loadPromises = new Set(); + const hook = asyncHooks.createHook({ + init(id, type, trigger) { + // Timers and immediates are the timer wrappers' to account for. + if (type === "Timeout" || type === "Immediate" || !recording) return; + if (type === "PROMISE") { + if (loadPromises.has(trigger)) { + internalCaller(); + effects.push("a promise reaction (" + lastCaller + ")"); + } else if (!internalCaller()) { + loadPromises.add(id); + } + return; + } + if (!internalCaller()) effects.push("async work (" + type + ", " + lastCaller + ")"); + }, + }); + hook.enable(); + undo.push(() => hook.disable()); + // The closure: a resolve hook (no frame of ours stays on the stack while + // modules evaluate, so Node's node_modules checks see the real callers). + const hooks = Module.registerHooks({ + resolve(specifier, context, nextResolve) { + const result = nextResolve(specifier, context); + if (recording && typeof result.url === "string" && result.url.startsWith("file:")) + files.add(require("node:url").fileURLToPath(result.url)); + return result; + }, + }); + undo.push(() => hooks.deregister()); + const watched = [ + ["globalThis", globalThis], ["Object", Object], ["Object.prototype", Object.prototype], + ["Function.prototype", Function.prototype], ["Array", Array], ["Array.prototype", Array.prototype], + ["String.prototype", String.prototype], ["Number.prototype", Number.prototype], + ["Boolean.prototype", Boolean.prototype], ["Symbol", Symbol], ["Symbol.prototype", Symbol.prototype], + ["Promise", Promise], ["Promise.prototype", Promise.prototype], ["Error", Error], + ["Error.prototype", Error.prototype], ["RegExp.prototype", RegExp.prototype], + ["Map.prototype", Map.prototype], ["Set.prototype", Set.prototype], ["JSON", JSON], + ["Math", Math], ["Reflect", Reflect], ["process", process], ["Buffer", Buffer], + ["Buffer.prototype", Buffer.prototype], + ]; + for (const name of ["fs", "path", "events", "stream", "util", "os", "child_process", "http", "https", + "net", "url", "buffer", "crypto", "zlib", "module", "assert", "string_decoder", "timers", "tty", + "fs/promises", "stream/promises", "util/types", "constants", "querystring", "readline"]) { + try { watched.push([name, require(name)]); } catch {} + } + try { watched.push(["events.prototype", require("events").prototype]); } catch {} + try { watched.push(["stream.prototype", require("stream").prototype]); } catch {} + const shot = () => watched.map(([name, o]) => { + const m = new Map(); + for (const k of Reflect.ownKeys(o)) { + const d = Object.getOwnPropertyDescriptor(o, k); + m.set(k, d === undefined ? undefined : "value" in d ? d.value : d.get); + } + return [name, m]; + }); + + const results = []; + for (const path of input.modules) { + inputs.length = volatile.length = effects.length = 0; + pendingTimers.clear(); + loadPromises.clear(); + tty.clear(); envKeys.clear(); files.clear(); + files.add(path); + currentFiles = files; + currentPackageDir = null; + for (let d = require("node:path").dirname(path); d !== require("node:path").dirname(d); d = require("node:path").dirname(d)) { + if (require("node:fs").existsSync(d + "/package.json")) { currentPackageDir = d; break; } + } + const before = shot(); + let exportsValue, threw; + recording = true; + try { exportsValue = createRequire(path)(path); } catch (e) { threw = String((e && e.message) || e); } + recording = false; + const after = shot(); + const root = threw === undefined ? value(exportsValue, 0) : undefined; + const mutations = []; + for (let i = 0; i < before.length; i++) { + const [name, b] = before[i]; + const a = after[i][1]; + for (const [k, v] of a) if (!b.has(k) || !Object.is(b.get(k), v)) mutations.push(name + "." + String(k)); + for (const k of b.keys()) if (!a.has(k)) mutations.push("delete " + name + "." + String(k)); + } + // Node's own undici creates its global-dispatcher symbol lazily, at + // times unrelated to the module; only code that names it can set it. + const names = (key) => { + for (const f of files) { + try { if (require("node:fs").readFileSync(f, "utf8").includes(key)) return true; } catch {} + } + return false; + }; + for (let i = mutations.length - 1; i >= 0; i--) { + const m = /^globalThis\.Symbol\((undici\.globalDispatcher\.\d+)\)$/.exec(mutations[i]); + if (m !== null && !names("undici.globalDispatcher")) mutations.splice(i, 1); + } + const record = { + path, + inputs: [...new Set(inputs)], + volatile: [...new Set(volatile)], + effects: [...new Set(effects.filter((x) => x !== null))], + mutations, + env: [...envKeys], + tty: [...tty], + files: [...files], + }; + if (threw !== undefined) { record.throws = threw; results.push(record); continue; } + if (!("o" in root)) { record.reason = "its value is not an object"; results.push(record); continue; } + record.root = root.o; + results.push(record); + } + for (const u of undo.reverse()) u(); + return { results, objects }; +}; +`; diff --git a/packages/compiler/src/node-islands/snapshot.ts b/packages/compiler/src/node-islands/snapshot.ts index bde0f9674..ab5aeb27b 100644 --- a/packages/compiler/src/node-islands/snapshot.ts +++ b/packages/compiler/src/node-islands/snapshot.ts @@ -7,16 +7,23 @@ * decides each one: * * - the package's `evaluate` setting (`scriptc.packages.`): - * "eager" keeps the module eager; "lazy" skips the static effect gate - * and the probe's effect log (the user judged the effects benign); - * "auto" (the default) needs both; - * - the static effect gate (effect-gate.ts) over the module's closure; + * "eager" keeps the module eager; "lazy" defers it whatever its load + * does (the user judged the effects benign); * - the probe: one run of the build-time probe host * (runtime src/tools/scr_node_island_probe.c) in a libnode environment - * from the same pack, which loads every candidate module, records what - * it can observe of their evaluation (output, warnings, listeners, - * timers, new globals, a throw) and takes the interface snapshot of - * each module's value; + * from the same pack, which loads every candidate module under the + * load recorder (probe-recorder.ts) and takes the interface snapshot of + * each module's value. "auto" (the default) defers a module whose + * recorded load threw nothing, had no effects and read no run-time + * input other than env keys and stdio TTY state, which become boot + * guards (the determinism argument in probe-recorder.ts). The static + * effect gate (effect-gate.ts) is reported alongside, as the tier that + * needs no recording; + * - per-module mode (NI-7b): when every static island import defers, + * none mutates builtin state and no island module loads at a call site, + * Node starting evaluates nothing by itself; components of deferred + * modules that share files evaluate on first use. Otherwise every + * deferred module evaluates when Node starts (in import order); * * and rewrites the call: a deferred module keeps `nodeIsland.defer` with * its snapshot text (format: runtime scr_node_island_boot.c), any other @@ -40,6 +47,7 @@ import type { IrExpr, IrModule } from "../ir/ir.js"; import { readProjectConfig } from "../project-config.js"; import { effectGate } from "./effect-gate.js"; import { packageDirOf } from "./origin.js"; +import { PROBE_RECORDER } from "./probe-recorder.js"; type DeferCall = Extract; @@ -87,6 +95,30 @@ function traceDecision(d: DeferralDecision): void { ); } +/** Island loads lowering emitted at call sites (`require()`, `import()`). */ +function hasDynamicLoads(mod: IrModule): boolean { + let found = false; + const seen = new Set(); + const walk = (v: unknown): void => { + if (found || v === null || typeof v !== "object" || seen.has(v)) return; + seen.add(v); + if (Array.isArray(v)) { + for (const x of v) walk(x); + return; + } + const o = v as Record; + if ( + o["kind"] === "libCall" && + (o["fn"] === "nodeIsland.require" || o["fn"] === "nodeIsland.import") + ) + found = true; + for (const k in o) if (k !== "loc" && k !== "type") walk(o[k]); + }; + walk(mod.functions); + walk(mod.globals); + return found; +} + export function finalizeDeferredIslands( mod: IrModule, entryPath: string, @@ -94,6 +126,7 @@ export function finalizeDeferredIslands( ): DeferralDecision[] { const calls = deferCalls(mod); if (calls.length === 0) return []; + const dynamicLoads = hasDynamicLoads(mod); const decisions: DeferralDecision[] = []; const decide = (call: DeferCall, deferred: boolean, reason: string): void => { const d = { path: pathOf(call), deferred, reason }; @@ -106,7 +139,7 @@ export function finalizeDeferredIslands( return decisions; } const config = readProjectConfig(entryPath); - const candidates: { call: DeferCall; path: string; lazy: boolean; files: string[] }[] = []; + const candidates: { call: DeferCall; path: string; lazy: boolean; gate: string }[] = []; for (const call of calls) { const path = pathOf(call); const pkg = packageDirOf(path); @@ -116,17 +149,14 @@ export function finalizeDeferredIslands( continue; } const gate = effectGate(path); - if (!gate.ok && evaluate !== "lazy") { - decide(call, false, `${gate.reason} in ${gate.file}`); - continue; - } candidates.push({ call, path, lazy: evaluate === "lazy", - files: gate.ok ? gate.files : [path], + gate: gate.ok ? "" : `${gate.reason} in ${gate.file}`, }); } + let allDeferred = candidates.length === calls.length; if (candidates.length === 0) return decisions; const probe = runProbe(packDir, [...new Set(candidates.map((c) => c.path))]); if (typeof probe === "string") { @@ -134,24 +164,71 @@ export function finalizeDeferredIslands( return decisions; } const packGuards = packGuardLines(packDir); + const deferred: { call: DeferCall; result: ProbeResult; why: string }[] = []; for (const c of candidates) { const result = probe.results.find((r) => r.path === c.path); - if (result?.throws !== undefined) { - decide(c.call, false, `its evaluation throws: ${result.throws}`); + const why = (() => { + if (result === undefined) return "the probe did not load it"; + if (result.throws !== undefined) return `its evaluation throws: ${result.throws}`; + if (result.root === undefined) return result.reason ?? "the probe did not load it"; + if (c.lazy) return null; + if (result.effects.length > 0) return `its load has effects: ${result.effects.join(", ")}`; + if (result.inputs.length > 0) return `its load reads ${result.inputs.join(", ")}`; + return null; + })(); + if (why !== null || result === undefined) { + allDeferred = false; + decide(c.call, false, why ?? ""); continue; } - if (result === undefined || result.root === undefined) { - decide(c.call, false, result?.reason ?? "the probe did not load it"); - continue; - } - if (result.effects.length > 0 && !c.lazy) { - decide(c.call, false, `the probe saw ${result.effects.join(", ")}`); - continue; - } - const text = encodeSnapshot(probe, result.root, [...packGuards, ...fileGuardLines(c.files)]); - const arg = c.call.args[1]; + const how = c.lazy + ? `"evaluate": "lazy"` + : c.gate === "" + ? "static gate" + : `recorded load is deterministic (static gate: ${c.gate})`; + deferred.push({ call: c.call, result, why: how }); + } + // Per-module evaluation once Node runs (no materialize-all at start): + // every static island import is deferred, none mutates builtin state, + // and the program loads no island module at a call site. Deferred + // modules whose closures share files form one component, evaluated + // together in import order. + const lazyMode = + allDeferred && !dynamicLoads && deferred.every((d) => d.result.mutations.length === 0); + const component = new Map(); + if (lazyMode) { + const parent = deferred.map((_, i) => i); + const find = (i: number): number => (parent[i] === i ? i : (parent[i] = find(parent[i]!))); + const owner = new Map(); + deferred.forEach((d, i) => { + for (const f of d.result.files) { + const j = owner.get(f); + if (j === undefined) owner.set(f, i); + else parent[find(i)] = find(j); + } + }); + deferred.forEach((d, i) => component.set(d.result.path, find(i) + 1)); + } + for (const d of deferred) { + const r = d.result; + const guards = [ + ...packGuards, + ...fileGuardLines(r.files), + ...r.env.map(([k, v]) => `E ${lstr(k)} ${v === null ? "0 0:" : `1 ${lstr(v)}`}`), + ...r.tty.map(([fd, isTty]) => `T ${fd} ${isTty ? 1 : 0}`), + ...(lazyMode ? [`M ${component.get(r.path)}`] : []), + ]; + const text = encodeSnapshot(probe, r.root!, guards, r.volatile.length === 0); + const arg = d.call.args[1]; if (arg !== undefined && arg.kind === "strLit") (arg as { value: string }).value = text; - decide(c.call, true, ""); + const notes = [ + d.why, + ...(r.env.length > 0 ? [`env guards ${r.env.map(([k]) => k).join(",")}`] : []), + ...(r.volatile.length > 0 ? [`volatile ${r.volatile.join(",")}: no constants`] : []), + ...(r.mutations.length > 0 ? [`mutates ${r.mutations.join(",")}`] : []), + lazyMode ? `per-module (component ${component.get(r.path)})` : "evaluated when Node starts", + ]; + decide(d.call, true, notes.join("; ")); } return decisions; } @@ -213,8 +290,22 @@ interface ProbeObject { m: [string, ProbeValue][]; } +interface ProbeResult { + path: string; + root?: number; + inputs: string[]; + volatile: string[]; + effects: string[]; + mutations: string[]; + env: [string, string | null][]; + tty: [number, boolean][]; + files: string[]; + throws?: string; + reason?: string; +} + interface ProbeOutput { - results: { path: string; root?: number; effects: string[]; throws?: string; reason?: string }[]; + results: ProbeResult[]; objects: Record; } @@ -228,7 +319,12 @@ function encodeValue(v: ProbeValue): string | null { return null; } -function encodeSnapshot(probe: ProbeOutput, root: number, guards: string[]): string { +function encodeSnapshot( + probe: ProbeOutput, + root: number, + guards: string[], + constants: boolean, +): string { const lines = [...guards]; const reached: number[] = []; const seen = new Set(); @@ -246,7 +342,7 @@ function encodeSnapshot(probe: ProbeOutput, root: number, guards: string[]): str for (const oid of reached) { for (const [key, v] of probe.objects[oid]!.m) { const enc = encodeValue(v); - if (enc !== null) lines.push(`K ${oid} ${lstr(key)} ${enc}`); + if (enc !== null && (constants || "o" in v)) lines.push(`K ${oid} ${lstr(key)} ${enc}`); } } lines.push(`R ${root}`); @@ -256,16 +352,16 @@ function encodeSnapshot(probe: ProbeOutput, root: number, guards: string[]): str /* ── the probe ────────────────────────────────────────────────────────── */ /** The bootstrap the probe host starts Node with: the dispatcher the - * bridge requires, and the probe as its `call`. */ -const PROBE_BOOTSTRAP = String.raw` -"use strict"; -const types = require("node:util").types; -const { createRequire } = require("node:module"); + * bridge requires, whose `call` runs the recorder (probe-recorder.ts) + * from `recorder`. */ +function probeBootstrap(recorder: string): string { + return `"use strict"; const noop = () => {}; globalThis.__scriptcNodeIsland = { call(id, sync, file, specifier, name, args) { try { - return "R" + JSON.stringify(probe(JSON.parse(args))); + const recorder = require("node:module").createRequire(${JSON.stringify(recorder)})(${JSON.stringify(recorder)}); + return "R" + JSON.stringify(recorder.probe(JSON.parse(args))); } catch (e) { return "T" + JSON.stringify(String((e && e.stack) || e)); } @@ -274,110 +370,17 @@ globalThis.__scriptcNodeIsland = { setImmediate: noop, clearImmediate: noop, refImmediate: noop, nextTick: noop, emit: noop, load: noop, sharedPush: noop, }; -const fnToString = Function.prototype.toString; -const hasOwn = Object.prototype.hasOwnProperty; -function isConstructor(f) { - try { Reflect.construct(String, [], f); return true; } catch { return false; } -} -function plainClass(f) { - let source = ""; - try { source = fnToString.call(f); } catch { return false; } - if (/\[native code\]\s*\}$/.test(source)) return false; - for (let o = f; o !== null && o !== Function.prototype; o = Object.getPrototypeOf(o)) - if (hasOwn.call(o, Symbol.hasInstance)) return false; - return true; -} -function probe(input) { - const objects = {}; - const oids = new Map(); - let next = 1; - const value = (v, depth) => { - if (v === undefined) return { u: 1 }; - if (v === null) return { l: 1 }; - switch (typeof v) { - case "boolean": return { b: v }; - case "number": return { n: Object.is(v, -0) ? "-0" : String(v) }; - case "string": return v.isWellFormed() ? { s: v } : { x: 1 }; - case "symbol": return object(v, 2, depth); - case "function": - if (types.isProxy(v)) return { x: 1 }; - return object(v, 1 | (isConstructor(v) ? 32 : 0), depth); - case "object": - if (types.isProxy(v) || types.isPromise(v) || types.isNativeError(v) || - ArrayBuffer.isView(v) || types.isExternal(v)) return { x: 1 }; - return object(v, Array.isArray(v) ? 8 : 0, depth); - default: return { x: 1 }; - } - }; - const object = (v, flags, depth) => { - if (oids.has(v)) return { o: oids.get(v) }; - const id = next++; - oids.set(v, id); - const entry = { f: flags, p: (flags & 1) !== 0 && plainClass(v), m: [] }; - objects[id] = entry; - if (typeof v === "symbol") return { o: id }; - // The module's value and its direct members: every own data property - // (namespace member lists); functions anywhere: name and length. - const keys = depth === 0 || (depth === 1 && typeof v !== "function") - ? Object.getOwnPropertyNames(v) - : typeof v === "function" ? ["name", "length"] : []; - for (const key of keys) { - let d; - try { d = Object.getOwnPropertyDescriptor(v, key); } catch { continue; } - if (d === undefined || !("value" in d)) continue; - entry.m.push([key, value(d.value, depth + 1)]); - } - return { o: id }; - }; - const effects = []; - const watch = (owner, key, label) => { - const original = owner[key]; - if (typeof original !== "function") return () => {}; - owner[key] = function (...a) { effects.push(label); return original.apply(this, a); }; - return () => { owner[key] = original; }; - }; - const results = []; - for (const path of input.modules) { - effects.length = 0; - const globals = new Set(Reflect.ownKeys(globalThis)); - const restore = [ - watch(process.stdout, "write", "stdout output"), - watch(process.stderr, "write", "stderr output"), - watch(process, "emitWarning", "a process warning"), - watch(process, "on", "a process listener"), - watch(process, "addListener", "a process listener"), - watch(process, "once", "a process listener"), - watch(process, "prependListener", "a process listener"), - watch(process, "nextTick", "process.nextTick"), - watch(globalThis, "setTimeout", "a timer"), - watch(globalThis, "setInterval", "a timer"), - watch(globalThis, "setImmediate", "an immediate"), - watch(globalThis, "queueMicrotask", "a microtask"), - ]; - let exportsValue, threw; - try { exportsValue = createRequire(path)(path); } catch (e) { threw = String((e && e.message) || e); } - for (const r of restore) r(); - for (const k of Reflect.ownKeys(globalThis)) - if (!globals.has(k)) effects.push("a new global " + String(k)); - if (threw !== undefined) { results.push({ path, effects: [...effects], throws: threw }); continue; } - const root = value(exportsValue, 0); - if (!("o" in root)) { - results.push({ path, effects: [...effects], reason: "its value is not an object" }); - continue; - } - results.push({ path, root: root.o, effects: [...new Set(effects)] }); - } - return { results, objects }; -} `; +} function runtimeSrc(): string { const require = createRequire(import.meta.url); return join(dirname(require.resolve("@scriptc/runtime/package.json")), "src"); } -/** The probe host executable, built once per probe source and bridge - * header (cached under the temporary directory). */ +/** The probe host executable with its bootstrap and recorder, built once + * per probe source, bridge header and recorder (cached under the + * temporary directory). */ function probeHost(): string | string[] { const src = runtimeSrc(); const source = join(src, "tools", "scr_node_island_probe.c"); @@ -385,13 +388,15 @@ function probeHost(): string | string[] { const hash = createHash("sha256") .update(readFileSync(source)) .update(readFileSync(header)) - .update(PROBE_BOOTSTRAP) + .update(PROBE_RECORDER) + .update(probeBootstrap("")) .digest("hex") .slice(0, 16); const dir = join(tmpdir(), `scriptc-node-probe-${process.getuid?.() ?? 0}`, hash); const exe = join(dir, "scr-node-probe"); const bootstrap = join(dir, "bootstrap.js"); - if (existsSync(exe) && existsSync(bootstrap)) return [exe, bootstrap]; + const recorder = join(dir, "recorder.cjs"); + if (existsSync(exe) && existsSync(bootstrap) && existsSync(recorder)) return [exe, bootstrap]; mkdirSync(dir, { recursive: true }); const tmp = `${exe}.${process.pid}`; const errors: string[] = []; @@ -399,7 +404,9 @@ function probeHost(): string | string[] { if (cc === undefined || cc === "") continue; const r = spawnSync(cc, ["-O1", "-I", src, source, "-o", tmp, "-ldl"], { encoding: "utf8" }); if (r.status === 0) { - writeFileSync(`${bootstrap}.${process.pid}`, PROBE_BOOTSTRAP); + writeFileSync(`${recorder}.${process.pid}`, PROBE_RECORDER); + renameSync(`${recorder}.${process.pid}`, recorder); + writeFileSync(`${bootstrap}.${process.pid}`, probeBootstrap(recorder)); renameSync(`${bootstrap}.${process.pid}`, bootstrap); renameSync(tmp, exe); return [exe, bootstrap]; @@ -413,24 +420,32 @@ function runProbe(packDir: string, modules: string[]): ProbeOutput | string { const host = probeHost(); if (typeof host === "string") return host; const [exe, bootstrap] = host as [string, string]; - const input = join(dirname(exe), `input.${process.pid}.${Date.now()}.json`); + const stem = join(dirname(exe), `run.${process.pid}.${Date.now()}`); + const input = `${stem}.in.json`; + const output = `${stem}.out.json`; writeFileSync(input, JSON.stringify({ modules })); let out: string; try { - out = execFileSync(exe, [packDir, bootstrap, input], { - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"], - maxBuffer: 64 * 1024 * 1024, + execFileSync(exe, [packDir, bootstrap, input, output], { + stdio: ["ignore", "ignore", "pipe"], timeout: 60_000, }); + out = readFileSync(output, "utf8"); } catch (e) { - const err = e as { stderr?: string; message: string }; - return `the probe failed: ${(err.stderr ?? "").trim().split("\n")[0] || err.message}`; - } - try { - rmSync(input); - } catch { - /* best effort */ + const err = e as { stderr?: Buffer | string; message: string }; + return `the probe failed: ${ + String(err.stderr ?? "") + .trim() + .split("\n")[0] || err.message + }`; + } finally { + for (const f of [input, output]) { + try { + rmSync(f); + } catch { + /* best effort */ + } + } } if (!out.startsWith("R")) return `the probe threw: ${out.slice(1, 400)}`; return JSON.parse(out.slice(1)) as ProbeOutput; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6843-node-island-deferred-per-module/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6843-node-island-deferred-per-module/main.ts.json new file mode 100644 index 000000000..11715bb4f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6843-node-island-deferred-per-module/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6843-node-island-deferred-per-module/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6844-node-island-deferred-env/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6844-node-island-deferred-env/main.ts.json new file mode 100644 index 000000000..67f2ff109 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6844-node-island-deferred-env/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6844-node-island-deferred-env/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6845-node-island-deferred-require/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6845-node-island-deferred-require/main.ts.json new file mode 100644 index 000000000..5620823dc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6845-node-island-deferred-require/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6845-node-island-deferred-require/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6846-node-island-deferred-own-files/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6846-node-island-deferred-own-files/main.ts.json new file mode 100644 index 000000000..64864302d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6846-node-island-deferred-own-files/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6846-node-island-deferred-own-files/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6847-node-island-punycode-warning/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6847-node-island-punycode-warning/main.ts.json new file mode 100644 index 000000000..333e3205b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6847-node-island-punycode-warning/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6847-node-island-punycode-warning/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_node_island.c b/packages/runtime/src/scr_node_island.c index 1c6bfbea3..a178d35e3 100644 --- a/packages/runtime/src/scr_node_island.c +++ b/packages/runtime/src/scr_node_island.c @@ -688,10 +688,11 @@ static void ni_start(void) { ni_node_version, bridge->node_version, bridge->module_version, dir); ni_fatal(message); } - ScrStr *self = scr_process_exec_path(); - char argv0[4096]; - snprintf(argv0, sizeof argv0, "%.*s", (int)self->len, self->data); - scr_str_release(self); + /* Node's argv[0] is "node": island process.argv0 reads it, and Node's + * warning hint is built from it ("(Use `node --trace-deprecation ...`"), + * which the runtime's own warnings print too. process.execPath stays the + * executable (libuv's exepath). */ + char argv0[] = "node"; /* Flush first: Node writes to the same stdout. */ fflush(stdout); ni_bridge = bridge; diff --git a/packages/runtime/src/scr_node_island.h b/packages/runtime/src/scr_node_island.h index 2241f3fc2..8dd800997 100644 --- a/packages/runtime/src/scr_node_island.h +++ b/packages/runtime/src/scr_node_island.h @@ -174,8 +174,10 @@ void *scr_ni_stub_of(const ScrDyn *value); /* the stub entry of an unmaterialize bool scr_ni_stub_reconcile(ScrDyn *stub, ScrDyn *real); ScrDyn *scr_ni_boot_member(void *stub, const char *key, size_t len); /* +1 or NULL: unknown */ bool scr_ni_boot_plain_class(void *stub); +bool scr_ni_boot_snapshot_valid(void *stub); /* its module has not evaluated */ const char *scr_ni_pack_dir(void); /* the libnode pack directory this run uses */ void scr_ni_boot_stub_failed(void *stub); /* raises the module's load error */ +void scr_ni_boot_materialize(void *stub); /* evaluates the stub's module (component) */ /* Called at the end of Node's start (scr_node_island.c), set by the boot unit. */ extern void (*scr_ni_started_hook)(void); diff --git a/packages/runtime/src/scr_node_island_boot.c b/packages/runtime/src/scr_node_island_boot.c index 51c95f51e..fe28d76cd 100644 --- a/packages/runtime/src/scr_node_island_boot.c +++ b/packages/runtime/src/scr_node_island_boot.c @@ -33,6 +33,9 @@ * O an object (SCR_NVF_* flags) * K : a data member of object * R the module's value + * E : <0|1> : an env key the load read (absent: 0) + * T <0|1> the isTTY state the load read + * M per-module mode (NI-7b): see below * where is o, u, l, t, f, n or s:. */ #define _XOPEN_SOURCE 700 #define _DARWIN_C_SOURCE 1 @@ -45,6 +48,7 @@ #include #include #include +#include #if !defined(_WIN32) && !defined(__wasi__) @@ -76,8 +80,18 @@ typedef struct NbStub { ScrCaught *error; /* why it could not be reconciled */ } NbStub; +typedef struct { + const char *key, *value; + size_t key_len, value_len; + bool present; +} NbEnv; + typedef struct NbModule { char *path; + uint32_t component; /* per-module mode: modules evaluated together */ + bool done; + NbEnv *env; + size_t nenv; char *snapshot; /* owns the strings the stubs point into */ uint32_t root; uint32_t *order; /* stubs to reconcile after the root, parents first */ @@ -90,6 +104,12 @@ static size_t nb_stubs_cap = 0; static NbModule **nb_modules = NULL; static size_t nb_nmodules = 0, nb_modules_cap = 0; static bool nb_installed = false; +/* Per-module mode (NI-7b, `M` records): Node starting does not evaluate + * every deferred module; each component evaluates on the first use of one + * of its stubs. The build chooses it only when every static island import + * is deferred, none mutates builtin state and no island module loads at a + * call site, so nothing but these stubs can reach the modules' objects. */ +static bool nb_lazy = false; static int nb_trace = -1; static bool nb_tracing(void) { @@ -195,10 +215,63 @@ static ScrCaught *nb_changed(const NbModule *m) { return scr_exc_take(); } +/* A NUL-terminated copy (the snapshot's strings are length-delimited). */ +static char *nb_cstr(const char *s, size_t n) { + char *c = malloc(n + 1); + if (c == NULL) nb_oom(); + memcpy(c, s, n); + c[n] = '\0'; + return c; +} + +static bool nb_env_matches(const NbEnv *e) { + char *key = nb_cstr(e->key, e->key_len); + const char *now = getenv(key); + free(key); + if (!e->present) return now == NULL; + return now != NULL && strlen(now) == e->value_len && memcmp(now, e->value, e->value_len) == 0; +} + +/* The module evaluates against the environment it saw at its import + * position (the boot guard checked those values then): keys the program + * changed since are set back for the evaluation and restored after it. */ +static void nb_env_swap(NbModule *m, char **saved, bool restore) { + for (size_t i = 0; i < m->nenv; i++) { + NbEnv *e = &m->env[i]; + char *key = nb_cstr(e->key, e->key_len); + if (!restore) { + const char *now = getenv(key); + saved[i] = now != NULL ? strdup(now) : NULL; + if (!nb_env_matches(e)) { + if (e->present) { + char *v = nb_cstr(e->value, e->value_len); + setenv(key, v, 1); + free(v); + } else { + unsetenv(key); + } + } + } else { + if (saved[i] != NULL) setenv(key, saved[i], 1); + else unsetenv(key); + free(saved[i]); + } + free(key); + } +} + static void nb_materialize(NbModule *m) { + if (m->done) return; + m->done = true; + if (nb_tracing()) fprintf(stderr, "[node-islands] evaluating deferred %s\n", m->path); + char **saved = m->nenv > 0 ? calloc(m->nenv, sizeof *saved) : NULL; + if (m->nenv > 0 && saved == NULL) nb_oom(); + nb_env_swap(m, saved, false); ScrStr *path = scr_str_new(m->path, strlen(m->path)); ScrDyn *real = scr_ni_require(path); scr_str_release(path); + nb_env_swap(m, saved, true); + free(saved); if (real == NULL) { m->error = scr_exc_take(); return; @@ -210,9 +283,23 @@ static void nb_materialize(NbModule *m) { m->error = nb_changed(m); return; } + /* Only stubs the program holds (their box has a reference besides this + * table's), and their parents, need their real values: a stub nobody + * took has no identity to keep, and reading its value would run getters + * the eager program never runs. Parents precede children in `order`. */ + bool *needed = m->norder > 0 ? calloc(m->norder, sizeof *needed) : NULL; + if (m->norder > 0 && needed == NULL) nb_oom(); + for (size_t i = m->norder; i-- > 0;) { + NbStub *s = nb_stub(m->order[i]); + if (s->box->rc > 1) needed[i] = true; + if (!needed[i]) continue; + for (size_t j = 0; j < i; j++) + if (m->order[j] == s->parent) needed[j] = true; + } for (size_t i = 0; i < m->norder; i++) { NbStub *s = nb_stub(m->order[i]); NbStub *parent = nb_stub(s->parent); + if (!needed[i]) continue; if (scr_ni_stub_of(s->box) == NULL) continue; /* reached through an earlier module */ if (parent->error != NULL || scr_ni_stub_of(parent->box) != NULL) { s->error = scr_caught_retain(parent->error != NULL ? parent->error : m->error); @@ -226,14 +313,25 @@ static void nb_materialize(NbModule *m) { if (!scr_ni_stub_reconcile(s->box, value)) s->error = nb_changed(m); scr_dyn_release(value); } + free(needed); } static void nb_started(void) { + if (nb_lazy) return; /* each component evaluates on first use */ if (nb_tracing() && nb_nmodules > 0) fprintf(stderr, "[node-islands] Node started: evaluating %zu deferred module(s)\n", nb_nmodules); for (size_t i = 0; i < nb_nmodules; i++) nb_materialize(nb_modules[i]); } +void scr_ni_boot_materialize(void *stub) { + NbModule *owner = ((NbStub *)stub)->module; + for (size_t i = 0; i < nb_nmodules; i++) { + NbModule *m = nb_modules[i]; + if (m == owner || (owner->component != 0 && m->component == owner->component)) + nb_materialize(m); + } +} + void scr_ni_boot_stub_failed(void *stub) { NbStub *s = stub; ScrCaught *error = s->error != NULL ? s->error : s->module->error; @@ -277,6 +375,14 @@ ScrDyn *scr_ni_boot_member(void *stub, const char *key, size_t len) { bool scr_ni_boot_plain_class(void *stub) { return ((NbStub *)stub)->plain; } +/* Whether the snapshot still describes the stub's value: its module has + * not evaluated. Before Node starts nothing has; in per-module mode, a + * component's objects are unreachable to any code until it evaluates. */ +bool scr_ni_boot_snapshot_valid(void *stub) { + NbStub *s = stub; + return !s->module->done && (nb_lazy || scr_ni_bridge_loaded() == NULL); +} + /* ── nodeIsland.defer ─────────────────────────────────────────────────── */ static void nb_cleanup(void) { @@ -295,6 +401,7 @@ static void nb_cleanup(void) { NbModule *m = nb_modules[i]; if (m->error != NULL) scr_caught_release(m->error); free(m->order); + free(m->env); free(m->snapshot); free(m->path); free(m); @@ -340,13 +447,40 @@ static NbStub *nb_new_stub(uint32_t oid, uint32_t flags, bool plain, NbModule *m } ScrDyn *scr_node_island_defer(ScrStr *path, ScrStr *snapshot) { - if (scr_ni_bridge_loaded() != NULL) return nb_eager(path, "Node is already running"); - nb_install(); - /* Pass 1: the guards. */ + /* Pass 1: the guards, and the mode. */ NbCursor c = {snapshot->data, snapshot->data + snapshot->len, false}; + bool lazy = false; while (c.p < c.end && !c.bad) { char tag = *c.p++; - if (tag == 'G') { + if (tag == 'M') lazy = true; + nb_line_end(&c); + } + /* In per-module mode a module can stay deferred while Node runs. */ + if (!lazy && scr_ni_bridge_loaded() != NULL) return nb_eager(path, "Node is already running"); + nb_install(); + c = (NbCursor){snapshot->data, snapshot->data + snapshot->len, false}; + while (c.p < c.end && !c.bad) { + char tag = *c.p++; + if (tag == 'E') { + NbEnv e = {0}; + e.key = nb_str(&c, &e.key_len); + e.present = nb_uint(&c) != 0; + e.value = nb_str(&c, &e.value_len); + if (c.bad) break; + if (!nb_env_matches(&e)) { + if (nb_tracing()) + fprintf(stderr, "[node-islands] boot guard failed: env %.*s\n", (int)e.key_len, e.key); + return nb_eager(path, "a boot guard failed"); + } + } else if (tag == 'T') { + int fd = (int)nb_uint(&c); + bool was = nb_uint(&c) != 0; + if (c.bad) break; + if ((isatty(fd) == 1) != was) { + if (nb_tracing()) fprintf(stderr, "[node-islands] boot guard failed: isatty(%d)\n", fd); + return nb_eager(path, "a boot guard failed"); + } + } else if (tag == 'G') { size_t len; const char *file = nb_str(&c, &len); uint64_t size = nb_uint(&c); @@ -409,11 +543,26 @@ ScrDyn *scr_node_island_defer(ScrStr *path, ScrStr *snapshot) { } } else if (tag == 'R') { m->root = (uint32_t)nb_uint(&c); + } else if (tag == 'M') { + m->component = (uint32_t)nb_uint(&c); + nb_lazy = true; + } else if (tag == 'E') { + NbEnv e = {0}; + e.key = nb_str(&c, &e.key_len); + e.present = nb_uint(&c) != 0; + e.value = nb_str(&c, &e.value_len); + if (!c.bad) { + NbEnv *grown = realloc(m->env, (m->nenv + 1) * sizeof *grown); + if (grown == NULL) nb_oom(); + m->env = grown; + m->env[m->nenv++] = e; + } } nb_line_end(&c); } NbStub *root = nb_stub(m->root); if (c.bad || root == NULL) { + free(m->env); free(m->snapshot); free(m->path); free(m); diff --git a/packages/runtime/src/scr_node_island_handle.c b/packages/runtime/src/scr_node_island_handle.c index bb3bcbd7d..960499af2 100644 --- a/packages/runtime/src/scr_node_island_handle.c +++ b/packages/runtime/src/scr_node_island_handle.c @@ -290,7 +290,8 @@ bool scr_ni_stub_reconcile(ScrDyn *stub, ScrDyn *real) { } /* A stub whose Node value is needed: starting Node materializes every - * deferred module (scr_node_island_boot.c), after which the ref is real. + * deferred module (scr_node_island_boot.c), or in per-module mode the + * boot unit evaluates the stub's component; then the ref is real. * False with the module's load error pending. */ ScrDyn *scr_ni_lazy_subclass_new(ScrDyn *const *args) { ScrNodeRef *ref = calloc(1, sizeof *ref); @@ -343,6 +344,9 @@ static bool nh_live(ScrNodeRef *ref) { if (ref->stub == NULL) return true; void *stub = ref->stub; scr_ni_bridge(); + /* Per-module mode (NI-7b): Node starting evaluated nothing; the stub's + * own component evaluates now. */ + if (ref->stub != NULL) scr_ni_boot_materialize(stub); if (ref->stub == NULL) return true; scr_ni_boot_stub_failed(stub); return false; @@ -923,8 +927,9 @@ ScrDyn *scr_ni_get_str(ScrDyn *obj, const char *key, size_t len) { if (nh_lazy_is(obj) && len == 4 && memcmp(key, "name", 4) == 0) return scr_dyn_retain(((ScrNodeRef *)obj->v.handle.ptr)->lazy[1]); void *stub = scr_ni_stub_of(obj); - if (stub != NULL && scr_ni_bridge_loaded() == NULL) { - /* Before Node starts, the interface snapshot answers what it holds. */ + if (stub != NULL && scr_ni_boot_snapshot_valid(stub)) { + /* Until its module evaluates, the interface snapshot answers what it + * holds. */ ScrDyn *known = scr_ni_boot_member(stub, key, len); if (known != NULL) return known; } @@ -1080,7 +1085,7 @@ int scr_ni_instanceof(ScrDyn *value, ScrDyn *ctor) { * native value crossing into Node is never an instance of an ordinary * island class (the snapshot says whether the class is one). */ void *stub = scr_ni_stub_of(ctor); - if (stub != NULL && scr_ni_bridge_loaded() == NULL && !scr_ni_is_handle(value) && + if (stub != NULL && scr_ni_boot_snapshot_valid(stub) && !scr_ni_is_handle(value) && scr_ni_boot_plain_class(stub)) return 0; /* A lazy subclass has no instances yet, and a native value is never one diff --git a/packages/runtime/src/tools/scr_node_island_probe.c b/packages/runtime/src/tools/scr_node_island_probe.c index 6354a0c86..559d8d0c0 100644 --- a/packages/runtime/src/tools/scr_node_island_probe.c +++ b/packages/runtime/src/tools/scr_node_island_probe.c @@ -3,12 +3,12 @@ * once per bridge ABI and runs it against the libnode pack the program * will load, so the interface snapshot comes from that exact Node. * - * scr-node-probe + * scr-node-probe * * It starts Node from the pack's bridge with the bootstrap (which defines * the dispatcher and the probe), makes one synchronous call with the input - * JSON, prints the outcome ("R" or "T") on stdout and stops - * Node. Never includes a Node or V8 header. */ + * JSON, writes the outcome ("R" or "T") to the output file + * (the probed modules may print) and exits. Never includes a Node or V8 header. */ #include "scr_node_bridge.h" #include @@ -142,8 +142,8 @@ static const ScrNodeBridgeHost kHost = { }; int main(int argc, char **argv) { - if (argc != 4) { - fprintf(stderr, "usage: scr-node-probe \n"); + if (argc != 5) { + fprintf(stderr, "usage: scr-node-probe \n"); return 2; } char path[4096]; @@ -174,8 +174,12 @@ int main(int argc, char **argv) { } size_t len = 0; char *outcome = bridge->call(0, 1, "", "", "probe", input, ilen, &len); - fwrite(outcome, 1, len, stdout); fflush(stdout); + FILE *out = fopen(argv[4], "wb"); + if (out == NULL || fwrite(outcome, 1, len, out) != len || fclose(out) != 0) { + fprintf(stderr, "scr-node-probe: cannot write %s\n", argv[4]); + _Exit(1); + } bridge->release(outcome); /* No orderly stop: the probe's modules may hold handles; the process * exits here. */ diff --git a/tests/corpus/6843-node-island-deferred-per-module/main.ts b/tests/corpus/6843-node-island-deferred-per-module/main.ts new file mode 100644 index 000000000..a5c10bfb3 --- /dev/null +++ b/tests/corpus/6843-node-island-deferred-per-module/main.ts @@ -0,0 +1,12 @@ +// @requires-libnode +// Two deferred island packages that share no files (scope-B NI-7b): every +// static island import is deferred and none changes builtin state, so +// each package evaluates on its own first use, not when Node starts. +import { hello, NAME } from "pkg-a"; +import { Counter, make } from "pkg-b"; + +console.log("start", NAME, typeof hello, typeof Counter, Counter.name); +console.log(hello("native"), hello("again")); +console.log("pkg-b untouched so far:", typeof make); +const c = make(); +console.log(c.next(), c.next(), c instanceof Counter); diff --git a/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-a/index.d.ts b/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-a/index.d.ts new file mode 100644 index 000000000..a512dae76 --- /dev/null +++ b/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-a/index.d.ts @@ -0,0 +1,2 @@ +export declare function hello(who: string): string; +export declare const NAME: string; diff --git a/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-a/index.js b/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-a/index.js new file mode 100644 index 000000000..3cc58bd5f --- /dev/null +++ b/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-a/index.js @@ -0,0 +1,8 @@ +"use strict"; +let greeted = 0; +function hello(who) { + greeted += 1; + return `hello ${who} (${greeted})`; +} +const NAME = "pkg-a"; +module.exports = { hello, NAME }; diff --git a/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-a/package.json b/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-a/package.json new file mode 100644 index 000000000..e0270f674 --- /dev/null +++ b/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-a/package.json @@ -0,0 +1 @@ +{ "name": "pkg-a", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-b/index.d.ts b/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-b/index.d.ts new file mode 100644 index 000000000..9da03b6cc --- /dev/null +++ b/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-b/index.d.ts @@ -0,0 +1,5 @@ +export declare class Counter { + n: number; + next(): number; +} +export declare function make(): Counter; diff --git a/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-b/index.js b/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-b/index.js new file mode 100644 index 000000000..029ca753a --- /dev/null +++ b/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-b/index.js @@ -0,0 +1,14 @@ +"use strict"; +class Counter { + constructor() { + this.n = 0; + } + next() { + this.n += 1; + return this.n; + } +} +function make() { + return new Counter(); +} +module.exports = { Counter, make }; diff --git a/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-b/package.json b/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-b/package.json new file mode 100644 index 000000000..539831790 --- /dev/null +++ b/tests/corpus/6843-node-island-deferred-per-module/node_modules/pkg-b/package.json @@ -0,0 +1 @@ +{ "name": "pkg-b", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6843-node-island-deferred-per-module/package.json b/tests/corpus/6843-node-island-deferred-per-module/package.json new file mode 100644 index 000000000..ee7d20a75 --- /dev/null +++ b/tests/corpus/6843-node-island-deferred-per-module/package.json @@ -0,0 +1 @@ +{ "name": "island-deferred-per-module", "private": true, "type": "module", "scriptc": { "packages": { "pkg-a": "node", "pkg-b": "node" } } } diff --git a/tests/corpus/6844-node-island-deferred-env/main.ts b/tests/corpus/6844-node-island-deferred-env/main.ts new file mode 100644 index 000000000..cb02e8597 --- /dev/null +++ b/tests/corpus/6844-node-island-deferred-env/main.ts @@ -0,0 +1,11 @@ +// @requires-libnode +// A deferred island package that reads an environment variable when it +// evaluates (scope-B NI-7b): the build records the key as a boot guard, +// and the late evaluation sees the value the variable had at the import +// position, even though the program changed it before the first use. +import { currentMode } from "env-pkg"; + +console.log("before", process.env.SCRIPTC_NI7B_MODE ?? "(unset)"); +process.env.SCRIPTC_NI7B_MODE = "changed"; +console.log("mode", currentMode()); +console.log("after", process.env.SCRIPTC_NI7B_MODE); diff --git a/tests/corpus/6844-node-island-deferred-env/node_modules/env-pkg/index.d.ts b/tests/corpus/6844-node-island-deferred-env/node_modules/env-pkg/index.d.ts new file mode 100644 index 000000000..9fd7e1c8c --- /dev/null +++ b/tests/corpus/6844-node-island-deferred-env/node_modules/env-pkg/index.d.ts @@ -0,0 +1 @@ +export declare function currentMode(): string; diff --git a/tests/corpus/6844-node-island-deferred-env/node_modules/env-pkg/index.js b/tests/corpus/6844-node-island-deferred-env/node_modules/env-pkg/index.js new file mode 100644 index 000000000..f2de7095c --- /dev/null +++ b/tests/corpus/6844-node-island-deferred-env/node_modules/env-pkg/index.js @@ -0,0 +1,7 @@ +"use strict"; +// Read once, when the module evaluates. +const mode = process.env.SCRIPTC_NI7B_MODE || "default"; +function currentMode() { + return mode; +} +module.exports = { currentMode }; diff --git a/tests/corpus/6844-node-island-deferred-env/node_modules/env-pkg/package.json b/tests/corpus/6844-node-island-deferred-env/node_modules/env-pkg/package.json new file mode 100644 index 000000000..05c1ee1d1 --- /dev/null +++ b/tests/corpus/6844-node-island-deferred-env/node_modules/env-pkg/package.json @@ -0,0 +1 @@ +{ "name": "env-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6844-node-island-deferred-env/package.json b/tests/corpus/6844-node-island-deferred-env/package.json new file mode 100644 index 000000000..d6751fc83 --- /dev/null +++ b/tests/corpus/6844-node-island-deferred-env/package.json @@ -0,0 +1 @@ +{ "name": "island-deferred-env", "private": true, "type": "module", "scriptc": { "packages": { "env-pkg": "node" } } } diff --git a/tests/corpus/6845-node-island-deferred-require/main.ts b/tests/corpus/6845-node-island-deferred-require/main.ts new file mode 100644 index 000000000..9a185ada9 --- /dev/null +++ b/tests/corpus/6845-node-island-deferred-require/main.ts @@ -0,0 +1,16 @@ +// @requires-libnode +// A native package's top-level require() of a Node island package is +// deferred like a static import (scope-B NI-7d): the island's load (which +// sets and clears a timer, leaving nothing behind) waits for the first +// call, and Node does not start at boot. +import { existsSync, readFileSync } from "node:fs"; +import { calls, quadruple } from "native-wrapper"; + +function libnodeMapped(): boolean { + if (!existsSync("/proc/self/maps")) return false; + return readFileSync("/proc/self/maps", "utf8").includes("libnode"); +} + +console.log("before first use, libnode mapped:", libnodeMapped()); +console.log("quadruple", quadruple(3), quadruple(5)); +console.log("calls", calls()); diff --git a/tests/corpus/6845-node-island-deferred-require/node_modules/native-wrapper/index.d.ts b/tests/corpus/6845-node-island-deferred-require/node_modules/native-wrapper/index.d.ts new file mode 100644 index 000000000..d2079967c --- /dev/null +++ b/tests/corpus/6845-node-island-deferred-require/node_modules/native-wrapper/index.d.ts @@ -0,0 +1,2 @@ +export declare function quadruple(n: number): number; +export declare function calls(): number; diff --git a/tests/corpus/6845-node-island-deferred-require/node_modules/native-wrapper/index.js b/tests/corpus/6845-node-island-deferred-require/node_modules/native-wrapper/index.js new file mode 100644 index 000000000..21c71d3d0 --- /dev/null +++ b/tests/corpus/6845-node-island-deferred-require/node_modules/native-wrapper/index.js @@ -0,0 +1,14 @@ +"use strict"; +// A native package that requires an island at its top level, as +// async-retry requires retry. +const timers = require("timer-pkg"); + +function quadruple(n) { + return timers.twice(timers.twice(n)); +} + +function calls() { + return timers.count(); +} + +module.exports = { quadruple, calls }; diff --git a/tests/corpus/6845-node-island-deferred-require/node_modules/native-wrapper/package.json b/tests/corpus/6845-node-island-deferred-require/node_modules/native-wrapper/package.json new file mode 100644 index 000000000..9100150e9 --- /dev/null +++ b/tests/corpus/6845-node-island-deferred-require/node_modules/native-wrapper/package.json @@ -0,0 +1 @@ +{ "name": "native-wrapper", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6845-node-island-deferred-require/node_modules/timer-pkg/index.d.ts b/tests/corpus/6845-node-island-deferred-require/node_modules/timer-pkg/index.d.ts new file mode 100644 index 000000000..c762be5bd --- /dev/null +++ b/tests/corpus/6845-node-island-deferred-require/node_modules/timer-pkg/index.d.ts @@ -0,0 +1,2 @@ +export declare function twice(n: number): number; +export declare function count(): number; diff --git a/tests/corpus/6845-node-island-deferred-require/node_modules/timer-pkg/index.js b/tests/corpus/6845-node-island-deferred-require/node_modules/timer-pkg/index.js new file mode 100644 index 000000000..88e279826 --- /dev/null +++ b/tests/corpus/6845-node-island-deferred-require/node_modules/timer-pkg/index.js @@ -0,0 +1,18 @@ +"use strict"; +// Load-time timer housekeeping that leaves nothing behind: a clear of +// nothing, and a timer set and cleared again (smol-toml-like). +clearTimeout(undefined); +const probe = setTimeout(() => { + throw new Error("never runs"); +}, 1000); +clearTimeout(probe); + +let calls = 0; +function twice(n) { + calls += 1; + return n * 2; +} +function count() { + return calls; +} +module.exports = { twice, count }; diff --git a/tests/corpus/6845-node-island-deferred-require/node_modules/timer-pkg/package.json b/tests/corpus/6845-node-island-deferred-require/node_modules/timer-pkg/package.json new file mode 100644 index 000000000..6aacd04f5 --- /dev/null +++ b/tests/corpus/6845-node-island-deferred-require/node_modules/timer-pkg/package.json @@ -0,0 +1 @@ +{ "name": "timer-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6845-node-island-deferred-require/package.json b/tests/corpus/6845-node-island-deferred-require/package.json new file mode 100644 index 000000000..b7bc84af0 --- /dev/null +++ b/tests/corpus/6845-node-island-deferred-require/package.json @@ -0,0 +1 @@ +{ "name": "island-deferred-require", "private": true, "type": "module", "scriptc": { "packages": { "timer-pkg": "node" } } } diff --git a/tests/corpus/6846-node-island-deferred-own-files/main.ts b/tests/corpus/6846-node-island-deferred-own-files/main.ts new file mode 100644 index 000000000..831a639e7 --- /dev/null +++ b/tests/corpus/6846-node-island-deferred-own-files/main.ts @@ -0,0 +1,14 @@ +// @requires-libnode +// A Node island package that reads its own files when it loads (scope-B +// NI-7e): reads inside its package are guarded inputs, so the load defers +// and the first call sees the same contents. +import { existsSync, readFileSync } from "node:fs"; +import { greet } from "data-pkg"; + +function libnodeMapped(): boolean { + if (!existsSync("/proc/self/maps")) return false; + return readFileSync("/proc/self/maps", "utf8").includes("libnode"); +} + +console.log("before first use, libnode mapped:", libnodeMapped()); +console.log(greet("native")); diff --git a/tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/data.txt b/tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/data.txt new file mode 100644 index 000000000..b888317a3 --- /dev/null +++ b/tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/data.txt @@ -0,0 +1 @@ +hello from data diff --git a/tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/index.d.ts b/tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/index.d.ts new file mode 100644 index 000000000..9141f57ce --- /dev/null +++ b/tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/index.d.ts @@ -0,0 +1 @@ +export declare function greet(who: string): string; diff --git a/tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/index.js b/tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/index.js new file mode 100644 index 000000000..729dd7938 --- /dev/null +++ b/tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/index.js @@ -0,0 +1,14 @@ +"use strict"; +// Reads its own package's files at load: guarded inputs (the build records +// their size and mtime as boot guards), so the load still defers. +const fs = require("fs"); +const path = require("path"); + +const dataFile = path.join(__dirname, "data.txt"); +const greeting = fs.existsSync(dataFile) ? fs.readFileSync(dataFile, "utf8").trim() : "missing"; +const version = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")).version; + +function greet(who) { + return `${greeting}, ${who} (v${version})`; +} +module.exports = { greet }; diff --git a/tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/package.json b/tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/package.json new file mode 100644 index 000000000..c08fc59d0 --- /dev/null +++ b/tests/corpus/6846-node-island-deferred-own-files/node_modules/data-pkg/package.json @@ -0,0 +1 @@ +{ "name": "data-pkg", "version": "2.3.4", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6846-node-island-deferred-own-files/package.json b/tests/corpus/6846-node-island-deferred-own-files/package.json new file mode 100644 index 000000000..cbd8a4f67 --- /dev/null +++ b/tests/corpus/6846-node-island-deferred-own-files/package.json @@ -0,0 +1 @@ +{ "name": "island-deferred-own-files", "private": true, "type": "module", "scriptc": { "packages": { "data-pkg": "node" } } } diff --git a/tests/corpus/6847-node-island-punycode-warning/legacy.cjs b/tests/corpus/6847-node-island-punycode-warning/legacy.cjs new file mode 100644 index 000000000..c8aef4746 --- /dev/null +++ b/tests/corpus/6847-node-island-punycode-warning/legacy.cjs @@ -0,0 +1,4 @@ +"use strict"; +// Island code outside node_modules that loads punycode on first use: Node +// prints its DEP0040 deprecation warning at that call. +exports.toAscii = (domain) => require("punycode").toASCII(domain); diff --git a/tests/corpus/6847-node-island-punycode-warning/main.ts b/tests/corpus/6847-node-island-punycode-warning/main.ts new file mode 100644 index 000000000..093ee4e2d --- /dev/null +++ b/tests/corpus/6847-node-island-punycode-warning/main.ts @@ -0,0 +1,18 @@ +// @requires-libnode +// @normalize-pid +// Node's deprecation warnings from island code (scope-B NI-7f): +// - punycode required first by island code outside node_modules: DEP0040 +// on stderr, with Node's text, at that call; +// - punycode required by an island package (inside node_modules, as +// node-fetch's whatwg-url): no warning, in Node as here; +// - a package that warns when it loads: not deferred (the probe records +// the warning), so it warns at its import position. +import { requireModule } from "@scriptc/node"; +import { toAscii } from "puny-pkg"; +import { ok } from "warn-pkg"; + +console.log("start", ok()); +const legacy = requireModule("./legacy.cjs"); +const ascii: string = legacy.toAscii("bücher.de"); +console.log("legacy", ascii); +console.log("package", toAscii("mañana.com")); diff --git a/tests/corpus/6847-node-island-punycode-warning/node_modules/puny-pkg/index.d.ts b/tests/corpus/6847-node-island-punycode-warning/node_modules/puny-pkg/index.d.ts new file mode 100644 index 000000000..eeb9a16c7 --- /dev/null +++ b/tests/corpus/6847-node-island-punycode-warning/node_modules/puny-pkg/index.d.ts @@ -0,0 +1 @@ +export declare function toAscii(domain: string): string; diff --git a/tests/corpus/6847-node-island-punycode-warning/node_modules/puny-pkg/index.js b/tests/corpus/6847-node-island-punycode-warning/node_modules/puny-pkg/index.js new file mode 100644 index 000000000..61174ecf1 --- /dev/null +++ b/tests/corpus/6847-node-island-punycode-warning/node_modules/puny-pkg/index.js @@ -0,0 +1,8 @@ +"use strict"; +// An island package that uses the deprecated punycode builtin, as +// node-fetch's whatwg-url does (here on first use). A require from inside +// node_modules does not warn. +function toAscii(domain) { + return require("punycode").toASCII(domain); +} +module.exports = { toAscii }; diff --git a/tests/corpus/6847-node-island-punycode-warning/node_modules/puny-pkg/package.json b/tests/corpus/6847-node-island-punycode-warning/node_modules/puny-pkg/package.json new file mode 100644 index 000000000..3146d3ea6 --- /dev/null +++ b/tests/corpus/6847-node-island-punycode-warning/node_modules/puny-pkg/package.json @@ -0,0 +1 @@ +{ "name": "puny-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6847-node-island-punycode-warning/node_modules/warn-pkg/index.d.ts b/tests/corpus/6847-node-island-punycode-warning/node_modules/warn-pkg/index.d.ts new file mode 100644 index 000000000..5ac877b56 --- /dev/null +++ b/tests/corpus/6847-node-island-punycode-warning/node_modules/warn-pkg/index.d.ts @@ -0,0 +1 @@ +export declare function ok(): string; diff --git a/tests/corpus/6847-node-island-punycode-warning/node_modules/warn-pkg/index.js b/tests/corpus/6847-node-island-punycode-warning/node_modules/warn-pkg/index.js new file mode 100644 index 000000000..f65bb862c --- /dev/null +++ b/tests/corpus/6847-node-island-punycode-warning/node_modules/warn-pkg/index.js @@ -0,0 +1,6 @@ +"use strict"; +// An island package that emits a deprecation warning when it loads: the +// build's probe records the warning, so the package is not deferred and the +// warning appears where Node prints it. +process.emitWarning("warn-pkg's default export is deprecated", "DeprecationWarning", "DEP_WARN_PKG"); +exports.ok = () => "warn-pkg ok"; diff --git a/tests/corpus/6847-node-island-punycode-warning/node_modules/warn-pkg/package.json b/tests/corpus/6847-node-island-punycode-warning/node_modules/warn-pkg/package.json new file mode 100644 index 000000000..8c7e6f0e4 --- /dev/null +++ b/tests/corpus/6847-node-island-punycode-warning/node_modules/warn-pkg/package.json @@ -0,0 +1 @@ +{ "name": "warn-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6847-node-island-punycode-warning/package.json b/tests/corpus/6847-node-island-punycode-warning/package.json new file mode 100644 index 000000000..31d5492e0 --- /dev/null +++ b/tests/corpus/6847-node-island-punycode-warning/package.json @@ -0,0 +1 @@ +{ "name": "island-punycode-warning", "private": true, "type": "module", "scriptc": { "packages": { "puny-pkg": "node", "warn-pkg": "node" } } } diff --git a/tests/harness/differential.test.ts b/tests/harness/differential.test.ts index f4c5c6202..00989972b 100644 --- a/tests/harness/differential.test.ts +++ b/tests/harness/differential.test.ts @@ -156,6 +156,18 @@ function wantsNoDeprecation(file: string): boolean { return directiveHead(file).some((l) => /^\/\/ @no-deprecation\s*$/.test(l)); } +/** `// @normalize-pid` in the directive head: Node's process warnings + * ("(node:) [DEP0040] ...") are compared with the pid replaced on both + * sides — for island programs, where Node itself prints the warning in the + * compiled program's process. */ +function wantsPidNormalized(file: string): boolean { + return directiveHead(file).some((l) => /^\/\/ @normalize-pid\s*$/.test(l)); +} + +function withoutWarningPid(stderr: Buffer): Buffer { + return Buffer.from(stderr.toString("utf8").replace(/^\(node:\d+\) /gm, "(node:PID) "), "utf8"); +} + /** The Node oracle's argv for a corpus entry (shims + the per-program * transform flag). */ /** `// @disallow-code-generation` in the directive head: the Node oracle @@ -426,9 +438,13 @@ describe(`differential corpus (${files.length} programs${sanitize ? ", sanitized // — their stderr carries the uncaught report, whose format is a // documented divergence. if (expectedExit === 0) { - const nativeErr = comparableStderr(nativeRes.stderr); - if (!nodeRes.stderr.equals(nativeErr)) { - expect(nativeErr.toString("utf8")).toBe(nodeRes.stderr.toString("utf8")); + const pidFree = wantsPidNormalized(file); + const nativeErr = pidFree + ? withoutWarningPid(comparableStderr(nativeRes.stderr)) + : comparableStderr(nativeRes.stderr); + const nodeErr = pidFree ? withoutWarningPid(nodeRes.stderr) : nodeRes.stderr; + if (!nodeErr.equals(nativeErr)) { + expect(nativeErr.toString("utf8")).toBe(nodeErr.toString("utf8")); expect.unreachable("stderr differed at byte level but not after utf8 decode"); } } diff --git a/tests/harness/llvm-differential.test.ts b/tests/harness/llvm-differential.test.ts index 8b266c083..f5cd3a170 100644 --- a/tests/harness/llvm-differential.test.ts +++ b/tests/harness/llvm-differential.test.ts @@ -146,6 +146,13 @@ function nodeOracleFile(file: string): string { return path; } +/** `// @normalize-pid` (differential.test.ts): Node's process warnings + * compare with the pid replaced on both sides. */ +function pidFree(file: string, stderr: Buffer): Buffer { + if (!directiveHead(file).some((l) => /^\/\/ @normalize-pid\s*$/.test(l))) return stderr; + return Buffer.from(stderr.toString("utf8").replace(/^\(node:\d+\) /gm, "(node:PID) "), "utf8"); +} + function wantsNoDeprecation(file: string): boolean { return directiveHead(file).some((l) => /^\/\/ @no-deprecation\s*$/.test(l)); } @@ -213,7 +220,9 @@ describe(`llvm differential corpus (${files.length} programs, ${optimizationMode const actual = await runBinary(result.binaryPath, []); expect(actual.stdout, `${optimization} stdout`).toEqual(oracle.stdout); if (expectedExit === 0) - expect(comparableStderr(actual.stderr), `${optimization} stderr`).toEqual(oracle.stderr); + expect(pidFree(file, comparableStderr(actual.stderr)), `${optimization} stderr`).toEqual( + pidFree(file, oracle.stderr), + ); expect(actual.exitCode, `${optimization} exit status`).toBe(expectedExit); } }, From 4c9cfc1cc40104d7cca147e0f3e5a96898ff74fb Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Mon, 26 Oct 2026 12:19:32 -0700 Subject: [PATCH 02/70] Run the deferral test's terminal case under macOS's script(1) BSD script takes the command after the transcript file, needs a stdin that is not a socket, and echoes the EOF it forwards from /dev/null. Co-Authored-By: Claude Opus 5.5 --- tests/harness/node-island-deferral.test.ts | 28 +++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tests/harness/node-island-deferral.test.ts b/tests/harness/node-island-deferral.test.ts index 05f8825a1..8b66bf3d1 100644 --- a/tests/harness/node-island-deferral.test.ts +++ b/tests/harness/node-island-deferral.test.ts @@ -5,7 +5,7 @@ * that is a pipe at build time and a terminal when the program runs. The * fixtures are in tests/fixtures/node-island/deferral. Needs a libnode pack * in SCRIPTC_LIBNODE (libnode.ts). */ -import { execFile } from "node:child_process"; +import { execFile, spawn } from "node:child_process"; import { mkdirSync, rmSync } from "node:fs"; import { join } from "node:path"; import { promisify } from "node:util"; @@ -13,6 +13,25 @@ import { describe, expect, test } from "vitest"; import { libnodePack } from "./libnode.js"; const execFileAsync = promisify(execFile); + +/** stdout of `binary` run under a pseudo-terminal by script(1). util-linux's + * takes the command with -c. The BSD one on macOS takes it after the + * transcript file, needs a stdin that is not a socket, and echoes the EOF + * it forwards from /dev/null as ^D and two backspaces. */ +async function underTerminal(binary: string, env: NodeJS.ProcessEnv): Promise { + if (process.platform !== "darwin") + return (await execFileAsync("script", ["-qec", binary, "/dev/null"], { env, encoding: "utf8" })) + .stdout; + const child = spawn("script", ["-q", "/dev/null", binary], { + env, + stdio: ["ignore", "pipe", "inherit"], + }); + let stdout = ""; + child.stdout.setEncoding("utf8").on("data", (chunk: string) => (stdout += chunk)); + const code = await new Promise((resolve) => child.on("close", resolve)); + expect(code).toBe(0); + return stdout.replace(/\^D\x08\x08/g, ""); +} const repoRoot = join(import.meta.dirname, "../.."); const cli = join(repoRoot, "packages/cli/dist/main.js"); const fixtures = join(repoRoot, "tests/fixtures/node-island/deferral"); @@ -71,10 +90,7 @@ describe.skipIf(skip)("deferred Node island modules", () => { const { binary } = await build("tty"); expect(await run(binary)).toBe("color false\n"); // Under a pseudo-terminal the module sees a terminal, as under Node. - const { stdout } = await execFileAsync("script", ["-qec", binary, "/dev/null"], { - env: { ...process.env, XDG_CACHE_HOME: join(outDir, "cache") }, - encoding: "utf8", - }); - expect(stdout.replace(/\r/g, "")).toBe("color true\n"); + const env = { ...process.env, XDG_CACHE_HOME: join(outDir, "cache") }; + expect((await underTerminal(binary, env)).replace(/\r/g, "")).toBe("color true\n"); }, 300_000); }); From b3decaa4cd6780b2f49ffa864d075122458fa2d0 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Mon, 26 Oct 2026 14:07:49 -0700 Subject: [PATCH 03/70] Strip script(1)'s EOF echo without a control-character regex Co-Authored-By: Claude Opus 5.5 --- tests/harness/node-island-deferral.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/harness/node-island-deferral.test.ts b/tests/harness/node-island-deferral.test.ts index 8b66bf3d1..c17b847fc 100644 --- a/tests/harness/node-island-deferral.test.ts +++ b/tests/harness/node-island-deferral.test.ts @@ -30,7 +30,7 @@ async function underTerminal(binary: string, env: NodeJS.ProcessEnv): Promise (stdout += chunk)); const code = await new Promise((resolve) => child.on("close", resolve)); expect(code).toBe(0); - return stdout.replace(/\^D\x08\x08/g, ""); + return stdout.replaceAll("^D\b\b", ""); } const repoRoot = join(import.meta.dirname, "../.."); const cli = join(repoRoot, "packages/cli/dist/main.js"); From d06155048e2a8e221b374391f8a9f23f37c50ae2 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 27 Oct 2026 19:06:41 -0700 Subject: [PATCH 04/70] Only bare binding getters are pure; compare getter exports for mutation A member read in an export getter (a.b or a[b]) can invoke another accessor that runs code on every read, so the recorder now accepts only a bare identifier as a plain binding getter. Everything else keeps its runtime read and is not snapshotted. The mutation check that makes an earlier island module eager copied only data properties of a module value, so an object exported through a binding getter could stay stale when a later load changed it. The shape now reads binding getters the same way the snapshot does, and compares them by reading again, so those modules no longer defer. Corpus fixtures pin both: a getter that reads a member counts every read, and a later load that mutates a getter-exported object keeps both modules eager. Co-Authored-By: Claude Opus 5.5 --- .../src/node-islands/probe-recorder.ts | 36 ++++++++++++------- .../deferral/getter-member/main.ts | 3 ++ .../node_modules/counter-pkg/index.d.ts | 2 ++ .../node_modules/counter-pkg/index.js | 7 ++++ .../node_modules/counter-pkg/package.json | 1 + .../deferral/getter-member/package.json | 1 + .../deferral/mutate-getter/main.ts | 4 +++ .../node_modules/pkg-a/index.d.ts | 1 + .../mutate-getter/node_modules/pkg-a/index.js | 4 +++ .../node_modules/pkg-a/package.json | 1 + .../node_modules/pkg-b/index.d.ts | 1 + .../mutate-getter/node_modules/pkg-b/index.js | 4 +++ .../node_modules/pkg-b/package.json | 1 + .../deferral/mutate-getter/package.json | 1 + tests/harness/node-island-deferral.test.ts | 16 +++++++++ 15 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 tests/fixtures/node-island/deferral/getter-member/main.ts create mode 100644 tests/fixtures/node-island/deferral/getter-member/node_modules/counter-pkg/index.d.ts create mode 100644 tests/fixtures/node-island/deferral/getter-member/node_modules/counter-pkg/index.js create mode 100644 tests/fixtures/node-island/deferral/getter-member/node_modules/counter-pkg/package.json create mode 100644 tests/fixtures/node-island/deferral/getter-member/package.json create mode 100644 tests/fixtures/node-island/deferral/mutate-getter/main.ts create mode 100644 tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-a/index.d.ts create mode 100644 tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-a/index.js create mode 100644 tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-a/package.json create mode 100644 tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-b/index.d.ts create mode 100644 tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-b/index.js create mode 100644 tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-b/package.json create mode 100644 tests/fixtures/node-island/deferral/mutate-getter/package.json diff --git a/packages/compiler/src/node-islands/probe-recorder.ts b/packages/compiler/src/node-islands/probe-recorder.ts index 1299c183f..499de3593 100644 --- a/packages/compiler/src/node-islands/probe-recorder.ts +++ b/packages/compiler/src/node-islands/probe-recorder.ts @@ -80,9 +80,11 @@ function internalCaller() { const fnToString = Function.prototype.toString; const hasOwn = Object.prototype.hasOwnProperty; // A getter that only reads a binding (esbuild's and tsc's export getters): -// reading it again reads the same binding. Any other getter can run code -// on every read, so its member is not snapshotted. -const BINDING_GETTER = /^(?:\(\)\s*=>\s*[\w$.]+|function\s*[\w$]*\s*\(\)\s*\{\s*return\s+[\w$.]+(?:\[[\w$]+\])?\s*;?\s*\}|get\s+[\w$]+\s*\(\)\s*\{\s*return\s+[\w$.]+\s*;?\s*\})$/; +// reading it again reads the same binding. A member read ('a.b', 'a[b]') +// can invoke another accessor that runs code on every read, so only a bare +// identifier qualifies. Any other getter can also run code on every read, +// so its member is not snapshotted. +const BINDING_GETTER = /^(?:\(\)\s*=>\s*[A-Za-z_$][\w$]*|function\s*[\w$]*\s*\(\)\s*\{\s*return\s+[A-Za-z_$][\w$]*\s*;?\s*\}|get\s+[\w$]+\s*\(\)\s*\{\s*return\s+[A-Za-z_$][\w$]*\s*;?\s*\})$/; function bindingGetter(fn) { let source = ""; try { source = fnToString.call(fn); } catch { return false; } @@ -463,20 +465,31 @@ exports.probe = function probe(input) { return [name, m]; }); + // What a member of a module value reads now: a data property's value, or + // a binding getter's (esbuild's and tsc's export getters). Any other + // accessor can run code, so it is not part of a shape. + const MISSING = {}; + const memberValue = (o, k) => { + const d = Object.getOwnPropertyDescriptor(o, k); + if (d === undefined) return MISSING; + if ("value" in d) return d.value; + if (typeof d.get !== "function" || !bindingGetter(d.get)) return MISSING; + try { return d.get.call(o); } catch { return MISSING; } + }; + // A module's exported values right after its load, at the depth the // snapshot covers (the module value's members, and theirs for objects). const exportShape = (v) => { const shape = []; if (v === null || (typeof v !== "object" && typeof v !== "function")) return shape; for (const k of Reflect.ownKeys(v)) { - const d = Object.getOwnPropertyDescriptor(v, k); - if (d === undefined || !("value" in d)) continue; - shape.push([v, k, d.value]); - const m = d.value; + const m = memberValue(v, k); + if (m === MISSING) continue; + shape.push([v, k, m]); if (m !== null && typeof m === "object") for (const k2 of Reflect.ownKeys(m)) { - const d2 = Object.getOwnPropertyDescriptor(m, k2); - if (d2 !== undefined && "value" in d2) shape.push([m, k2, d2.value]); + const m2 = memberValue(m, k2); + if (m2 !== MISSING) shape.push([m, k2, m2]); } } return shape; @@ -537,10 +550,7 @@ exports.probe = function probe(input) { // earlier snapshot no longer holds, and this load's change has to happen // where the program imports it). for (const [earlier, shape] of shapes) { - const changed = shape.some(([o, k, v]) => { - const d = Object.getOwnPropertyDescriptor(o, k); - return d === undefined || !("value" in d) || !Object.is(d.value, v); - }); + const changed = shape.some(([o, k, v]) => !Object.is(memberValue(o, k), v)); if (!changed) continue; delete earlier.root; earlier.reason = "a later island module's load changes its exports"; diff --git a/tests/fixtures/node-island/deferral/getter-member/main.ts b/tests/fixtures/node-island/deferral/getter-member/main.ts new file mode 100644 index 000000000..96223673c --- /dev/null +++ b/tests/fixtures/node-island/deferral/getter-member/main.ts @@ -0,0 +1,3 @@ +import counter from "counter-pkg"; + +console.log(counter.label, counter.count, counter.count); diff --git a/tests/fixtures/node-island/deferral/getter-member/node_modules/counter-pkg/index.d.ts b/tests/fixtures/node-island/deferral/getter-member/node_modules/counter-pkg/index.d.ts new file mode 100644 index 000000000..82afbe16d --- /dev/null +++ b/tests/fixtures/node-island/deferral/getter-member/node_modules/counter-pkg/index.d.ts @@ -0,0 +1,2 @@ +declare const counter: { readonly count: number; readonly label: string }; +export default counter; diff --git a/tests/fixtures/node-island/deferral/getter-member/node_modules/counter-pkg/index.js b/tests/fixtures/node-island/deferral/getter-member/node_modules/counter-pkg/index.js new file mode 100644 index 000000000..8ad752913 --- /dev/null +++ b/tests/fixtures/node-island/deferral/getter-member/node_modules/counter-pkg/index.js @@ -0,0 +1,7 @@ +"use strict"; +let reads = 0; +// The export getter reads a member, not a binding: that member's getter +// counts the read, so it is not a plain export getter. +const state = { get count() { return ++reads; } }; +exports.label = "counter"; +Object.defineProperty(exports, "count", { enumerable: true, get: () => state.count }); diff --git a/tests/fixtures/node-island/deferral/getter-member/node_modules/counter-pkg/package.json b/tests/fixtures/node-island/deferral/getter-member/node_modules/counter-pkg/package.json new file mode 100644 index 000000000..50ce07f31 --- /dev/null +++ b/tests/fixtures/node-island/deferral/getter-member/node_modules/counter-pkg/package.json @@ -0,0 +1 @@ +{ "name": "counter-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/node-island/deferral/getter-member/package.json b/tests/fixtures/node-island/deferral/getter-member/package.json new file mode 100644 index 000000000..6d5d7c46b --- /dev/null +++ b/tests/fixtures/node-island/deferral/getter-member/package.json @@ -0,0 +1 @@ +{ "name": "deferral-getter-member", "private": true, "type": "module", "scriptc": { "packages": { "counter-pkg": "node" } } } diff --git a/tests/fixtures/node-island/deferral/mutate-getter/main.ts b/tests/fixtures/node-island/deferral/mutate-getter/main.ts new file mode 100644 index 000000000..f7dbae8e2 --- /dev/null +++ b/tests/fixtures/node-island/deferral/mutate-getter/main.ts @@ -0,0 +1,4 @@ +import { config } from "pkg-a"; +import { ok } from "pkg-b"; + +console.log(config.mode, ok); diff --git a/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-a/index.d.ts b/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-a/index.d.ts new file mode 100644 index 000000000..8b403d629 --- /dev/null +++ b/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-a/index.d.ts @@ -0,0 +1 @@ +export declare const config: { mode: string }; diff --git a/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-a/index.js b/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-a/index.js new file mode 100644 index 000000000..c46d72b3a --- /dev/null +++ b/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-a/index.js @@ -0,0 +1,4 @@ +"use strict"; +// pkg-a exports through a binding getter over its own object. +const config = { mode: "initial" }; +Object.defineProperty(exports, "config", { enumerable: true, get: () => config }); diff --git a/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-a/package.json b/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-a/package.json new file mode 100644 index 000000000..e0270f674 --- /dev/null +++ b/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-a/package.json @@ -0,0 +1 @@ +{ "name": "pkg-a", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-b/index.d.ts b/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-b/index.d.ts new file mode 100644 index 000000000..f29ace298 --- /dev/null +++ b/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-b/index.d.ts @@ -0,0 +1 @@ +export declare const ok: boolean; diff --git a/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-b/index.js b/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-b/index.js new file mode 100644 index 000000000..749c51d3c --- /dev/null +++ b/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-b/index.js @@ -0,0 +1,4 @@ +"use strict"; +// Its load changes what pkg-a exports, through pkg-a's binding getter. +require("pkg-a").config.mode = "set by pkg-b"; +exports.ok = true; diff --git a/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-b/package.json b/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-b/package.json new file mode 100644 index 000000000..ea2784527 --- /dev/null +++ b/tests/fixtures/node-island/deferral/mutate-getter/node_modules/pkg-b/package.json @@ -0,0 +1 @@ +{ "name": "pkg-b", "version": "1.0.0", "main": "index.js", "types": "index.d.ts", "dependencies": { "pkg-a": "1.0.0" } } diff --git a/tests/fixtures/node-island/deferral/mutate-getter/package.json b/tests/fixtures/node-island/deferral/mutate-getter/package.json new file mode 100644 index 000000000..0416d97bc --- /dev/null +++ b/tests/fixtures/node-island/deferral/mutate-getter/package.json @@ -0,0 +1 @@ +{ "name": "deferral-mutate-getter", "private": true, "type": "module", "scriptc": { "packages": { "pkg-a": "node", "pkg-b": { "run": "node", "evaluate": "lazy" } } } } diff --git a/tests/harness/node-island-deferral.test.ts b/tests/harness/node-island-deferral.test.ts index c17b847fc..93dce44cb 100644 --- a/tests/harness/node-island-deferral.test.ts +++ b/tests/harness/node-island-deferral.test.ts @@ -75,11 +75,27 @@ describe.skipIf(skip)("deferred Node island modules", () => { expect(await run(binary)).toBe("set by pkg-b true\n"); }, 300_000); + test("a load that changes an object exported through a getter defers neither", async () => { + const { binary, trace } = await build("mutate-getter"); + expect(trace.some((l) => /eager .*pkg-a.*a later island module's load changes/.test(l))).toBe( + true, + ); + expect( + trace.some((l) => /eager .*pkg-b.*changes another island module's exports/.test(l)), + ).toBe(true); + expect(await run(binary)).toBe("set by pkg-b true\n"); + }, 300_000); + test("an export getter that runs code runs on every read", async () => { const { binary } = await build("getter"); expect(await run(binary)).toBe("counter 1 2\n"); }, 300_000); + test("an export getter that reads a member runs on every read", async () => { + const { binary } = await build("getter-member"); + expect(await run(binary)).toBe("counter 1 2\n"); + }, 300_000); + test("a package file missing when the build probes keeps the module eager", async () => { const { binary, trace } = await build("missing-file"); expect(trace.some((l) => /eager .*probe-pkg.*existsSync/.test(l))).toBe(true); From 7a7fd8ce1db5a4e1f617b9d1d0d2622c8208813e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 28 Oct 2026 19:45:36 -0500 Subject: [PATCH 05/70] Match Node's node:path behavior from Node 24's path tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit path.posix, path.win32 and the bare path module are one module object each, the handle process.getBuiltinModule answers, so require('path/posix'), path.posix and the target platform's bare path compare as Node's do, a namespace beside other values in one slot is untyped, and path[family] reads that object. Functions read through them take checked arguments: each string parameter passes Node's validateString under its own name (basename's suffix first), resolve validates paths[i] only as far as its last-first walk reads them, parse answers an untyped { root, dir, base, ext, name }, format follows _format over any object, and toNamespacedPath answers a non-string as it is. A JavaScript call whose arguments are not strings, or omit one, goes through the same checks. Typed slots keep the typed function values. path.win32.relative lowercases both resolved paths with String.prototype.toLowerCase and walks them by UTF-16 unit, comparing segments instead when lowercasing changed a length, as Node does. It folded ASCII only, so a path with İ or Ⱥ in a different case gave a longer relative path. The function moves into its own runtime unit, scr_path_win32_relative.c, which links with the regex unit whose case mapping it uses. scr_path.c stays free of it, so library and cross-target builds that link the path functions without the regex unit still link. A JavaScript binding whose inferred type holds a union of functions and arrays (a test table of [fn, cases] rows, or one destructured row) takes the checked-dynamic kind that dynFallbackType gives the unmappable spellings of such tables. Its rows keep their functions' names, and for-of, forEach and destructuring read the items as untyped values. TypeScript bindings keep their typed layout. --- .../limitations/builtin-function-values.mdx | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 6 + .../compiler/src/backend/native/executable.ts | 8 +- .../compiler/src/backend/native/library.ts | 2 +- .../lowering/builtins/module-calls.ts | 5 + .../frontend/lowering/expressions/binary.ts | 21 +- .../lowering/expressions/object-literals.ts | 4 +- .../src/frontend/lowering/lower-assert.ts | 14 +- .../frontend/lowering/lower-builtin-values.ts | 197 +++++++++++- .../src/frontend/lowering/lower-calls.ts | 19 +- .../compiler/src/frontend/lowering/lowerer.ts | 69 ++++- .../lowering/module-namespace-elements.ts | 14 + .../src/frontend/lowering/strict-equality.ts | 16 + .../src/frontend/lowering/surfaces.ts | 111 +++++-- packages/compiler/src/frontend/type-mapper.ts | 12 + packages/compiler/src/ir/builtin-effects.ts | 5 + .../compiler/src/ir/builtin-signatures.ts | 8 + packages/compiler/src/ir/runtime-features.ts | 9 + .../test/process-builtin-values.test.ts | 4 +- .../corpus/js-function-array-tables.cjs.json | 6 + .../corpus/path-function-values.cjs.json | 6 + .../tests/corpus/path-untyped-calls.cjs.json | 6 + .../corpus/path-win32-relative-case.cjs.json | 6 + .../runtime-pack-common/library-matrix.mjs | 1 + .../runtime-pack-matrix.mjs | 2 + packages/runtime/src/scr_builtin_values.c | 283 ++++++++++++++++++ packages/runtime/src/scr_dyn.h | 6 + packages/runtime/src/scr_path.c | 115 +------ packages/runtime/src/scr_path.h | 14 + .../runtime/src/scr_path_win32_relative.c | 169 +++++++++++ packages/runtime/test/path.test.ts | 10 + tests/corpus/js-function-array-tables.cjs | 24 ++ tests/corpus/path-function-values.cjs | 60 ++++ tests/corpus/path-untyped-calls.cjs | 39 +++ tests/corpus/path-win32-relative-case.cjs | 22 ++ 35 files changed, 1123 insertions(+), 171 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-function-array-tables.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-function-values.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-untyped-calls.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-win32-relative-case.cjs.json create mode 100644 packages/runtime/src/scr_path_win32_relative.c create mode 100644 tests/corpus/js-function-array-tables.cjs create mode 100644 tests/corpus/path-function-values.cjs create mode 100644 tests/corpus/path-untyped-calls.cjs create mode 100644 tests/corpus/path-win32-relative-case.cjs diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index 96c48375c..d650eef3f 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -1,6 +1,7 @@ Some builtins can be stored and passed as functions: - Lowered `node:path` functions across the bare, POSIX, and win32 modules, including optional `basename` and variadic `join`/`resolve`. + Called through an untyped value, they validate their arguments with Node's `ERR_INVALID_ARG_TYPE`. - Supported zero-argument `node:os` functions and `querystring.escape`/`unescape`. - `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, `toUSVString`, `inherits`, `deprecate`, `callbackify`, `debuglog`, and `debug`. - `fs.existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, and `closeSync`, and `fs/promises.unlink`, `chmod`, and `rename`. diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 02479a922..00b72b071 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -440,6 +440,12 @@ export const LIB_FN_SYMS: Record = { "fsp.globError": "scr_fsp_glob_error", "fs.realpathSync": "scr_fs_realpath", "fs.realpathValue": "scr_fs_realpath_value", + "dyn.validateString": "scr_value_string", + "dyn.validateStrings": "scr_value_strings", + "path.resolveValue": "scr_path_resolve_value", + "path.parseValue": "scr_path_parse_value", + "path.formatValue": "scr_path_format_value", + "path.toNamespacedValue": "scr_path_to_namespaced_value", "fs.realpathNativeSync": "scr_fs_realpath_promise", "fs.unlinkSync": "scr_fs_unlink", "fs.chmodSync": "scr_fs_chmod", diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 3515bc387..80e72e0c4 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -169,9 +169,10 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ // SCR_MUSL flag and this target-selected TU travel together. unit((u) => isMuslTarget(u.driver), "scr_musl.c"), // scr_glob.c (path.matchesGlob) compiles its segments through the - // regex unit and scr_idna.c (node:url's IDNA) normalizes through it, - // so both ride the same switch; section GC drops them from binaries - // that never use them. + // regex unit, scr_idna.c (node:url's IDNA) normalizes through it and + // scr_path_win32_relative.c lowercases through its case mapping, so all + // three ride the same switch; section GC drops them from binaries that + // never use them. unitArgs((u) => u.regex ? [ @@ -180,6 +181,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ u.rt(join(u.rtDir, "scr_regex.c")), u.rt(join(u.rtDir, "scr_glob.c")), u.rt(join(u.rtDir, "scr_idna.c")), + u.rt(join(u.rtDir, "scr_path_win32_relative.c")), ...u.lreObjects, ] : [], diff --git a/packages/compiler/src/backend/native/library.ts b/packages/compiler/src/backend/native/library.ts index 3cf4afa53..3585ab6a6 100644 --- a/packages/compiler/src/backend/native/library.ts +++ b/packages/compiler/src/backend/native/library.ts @@ -104,7 +104,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise // Zig's musl sysroot does not provide arc4random_buf. Keep the fallback // inside the archive so library embedders need no extra system library. ...(isMuslTarget(driver) ? ["scr_musl.c"] : []), - ...(regex ? ["scr_regex.c", "scr_glob.c"] : []), + ...(regex ? ["scr_regex.c", "scr_glob.c", "scr_path_win32_relative.c"] : []), ...(opts.assert || regex || opts.symbol ? ["scr_assert.c"] : []), ...(opts.inspect ? ["scr_inspect.c", "scr_console_native.c"] : []), ...(opts.symbol ? ["scr_symbol.c"] : []), diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts index bf5223d2f..8733804af 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts @@ -32,6 +32,7 @@ import { lowerUtilTypeCall } from "./util-types.js"; import { InternalCompilerError } from "../../../errors.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; +import { jsCheckedBuiltinCall } from "../lower-builtin-values.js"; import { type Lowerer } from "../lowerer.js"; import { type BuiltinModuleFn } from "../surfaces.js"; import { type ParamShape } from "../call-signatures.js"; @@ -67,6 +68,10 @@ export function lowerBuiltinModuleCall( ); if (fallible) return fallible; checkFsUrlPaths(lowerer, expr, bi); + { + const checked = jsCheckedBuiltinCall(lowerer, expr, fn, `${bi.module}.${bi.member}`, loc); + if (checked) return checked; + } if (bi.module === "util/types") { return lowerUtilTypeCall(lowerer, expr, bi, loc, name); } diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 58ae3e7bd..0c19143ad 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -61,7 +61,7 @@ import { classToStringConversion, classUnionToString } from "../class-to-primiti 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 { boolLit, numLit, varRef } from "../../../ir/build.js"; import { lowerUrlAssignment } from "../builtins/url.js"; import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js"; import { lowerShortCircuitAssignment } from "./nullish-assignment.js"; @@ -81,7 +81,7 @@ import { runtimeConstructorReceiver, } from "../builtins/runtime-constructor.js"; import { tryLowerIndexedComparison } from "../indexed-comparison.js"; -import { lowerUnionEquality, tagEqualityMayMissAlias } from "../strict-equality.js"; +import { lowerUnionEquality, sameNamespace, tagEqualityMayMissAlias } from "../strict-equality.js"; import { withUnitArm } from "../../type-mapper.js"; import { npmStaticPackageOfPath } from "../../npm-static.js"; import { lowerWebAssemblyTypeof } from "../builtins/webassembly.js"; @@ -1613,9 +1613,11 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr const shape = type.kind === "record" ? lowerer.shapes.get(type.shapeId) : undefined; return !!shape && (shape.fields.length > 0 || shape.indexValue !== undefined); }; + // A builtin function boxes as its checked twin, the value its + // module handle answers, so the two compare equal. const boxed: IrExpr = { kind: "dynFrom", - value: scalarSide, + value: lowerer.checkedBuiltinValue(scalarSide), type: DYN, loc: scalarSide.loc, ...(reference.kind === "record" || @@ -1634,13 +1636,16 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr loc, }; } - // EventEmitter or a stream class against an untyped value in - // JavaScript: the untyped value can hold the class's untyped - // constructor (coerceToExpected), which compares by identity. + // EventEmitter, a stream class or a node:path family against an + // untyped value in JavaScript: the untyped value can hold the class's + // untyped constructor or the module object (coerceToExpected), which + // compare by identity. if ( dynSide.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile()) && - ((scalarSide.type.kind === "moduleNs" && scalarSide.type.moduleId === "builtin:events") || + ((scalarSide.type.kind === "moduleNs" && + (scalarSide.type.moduleId === "builtin:events" || + scalarSide.type.moduleId.startsWith("builtin:path"))) || (scalarSide.kind === "classRef" && builtinClassKind(scalarSide.className) !== null)) ) { const boxed = lowerer.coerceToExpected(scalarSide, DYN); @@ -2584,6 +2589,8 @@ export function lowerStrictEquality( }; } if (idLeft.type.kind === "moduleNs" && idRight.type.kind === "moduleNs") { + if (idLeft.kind === "moduleNsRef" && idRight.kind === "moduleNsRef") + return boolLit(sameNamespace(lowerer, idLeft.moduleId, idRight.moduleId) !== negated, loc); return { kind: "bin", op: negated ? "!==" : "===", diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 9699a4fb9..9156c0741 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -1,7 +1,7 @@ import { keyedHelperName } from "../stable-names.js"; import { dateJsonType, isDateJsonSource } from "../builtins/dates.js"; import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js"; -import { pathModuleValue } from "../lower-builtin-values.js"; +import { pathModuleObject } from "../lower-builtin-values.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; import { @@ -307,7 +307,7 @@ export function lowerDynObjectLiteral( const builtin = lowerer.builtinNamespaceModuleOf(prop.expression); const raw = builtin === "path" || builtin === "path/posix" || builtin === "path/win32" - ? pathModuleValue(lowerer, builtin === "path" ? "path/posix" : builtin, locOf(prop)) + ? pathModuleObject(lowerer, builtin === "path" ? "path/posix" : builtin, locOf(prop)) : !boxValue && conditionalArms && (conditionalSpreadOf(prop.expression) !== null || diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index 02faa7ade..704314eaf 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -1290,9 +1290,21 @@ function lowerAssertEqual( ?.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))) { + if ( + a.type.kind === "dyn" || + b.type.kind === "dyn" || + a.type.kind === "moduleNs" || + b.type.kind === "moduleNs" || + (instance(a) && instance(b)) + ) { const box = (e: IrExpr, node: ts.Expression): IrExpr => { if (e.type.kind === "dyn") return e; + // A builtin module namespace crosses as its module object, whose + // identity is the module's (`assert.strictEqual(path, path.posix)`). + if (e.type.kind === "moduleNs") { + const value = lowerer.coerceToExpected(e, DYN); + if (value.type.kind === "dyn") return value; + } const k = e.type.kind; const ok = e.kind === "unitLit" || diff --git a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts index d59b234d3..2d9eb8964 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts @@ -10,9 +10,15 @@ import { type IrExpr, type IrLibFn, type IrStmt, + type IrType, type SrcLoc, + typeEquals, + typeKey, } from "../../ir/ir.js"; -import { strLit, varRef } from "../../ir/build.js"; +import { boolLit, dynUndefinedExpr, strLit, varRef } from "../../ir/build.js"; +import { isJsSourceFile } from "../program.js"; +import { funcTypeFromParamShapes, type ParamShape } from "./call-signatures.js"; +import { keyedHelperName } from "./stable-names.js"; import type { Lowerer } from "./lowerer.js"; import { BUILTIN_MODULE_CONSTS, @@ -22,6 +28,8 @@ import { builtinModuleConstOf, builtinModulesArrayLit, stdlibGlobalNameOf, + type BuiltinModuleFn, + type BuiltinValueParam, } from "./surfaces.js"; import { CRYPTO_CONSTANTS } from "./crypto-tables.js"; @@ -425,14 +433,24 @@ const MODULES = ["path/posix", "path/win32", "os", "worker_threads", "util/types const box = (value: IrExpr): IrExpr => value.type.kind === "dyn" ? value : { kind: "dynFrom", value, type: DYN, loc: value.loc }; +/** `path.posix`, `path.win32` and the bare module as untyped values: the + * same module handle process.getBuiltinModule answers, so each family is + * one object (`require("path/posix") === require("path").posix`) whose + * members are the interned function values. */ export function pathModuleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr { + return moduleValue(lowerer, module, loc); +} + +/** A path family's members as a fresh object, for spreading (`{ ...path }` + * copies the enumerable members). */ +export function pathModuleObject(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr { const fields: { key: IrExpr; value: IrExpr }[] = []; const add = (member: string, value: IrExpr): void => { fields.push({ key: strLit(member, loc), value: box(value) }); }; for (const member of Object.keys(builtinModuleFns()[module] ?? {})) { const value = lowerer.lowerBuiltinCallableValue({ module, member }, loc); - if (value) add(member, value); + if (value) add(member, lowerer.checkedBuiltinValue(value)); } for (const member of Object.keys(BUILTIN_MODULE_CONSTS[module] ?? {})) { const value = builtinModuleConstOf(lowerer, module, member); @@ -514,7 +532,7 @@ function moduleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr { }; for (const member of Object.keys(builtinModuleFns()[module] ?? {})) { const callable = lowerer.lowerBuiltinCallableValue({ module, member }, loc); - if (callable) add(member, callable); + if (callable) add(member, lowerer.checkedBuiltinValue(callable)); } for (const member of Object.keys(BUILTIN_MODULE_CONSTS[module] ?? {})) { const worker = module === "worker_threads" ? lowerWorkerMetadata(member, loc) : null; @@ -525,20 +543,27 @@ function moduleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr { if (module.startsWith("path/")) { add("posix", moduleValue(lowerer, "path/posix", loc)); add("win32", moduleValue(lowerer, "path/win32", loc)); + // Node's deprecated alias of toNamespacedPath. + const alias = lowerer.lowerBuiltinCallableValue({ module, member: "toNamespacedPath" }, loc); + if (alias) add("_makeLong", lowerer.checkedBuiltinValue(alias)); } if (module === "worker_threads") { for (const member of ["isInternalThread", "parentPort", "workerData"]) add(member, lowerWorkerMetadata(member, loc)!); } + // A path family is the whole of Node's object, so another key is + // absent there too. body.push({ kind: "return", - value: { - kind: "libCall", - fn: "process.builtinUnsupported", - args: [strLit(module, loc), key], - type: DYN, - loc, - }, + value: module.startsWith("path/") + ? dynUndefinedExpr(loc) + : { + kind: "libCall", + fn: "process.builtinUnsupported", + args: [strLit(module, loc), key], + type: DYN, + loc, + }, loc, }); lowerer.liftedFns.push({ @@ -711,3 +736,155 @@ export function builtinConstantsObject(lowerer: Lowerer, loc: SrcLoc): IrExpr { }; return { kind: "libCall", fn: "constants.object", args: [box(table)], type: DYN, loc }; } + +/** The untyped-caller twin of a builtin function value whose parameters + * carry Node's validator names (BuiltinValueParam.name): every argument + * arrives as a checked-dynamic value. With a `checked` entry point the + * arguments reach it as given, followed by its constants; otherwise string + * parameters pass through validateString in Node's order (`valueOrder`) + * before the direct call's entry point. One interned adapter per entry, + * so each read of the value is the same closure. */ +export function checkedNativeCallableValue( + lowerer: Lowerer, + fn: BuiltinModuleFn, + display: string, + loc: SrcLoc, +): IrExpr { + const valueParams = fn.valueParams ?? []; + const callee = fn.checked?.fn ?? fn.valueFn ?? fn.fn; + const result = fn.checked ? (fn.checked.result ?? fn.result) : fn.result; + const raw = fn.checked !== undefined; + const shapes: ParamShape[] = valueParams.map((param): ParamShape => ({ + mode: param.mode === "rest" ? "dynRest" : param.mode === "optional" ? "omittable" : "required", + type: DYN, + })); + const funcType = funcTypeFromParamShapes(shapes, result); + const key = `checked:${callee}:${JSON.stringify(fn.checked?.args ?? [])}:${typeKey(funcType)}`; + let fnName = lowerer.builtinCallableValueFns.get(key); + if (!fnName) { + fnName = keyedHelperName(lowerer, `%builtin.value.${callee}`, key); + lowerer.builtinCallableValueFns.set(key, fnName); + lowerer.builtinCallableValueNames.set(fnName, display.slice(display.lastIndexOf(".") + 1)); + const params = valueParams.map((_, index) => ({ + localId: `arg.${index}`, + name: `arg${index}`, + type: DYN, + })); + const locals = params.map((param) => ({ + id: param.localId, + name: param.name, + type: param.type as IrType, + mutable: false, + })); + // validateString under Node's name; an omitted optional argument (or + // undefined) takes its default without a check, as Node's + // `if (suffix !== undefined)` does. + const checked = (param: BuiltinValueParam, index: number): IrExpr => { + const ref = varRef(`arg.${index}`, DYN, loc); + const name = strLit(param.name ?? `arg${index}`, loc); + if (param.mode === "rest") { + return { + kind: "libCall", + fn: "dyn.validateStrings", + args: [ref, name], + type: arrayOf(STRING), + loc, + }; + } + const value: IrExpr = { + kind: "libCall", + fn: "dyn.validateString", + args: [ref, name], + type: STRING, + loc, + }; + if (param.mode !== "optional" || typeof param.defaultValue !== "string") return value; + return { + kind: "ternary", + cond: { kind: "dynTest", test: "undefined", value: ref, type: BOOL, loc }, + then: strLit(param.defaultValue, loc), + else_: value, + type: STRING, + loc, + }; + }; + const body: IrStmt[] = []; + const args: IrExpr[] = valueParams.map((param, index) => + raw || param.type.kind === "dyn" + ? varRef(`arg.${index}`, DYN, loc) + : varRef(`checked.${index}`, param.mode === "rest" ? arrayOf(STRING) : STRING, loc), + ); + // Validate in Node's order, then call with the arguments in place. + for (const index of fn.valueOrder ?? valueParams.map((_, i) => i)) { + const param = valueParams[index]!; + if (raw || param.type.kind === "dyn") continue; + const init = checked(param, index); + locals.push({ + id: `checked.${index}`, + name: `checked${index}`, + type: init.type, + mutable: false, + }); + body.push({ kind: "varDecl", localId: `checked.${index}`, init, loc }); + } + const call: IrExpr = { + kind: "libCall", + fn: callee, + args: [...args, ...(fn.checked?.args ?? []).map((value) => boolLit(value, loc))], + type: result, + loc, + }; + body.push( + result.kind === "void" + ? { kind: "exprStmt", expr: call, loc } + : { kind: "return", value: call, loc }, + ); + if (result.kind === "void") body.push({ kind: "return", value: null, loc }); + lowerer.liftedFns.push({ name: fnName, params, returnType: result, locals, body, loc }); + } + return { kind: "closure", fnName, captures: [], type: funcType, loc }; +} + +/** A JavaScript call of a builtin with named value parameters whose + * arguments are not the native parameters' types or omit one + * (`path.matchesGlob(123, glob)`, `path.toNamespacedPath()`): the call goes + * through the checked + * adapter (checkedNativeCallableValue), which applies Node's validation and + * answers the value Node answers. Null for every other call, which keeps + * the direct lowering. */ +export function jsCheckedBuiltinCall( + lowerer: Lowerer, + call: ts.CallExpression, + fn: BuiltinModuleFn, + display: string, + loc: SrcLoc, +): IrExpr | null { + const params = fn.valueParams; + if (!params?.some((param) => param.name !== undefined)) return null; + if (params.some((param) => param.mode === "rest")) return null; + if (!isJsSourceFile(ts.getSourceFileOfNode(call))) return null; + const args = call.arguments.nodes; + if (args.length > params.length || args.some((arg) => ts.isSpreadElement(arg))) return null; + // An omitted required argument is undefined to Node's validation too. + const required = params.filter((param) => param.mode === "required").length; + const mismatch = + args.length < required || + args.some((arg, index) => { + const want = params[index]!.type; + if (want.kind === "dyn") return false; + const type = lowerer.mapTypeOf(lowerer.typeOf(arg)); + return type === null || !typeEquals(type, want); + }); + if (!mismatch) return null; + const adapter = checkedNativeCallableValue(lowerer, fn, display, loc); + if (adapter.kind !== "closure") return null; + return { + kind: "call", + callee: adapter.fnName, + args: params.map((_, index) => + index < args.length ? lowerer.lowerExprExpecting(args[index]!, DYN) : dynUndefinedExpr(loc), + ), + type: fn.checked?.result ?? fn.result, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index de5bab8b7..c58d20c2d 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -256,7 +256,11 @@ import { nsUndefRead, } from "./lower-namespaces.js"; import { expandoMemberRead } from "./lower-expando.js"; -import { fenceNodeModuleMutationCall, lowerRequireCacheKeys } from "./lower-node-module.js"; +import { + fenceNodeModuleMutationCall, + isCjsRequireGlobal, + lowerRequireCacheKeys, +} from "./lower-node-module.js"; import { defaultAfterUndefined, lowerOptionalArgument, @@ -468,6 +472,19 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const inlineValue = lowerInlineRequireValue(lowerer, expr); if (inlineValue) return inlineValue; } + // `require("path/posix")` of a builtin as a value: the module's + // namespace, as a `const posix = require("path/posix")` binding is. + if ( + ts.isIdentifier(expr.expression) && + expr.expression.text === "require" && + isCjsRequireGlobal(lowerer, expr.expression) + ) { + const module = lowerer.builtinNamespaceModuleOf(expr); + if (module !== null && module !== "constants") { + const moduleId = `builtin:${module}`; + return { kind: "moduleNsRef", moduleId, type: { kind: "moduleNs", moduleId }, loc }; + } + } // A call whose chain ROOTS at an ambient-undefined name (`declare // const value: Y | undefined; value?.foo("a")`, `declare function diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 417f13c20..c56f6187e 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -180,6 +180,7 @@ import { import { assertedConversion } from "./coercions/asserted.js"; import { lowerUtilTypeValue, + checkedNativeCallableValue, pathModuleValue, utilTypesModuleValue, } from "./lower-builtin-values.js"; @@ -3419,6 +3420,8 @@ export class Lowerer { /** Builtin function values whose `length` differs from their adapter's * parameter count (BuiltinModuleFn.valueLength), by adapter name. */ readonly builtinCallableValueLengths = new Map(); + /** Typed builtin adapters with a checked twin (checkedBuiltinValue). */ + readonly builtinCheckedValues = new Map IrExpr>(); /** 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 @@ -11845,9 +11848,28 @@ export class Lowerer { if (dyn) return dyn; this.badType(node, t); } + // A JavaScript binding whose inferred type holds a union of functions + // and arrays (a test table of `[fn, cases]` rows, or one row + // destructured): the checked-dynamic kind dynFallbackType gives the + // unmappable spellings of such tables, so the rows keep their functions' + // names and the destructured items their values. + if (isJsSourceFile(ts.getSourceFileOfNode(node)) && this.mixesFunctionsWithArrays(mapped)) + return DYN; return jsOpenObjectType(node, mapped, this.shapes, this.unions); } + /** Whether `type`, or an array element type inside it, is a union with + * both a function arm and an array arm. */ + private mixesFunctionsWithArrays(type: IrType): boolean { + if (type.kind === "array") return this.mixesFunctionsWithArrays(type.elem); + if (type.kind !== "union") return false; + const arms = this.unions.get(type.unionId)?.arms ?? []; + return ( + (arms.some((arm) => arm.kind === "func") && arms.some((arm) => arm.kind === "array")) || + arms.some((arm) => arm.kind === "array" && this.mixesFunctionsWithArrays(arm)) + ); + } + /** Exact-shape enforcement (SC2002). Records are monomorphic structs, so * everywhere a value flows into a typed slot (call arg, initializer, * assignment, field, return) the shapes must MATCH — or width-coerce: @@ -12260,6 +12282,30 @@ export class Lowerer { ); if (boxed) return boxed; } + // A node:path family is its module object (pathModuleValue), the + // bare module the target platform's family. + if ( + expr.type.kind === "moduleNs" && + (expr.type.moduleId === "builtin:path" || + expr.type.moduleId === "builtin:path/posix" || + expr.type.moduleId === "builtin:path/win32") + ) { + const family = + expr.type.moduleId !== "builtin:path" + ? expr.type.moduleId.slice("builtin:".length) + : builtinModuleConstOf(this, "path", "sep") === "\\" + ? "path/win32" + : "path/posix"; + const value = pathModuleValue(this, family, expr.loc); + if (expr.kind === "moduleNsRef") return value; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr, loc: expr.loc }], + result: value, + type: DYN, + loc: expr.loc, + }; + } if ( expr.type.kind === "moduleNs" && expr.type.moduleId === "builtin:events" && @@ -12311,7 +12357,7 @@ export class Lowerer { expr.kind === "closure" ? this.builtinCallableValueLengths.get(expr.fnName) : undefined; return { kind: "dynFrom", - value: expr, + value: this.checkedBuiltinValue(expr), type: DYN, loc: expr.loc, ...(liveRef ? { liveRef: true as const } : {}), @@ -16736,7 +16782,10 @@ export class Lowerer { (bi.module === "path" || bi.module === "path/posix" || bi.module === "path/win32") && (bi.member === "posix" || bi.member === "win32") ) { - return pathModuleValue(this, `path/${bi.member}`, loc); + // The family's namespace; an untyped use converts it to the module + // object (pathModuleValue, through coerceToExpected). + const moduleId = `builtin:path/${bi.member}`; + return { kind: "moduleNsRef", moduleId, type: { kind: "moduleNs", moduleId }, loc }; } // events.defaultMaxListeners READS the process-wide default (its // write twin routes through emitter.setDefaultMaxChk). Through an ES @@ -16879,6 +16928,7 @@ export class Lowerer { lowerNativeCallableValue(fn: BuiltinModuleFn, display: string, loc: SrcLoc): IrExpr | null { const valueParams = fn.valueParams; if (!valueParams) return null; + if (fn.checked?.only) return checkedNativeCallableValue(this, fn, display, loc); const shapes: ParamShape[] = valueParams.map((param): ParamShape => { if (param.mode === "rest") return { mode: "rest", type: arrayOf(param.type) }; if (param.mode === "optional") { @@ -16988,9 +17038,24 @@ export class Lowerer { loc, }); } + if (valueParams.some((param) => param.name !== undefined)) { + this.builtinCheckedValues.set(fnName, () => + checkedNativeCallableValue(this, fn, display, loc), + ); + } return { kind: "closure", fnName, captures: [], type: funcType, loc }; } + /** A builtin function value about to become untyped: its checked twin + * when the builtin names its parameters (checkedNativeCallableValue), so + * untyped callers meet Node's argument validation. Typed slots keep the + * typed adapter. */ + checkedBuiltinValue(expr: IrExpr): IrExpr { + const checked = + expr.kind === "closure" ? this.builtinCheckedValues.get(expr.fnName) : undefined; + return checked ? checked() : expr; + } + isBuiltinCallableValue(expr: IrExpr): boolean { return ( expr.kind === "closure" && diff --git a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts index 405d2af5f..b7542aae3 100644 --- a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts +++ b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts @@ -22,6 +22,20 @@ export function lowerModuleNamespaceElement( ): IrExpr | null { const type = lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); if (type?.kind !== "moduleNs") return null; + // A builtin namespace with a module object (`path[platform]`): the keyed + // read of that object. + if (type.moduleId.startsWith("builtin:")) { + const receiver = lowerer.coerceToExpected(lowerer.lowerExpr(expr.expression), DYN); + if (receiver.type.kind !== "dyn") return null; + const key = lowerer.lowerExpr(expr.argumentExpression); + return { + kind: "dynKeyGet", + key: key.type.kind === "string" ? key : lowerer.coerceInto(expr.argumentExpression, key, DYN), + value: receiver, + type: DYN, + loc: locOf(expr), + }; + } const source = lowerer.sourceFileOfModuleNamespace(type); if (!source) return null; const loc = locOf(expr); diff --git a/packages/compiler/src/frontend/lowering/strict-equality.ts b/packages/compiler/src/frontend/lowering/strict-equality.ts index 8a87c8111..06b731dfa 100644 --- a/packages/compiler/src/frontend/lowering/strict-equality.ts +++ b/packages/compiler/src/frontend/lowering/strict-equality.ts @@ -12,6 +12,7 @@ import { } from "../../ir/ir.js"; import type { Lowerer } from "./lowerer.js"; import { unionComparison } from "./union-comparison.js"; +import { builtinModuleConstOf } from "./surfaces.js"; const IDENTITY_KINDS: ReadonlySet = new Set([ "array", @@ -29,6 +30,19 @@ const IDENTITY_KINDS: ReadonlySet = new Set([ "moduleNs", ]); +/** Whether two namespace references are the same module object: the bare + * node:path namespace IS its target platform's family (`path === + * path.posix` on POSIX targets); other namespaces are their module. */ +export function sameNamespace(lowerer: Lowerer, a: string, b: string): boolean { + const family = (id: string): string => + id !== "builtin:path" + ? id + : builtinModuleConstOf(lowerer, "path", "sep") === "\\" + ? "builtin:path/win32" + : "builtin:path/posix"; + return family(a) === family(b); +} + function atomicEquality( lowerer: Lowerer, left: IrExpr, @@ -41,6 +55,8 @@ function atomicEquality( if ([a.kind, b.kind].some((kind) => kind === "dyn" || kind === "caught" || kind === "void")) return null; if (isUnitType(a) || isUnitType(b)) return boolLit(a.kind === b.kind, loc); + if (left.kind === "moduleNsRef" && right.kind === "moduleNsRef") + return boolLit(sameNamespace(lowerer, left.moduleId, right.moduleId), loc); if (a.kind === "object" && b.kind === "object" && !typeEquals(a, b)) { if (lowerer.isSubclassOf(a.className, b.className)) left = lowerer.upcastTo(left, b.className); else if (lowerer.isSubclassOf(b.className, a.className)) diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 03b65bd54..eef92c26b 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -823,12 +823,25 @@ export interface BuiltinModuleFn { /** 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; + /** The order Node validates named value parameters in, when it is not + * the parameter order (basename checks its suffix first). */ + valueOrder?: readonly number[]; + /** The runtime entry point an untyped caller's adapter calls with every + * argument as given, when it validates them itself (resolve's last-first + * walk) or the value has no typed form (`only`: parse's record result, + * format's untyped pathObject). `args` are constants passed after the + * arguments (the win32 flag of an entry both path families share). */ + checked?: { fn: IrLibFn; args: readonly boolean[]; result?: IrType; only?: true }; } +/** `name` is the argument name Node's validator reports + * (ERR_INVALID_ARG_TYPE's 'The "path" argument ...'): the adapter accepts + * any value there and validates it, so a string parameter arrives checked + * and a dyn one reaches its valueFn as given. */ export type BuiltinValueParam = - | { mode: "required"; type: IrType } - | { mode: "optional"; type: IrType; defaultValue: string | number | undefined } - | { mode: "rest"; type: IrType }; + | { mode: "required"; type: IrType; name?: string } + | { mode: "optional"; type: IrType; defaultValue: string | number | undefined; name?: string } + | { mode: "rest"; type: IrType; name?: string }; /** The common first-class shape for builtin functions whose supported * source signature is already exactly the fixed native ABI. Keeping the @@ -930,26 +943,27 @@ const PATH_MODULE_FNS: Record = { params: [STRING], result: STRING, variadicPack: true, - valueParams: [{ mode: "rest", type: STRING }], + valueParams: [{ mode: "rest", type: STRING, name: "path" }], }, resolve: { fn: "path.resolve", params: [STRING], result: STRING, variadicPack: true, - valueParams: [{ mode: "rest", type: STRING }], + valueParams: [{ mode: "rest", type: STRING, name: "paths" }], + checked: { fn: "path.resolveValue", args: [false] }, }, normalize: { fn: "path.normalize", params: [STRING], result: STRING, - valueParams: [{ mode: "required", type: STRING }], + valueParams: [{ mode: "required", type: STRING, name: "path" }], }, dirname: { fn: "path.dirname", params: [STRING], result: STRING, - valueParams: [{ mode: "required", type: STRING }], + valueParams: [{ mode: "required", type: STRING, name: "path" }], }, basename: { fn: "path.basename", @@ -957,48 +971,65 @@ const PATH_MODULE_FNS: Record = { result: STRING, defaults: [""], valueParams: [ - { mode: "required", type: STRING }, - { mode: "optional", type: STRING, defaultValue: "" }, + { mode: "required", type: STRING, name: "path" }, + { mode: "optional", type: STRING, defaultValue: "", name: "suffix" }, ], + valueOrder: [1, 0], }, extname: { fn: "path.extname", params: [STRING], result: STRING, - valueParams: [{ mode: "required", type: STRING }], + valueParams: [{ mode: "required", type: STRING, name: "path" }], }, isAbsolute: { fn: "path.isAbsolute", params: [STRING], result: BOOL, - valueParams: [{ mode: "required", type: STRING }], + valueParams: [{ mode: "required", type: STRING, name: "path" }], }, relative: { fn: "path.relative", params: [STRING, STRING], result: STRING, valueParams: [ - { mode: "required", type: STRING }, - { mode: "required", type: STRING }, + { mode: "required", type: STRING, name: "from" }, + { mode: "required", type: STRING, name: "to" }, ], }, toNamespacedPath: { fn: "path.toNamespacedPath", params: [STRING], result: STRING, - valueParams: [{ mode: "required", type: STRING }], + valueParams: [{ mode: "required", type: STRING, name: "path" }], + checked: { fn: "path.toNamespacedValue", args: [false], result: DYN }, }, matchesGlob: { fn: "path.matchesGlob", params: [STRING, STRING], result: BOOL, - valueParams: exactValueParams(STRING, STRING), + valueParams: [ + { mode: "required", type: STRING, name: "path" }, + { mode: "required", type: STRING, name: "pattern" }, + ], }, // Special-cased (lowerPathParseCall / lowerPathFormatCall): the // ParsedPath record and the pathObject argument follow the call site's // mapped shapes. - parse: { fn: "path.parse", params: [STRING], result: VOID }, - format: { fn: "path.format", params: [], result: STRING }, + parse: { + fn: "path.parse", + params: [STRING], + result: VOID, + valueParams: [{ mode: "required", type: STRING, name: "path" }], + checked: { fn: "path.parseValue", args: [false], result: DYN, only: true }, + }, + format: { + fn: "path.format", + params: [], + result: STRING, + valueParams: [{ mode: "required", type: DYN, name: "pathObject" }], + checked: { fn: "path.formatValue", args: [false], result: DYN, only: true }, + }, }; /** The win32 twins (scr_path.c's exact port of Node-v24 path.win32): @@ -1010,26 +1041,27 @@ const PATH_WIN32_MODULE_FNS: Record = { params: [STRING], result: STRING, variadicPack: true, - valueParams: [{ mode: "rest", type: STRING }], + valueParams: [{ mode: "rest", type: STRING, name: "path" }], }, resolve: { fn: "path.win32Resolve", params: [STRING], result: STRING, variadicPack: true, - valueParams: [{ mode: "rest", type: STRING }], + valueParams: [{ mode: "rest", type: STRING, name: "paths" }], + checked: { fn: "path.resolveValue", args: [true] }, }, normalize: { fn: "path.win32Normalize", params: [STRING], result: STRING, - valueParams: [{ mode: "required", type: STRING }], + valueParams: [{ mode: "required", type: STRING, name: "path" }], }, dirname: { fn: "path.win32Dirname", params: [STRING], result: STRING, - valueParams: [{ mode: "required", type: STRING }], + valueParams: [{ mode: "required", type: STRING, name: "path" }], }, basename: { fn: "path.win32Basename", @@ -1037,48 +1069,65 @@ const PATH_WIN32_MODULE_FNS: Record = { result: STRING, defaults: [""], valueParams: [ - { mode: "required", type: STRING }, - { mode: "optional", type: STRING, defaultValue: "" }, + { mode: "required", type: STRING, name: "path" }, + { mode: "optional", type: STRING, defaultValue: "", name: "suffix" }, ], + valueOrder: [1, 0], }, extname: { fn: "path.win32Extname", params: [STRING], result: STRING, - valueParams: [{ mode: "required", type: STRING }], + valueParams: [{ mode: "required", type: STRING, name: "path" }], }, isAbsolute: { fn: "path.win32IsAbsolute", params: [STRING], result: BOOL, - valueParams: [{ mode: "required", type: STRING }], + valueParams: [{ mode: "required", type: STRING, name: "path" }], }, relative: { fn: "path.win32Relative", params: [STRING, STRING], result: STRING, valueParams: [ - { mode: "required", type: STRING }, - { mode: "required", type: STRING }, + { mode: "required", type: STRING, name: "from" }, + { mode: "required", type: STRING, name: "to" }, ], }, toNamespacedPath: { fn: "path.win32ToNamespacedPath", params: [STRING], result: STRING, - valueParams: [{ mode: "required", type: STRING }], + valueParams: [{ mode: "required", type: STRING, name: "path" }], + checked: { fn: "path.toNamespacedValue", args: [true], result: DYN }, }, matchesGlob: { fn: "path.win32MatchesGlob", params: [STRING, STRING], result: BOOL, - valueParams: exactValueParams(STRING, STRING), + valueParams: [ + { mode: "required", type: STRING, name: "path" }, + { mode: "required", type: STRING, name: "pattern" }, + ], }, // Special-cased (lowerPathParseCall / lowerPathFormatCall): the // ParsedPath record and the pathObject argument follow the call site's // mapped shapes. - parse: { fn: "path.win32Parse", params: [STRING], result: VOID }, - format: { fn: "path.win32Format", params: [], result: STRING }, + parse: { + fn: "path.win32Parse", + params: [STRING], + result: VOID, + valueParams: [{ mode: "required", type: STRING, name: "path" }], + checked: { fn: "path.parseValue", args: [true], result: DYN, only: true }, + }, + format: { + fn: "path.win32Format", + params: [], + result: STRING, + valueParams: [{ mode: "required", type: DYN, name: "pathObject" }], + checked: { fn: "path.formatValue", args: [true], result: DYN, only: true }, + }, }; /** util.types's own keys in Node's order (Node 24.22.0). The table is the diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 55a7631a4..8a3a03020 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -5419,6 +5419,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ) { return DYN; } + // A module namespace beside another value in one slot (`[path.posix, + // path.win32]`, a `['posix', path.posix]` table row): a namespace has + // no union representation, and as a value it is the runtime's module + // object, so the slot is checked-dynamic. A namespace or null keeps + // its union (a probed process.getBuiltinModule result). + if ( + arms.some((a) => a.kind === "moduleNs") && + arms.filter((a) => !isUnitType(a)).length > 1 && + !unions.recursivePending(widened) + ) { + return DYN; + } if ( arms.some( (a) => diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 60f46e7ee..6272b9e44 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -131,6 +131,11 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "buffer.byteLenValue", "buffer.allocValue", "fs.realpathValue", + "dyn.validateString", + "dyn.validateStrings", + "path.resolveValue", + "path.parseValue", + "path.formatValue", "bytes.construct", "arrayBuffer.new", "sharedArrayBuffer.new", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 024f11277..ea87f5bc8 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -802,6 +802,14 @@ function buildLibFnSignatures() { * 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 }, + // Builtin functions read as values: validateString under Node's argument + // name, and node:path's value adapters (scr_builtin_values.c). + "dyn.validateString": { argTypes: [DYN, STRING], result: STRING }, + "dyn.validateStrings": { argTypes: [DYN, STRING], result: arrayOf(STRING) }, + "path.resolveValue": { argTypes: [DYN, BOOL], result: STRING }, + "path.parseValue": { argTypes: [DYN, BOOL], result: DYN }, + "path.formatValue": { argTypes: [DYN, BOOL], result: DYN }, + "path.toNamespacedValue": { argTypes: [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, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index da4f11623..ec61ee77c 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -1020,6 +1020,12 @@ const RUNTIME_FEATURE_RULES = { "buffer.installValue", "buffer.staticValue", "buffer.prototypeMember", + "dyn.validateString", + "dyn.validateStrings", + "path.resolveValue", + "path.parseValue", + "path.formatValue", + "path.toNamespacedValue", ], }), /** The stream unit emits through the emitter unit, also when the @@ -1160,6 +1166,9 @@ const RUNTIME_FEATURE_RULES = { ...UTIL_CALLABLE_LIB_FNS, "rl.terminal", "str.localeCase", + // win32.relative compares with String.prototype.toLowerCase's + // Unicode case mapping. + "path.win32Relative", ], prefixes: URL_PREFIXES, expr: (node, _tables) => diff --git a/packages/compiler/test/process-builtin-values.test.ts b/packages/compiler/test/process-builtin-values.test.ts index 1ced69b04..4b760f41b 100644 --- a/packages/compiler/test/process-builtin-values.test.ts +++ b/packages/compiler/test/process-builtin-values.test.ts @@ -16,7 +16,7 @@ test.each(["llvm"] as const)( const load = process.getBuiltinModule; try { load("node:fs"); } catch (error) { console.log(error.code, error.message.includes("native builtin 'fs'")); } const path = load("path"); -try { path.parse("/a"); } catch (error) { console.log(error.code, error.message.includes(".parse'")); } +console.log(JSON.stringify(path.parse("/a"))); const worker = load("worker_threads"); try { console.log(worker.Worker); } catch (error) { console.log(error.code, error.message.includes("worker_threads.Worker")); } console.log(path.posix.join("still", "works"), worker.isMainThread); @@ -39,7 +39,7 @@ try { console.log(segmenter.segment); } catch (error) { console.log(error.messag expect(child.status, child.stderr).toBe(0); expect(child.stderr).toBe(""); expect(child.stdout).toBe( - "SC2020 true\nSC2020 true\nSC2020 true\nstill/works true\nSC2020 true\ntrue\ntrue\n", + 'SC2020 true\n{"root":"/","dir":"/","base":"a","ext":"","name":"a"}\nSC2020 true\nstill/works true\nSC2020 true\ntrue\ntrue\n', ); } finally { rmSync(dir, { recursive: true, force: true }); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-function-array-tables.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-function-array-tables.cjs.json new file mode 100644 index 000000000..e7df3aee6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-function-array-tables.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/js-function-array-tables.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-function-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-function-values.cjs.json new file mode 100644 index 000000000..30776ba0d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-function-values.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/path-function-values.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-untyped-calls.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-untyped-calls.cjs.json new file mode 100644 index 000000000..c67256c42 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-untyped-calls.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/path-untyped-calls.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-win32-relative-case.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-win32-relative-case.cjs.json new file mode 100644 index 000000000..451244794 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-win32-relative-case.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/path-win32-relative-case.cjs" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/library-matrix.mjs b/packages/runtime-pack-common/library-matrix.mjs index 1a621a375..a6568894e 100644 --- a/packages/runtime-pack-common/library-matrix.mjs +++ b/packages/runtime-pack-common/library-matrix.mjs @@ -3,6 +3,7 @@ export function withLibraryRuntimeFlavors(matrix) { const omitted = new Set(["scr_async.c", "scr_crypto_async.c", "scr_child.c", "scr_ffi.c"]); const optional = new Set([ "scr_regex.c", + "scr_path_win32_relative.c", "scr_glob.c", "scr_assert.c", "scr_inspect.c", diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 72e888dce..e8520c427 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -63,6 +63,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_copying.c", "copying"], ["scr_file_handle.c", "fileHandle"], ["scr_regex.c", "regex"], + // path.win32.relative uses the regex unit's Unicode case mapping. + ["scr_path_win32_relative.c", "regex"], ["scr_glob.c", "regex"], ["scr_idna.c", "regex"], ["scr_assert.c", any("assert", "regex", "symbol")], diff --git a/packages/runtime/src/scr_builtin_values.c b/packages/runtime/src/scr_builtin_values.c index 084a1d50d..aa8124903 100644 --- a/packages/runtime/src/scr_builtin_values.c +++ b/packages/runtime/src/scr_builtin_values.c @@ -472,3 +472,286 @@ ScrDyn *scr_buffer_static_value(const ScrStr *name) { scr_dyn_release(buffer); return member; } + +/* ── node:path functions read as values ────────────────────────────── + * A stored `path.posix.join` or a `path.win32` namespace passed around + * receives unchecked arguments, so these entries apply lib/path.js's + * validators (validateString, validateObject) under Node's argument names + * before the string algorithms in scr_path.c run. All borrow; results are + * +1, or NULL with Node's ERR_INVALID_ARG_TYPE pending. */ + +/* validateString(value, name) */ +static bool scr_bv_path_string(const ScrDyn *value, const char *name, size_t name_len) { + if (value->kind == SCR_DYN_STR) return true; + char text[64]; + int n = snprintf(text, sizeof text, "%.*s", (int)name_len, name); + if (n < 0) text[0] = 0; + scr_dyn_arg_type_fail(text, "of type string", value); + return false; +} + +ScrStr *scr_value_string(const ScrDyn *value, ScrStr *name) { + if (!scr_bv_path_string(value, name->data, name->len)) return NULL; + return scr_str_retain(value->v.str); +} + +/* validateString over every argument of a rest list, in order (join's). */ +ScrArr *scr_value_strings(const ScrDyn *args, ScrStr *name) { + size_t n = (size_t)scr_dyn_arr_len(args); + ScrArr *out = scr_arr_new(SCR_ELEM_STR, n); + for (size_t i = 0; i < n; i++) { + ScrDyn *arg = scr_dyn_arr_at(args, (double)i); + bool ok = scr_bv_path_string(arg, name->data, name->len); + if (ok) scr_arr_push_ref(out, scr_str_retain(arg->v.str)); + scr_dyn_release(arg); + if (!ok) { + scr_arr_release(out); + return NULL; + } + } + return out; +} + +static bool scr_bv_w32_sep(char c) { return c == '/' || c == '\\'; } + +static bool scr_bv_w32_ieq(const char *a, const char *b, size_t n) { + for (size_t i = 0; i < n; i++) { + char x = a[i] >= 'A' && a[i] <= 'Z' ? (char)(a[i] + 32) : a[i]; + char y = b[i] >= 'A' && b[i] <= 'Z' ? (char)(b[i] + 32) : b[i]; + if (x != y) return false; + } + return true; +} + +/* win32.resolve's root match for one argument: the device it names (a UNC + * share, a device namespace or a drive letter) written into `device` + * (capacity len + 4), its length returned, and whether it is absolute. */ +static size_t scr_bv_w32_device(const char *path, size_t len, char *device, bool *absolute) { + *absolute = false; + char code = path[0]; + if (len == 1) { + *absolute = scr_bv_w32_sep(code); + return 0; + } + if (scr_bv_w32_sep(code)) { + *absolute = true; + if (!scr_bv_w32_sep(path[1])) return 0; + size_t j = 2, last = 2; + while (j < len && !scr_bv_w32_sep(path[j])) j++; + if (!(j < len && j != last)) return 0; + size_t first = last, first_len = j - last; + last = j; + while (j < len && scr_bv_w32_sep(path[j])) j++; + if (!(j < len && j != last)) return 0; + last = j; + while (j < len && !scr_bv_w32_sep(path[j])) j++; + if (!(j == len || j != last)) return 0; + size_t n = 0; + device[n++] = '\\'; + device[n++] = '\\'; + memcpy(device + n, path + first, first_len); + n += first_len; + if (!(first_len == 1 && (path[first] == '.' || path[first] == '?'))) { + device[n++] = '\\'; + memcpy(device + n, path + last, j - last); + n += j - last; + } + return n; + } + if (((code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z')) && path[1] == ':') { + device[0] = path[0]; + device[1] = ':'; + *absolute = len > 2 && scr_bv_w32_sep(path[2]); + return 2; + } + return 0; +} + +/* Whether resolve's last-first walk over args[from, n) — all strings — + * stops before it reaches args[from - 1]: posix stops at the first absolute + * argument, win32 once it has an absolute path on a resolved device. */ +static bool scr_bv_resolve_stops(const ScrDyn *args, long from, long n, bool win32) { + bool stops = false, absolute = false; + char *dev = NULL; + size_t dev_len = 0; + for (long i = n - 1; i >= from && !stops; i--) { + ScrDyn *arg = scr_dyn_arr_at(args, (double)i); + const ScrStr *s = arg->v.str; + if (s->len > 0 && !win32) { + stops = s->data[0] == '/'; + } else if (s->len > 0) { + char *device = malloc(s->len + 4); + if (!device) scr_trap("scriptc: out of memory\n"); + bool is_absolute; + size_t len = scr_bv_w32_device(s->data, s->len, device, &is_absolute); + bool other = false; + if (len > 0 && dev_len > 0) { + other = !(len == dev_len && scr_bv_w32_ieq(device, dev, len)); + } else if (len > 0) { + free(dev); + dev = device; + dev_len = len; + device = NULL; + } + free(device); + if (!other && absolute) { + stops = dev_len > 0; + } else if (!other) { + absolute = is_absolute; + stops = is_absolute && dev_len > 0; + } + } + scr_dyn_release(arg); + } + free(dev); + return stops; +} + +/* resolve(...args): Node validates `paths[i]` as its walk reaches each + * argument, last first, and the walk ends at an absolute path, so a + * non-string before that point is never read. */ +ScrStr *scr_path_resolve_value(const ScrDyn *args, bool win32) { + long n = (long)scr_dyn_arr_len(args); + long last_bad = n - 1; + for (; last_bad >= 0; last_bad--) { + ScrDyn *arg = scr_dyn_arr_at(args, (double)last_bad); + bool string = arg->kind == SCR_DYN_STR; + scr_dyn_release(arg); + if (!string) break; + } + if (last_bad >= 0 && !scr_bv_resolve_stops(args, last_bad + 1, n, win32)) { + char name[32]; + int len = snprintf(name, sizeof name, "paths[%ld]", last_bad); + ScrDyn *arg = scr_dyn_arr_at(args, (double)last_bad); + scr_bv_path_string(arg, name, len < 0 ? 0 : (size_t)len); + scr_dyn_release(arg); + return NULL; + } + ScrArr *parts = scr_arr_new(SCR_ELEM_STR, (size_t)(n - last_bad - 1)); + for (long i = last_bad + 1; i < n; i++) { + ScrDyn *arg = scr_dyn_arr_at(args, (double)i); + scr_arr_push_ref(parts, scr_str_retain(arg->v.str)); + scr_dyn_release(arg); + } + ScrStr *out = win32 ? scr_path_win32_resolve(parts) : scr_path_resolve(parts); + scr_arr_release(parts); + return out; +} + +/* parse(path): validateString, then a fresh { root, dir, base, ext, name }. */ +ScrDyn *scr_path_parse_value(const ScrDyn *path, bool win32) { + if (!scr_bv_path_string(path, "path", 4)) return NULL; + ScrArr *parts = win32 ? scr_path_win32_parse(path->v.str) : scr_path_parse(path->v.str); + static const char *const keys[] = {"root", "dir", "base", "ext", "name"}; + ScrDyn *out = scr_dyn_new_obj(); + for (size_t i = 0; i < 5; i++) { + ScrStr *part = scr_arr_get_ref(parts, (double)i); + scr_dyn_obj_set(out, keys[i], strlen(keys[i]), scr_dyn_new_str(part)); + scr_str_release(part); + } + scr_arr_release(parts); + return out; +} + +/* ToString for a template position (`${value}`); NULL with the error + * pending (a Symbol). */ +static bool scr_bv_append(ScrJsonBuf *b, const ScrDyn *value) { + ScrStr *text = scr_dyn_string_coerce_js(value); + if (!text) return false; + scr_jb_write(b, text->data, text->len); + scr_str_release(text); + return true; +} + +/* format(pathObject): lib/path.js's _format and formatExt over any object, + * reading each property as Node does. The result is `base` itself when + * there is no dir, so it need not be a string. */ +ScrDyn *scr_path_format_value(const ScrDyn *object, bool win32) { + ScrStr *type = scr_dyn_typeof(object); + bool is_object = type->len == 6 && memcmp(type->data, "object", 6) == 0; + scr_str_release(type); + if (!is_object || object->kind == SCR_DYN_NULL || object->kind == SCR_DYN_ARR) { + scr_dyn_arg_type_fail("pathObject", "of type object", object); + return NULL; + } + ScrDyn *dir = scr_dyn_obj_read(object, "dir", 3); + if (!dir) return NULL; + if (!scr_dyn_truthy(dir)) { + scr_dyn_release(dir); + dir = scr_dyn_obj_read(object, "root", 4); + if (!dir) return NULL; + } + ScrDyn *base = scr_dyn_obj_read(object, "base", 4); + if (!base) goto fail_dir; + if (!scr_dyn_truthy(base)) { + scr_dyn_release(base); + base = NULL; + ScrJsonBuf b; + scr_jb_init(&b); + ScrDyn *name = scr_dyn_obj_read(object, "name", 4); + bool ok = name != NULL && (!scr_dyn_truthy(name) || scr_bv_append(&b, name)); + if (name) scr_dyn_release(name); + ScrDyn *ext = ok ? scr_dyn_obj_read(object, "ext", 3) : NULL; + ok = ext != NULL; + if (ok && scr_dyn_truthy(ext)) { + /* `${ext[0] === '.' ? '' : '.'}${ext}` */ + bool dot = false; + if (ext->kind == SCR_DYN_STR) { + dot = ext->v.str->len > 0 && ext->v.str->data[0] == '.'; + } else if (ext->kind == SCR_DYN_OBJ || ext->kind == SCR_DYN_ARR || ext->kind == SCR_DYN_FUNC) { + ScrDyn *first = ext->kind == SCR_DYN_ARR ? scr_dyn_arr_at(ext, 0) : scr_dyn_obj_read(ext, "0", 1); + ok = first != NULL; + dot = ok && first->kind == SCR_DYN_STR && first->v.str->len == 1 && first->v.str->data[0] == '.'; + if (first) scr_dyn_release(first); + } + if (ok && !dot) scr_jb_putc(&b, '.'); + ok = ok && scr_bv_append(&b, ext); + } + if (ext) scr_dyn_release(ext); + ScrStr *text = scr_jb_finish(&b); + if (!ok) { + scr_str_release(text); + goto fail_dir; + } + base = scr_dyn_new_str(text); + scr_str_release(text); + } + if (!scr_dyn_truthy(dir)) { + scr_dyn_release(dir); + return base; + } + ScrDyn *root = scr_dyn_obj_read(object, "root", 4); + if (!root) goto fail_base; + bool same = scr_dyn_strict_eq(dir, root); + scr_dyn_release(root); + ScrJsonBuf b; + scr_jb_init(&b); + bool ok = scr_bv_append(&b, dir); + if (ok && !same) scr_jb_putc(&b, win32 ? '\\' : '/'); + ok = ok && scr_bv_append(&b, base); + ScrStr *text = scr_jb_finish(&b); + scr_dyn_release(dir); + scr_dyn_release(base); + if (!ok) { + scr_str_release(text); + return NULL; + } + ScrDyn *out = scr_dyn_new_str(text); + scr_str_release(text); + return out; +fail_base: + scr_dyn_release(base); +fail_dir: + scr_dyn_release(dir); + return NULL; +} + +/* toNamespacedPath(path): posix answers its argument; win32 answers a + * non-string or empty argument as it is and namespaces a string. */ +ScrDyn *scr_path_to_namespaced_value(ScrDyn *path, bool win32) { + if (!win32 || path->kind != SCR_DYN_STR || path->v.str->len == 0) return scr_dyn_retain(path); + ScrStr *text = scr_path_win32_to_namespaced_path(path->v.str); + ScrDyn *out = scr_dyn_new_str(text); + scr_str_release(text); + return out; +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 343930580..e3e118622 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -2275,6 +2275,12 @@ const char *scr_dyn_specific_type(const ScrDyn *v, char *buf, size_t cap); * error.argTypeThrow libCall). Borrows all three; always throws. */ SCR_COLD void scr_throw_arg_type(const ScrStr *argname, const ScrStr *expected, const ScrDyn *got); SCR_COLD void scr_dyn_arg_type_fail(const char *argname, const char *expected, const ScrDyn *got); +/* validateString(value, name) for a builtin function read as a value + * (scr_builtin_values.c): the string (+1), or NULL with Node's + * ERR_INVALID_ARG_TYPE pending; strings checks each element of a rest list + * in order and answers them as a string[]. Borrows both. */ +ScrStr *scr_value_string(const ScrDyn *value, ScrStr *name); +ScrArr *scr_value_strings(const ScrDyn *args, ScrStr *name); /* The property flavor ("The \"options.x\" property must be ...") — the * option-bag validators' gate (error.propTypeThrow). Always throws. */ SCR_COLD void scr_throw_prop_type(const ScrStr *name, const ScrStr *expected, const ScrDyn *got); diff --git a/packages/runtime/src/scr_path.c b/packages/runtime/src/scr_path.c index c845d470d..310ad09c2 100644 --- a/packages/runtime/src/scr_path.c +++ b/packages/runtime/src/scr_path.c @@ -491,9 +491,10 @@ ScrStr *scr_path_relative(ScrStr *from, ScrStr *to) { * * Two knowingly-ASCII spots (documented divergences): the * case-insensitive comparisons Node does with String.prototype. - * toLowerCase/toUpperCase (win32.relative, resolve's device compare, the - * reserved-name check) fold ASCII only here — non-ASCII device/server - * names whose Unicode case-folding differs compare case-SENSITIVELY. */ + * toLowerCase/toUpperCase in resolve's device compare and the reserved-name + * check fold ASCII only here — non-ASCII device/server names whose Unicode + * case-folding differs compare case-SENSITIVELY. win32.relative lowercases + * with String.prototype.toLowerCase and counts UTF-16 units, as Node does. */ static bool scr_path_w32_is_device_root(char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); @@ -975,114 +976,6 @@ bool scr_path_win32_is_absolute(ScrStr *path) { scr_path_w32_is_sep(p[2])); } -ScrStr *scr_path_win32_relative(ScrStr *from, ScrStr *to) { - if (from->len == to->len && memcmp(from->data, to->data, from->len) == 0) { - return scr_str_new("", 0); - } - /* fromOrig = win32.resolve(from), toOrig = win32.resolve(to). */ - ScrArr *pack = scr_arr_new(SCR_ELEM_STR, 1); - scr_arr_push_ref(pack, scr_str_retain(from)); - ScrStr *rfrom = scr_path_win32_resolve(pack); - scr_arr_release(pack); - pack = scr_arr_new(SCR_ELEM_STR, 1); - scr_arr_push_ref(pack, scr_str_retain(to)); - ScrStr *rto = scr_path_win32_resolve(pack); - scr_arr_release(pack); - - if (rfrom->len == rto->len && memcmp(rfrom->data, rto->data, rfrom->len) == 0) { - scr_str_release(rfrom); - scr_str_release(rto); - return scr_str_new("", 0); - } - /* Node lowercases both sides (case-insensitive comparison, ASCII fold - * here — see the header comment). Byte-lowercasing never changes - * lengths, so Node's changed-length Unicode branch cannot trigger. */ - char *flow = malloc(rfrom->len ? rfrom->len : 1); - char *tlow = malloc(rto->len ? rto->len : 1); - if (!flow || !tlow) { - scr_trap("scriptc: out of memory\n"); - } - for (size_t k = 0; k < rfrom->len; k++) flow[k] = scr_path_w32_lower(rfrom->data[k]); - for (size_t k = 0; k < rto->len; k++) tlow[k] = scr_path_w32_lower(rto->data[k]); - if (rfrom->len == rto->len && memcmp(flow, tlow, rfrom->len) == 0) { - free(flow); - free(tlow); - scr_str_release(rfrom); - scr_str_release(rto); - return scr_str_new("", 0); - } - - /* Trim leading backslashes, and trailing ones (UNC paths only). */ - long from_start = 0; - while (from_start < (long)rfrom->len && flow[from_start] == '\\') from_start++; - long from_end = (long)rfrom->len; - while (from_end - 1 > from_start && flow[from_end - 1] == '\\') from_end--; - const long from_len = from_end - from_start; - - long to_start = 0; - while (to_start < (long)rto->len && tlow[to_start] == '\\') to_start++; - long to_end = (long)rto->len; - while (to_end - 1 > to_start && tlow[to_end - 1] == '\\') to_end--; - const long to_len = to_end - to_start; - - const long length = from_len < to_len ? from_len : to_len; - long last_common_sep = -1; - long i = 0; - for (; i < length; i++) { - char from_code = flow[from_start + i]; - if (from_code != tlow[to_start + i]) break; - if (from_code == '\\') last_common_sep = i; - } - ScrStr *result = NULL; - if (i != length) { - /* Mismatch before any common separator: return toOrig. */ - if (last_common_sep == -1) result = scr_str_new(rto->data, rto->len); - } else { - if (to_len > length) { - if (tlow[to_start + i] == '\\') { - /* `from` is the exact base path of `to`. */ - result = scr_str_new(rto->data + to_start + i + 1, (size_t)(to_end - (to_start + i + 1))); - } else if (i == 2) { - /* `from` is the device root. */ - result = scr_str_new(rto->data + to_start + i, (size_t)(to_end - (to_start + i))); - } - } - if (result == NULL && from_len > length) { - if (flow[from_start + i] == '\\') last_common_sep = i; - else if (i == 2) last_common_sep = 3; - } - if (result == NULL && last_common_sep == -1) last_common_sep = 0; - } - if (result != NULL) { - free(flow); - free(tlow); - scr_str_release(rfrom); - scr_str_release(rto); - return result; - } - - PathBuf out; - pb_init(&out); - for (i = from_start + last_common_sep + 1; i <= from_end; ++i) { - if (i == from_end || flow[i] == '\\') { - if (out.len == 0) pb_append(&out, "..", 2); - else pb_append(&out, "\\..", 3); - } - } - to_start += last_common_sep; - if (out.len == 0) { - /* Node reads toOrig.charCodeAt(toStart) — NaN out of range. */ - if (to_start < (long)rto->len && rto->data[to_start] == '\\') ++to_start; - } - /* toOrig.slice(toStart, toEnd) — "" when the range is empty/inverted. */ - if (to_end > to_start) pb_append(&out, rto->data + to_start, (size_t)(to_end - to_start)); - free(flow); - free(tlow); - scr_str_release(rfrom); - scr_str_release(rto); - return pb_take(&out); -} - ScrStr *scr_path_win32_to_namespaced_path(ScrStr *path) { if (path->len == 0) return scr_str_retain(path); ScrArr *pack = scr_arr_new(SCR_ELEM_STR, 1); diff --git a/packages/runtime/src/scr_path.h b/packages/runtime/src/scr_path.h index de5e85491..3c9e5e5e1 100644 --- a/packages/runtime/src/scr_path.h +++ b/packages/runtime/src/scr_path.h @@ -53,6 +53,20 @@ ScrArr *scr_path_win32_parse(ScrStr *path); ScrStr *scr_path_format(ScrStr *root, ScrStr *dir, ScrStr *base, ScrStr *name, ScrStr *ext); ScrStr *scr_path_win32_format(ScrStr *root, ScrStr *dir, ScrStr *base, ScrStr *name, ScrStr *ext); +/* ── node:path functions read as values (scr_builtin_values.c) ─────── + * The adapters behind a stored `path.posix.join` or a `path.win32` + * namespace passed around, whose arguments arrive unchecked: lib/path.js's + * validateString/validateObject under Node's argument names, then the + * functions above. All borrow; results are +1, or NULL with Node's + * ERR_INVALID_ARG_TYPE pending. resolve_value validates `paths[i]` only as + * far as Node's last-first walk reads them; format_value answers `base` + * itself when there is no dir (Node's _format), so its result is a dyn + * value. */ +struct ScrDyn; +ScrStr *scr_path_resolve_value(const struct ScrDyn *args, bool win32); +struct ScrDyn *scr_path_parse_value(const struct ScrDyn *path, bool win32); +struct ScrDyn *scr_path_format_value(const struct ScrDyn *object, bool win32); +struct ScrDyn *scr_path_to_namespaced_value(struct ScrDyn *path, bool win32); /* path.matchesGlob (scr_glob.c): Node 24's minimatch with matchGlobPattern's * options, case-insensitive on macOS and Windows hosts like Node. Borrows * both; a brace sequence over 2^32 - 1 elements throws Node's RangeError, a diff --git a/packages/runtime/src/scr_path_win32_relative.c b/packages/runtime/src/scr_path_win32_relative.c new file mode 100644 index 000000000..b827b9c5f --- /dev/null +++ b/packages/runtime/src/scr_path_win32_relative.c @@ -0,0 +1,169 @@ +/* node:path's win32.relative (scr_path.c holds the rest of path.win32). + * Node lowercases both resolved paths with String.prototype.toLowerCase, + * so the comparison uses the runtime's Unicode case mapping + * (scr_str_to_lower, scr_regex.c). That keeps scr_path.c free of the case + * tables: this unit links with the regex unit, which the compiler selects + * for every program that calls path.win32.relative. + * + * Inputs are BORROWED; results are fresh (+1). Nothing here throws. */ +#include "scr_runtime.h" + +#include + +/* win32.relative when lowercasing changed a length: Node compares the + * '\\'-separated segments case-insensitively instead (a trailing empty + * segment dropped) and joins the rest of `to`. */ +static ScrStr *scr_path_w32_relative_split(ScrStr *from, ScrStr *to) { + ScrArr *parts[2]; + ScrStr *sides[2] = {from, to}; + for (int side = 0; side < 2; side++) { + ScrStr *s = sides[side]; + parts[side] = scr_arr_new(SCR_ELEM_STR, 4); + size_t start = 0; + for (size_t k = 0; k <= s->len; k++) { + if (k == s->len || s->data[k] == '\\') { + if (!(k == s->len && k == start)) { + scr_arr_push_ref(parts[side], scr_str_new(s->data + start, k - start)); + } + start = k + 1; + } + } + } + size_t from_len = scr_arr_len(parts[0]), to_len = scr_arr_len(parts[1]); + size_t length = from_len < to_len ? from_len : to_len; + size_t i = 0; + for (; i < length; i++) { + ScrStr *a = scr_arr_get_ref(parts[0], (double)i); + ScrStr *b = scr_arr_get_ref(parts[1], (double)i); + ScrStr *la = scr_str_to_lower(a); + ScrStr *lb = scr_str_to_lower(b); + bool same = la->len == lb->len && memcmp(la->data, lb->data, la->len) == 0; + scr_str_release(la); + scr_str_release(lb); + scr_str_release(a); + scr_str_release(b); + if (!same) break; + } + ScrJsonBuf out; + scr_jb_init(&out); + if (i == 0) { + scr_jb_write(&out, to->data, to->len); + } else { + size_t ups = i == length ? (to_len > length ? 0 : from_len > length ? from_len - i : 0) : from_len - i; + for (size_t k = 0; k < ups; k++) { + if (i == length && k + 1 == ups) scr_jb_write(&out, "..", 2); + else scr_jb_write(&out, "..\\", 3); + } + if (i < length || to_len > length) { + for (size_t k = i; k < to_len; k++) { + ScrStr *part = scr_arr_get_ref(parts[1], (double)k); + if (k > i) scr_jb_putc(&out, '\\'); + scr_jb_write(&out, part->data, part->len); + scr_str_release(part); + } + } + } + scr_arr_release(parts[0]); + scr_arr_release(parts[1]); + return scr_jb_finish(&out); +} + +/* win32.relative over the lowercased paths `from` and `to`, whose UTF-16 + * lengths are the originals': Node's longest-common-path walk by UTF-16 + * unit, slicing the original `orig_to`. */ +static ScrStr *scr_path_w32_relative_units(ScrStr *from, ScrStr *to, ScrStr *orig_to) { + const double bs = '\\'; + double from_n = scr_str_utf16_len(from), to_n = scr_str_utf16_len(to); + /* Trim leading backslashes, and trailing ones (UNC paths only). */ + double from_start = 0; + while (from_start < from_n && scr_str_char_code_at(from, from_start) == bs) from_start++; + double from_end = from_n; + while (from_end - 1 > from_start && scr_str_char_code_at(from, from_end - 1) == bs) from_end--; + const double from_len = from_end - from_start; + double to_start = 0; + while (to_start < to_n && scr_str_char_code_at(to, to_start) == bs) to_start++; + double to_end = to_n; + while (to_end - 1 > to_start && scr_str_char_code_at(to, to_end - 1) == bs) to_end--; + const double to_len = to_end - to_start; + + const double length = from_len < to_len ? from_len : to_len; + double last_common_sep = -1; + double i = 0; + for (; i < length; i++) { + double from_code = scr_str_char_code_at(from, from_start + i); + if (from_code != scr_str_char_code_at(to, to_start + i)) break; + if (from_code == bs) last_common_sep = i; + } + if (i != length) { + /* Mismatch before any common separator: return toOrig. */ + if (last_common_sep == -1) return scr_str_retain(orig_to); + } else { + if (to_len > length) { + /* `from` is the exact base path of `to`, or the device root. */ + if (scr_str_char_code_at(to, to_start + i) == bs) + return scr_str_slice(orig_to, to_start + i + 1, to_n); + if (i == 2) return scr_str_slice(orig_to, to_start + i, to_n); + } + if (from_len > length) { + if (scr_str_char_code_at(from, from_start + i) == bs) last_common_sep = i; + else if (i == 2) last_common_sep = 3; + } + if (last_common_sep == -1) last_common_sep = 0; + } + ScrJsonBuf out; + scr_jb_init(&out); + for (i = from_start + last_common_sep + 1; i <= from_end; ++i) { + if (i == from_end || scr_str_char_code_at(from, i) == bs) { + if (out.len == 0) scr_jb_write(&out, "..", 2); + else scr_jb_write(&out, "\\..", 3); + } + } + to_start += last_common_sep; + /* Node reads toOrig.charCodeAt(toStart) — NaN out of range. */ + if (out.len == 0 && scr_str_char_code_at(orig_to, to_start) == bs) ++to_start; + /* toOrig.slice(toStart, toEnd) — "" when the range is empty/inverted. */ + ScrStr *tail = scr_str_slice(orig_to, to_start, to_end); + scr_jb_write(&out, tail->data, tail->len); + scr_str_release(tail); + return scr_jb_finish(&out); +} + +ScrStr *scr_path_win32_relative(ScrStr *from, ScrStr *to) { + if (from->len == to->len && memcmp(from->data, to->data, from->len) == 0) { + return scr_str_new("", 0); + } + /* fromOrig = win32.resolve(from), toOrig = win32.resolve(to). */ + ScrArr *pack = scr_arr_new(SCR_ELEM_STR, 1); + scr_arr_push_ref(pack, scr_str_retain(from)); + ScrStr *rfrom = scr_path_win32_resolve(pack); + scr_arr_release(pack); + pack = scr_arr_new(SCR_ELEM_STR, 1); + scr_arr_push_ref(pack, scr_str_retain(to)); + ScrStr *rto = scr_path_win32_resolve(pack); + scr_arr_release(pack); + + if (rfrom->len == rto->len && memcmp(rfrom->data, rto->data, rfrom->len) == 0) { + scr_str_release(rfrom); + scr_str_release(rto); + return scr_str_new("", 0); + } + /* Node compares the lowercased paths (String.prototype.toLowerCase), + * indexing UTF-16 units. */ + ScrStr *flow = scr_str_to_lower(rfrom); + ScrStr *tlow = scr_str_to_lower(rto); + ScrStr *result = NULL; + if (flow->len == tlow->len && memcmp(flow->data, tlow->data, flow->len) == 0) { + result = scr_str_new("", 0); + } else if (scr_str_utf16_len(rfrom) != scr_str_utf16_len(flow) || + scr_str_utf16_len(rto) != scr_str_utf16_len(tlow)) { + result = scr_path_w32_relative_split(rfrom, rto); + } else { + result = scr_path_w32_relative_units(flow, tlow, rto); + } + scr_str_release(flow); + scr_str_release(tlow); + scr_str_release(rfrom); + scr_str_release(rto); + return result; +} + diff --git a/packages/runtime/test/path.test.ts b/packages/runtime/test/path.test.ts index 79d34c1c3..578c42a30 100644 --- a/packages/runtime/test/path.test.ts +++ b/packages/runtime/test/path.test.ts @@ -7,6 +7,7 @@ import { beforeAll, expect, test } from "vitest"; const execFileAsync = promisify(execFile); const testDir = import.meta.dirname; const bin = join(testDir, "build", "test_path"); +const vendorDir = join(testDir, "../vendor/quickjs-ng"); // Compiles the C-side oracle test once. Built with ASan + the RC audit so // the oracle run also proves the win32 path functions neither leak nor @@ -20,10 +21,19 @@ beforeAll(async () => { "-Wextra", "-fsanitize=address", "-DSCR_RC_AUDIT", + "-I", + vendorDir, "-o", bin, join(testDir, "test_path.c"), join(testDir, "../src/scr_path.c"), + // win32.relative lowercases through the regex unit's case mapping; the + // regex unit brings scr_assert.c and the vendored engine (regex.test.ts). + join(testDir, "../src/scr_path_win32_relative.c"), + join(testDir, "../src/scr_regex.c"), + join(testDir, "../src/scr_assert.c"), + join(vendorDir, "libregexp.c"), + join(vendorDir, "libunicode.c"), // join/resolve take a packed string[]; the array module and its own // dependencies join the link (the array.test.ts set). join(testDir, "../src/scr_string.c"), diff --git a/tests/corpus/js-function-array-tables.cjs b/tests/corpus/js-function-array-tables.cjs new file mode 100644 index 000000000..45a9fad15 --- /dev/null +++ b/tests/corpus/js-function-array-tables.cjs @@ -0,0 +1,24 @@ +'use strict'; +// Test tables that mix functions with arrays of cases, iterated through +// for-of and forEach with destructuring (JavaScript infers a union of +// functions and arrays for each row's items). +function add(a, b) { return a + b; } +function mul(a, b) { return a * b; } + +const table = [ + [add, [[1, 2], [3, 4]]], + [mul, [[5, 6], [0, 9]]], +]; +for (const [fn, cases] of table) { + for (const [a, b] of cases) console.log(fn.name, a, b, fn(a, b)); +} +table.forEach(([fn, cases]) => { + cases.forEach(([a, b]) => console.log('forEach', fn.name, fn.apply(null, [a, b]))); +}); +for (const row of table) { + const [fn, cases] = row; + console.log(typeof fn, Array.isArray(cases), cases.length, fn === add || fn === mul); +} + +const nested = [[Math.max, [[1, 7, 3]]], [Math.min, [[4, -2]]]]; +for (const [pick, lists] of nested) for (const list of lists) console.log(pick(...list)); diff --git a/tests/corpus/path-function-values.cjs b/tests/corpus/path-function-values.cjs new file mode 100644 index 000000000..3a96e2d3f --- /dev/null +++ b/tests/corpus/path-function-values.cjs @@ -0,0 +1,60 @@ +'use strict'; +// node:path namespaces and functions used as values: identity, Node's +// argument validation, and the parse/format/resolve/toNamespacedPath forms. +const path = require('path'); + +const platform = process.platform === 'win32' ? path.win32 : path.posix; +console.log('identity', + path === platform, + require('path/posix') === path.posix, + require('path/win32') === path.win32, + path.posix.win32 === path.win32, + path.win32.posix === path.posix); + +function call(fn, args) { + try { + return JSON.stringify(fn.apply(null, args)); + } catch (e) { + return `${e.name} ${e.code} ${e.message}`; + } +} + +for (const [name, ns] of [['posix', path.posix], ['win32', path.win32]]) { + console.log(`-- ${name}`); + for (const value of [true, 7, null, {}, undefined, []]) { + console.log('join', call(ns.join, ['a', value])); + console.log('normalize', call(ns.normalize, [value])); + console.log('basename suffix', call(ns.basename, [value, 7])); + console.log('basename', call(ns.basename, [value])); + console.log('relative from', call(ns.relative, [value, 'x'])); + console.log('relative to', call(ns.relative, ['x', value])); + console.log('matchesGlob', call(ns.matchesGlob, ['a', value])); + console.log('parse', call(ns.parse, [value])); + console.log('format', call(ns.format, [value])); + console.log('toNamespacedPath', call(ns.toNamespacedPath, [value])); + } + console.log('join none', call(ns.join, [])); + console.log('basename undefined suffix', call(ns.basename, ['/a/b.js', undefined])); + // resolve reads its arguments last first and stops at an absolute path. + console.log('resolve stop', call(ns.resolve, [7, '/abs/x'])); + console.log('resolve bad', call(ns.resolve, ['/abs', 7, 'y'])); + console.log('resolve bad last', call(ns.resolve, ['/abs', 'y', null])); + console.log('parse', call(ns.parse, ['/home/user/dir/file.txt'])); + console.log('format base', call(ns.format, [{ base: 5 }])); + console.log('format dir root', call(ns.format, [{ root: '/', dir: '/', base: 'x' }])); + console.log('format ext', call(ns.format, [{ name: 'x', ext: 'png' }]), + call(ns.format, [{ name: 'x', ext: '.png' }]), call(ns.format, [{ name: 'x', ext: 5 }])); + console.log('format empty', call(ns.format, [{}])); +} +console.log('win32 resolve devices', + call(path.win32.resolve, [null, 'c:\\a', 'd:\\b']), + call(path.win32.resolve, [null, 'c:a', 'd:\\b']), + call(path.win32.resolve, [null, '\\\\server\\share\\x']), + call(path.win32.resolve, ['d:x', null, 'c:\\b'])); +console.log('win32 namespaced', call(path.win32.toNamespacedPath, ['c:\\x']), + call(path.win32.toNamespacedPath, [''])); + +// The same members reached through the namespace object are the same values. +const ns = path.posix; +console.log('members', ns.join === path.posix.join, typeof ns.parse, ns.sep, path.win32.delimiter); +for (const key of ['basename', 'extname']) console.log(key, path[key]('/a/b.txt')); diff --git a/tests/corpus/path-untyped-calls.cjs b/tests/corpus/path-untyped-calls.cjs new file mode 100644 index 000000000..6756f2031 --- /dev/null +++ b/tests/corpus/path-untyped-calls.cjs @@ -0,0 +1,39 @@ +'use strict'; +// Direct node:path calls whose JavaScript arguments are not strings, and +// computed reads of the path namespaces. +const path = require('path'); + +function attempt(label, fn) { + try { + console.log(label, JSON.stringify(fn())); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +// toNamespacedPath answers a non-string argument as it is. +console.log('toNamespacedPath', path.toNamespacedPath(null), path.toNamespacedPath(100), + path.toNamespacedPath(false), path.posix.toNamespacedPath(), path.toNamespacedPath(path) === path, + path.win32.toNamespacedPath(7), path.win32.toNamespacedPath('')); +attempt('matchesGlob path', () => path.matchesGlob(123, 'foo/bar/baz')); +attempt('matchesGlob pattern', () => path.matchesGlob('foo/bar/baz', 123)); +attempt('basename suffix', () => path.basename('/a/b.js', 7)); +attempt('basename undefined suffix', () => path.basename('/a/b.js', undefined)); +attempt('dirname null', () => path.dirname(null)); +attempt('extname object', () => path.extname({})); +attempt('isAbsolute undefined', () => path.isAbsolute(undefined)); +attempt('relative', () => path.relative('/a', 5)); +attempt('normalize none', () => path.normalize()); +attempt('parse number', () => path.parse(1)); + +for (const family of ['posix', 'win32']) { + const ns = path[family]; + console.log(family, ns.sep, ns.delimiter, path[family].basename('/x/y.txt'), ns === path[family]); + attempt(`${family} computed join`, () => path[family]['join']('a', 'b')); +} +const assert = require('assert'); +assert.strictEqual(path, process.platform === 'win32' ? path.win32 : path.posix); +assert.notStrictEqual(path.posix, path.win32); +assert.strictEqual(require('path/posix'), path.posix); +const key = 'missing'; +console.log('missing member', typeof path[key], typeof path.posix['_makeLong']); diff --git a/tests/corpus/path-win32-relative-case.cjs b/tests/corpus/path-win32-relative-case.cjs new file mode 100644 index 000000000..93a1c9857 --- /dev/null +++ b/tests/corpus/path-win32-relative-case.cjs @@ -0,0 +1,22 @@ +'use strict'; +// path.win32.relative compares paths case-insensitively with +// String.prototype.toLowerCase, including characters whose lowercase form +// changes length (İ) or encoding width (Ⱥ). +const path = require('path'); + +const cases = [ + ['c:\\i̇\\a\\İ', 'c:\\İ\\b\\İ\\test.txt'], + ['c:\\İ\\a', 'c:\\i̇\\a\\b'], + ['c:\\İ\\a\\b', 'c:\\i̇\\a'], + ['c:\\İ', 'C:\\I'], + ['c:\\Ⱥ\\x\\y', 'c:\\ⱥ\\x\\z'], + ['c:\\Ⱥ\\x', 'C:\\ⱥ\\X\\Y\\Z'], + ['C:\\ΣΑΣ\\a', 'c:\\σας\\b'], + ['c:\\straße\\a', 'C:\\STRASSE\\a'], + ['\\\\Server\\Share\\İ\\a', '\\\\server\\share\\i̇\\b'], + ['c:\\a\\b', 'C:\\A\\B'], + ['c:\\a\\🙂\\x', 'c:\\A\\🙂\\y'], +]; +for (const [from, to] of cases) { + console.log(JSON.stringify([from, to]), JSON.stringify(path.win32.relative(from, to))); +} From 93765cd6a2b6439f8447959abdec6a8573c393d6 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 14:15:56 -0700 Subject: [PATCH 06/70] Pass enclosing-frame values to generic arrow instances as trailing parameters A generic arrow or function-expression binding declared inside a function used to be refused (SC1090) whenever its body read anything from the enclosing frame, because its monomorphized instances are module functions with no closure. Give each instance the frame it needs: every value the binding reads from the enclosing function becomes a TRAILING parameter of the instance, and every call site (which sits in the declaring frame or in a closure over it) passes the current value after the declared arguments. The instance body binds the same symbol, so it resolves the reference like any parameter. Sound only while the original binding never changes, so the fence now covers exactly what a copy cannot stand in for: `this`, `super`, `new.target`, `arguments`, an enclosing type parameter, an enclosing generic scope (per-instantiation types), and any binding with a write anywhere in the file or declared after the binding (temporal dead zone). Corpus: withClientIdentity/withIdentity, physicalModels (captured Set), listTwo (captured local function), runTracked (async, call from a nested closure) and twice (two instances, different capture values). The generic-value-bindings diagnostics fixture now pins the fences that remain (a capture the file reassigns, and an enclosing generic scope) instead of the never-reassigned parameter it used to. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 5b23e6ccf110c66e96fad472265d7b53db0f342a) --- .../frontend/lowering/generic-functions.ts | 162 +++++++++++++++++- .../generic-arrow-enclosing-captures.ts.json | 6 + .../generic-arrow-enclosing-captures.ts | 74 ++++++++ tests/diagnostics/generic-value-bindings.ts | 18 +- .../generic-value-bindings.ts.txt | 66 ++++--- .../generic-arrow-enclosing-captures.ts.json | 1 + 6 files changed, 290 insertions(+), 37 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-arrow-enclosing-captures.ts.json create mode 100644 tests/corpus/generic-arrow-enclosing-captures.ts create mode 100644 tests/harness/corpus-costs/generic-arrow-enclosing-captures.ts.json diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 046b34008..8397bbc0a 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -101,6 +101,14 @@ export interface GenericFnInfo { * declaration makes a new function object in JavaScript, while the * compiled instances are shared module functions, so only calls lower. */ functionScoped?: true; + /** The enclosing-frame values a function-scoped generic arrow reads. + * The instances are module functions, so each captured binding travels + * as a TRAILING parameter: the call sites (which sit in the declaring + * frame or in a closure over it) pass the value, in this order, and the + * instance body reads it through the same symbol. Only never-reassigned + * bindings qualify — a shared copy is exact exactly as long as nothing + * writes the original. */ + captures?: FrameCapture[]; /** The declaration of a generic arrow/function-expression binding. */ bindingDecl?: ts.VariableDeclaration; /** A module-scope binding that code can use before its declaration @@ -131,6 +139,17 @@ export interface GenericFnInfo { nameReadParams?: Map; } +/** One enclosing-frame value a generic arrow/function-expression binding + * reads (GenericFnInfo.captures). `nameNode` is the binding's own + * declaration identifier: declaring a local for it binds the SYMBOL, so + * the instance body resolves the reference like any parameter. */ +export interface FrameCapture { + symbol: ts.Symbol; + nameNode: ts.Node; + name: string; + type: IrType; +} + export interface GenericInstance { path: InstantiationPath; name: string; @@ -285,6 +304,11 @@ export function lowerGenericCall( ? implicitCallInstance(lowerer, expr, info) : genericCallInstance(lowerer, expr, info); const args = lowerer.completeArgs(expr.arguments.nodes, instance.params, loc, expr); + // The enclosing-frame values follow the declared arguments, the order + // the instance takes them in (lowerGenericInstance). + for (const capture of info.captures ?? []) { + args.push(lowerer.lowerExprExpecting(capture.nameNode as ts.Expression, capture.type)); + } const call: IrExpr & { kind: "call" } = { kind: "call", callee: instance.name, @@ -1250,6 +1274,14 @@ export function lowerGenericInstance( // threaded — a default mentioning T resolves like any body expression. const declared = lowerer.declareParams(decl.parameters.nodes, inst.params); params.push(...declared.params); + // The enclosing-frame values travel as TRAILING parameters, after the + // declared ones: a call lowers its arguments first (JavaScript's + // order), then reads the captures. Binding each local to the captured + // symbol is what lets the body resolve the reference. + for (const capture of info.captures ?? []) { + const local = lowerer.declareLocal(capture.nameNode, capture.name, capture.type, false); + params.push({ localId: local.id, name: capture.name, type: capture.type }); + } const body = [...receiverInit, ...declared.prologue]; const bodyBlock = blockBodyOf(decl); if (bodyBlock) { @@ -2638,6 +2670,118 @@ function capturesEnclosingFrame( return captures; } +/** The enclosing-frame VALUES `fn`, bound by `decl`, reads — in first-use + * order, the order the instances take them as trailing parameters. The + * shape is the one the instance body resolves: a binding declared by an + * enclosing function (a local, a parameter, a local function or class). + * + * Module-scope declarations and the binding itself are reachable from the + * module functions the instances compile to, so they are not captures; nor + * are type-only declarations, whose mapping is static. `this`, `super`, + * `arguments`, `new.target`, and an enclosing generic's type parameter (or + * any binding the closure could observe a LATER write of) have no value a + * parameter can stand in for and stay fenced. */ +function enclosingFrameCaptures( + lowerer: Lowerer, + fn: ts.FunctionExpression | ts.ArrowFunction, + decl: ts.VariableDeclaration, + scope: ts.Node, + name: string, +): FrameCapture[] { + const file = ts.getSourceFileOfNode(decl); + const self = lowerer.checker.getSymbolAtLocation(decl.name); + const frameFence = (): never => + lowerer.unsupported( + "SC1090", + fn, + `generic arrow/function-expression bindings that use the frames of their enclosing functions (the compiled instantiations of '${name}' are module functions and cannot capture its locals, parameters, type parameters or 'this' — pass them as arguments, or declare the binding at module scope)`, + ); + const captures: FrameCapture[] = []; + const seen = new Set(); + const scan = (n: ts.Node): void => { + if ( + n.kind === ts.SyntaxKind.ThisKeyword || + n.kind === ts.SyntaxKind.SuperKeyword || + ts.isMetaProperty(n) + ) { + // Arrow bodies see the ENCLOSING this; function expressions + // rebind their own — but a bare `this` there is untyped JS + // dynamism either way. Reject both, cheaply and soundly. + frameFence(); + } + if (ts.isIdentifier(n)) { + const s = lowerer.checker.getSymbolAtLocation(n); + if (s === undefined) { + if (n.text === "arguments") frameFence(); + } else if (s !== self && !seen.has(s)) { + for (const d of s.declarations) { + if (ts.getSourceFileOfNode(d) !== file || (d.pos >= fn.pos && d.end <= fn.end)) continue; + let enclosing = false; + for ( + let p: ts.Node | undefined = d.parent; + p !== undefined && !ts.isSourceFile(p); + p = p.parent + ) { + if (ts.isFunctionLike(p)) { + enclosing = true; + break; + } + } + if (!enclosing) continue; + // A type parameter (of the enclosing function or class) keys + // the enclosing instantiation; the instance would need that + // frame's type arguments, which this binding cannot carry. + if ((s.flags & ts.SymbolFlags.TypeParameter) !== 0) frameFence(); + // Type-only declarations cost no runtime slot. + if ((s.flags & ts.SymbolFlags.Value) === 0) break; + // The instance holds a COPY of the value, exact only while the + // original never changes: a write anywhere in the file could + // make the closure observe a different binding state later. + if (!bindingNeverReassigned(lowerer, s, d)) frameFence(); + // ...and exact only once the original is INITIALIZED. A capture + // declared after the binding could be read from a call that runs + // before its initializer — a temporal dead zone JavaScript would + // throw on. Declared before, the copy is always the initialized + // value: reaching the binding's own declaration ran it. + if (d.pos > decl.pos) frameFence(); + // The declaration's own identifier (a binding element's for a + // destructured name): lowering it resolves the SYMBOL, so the + // instance body reads the parameter we declare for it. + const declaredName = (d as { name?: ts.Node }).name; + const nameNode = + declaredName !== undefined && ts.isIdentifier(declaredName) ? declaredName : d; + const type = lowerer.irTypeOf(nameNode); + captures.push({ + symbol: s, + nameNode, + name: ts.isIdentifier(nameNode) ? nameNode.text : ts.symbolName(s), + type, + }); + seen.add(s); + break; + } + } + } + ts.forEachChild(n, scan); + }; + scan(scope); + // A capture's type is fixed when the info is registered, once per + // binding, while an enclosing generic function or class re-lowers its + // frame per instantiation: the second instantiation's value would have a + // different type than the parameter the shared instance declares. + // Generic enclosing scopes keep the fence; non-generic ones carry no + // type argument for the instance to miss. + if (captures.length > 0) { + for (let p: ts.Node | undefined = fn.parent; p && !ts.isSourceFile(p); p = p.parent) { + if (!ts.isFunctionLike(p) && !ts.isClassLikeDeclaration(p)) continue; + const typeParameters = (p as { typeParameters?: ts.NodeArray }) + .typeParameters; + if (typeParameters !== undefined && typeParameters.nodes.length > 0) frameFence(); + } + } + return captures; +} + /** Registers (or returns) the GenericFnInfo of a qualifying implicit-any * function-value binding — implicitLocalFnNodeOf's companion, the * bindingGenericFnInfoOf shape: the info enters genericFnsBySymbol under @@ -2848,22 +2992,21 @@ export function bindingGenericFnInfoOf( if (existing) return existing; const name = (decl.name as ts.Identifier).text; if (fnNode.asteriskToken) lowerer.unsupported("SC1071", fnNode); - // Inside a function, the compiled instances are module functions, so - // the initializer may not use the enclosing frame. + // Inside a function, the compiled instances are module functions: the + // values the initializer reads from the enclosing frame travel as + // trailing parameters (FunctionCapture). `this`, `arguments` and an + // enclosing generic's type parameters have no value to pass and stay + // fenced. let functionScoped = false; + let captures: FrameCapture[] | undefined; for ( let n: ts.Node | undefined = decl.parent; n !== undefined && !ts.isSourceFile(n); n = n.parent ) { if (ts.isFunctionLike(n)) { - if (capturesEnclosingFrame(lowerer, fnNode, decl, fnNode)) { - lowerer.unsupported( - "SC1090", - fnNode, - `generic arrow/function-expression bindings that use the frames of their enclosing functions (the compiled instantiations of '${name}' are module functions and cannot capture its locals, parameters, type parameters or 'this' — pass them as arguments, or declare the binding at module scope)`, - ); - } + const found = enclosingFrameCaptures(lowerer, fnNode, decl, fnNode, name); + if (found.length > 0) captures = found; functionScoped = true; break; } @@ -2912,6 +3055,7 @@ export function bindingGenericFnInfoOf( typeParams, instances: new Map(), ...(functionScoped ? { functionScoped: true as const } : {}), + ...(captures ? { captures } : {}), bindingDecl: decl, }; lowerer.bindingGenericFns.set(fnNode, info); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-arrow-enclosing-captures.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-arrow-enclosing-captures.ts.json new file mode 100644 index 000000000..056ab519d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-arrow-enclosing-captures.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/generic-arrow-enclosing-captures.ts" + ], + "diags": [] +} diff --git a/tests/corpus/generic-arrow-enclosing-captures.ts b/tests/corpus/generic-arrow-enclosing-captures.ts new file mode 100644 index 000000000..3dac369da --- /dev/null +++ b/tests/corpus/generic-arrow-enclosing-captures.ts @@ -0,0 +1,74 @@ +// A generic arrow or function-expression binding declared inside a function +// may capture that function's values. Each monomorphized instance is still a +// module function: every captured binding becomes a leading parameter and the +// call sites pass the current value, so the instance reads what JavaScript's +// closure would read. +// +// The capture may be a parameter, a local, a local function declaration, or a +// binding read through a nested closure created around the call site. + +interface ClientIdentity { + id: string; + scope?: string; +} + +function mergeClientIdentity(base: ClientIdentity, other?: ClientIdentity): ClientIdentity { + return other === undefined ? base : { id: other.id, scope: other.scope ?? base.scope }; +} + +function withClientIdentity(identity: ClientIdentity, describe: (o: ClientIdentity | undefined) => string): string { + const withIdentity = (options: T | undefined): ClientIdentity => + mergeClientIdentity(identity, options?.identity); + return describe(withIdentity<{ identity?: ClientIdentity }>({ identity: undefined })); +} + +console.log(withClientIdentity({ id: "base", scope: "read" }, (o) => `${o?.id}/${o?.scope}`)); +console.log(withClientIdentity({ id: "base" }, (o) => (o === undefined ? "none" : `${o.id}/${o.scope ?? "-"}`))); + +// A captured local Set and an enclosing parameter, read from a narrower +// callback inside the generic body. +function physicalModels(models: { id: string }[], virtual: { id: string }[]): string[] { + const ids = new Set(virtual.map((model) => model.id)); + const physical = (list: readonly T[]): T[] => + list.filter((model) => !model.id.startsWith("virtual:") && !ids.has(model.id)); + return physical(models).map((model) => model.id); +} + +console.log(physicalModels([{ id: "a" }, { id: "virt" }, { id: "virtual:b" }], [{ id: "virt" }])); + +// The captured binding is a local function declaration, and the generic body +// calls it. +function listTwo(values: string[], key: string): string[] { + const label = (value: string): string => `${key}:${value}`; + const list = (items: readonly T[]): string[] => items.map((item) => label(item)); + return list(values); +} + +console.log(listTwo(["x", "y"], "k").join(",")); + +// An async generic arrow capturing a local Set; the call sits in a nested +// closure declared after the capture. +async function runTracked(work: Promise): Promise { + const trackedIds = new Set(); + const track = (id: number, item: Promise): Promise => { + trackedIds.add(id); + return item; + }; + const wrapped = (): Promise => track(1, work); + const value = await wrapped(); + console.log("tracked", trackedIds.size, trackedIds.has(1), value); + return value; +} + +void runTracked(Promise.resolve(7)); + +// Two instances of one generic arrow over different signatures share the +// capture plumbing; the capture value differs per enclosing call. +function twice(offset: number, value: string): string { + const add = (item: T): T | string => + (typeof item === "number" ? item + offset : `${item}#${offset}`) as T | string; + return `${add(value)}|${add(1)}`; +} + +console.log(twice(10, "v")); +console.log(twice(20, "w")); diff --git a/tests/diagnostics/generic-value-bindings.ts b/tests/diagnostics/generic-value-bindings.ts index 7c786c67c..18e41ee77 100644 --- a/tests/diagnostics/generic-value-bindings.ts +++ b/tests/diagnostics/generic-value-bindings.ts @@ -19,14 +19,26 @@ console.log(typeof stored); const flipAlias = flip; console.log(flipAlias(5)); -// Declared inside a function and reading its parameter: instances are -// module functions and cannot capture the enclosing frame. +// Declared inside a function and reading a binding that is written after +// the binding: the instance would hold a copy, while the closure reads +// whatever the write left behind. (A binding that is never reassigned is +// supported — it travels as a trailing parameter.) function outer(base: number): number { - const inner = (x: T): [T, number] => [x, base]; + let scale = base; + const inner = (x: T): [T, number] => [x, scale]; + scale += 1; return inner(3)[1]; } console.log(outer(4)); +// Declared inside a GENERIC function: the enclosing frame re-lowers per +// instantiation, so the shared instance cannot carry its type argument. +function outerGeneric(seed: U): string { + const inner = (x: T): string => `${String(x)}${String(seed)}`; + return inner(3); +} +console.log(outerGeneric(1)); + // Used as a value inside a function: each call of the enclosing function // makes a new function object (`make() !== make()`), and the compiled // instances are shared module functions. diff --git a/tests/harness/__snapshots__/generic-value-bindings.ts.txt b/tests/harness/__snapshots__/generic-value-bindings.ts.txt index 835b84fa7..28c1e9bc6 100644 --- a/tests/harness/__snapshots__/generic-value-bindings.ts.txt +++ b/tests/harness/__snapshots__/generic-value-bindings.ts.txt @@ -57,46 +57,62 @@ generic-value-bindings.ts:20:13 - error SC2004: uses of 'flipAlias' inherit the hint: the declaration of 'flipAlias' did not compile — fix the diagnostic reported there and these sites clear with it -generic-value-bindings.ts:25:17 - error SC1090: generic arrow/function-expression bindings that use the frames of their enclosing functions (the compiled instantiations of 'inner' are module functions and cannot capture its locals, parameters, type parameters or 'this' — pass them as arguments, or declare the binding at module scope) are not supported yet +generic-value-bindings.ts:28:17 - error SC1090: generic arrow/function-expression bindings that use the frames of their enclosing functions (the compiled instantiations of 'inner' are module functions and cannot capture its locals, parameters, type parameters or 'this' — pass them as arguments, or declare the binding at module scope) are not supported yet - 24 | function outer(base: number): number { - 25 | const inner = (x: T): [T, number] => [x, base]; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 26 | return inner(3)[1]; + 27 | let scale = base; + 28 | const inner = (x: T): [T, number] => [x, scale]; + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 29 | scale += 1; -generic-value-bindings.ts:26:10 - error SC2004: uses of 'inner' inherit the blocker on its declaration +generic-value-bindings.ts:30:10 - error SC2004: uses of 'inner' inherit the blocker on its declaration - 25 | const inner = (x: T): [T, number] => [x, base]; - 26 | return inner(3)[1]; + 29 | scale += 1; + 30 | return inner(3)[1]; | ^~~~~ - 27 | } + 31 | } hint: the declaration of 'inner' did not compile — fix the diagnostic reported there and these sites clear with it -generic-value-bindings.ts:35:10 - error SC1090: generic arrow/function-expression bindings declared inside functions as values (each call of the enclosing function makes a new 'same' function object, while its compiled instances are shared module functions — call 'same' directly, or declare it at module scope) are not supported yet +generic-value-bindings.ts:37:17 - error SC1090: generic arrow/function-expression bindings that use the frames of their enclosing functions (the compiled instantiations of 'inner' are module functions and cannot capture its locals, parameters, type parameters or 'this' — pass them as arguments, or declare the binding at module scope) are not supported yet (instantiating 'outerGeneric' with ) - 34 | const same = (x: T): T => x; - 35 | return same; + 36 | function outerGeneric(seed: U): string { + 37 | const inner = (x: T): string => `${String(x)}${String(seed)}`; + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 38 | return inner(3); + +generic-value-bindings.ts:38:10 - error SC2004: uses of 'inner' inherit the blocker on its declaration (instantiating 'outerGeneric' with ) + + 37 | const inner = (x: T): string => `${String(x)}${String(seed)}`; + 38 | return inner(3); + | ^~~~~ + 39 | } + + hint: the declaration of 'inner' did not compile — fix the diagnostic reported there and these sites clear with it + +generic-value-bindings.ts:47:10 - error SC1090: generic arrow/function-expression bindings declared inside functions as values (each call of the enclosing function makes a new 'same' function object, while its compiled instances are shared module functions — call 'same' directly, or declare it at module scope) are not supported yet + + 46 | const same = (x: T): T => x; + 47 | return same; | ^~~~~~~~~~~~ - 36 | } + 48 | } -generic-value-bindings.ts:48:13 - error SC1090: results that are themselves generic functions ('(y: U) => string' keeps its type parameters — no call-site instantiation pins them, and the returned value would need the producing call's frame; restructure to one generic function taking all arguments) are not supported yet +generic-value-bindings.ts:60:13 - error SC1090: results that are themselves generic functions ('(y: U) => string' keeps its type parameters — no call-site instantiation pins them, and the returned value would need the producing call's frame; restructure to one generic function taking all arguments) are not supported yet - 47 | const curried = (x: T) => (y: U): string => `${x}|${y}`; - 48 | console.log(curried(1)("a")); + 59 | const curried = (x: T) => (y: U): string => `${x}|${y}`; + 60 | console.log(curried(1)("a")); | ^~~~~~~~~~ - 49 | + 61 | -generic-value-bindings.ts:60:15 - error SC1090: calls of the generic method 'm' through this receiver (no compiled declaration with a body resolves statically here — ambient 'declare class' and interface-only methods are signature-only, and only class, static, and object-literal generic methods with bodies monomorphize) is not supported yet +generic-value-bindings.ts:72:15 - error SC1090: calls of the generic method 'm' through this receiver (no compiled declaration with a body resolves statically here — ambient 'declare class' and interface-only methods are signature-only, and only class, static, and object-literal generic methods with bodies monomorphize) is not supported yet - 59 | function throughParam(a: Amb): void { - 60 | console.log(a.m(4)); + 71 | function throughParam(a: Amb): void { + 72 | console.log(a.m(4)); | ^~~~~~ - 61 | } + 73 | } -generic-value-bindings.ts:71:32 - error SC1090: generic arrow/function-expression bindings declared inside functions that code can call before their declaration runs (the binding 'picked' is a 'var' in a branch or block, or is called from a place that can run first — declare it with const before its uses) are not supported yet +generic-value-bindings.ts:83:32 - error SC1090: generic arrow/function-expression bindings declared inside functions that code can call before their declaration runs (the binding 'picked' is a 'var' in a branch or block, or is called from a place that can run first — declare it with const before its uses) are not supported yet - 70 | } - 71 | const invoke = (): number => picked(1); + 82 | } + 83 | const invoke = (): number => picked(1); | ^~~~~~ - 72 | return invoke(); \ No newline at end of file + 84 | return invoke(); \ No newline at end of file diff --git a/tests/harness/corpus-costs/generic-arrow-enclosing-captures.ts.json b/tests/harness/corpus-costs/generic-arrow-enclosing-captures.ts.json new file mode 100644 index 000000000..5f3157836 --- /dev/null +++ b/tests/harness/corpus-costs/generic-arrow-enclosing-captures.ts.json @@ -0,0 +1 @@ +{ "plain": 5849, "san": 4152 } From 5f169ed6fe49040fac26cc104c1ce856e3f0df80 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 14:16:01 -0700 Subject: [PATCH 07/70] Complete function-value arguments in optional calls The `f?.()` branch of the optional-chain lowering forwarded a spread argument list through completeFuncValueArgs but mapped a plain argument list itself, so an omitted optional parameter was never padded with the callee's undefined arm. The lib-boundary arity fence then refused the call ("2 arguments where its lowered signature takes 3"), which is what `callbacks?.onRetryFinished?.(false, attempt)` hit: an unguarded call completes its arguments, a guarded one did not. Run both spellings through the same completion. It evaluates inside the guard, so a nullish callee still evaluates no argument, and a typed rest now packs from the guarded call too. Corpus: optional-chained callbacks with a trailing optional parameter and with a typed rest, through a present, absent, and undefined receiver. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit f574073e2392ec24cd2cfca61651759d3359e603) --- .../lowering/expressions/optional-chains.ts | 33 +++++------ .../function-value-arity-completion.ts.json | 6 ++ .../corpus/function-value-arity-completion.ts | 57 +++++++++++++++++++ .../function-value-arity-completion.ts.json | 1 + 4 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-value-arity-completion.ts.json create mode 100644 tests/corpus/function-value-arity-completion.ts create mode 100644 tests/harness/corpus-costs/function-value-arity-completion.ts.json diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 104d85a80..717fd8c66 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -1,5 +1,4 @@ import { isNavigatorGlobal } from "../builtins/navigator.js"; -import { isSafeToDiscard } from "./evaluation-safety.js"; import { completeFuncValueArgs } from "../call-arguments.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; @@ -449,24 +448,20 @@ export function lowerOptionalChain( // directly (no member dispatch exists to re-enter). if (dotNode === expr && ts.isCallExpression(expr)) { if (narrowed.kind !== "func") lowerer.badType(recvNode, lowerer.typeOf(recvNode)); - const params = narrowed.params; - // Forwarded fixed tuples (`f?.(...args)`) complete into the slots. - if (expr.arguments.nodes.some(ts.isSpreadElement)) { - const completed = completeFuncValueArgs(lowerer, expr, narrowed, loc); - const call: IrExpr = { - kind: "callValue", - callee: recvRef, - args: completed, - type: narrowed.ret, - loc, - }; - return lowerer.finishOptionalChain(expr, id, receiver, call, loc); - } - const args = expr.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, params[i])); - // Effect-free surplus arguments (a dropped `void` slot) are ignored. - while (narrowed.rest !== true && args.length > params.length && isSafeToDiscard(args.at(-1)!)) - args.pop(); - const body: IrExpr = { kind: "callValue", callee: recvRef, args, type: narrowed.ret, loc }; + // The same completed function-value ABI an unguarded call uses: an + // omitted optional argument still reaches the callee as undefined + // (`cb?.onRetryFinished?.(false, attempt)` must see its third + // parameter), a forwarded tuple completes into the slots, and an + // effect-free surplus argument is ignored. The completion runs INSIDE + // the guard, so a nullish callee still evaluates no argument. + const completed = completeFuncValueArgs(lowerer, expr, narrowed, loc); + const body: IrExpr = { + kind: "callValue", + callee: recvRef, + args: completed, + type: narrowed.ret, + loc, + }; return lowerer.finishOptionalChain(expr, id, receiver, body, loc); } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-value-arity-completion.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-value-arity-completion.ts.json new file mode 100644 index 000000000..5f4eb9487 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-value-arity-completion.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/function-value-arity-completion.ts" + ], + "diags": [] +} diff --git a/tests/corpus/function-value-arity-completion.ts b/tests/corpus/function-value-arity-completion.ts new file mode 100644 index 000000000..b2dddaa06 --- /dev/null +++ b/tests/corpus/function-value-arity-completion.ts @@ -0,0 +1,57 @@ +// Calling a function value short is legal TypeScript whenever the omitted +// suffix is optional: the native callee still receives every parameter, so +// the call completes the missing slots with undefined exactly as the JS call +// would. +// +// The same holds for a call through an optional-chained property +// (`cb?.onRetryFinished?.(false, attempt)`), which evaluates the callee and +// its arguments once, in order. + +interface RetryCallbacks { + onRetryFinished?: (success: boolean, attempt: number, finalError?: string) => void; + onAttempt?: (attempt: number, ...notes: string[]) => string; +} + +function finish(callbacks: RetryCallbacks | undefined, ok: boolean, attempt: number): string[] { + const log: string[] = []; + const sink: RetryCallbacks = { + onRetryFinished: (success, n, finalError) => { + log.push(`finished ${success ? "ok" : "no"} ${n} ${finalError === undefined ? "none" : finalError}`); + }, + onAttempt: (n, ...notes) => `${n}:${notes.join("+")}`, + }; + sink.onRetryFinished?.(ok, attempt); + sink.onRetryFinished?.(ok, attempt, "boom"); + callbacks?.onRetryFinished?.(ok, attempt + 1); + callbacks?.onAttempt?.(attempt, "a", "b"); + return log; +} + +console.log(finish(undefined, true, 1).join("; ")); +console.log( + finish( + { + onRetryFinished: (success, attempt, finalError) => { + console.log(`callback ${success} ${attempt} ${finalError ?? "-"}`); + }, + onAttempt: (attempt, ...notes) => { + console.log(`attempt ${attempt} ${notes.length}`); + return "ignored"; + }, + }, + false, + 2, + ).join("; "), +); + +// An omitted trailing argument for a callback whose parameter is optional in +// the middle of a longer signature. +type Reporter = (message: string, code?: number, detail?: string) => string; + +function report(to: Reporter): void { + console.log(to("first")); + console.log(to("second", 42)); + console.log(to("third", undefined, "why")); +} + +report((message, code, detail) => `${message}/${code ?? 0}/${detail ?? "-"}`); diff --git a/tests/harness/corpus-costs/function-value-arity-completion.ts.json b/tests/harness/corpus-costs/function-value-arity-completion.ts.json new file mode 100644 index 000000000..23a63e608 --- /dev/null +++ b/tests/harness/corpus-costs/function-value-arity-completion.ts.json @@ -0,0 +1 @@ +{ "plain": 5094, "san": 6883 } From 1846753a3c8bd6389f031e2c97dfe0f9e3870bd5 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 14:16:06 -0700 Subject: [PATCH 08/70] Pin declared-arity narrowing as a loud refusal, and the accepted shapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A function value's declared type may name fewer parameters than the function it holds, and a later widening call then passes arguments the declared type never spelled. JavaScript's function object carries all of its parameters, so nothing may be dropped on that path — that would be a silent divergence from Node. The narrowed store is already a named refusal (a compiled function value is one concrete native ABI: "'(string, number) => number' values where '(string) => number' is expected"), reported at the narrowing site for a binding and for an object-literal member slot. Record both in the diagnostics snapshot so the refusal cannot regress into a silent drop. Pin the shapes that DO keep every argument: a class member whose overloads spell fewer parameters than its implementation, the member read as a value (the read keeps the implementation's JS arity), and a value that entered through an untyped slot and is widened back. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 94d44e947c48dc0184c941d526380ebd1b1b443d) --- .../function-value-declared-arity.ts.json | 6 ++++ ...ion-value-declared-arity-narrowing.ts.json | 6 ++++ tests/corpus/function-value-declared-arity.ts | 31 +++++++++++++++++++ ...function-value-declared-arity-narrowing.ts | 25 +++++++++++++++ ...tion-value-declared-arity-narrowing.ts.txt | 13 ++++++++ .../function-value-declared-arity.ts.json | 1 + 6 files changed, 82 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-value-declared-arity.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-declared-arity-narrowing.ts.json create mode 100644 tests/corpus/function-value-declared-arity.ts create mode 100644 tests/diagnostics/function-value-declared-arity-narrowing.ts create mode 100644 tests/harness/__snapshots__/function-value-declared-arity-narrowing.ts.txt create mode 100644 tests/harness/corpus-costs/function-value-declared-arity.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-value-declared-arity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-value-declared-arity.ts.json new file mode 100644 index 000000000..630c6a4e3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-value-declared-arity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/function-value-declared-arity.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-declared-arity-narrowing.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-declared-arity-narrowing.ts.json new file mode 100644 index 000000000..ecea4a8c4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-declared-arity-narrowing.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/function-value-declared-arity-narrowing.ts" + ], + "diags": [] +} diff --git a/tests/corpus/function-value-declared-arity.ts b/tests/corpus/function-value-declared-arity.ts new file mode 100644 index 000000000..e9eeb529b --- /dev/null +++ b/tests/corpus/function-value-declared-arity.ts @@ -0,0 +1,31 @@ +// A function holds ONE implementation, and a declared type only constrains +// call sites. An overloaded member's implementation may declare more +// parameters than any single overload spells; every overload's arguments +// still reach it — and so do the arguments of a later WIDENED call, because +// the value read out of the member keeps the implementation's full arity +// (JavaScript has no arity check; a missing argument is just undefined). + +class Writer { + write(text: string): number; + write(text: string, times: number): number; + write(text: string, times?: number): number { + return text.length * (times === undefined ? 1 : times); + } +} + +const writer = new Writer(); +console.log(writer.write("abc")); +console.log(writer.write("abcd", 3)); + +// The member read as a value carries the same one implementation, so the +// two-argument call reaches its second parameter. +const writeValue = writer.write; +console.log(writeValue("abc")); +console.log(writeValue("abcd", 3)); + +// A value that entered through an untyped slot keeps the arity it was +// created with; widening it back delivers every argument. +const loose: any = (text: string, times: number) => text.length * times; +const widened = loose as (text: string, times: number) => number; +console.log(widened("abcd", 3)); +console.log(widened("xy", 10)); diff --git a/tests/diagnostics/function-value-declared-arity-narrowing.ts b/tests/diagnostics/function-value-declared-arity-narrowing.ts new file mode 100644 index 000000000..922b2eb51 --- /dev/null +++ b/tests/diagnostics/function-value-declared-arity-narrowing.ts @@ -0,0 +1,25 @@ +// Declared-arity narrowing: a function value stored under a type that names +// FEWER parameters than the value's own signature. Node's function object +// still carries every parameter, so a later widened call would pass all of +// its arguments (the surplus must not be dropped); a compiled function value +// is one concrete native ABI, so the narrowing store is refused at every +// observable use instead. +const impl = (text: string, times: number) => text.length * times; + +// (1) Narrowing at a binding, then calling through the narrow type. +const narrowed = impl as unknown as (text: string) => number; +console.log(narrowed("abc")); + +// (2) The same narrowing in an object literal's member slot, with the member +// read back and widened to the implementation's arity. +interface Sink { + write(text: string): number; +} + +function makeWriter(): Sink { + return { write: impl as unknown as (text: string) => number }; +} + +const sink = makeWriter(); +const widened = sink.write as (text: string, times: number) => number; +console.log(widened("abcd", 3)); diff --git a/tests/harness/__snapshots__/function-value-declared-arity-narrowing.ts.txt b/tests/harness/__snapshots__/function-value-declared-arity-narrowing.ts.txt new file mode 100644 index 000000000..e2163063f --- /dev/null +++ b/tests/harness/__snapshots__/function-value-declared-arity-narrowing.ts.txt @@ -0,0 +1,13 @@ +function-value-declared-arity-narrowing.ts:10:18 - error SC1090: '(string, number) => number' values where '(string) => number' is expected is not supported yet + + 9 | // (1) Narrowing at a binding, then calling through the narrow type. + 10 | const narrowed = impl as unknown as (text: string) => number; + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 11 | console.log(narrowed("abc")); + +function-value-declared-arity-narrowing.ts:20:19 - error SC1090: '(string, number) => number' values where '(string) => number' is expected is not supported yet + + 19 | function makeWriter(): Sink { + 20 | return { write: impl as unknown as (text: string) => number }; + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 21 | } \ No newline at end of file diff --git a/tests/harness/corpus-costs/function-value-declared-arity.ts.json b/tests/harness/corpus-costs/function-value-declared-arity.ts.json new file mode 100644 index 000000000..b99ff1c44 --- /dev/null +++ b/tests/harness/corpus-costs/function-value-declared-arity.ts.json @@ -0,0 +1 @@ +{ "plain": 5138, "san": 7006 } From 3e2b4ab25b88c027ba18ec795a9b9938a3f84c85 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 18:59:19 -0700 Subject: [PATCH 09/70] Share reassigned bindings with generic arrow instances through closures A generic arrow declared inside a function compiles to shared module instances that took the enclosing values they read as trailing parameters: a copy, so any binding written in the file was refused, including a counter the arrow itself increments. When one of its captures is reassigned, the instance is now a lifted function that receives every capture through a closure environment, and each call site calls a closure over its own boxes, the representation ordinary closures use. Writes from the instance, the frame and other closures meet in the same box, as in Node. Arrows whose captures never change keep the trailing parameters. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/generic-functions.ts | 108 ++++++++++++++++-- .../generic-arrow-mutable-captures.ts.json | 6 + .../corpus/generic-arrow-mutable-captures.ts | 86 ++++++++++++++ tests/diagnostics/generic-value-bindings.ts | 11 +- .../generic-value-bindings.ts.txt | 64 +++++------ .../generic-arrow-mutable-captures.ts.json | 1 + 6 files changed, 226 insertions(+), 50 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-arrow-mutable-captures.ts.json create mode 100644 tests/corpus/generic-arrow-mutable-captures.ts create mode 100644 tests/harness/corpus-costs/generic-arrow-mutable-captures.ts.json diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 8397bbc0a..50f5930f4 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -48,6 +48,7 @@ import { promiseCarriesDyn, } from "./function-returns.js"; import { bindingNeverReassigned, stripValueWrappers } from "./binding-analysis.js"; +import { declareContextLocal } from "./function-context.js"; import { extendInstantiationPath, MAX_INSTANTIATION_RECURSION, @@ -109,6 +110,13 @@ export interface GenericFnInfo { * bindings qualify — a shared copy is exact exactly as long as nothing * writes the original. */ captures?: FrameCapture[]; + /** "env": some capture is reassigned, so a copy cannot stand in for it. + * The instances are then LIFTED functions that receive every capture + * through a closure environment (boxed, shared with the declaring frame + * and every ordinary closure over it), and each call site calls a fresh + * closure over its own boxes. Absent: captures travel as trailing + * parameters. */ + captureMode?: "env"; /** The declaration of a generic arrow/function-expression binding. */ bindingDecl?: ts.VariableDeclaration; /** A module-scope binding that code can use before its declaration @@ -148,6 +156,8 @@ export interface FrameCapture { nameNode: ts.Node; name: string; type: IrType; + /** Something in the file writes the binding (bindingNeverReassigned). */ + reassigned: boolean; } export interface GenericInstance { @@ -304,6 +314,52 @@ export function lowerGenericCall( ? implicitCallInstance(lowerer, expr, info) : genericCallInstance(lowerer, expr, info); const args = lowerer.completeArgs(expr.arguments.nodes, instance.params, loc, expr); + if (info.captureMode === "env") { + // The environment form: a closure over this call site's boxes of the + // captured bindings (resolveKey threads them through any closures in + // between and boxes the origin), called at once. Arguments evaluate + // after the callee, as in JavaScript; building the closure only reads + // the boxes. + if ( + instance.params.some( + (p) => p.mode === "rest" || p.mode === "dynRest" || p.mode === "arguments", + ) + ) { + lowerer.unsupported( + "SC1090", + expr, + `generic arrow/function-expression bindings with rest parameters that write the frames of their enclosing functions ('${info.baseName}')`, + ); + } + const sources = info.captures!.map((capture) => { + const local = lowerer.resolveKey(capture.symbol, expr); + if (local === null || !typeEquals(local.type, capture.type)) { + lowerer.unsupported( + "SC1090", + expr, + `generic arrow/function-expression bindings sharing '${capture.name}' with their enclosing function when its storage differs from its declared type ('${info.baseName}')`, + ); + } + // An origin read from its own frame is not boxed by resolveKey; the + // closure needs the box (captureContextBinding's origin rule). + local.boxed = true; + return local.id; + }); + const call: IrExpr & { kind: "callValue" } = { + kind: "callValue", + callee: { + kind: "closure", + fnName: instance.name, + captures: sources, + type: funcTypeFromParamShapes(instance.params, instance.returnType), + loc, + }, + args, + type: instance.returnType, + loc, + }; + return guardEarlyBindingCall(lowerer, info, expr, call); + } // The enclosing-frame values follow the declared arguments, the order // the instance takes them in (lowerGenericInstance). for (const capture of info.captures ?? []) { @@ -1226,7 +1282,10 @@ export function lowerGenericInstance( let bodyReturn = isGenerator ? lowerer.genBodyReturnType(inst.returnType) : lowerer.bodyReturnType(isAsync, inst.returnType); - const fnCtx = newFnCtx(false, null, null, bodyReturn); + // The environment form lowers as a LIFTED function: its captures arrive + // in the closure environment, never as parameters. + const envForm = info.captureMode === "env"; + const fnCtx = newFnCtx(envForm, null, null, bodyReturn); fnCtx.isAsync = isAsync; // Instances lower on demand, on top of whichever caller reached them. fnCtx.thisBoundary = true; @@ -1270,6 +1329,26 @@ export function lowerGenericInstance( const thisLocal = lowerer.declareThis(thisType); params.push({ localId: thisLocal.id, name: "this", type: thisType }); } + // Environment form: one boxed capture entry per captured binding, in + // FrameCapture order (the call sites' closure caps[] order), keyed by + // the binding's symbol so the body (parameter defaults included) + // reads and writes the shared box. Pre-registering them also keeps + // resolution from walking past this instance's frame. + if (envForm) { + for (const capture of info.captures!) { + const entry = declareContextLocal( + fnCtx, + capture.name, + capture.type, + capture.reassigned, + undefined, + undefined, + ); + entry.boxed = true; + fnCtx.captureBySymbol.set(capture.symbol, entry); + fnCtx.captures!.push({ localId: entry.id, name: entry.name, type: entry.type }); + } + } // Default-param initializers lower per instance, with the bindings // threaded — a default mentioning T resolves like any body expression. const declared = lowerer.declareParams(decl.parameters.nodes, inst.params); @@ -1278,7 +1357,7 @@ export function lowerGenericInstance( // declared ones: a call lowers its arguments first (JavaScript's // order), then reads the captures. Binding each local to the captured // symbol is what lets the body resolve the reference. - for (const capture of info.captures ?? []) { + for (const capture of envForm ? [] : (info.captures ?? [])) { const local = lowerer.declareLocal(capture.nameNode, capture.name, capture.type, false); params.push({ localId: local.id, name: capture.name, type: capture.type }); } @@ -1336,6 +1415,7 @@ export function lowerGenericInstance( params, returnType: bodyReturn, locals: lowerer.ctx.locals, + ...(envForm ? { captures: fnCtx.captures! } : {}), body, loc: locOf(decl), }; @@ -2678,9 +2758,10 @@ function capturesEnclosingFrame( * Module-scope declarations and the binding itself are reachable from the * module functions the instances compile to, so they are not captures; nor * are type-only declarations, whose mapping is static. `this`, `super`, - * `arguments`, `new.target`, and an enclosing generic's type parameter (or - * any binding the closure could observe a LATER write of) have no value a - * parameter can stand in for and stay fenced. */ + * `arguments`, `new.target`, and an enclosing generic's type parameter + * have no value a parameter can stand in for and stay fenced. A + * reassigned binding is shared through the environment form instead of + * copied. */ function enclosingFrameCaptures( lowerer: Lowerer, fn: ts.FunctionExpression | ts.ArrowFunction, @@ -2734,10 +2815,10 @@ function enclosingFrameCaptures( if ((s.flags & ts.SymbolFlags.TypeParameter) !== 0) frameFence(); // Type-only declarations cost no runtime slot. if ((s.flags & ts.SymbolFlags.Value) === 0) break; - // The instance holds a COPY of the value, exact only while the - // original never changes: a write anywhere in the file could - // make the closure observe a different binding state later. - if (!bindingNeverReassigned(lowerer, s, d)) frameFence(); + // A trailing-parameter COPY is exact only while the original + // never changes; a binding written anywhere in the file puts the + // whole binding in the environment form (GenericFnInfo.captureMode). + const reassigned = !bindingNeverReassigned(lowerer, s, d); // ...and exact only once the original is INITIALIZED. A capture // declared after the binding could be read from a call that runs // before its initializer — a temporal dead zone JavaScript would @@ -2756,6 +2837,7 @@ function enclosingFrameCaptures( nameNode, name: ts.isIdentifier(nameNode) ? nameNode.text : ts.symbolName(s), type, + reassigned, }); seen.add(s); break; @@ -2994,8 +3076,9 @@ export function bindingGenericFnInfoOf( if (fnNode.asteriskToken) lowerer.unsupported("SC1071", fnNode); // Inside a function, the compiled instances are module functions: the // values the initializer reads from the enclosing frame travel as - // trailing parameters (FunctionCapture). `this`, `arguments` and an - // enclosing generic's type parameters have no value to pass and stay + // trailing parameters (FrameCapture), or through a closure environment + // when one of them is reassigned (captureMode). `this`, `arguments` and + // an enclosing generic's type parameters have no value to pass and stay // fenced. let functionScoped = false; let captures: FrameCapture[] | undefined; @@ -3056,6 +3139,7 @@ export function bindingGenericFnInfoOf( instances: new Map(), ...(functionScoped ? { functionScoped: true as const } : {}), ...(captures ? { captures } : {}), + ...(captures?.some((capture) => capture.reassigned) ? { captureMode: "env" as const } : {}), bindingDecl: decl, }; lowerer.bindingGenericFns.set(fnNode, info); @@ -3239,7 +3323,7 @@ function guardEarlyBindingCall( lowerer: Lowerer, info: GenericFnInfo, expr: ts.CallExpression, - call: IrExpr & { kind: "call" }, + call: IrExpr & { kind: "call" | "callValue" }, ): IrExpr { if (info.bindingDecl === undefined || runsAfterDeclaration(lowerer, info, expr)) return call; const loc = call.loc; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-arrow-mutable-captures.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-arrow-mutable-captures.ts.json new file mode 100644 index 000000000..401f68286 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-arrow-mutable-captures.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/generic-arrow-mutable-captures.ts" + ], + "diags": [] +} diff --git a/tests/corpus/generic-arrow-mutable-captures.ts b/tests/corpus/generic-arrow-mutable-captures.ts new file mode 100644 index 000000000..50cb53e6e --- /dev/null +++ b/tests/corpus/generic-arrow-mutable-captures.ts @@ -0,0 +1,86 @@ +// Generic arrow bindings inside functions that WRITE a binding of the +// enclosing frame (`++callCount`, `total += n`) or read one the frame +// reassigns. Every instance shares the frame's binding with the frame +// and with ordinary closures over it: writes from either side are seen by +// the other, as in Node. +interface Result { + ok: boolean; +} + +// The counter idiom: an async generic arrow numbering its calls, called +// from closures in a returned record, with a reassignment outside. +function makeRunner(prefix: string): { run: (n: number) => Promise } { + let callCount = 0; + const ids: string[] = []; + const runCall = async (name: string, make: () => Promise): Promise => { + const id = `${prefix}/${name}/${++callCount}`; + ids.push(id); + const result = await make(); + ids.push(`${id}:${result.ok}`); + return result; + }; + return { + run: async (n) => { + for (let i = 0; i < n; i++) { + await runCall("a", async () => ({ ok: true })); + const b = await runCall("b", async () => ({ ok: false, extra: i })); + ids.push(`extra=${b.extra}`); + } + callCount += 100; + await runCall("c", async () => ({ ok: true })); + return ids; + }, + }; +} + +// Sync: two instances (different T) sharing two captured lets, a call from +// the declaring frame and from a nested arrow, a parameter captured and +// reassigned, and a frame write between calls. +function accumulate(start: number): string { + let total = start; + let last = "none"; + const add = (item: T): T => { + total += item.n; + last = `${item.n}@${start}`; + return item; + }; + add({ n: 1 }); + const later = (): { n: number; tag: string } => add({ n: 2, tag: "x" }); + const tagged = later(); + total = total * 10; + start = -1; + add({ n: 3 }); + return `${total} ${last} ${tagged.tag}`; +} + +// A generic arrow that reads a binding only the frame writes, called in a +// loop: each call sees the current value, never a copy from an earlier one. +function snapshots(): string { + let step = 0; + const seen: string[] = []; + const note = (value: T): T => { + seen.push(`${step}:${String(value)}`); + return value; + }; + for (; step < 3; step++) note(step % 2 === 0 ? "even" : step); + note(true); + return seen.join(" "); +} + +// A recursive generic arrow over a captured counter. +function depth(): number { + let calls = 0; + const walk = (items: T[], index: number): number => { + calls++; + return index >= items.length ? 0 : 1 + walk(items, index + 1); + }; + return walk(["a", "b", "c"], 0) * 10 + calls; +} + +async function main(): Promise { + console.log((await makeRunner("p").run(2)).join(" ")); + console.log(accumulate(5)); + console.log(snapshots()); + console.log(depth()); +} +main(); diff --git a/tests/diagnostics/generic-value-bindings.ts b/tests/diagnostics/generic-value-bindings.ts index 18e41ee77..e13f3d9e2 100644 --- a/tests/diagnostics/generic-value-bindings.ts +++ b/tests/diagnostics/generic-value-bindings.ts @@ -19,14 +19,13 @@ console.log(typeof stored); const flipAlias = flip; console.log(flipAlias(5)); -// Declared inside a function and reading a binding that is written after -// the binding: the instance would hold a copy, while the closure reads -// whatever the write left behind. (A binding that is never reassigned is -// supported — it travels as a trailing parameter.) +// Declared inside a function and reading a binding declared AFTER it: a +// call could run before the binding's initializer (a temporal dead zone). +// (Bindings declared before it are supported — copied as trailing +// parameters, or shared through a closure environment when reassigned.) function outer(base: number): number { - let scale = base; const inner = (x: T): [T, number] => [x, scale]; - scale += 1; + const scale = base + 1; return inner(3)[1]; } console.log(outer(4)); diff --git a/tests/harness/__snapshots__/generic-value-bindings.ts.txt b/tests/harness/__snapshots__/generic-value-bindings.ts.txt index 28c1e9bc6..d09dcce80 100644 --- a/tests/harness/__snapshots__/generic-value-bindings.ts.txt +++ b/tests/harness/__snapshots__/generic-value-bindings.ts.txt @@ -57,62 +57,62 @@ generic-value-bindings.ts:20:13 - error SC2004: uses of 'flipAlias' inherit the hint: the declaration of 'flipAlias' did not compile — fix the diagnostic reported there and these sites clear with it -generic-value-bindings.ts:28:17 - error SC1090: generic arrow/function-expression bindings that use the frames of their enclosing functions (the compiled instantiations of 'inner' are module functions and cannot capture its locals, parameters, type parameters or 'this' — pass them as arguments, or declare the binding at module scope) are not supported yet +generic-value-bindings.ts:27:17 - error SC1090: generic arrow/function-expression bindings that use the frames of their enclosing functions (the compiled instantiations of 'inner' are module functions and cannot capture its locals, parameters, type parameters or 'this' — pass them as arguments, or declare the binding at module scope) are not supported yet - 27 | let scale = base; - 28 | const inner = (x: T): [T, number] => [x, scale]; + 26 | function outer(base: number): number { + 27 | const inner = (x: T): [T, number] => [x, scale]; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 29 | scale += 1; + 28 | const scale = base + 1; -generic-value-bindings.ts:30:10 - error SC2004: uses of 'inner' inherit the blocker on its declaration +generic-value-bindings.ts:29:10 - error SC2004: uses of 'inner' inherit the blocker on its declaration - 29 | scale += 1; - 30 | return inner(3)[1]; + 28 | const scale = base + 1; + 29 | return inner(3)[1]; | ^~~~~ - 31 | } + 30 | } hint: the declaration of 'inner' did not compile — fix the diagnostic reported there and these sites clear with it -generic-value-bindings.ts:37:17 - error SC1090: generic arrow/function-expression bindings that use the frames of their enclosing functions (the compiled instantiations of 'inner' are module functions and cannot capture its locals, parameters, type parameters or 'this' — pass them as arguments, or declare the binding at module scope) are not supported yet (instantiating 'outerGeneric' with ) +generic-value-bindings.ts:36:17 - error SC1090: generic arrow/function-expression bindings that use the frames of their enclosing functions (the compiled instantiations of 'inner' are module functions and cannot capture its locals, parameters, type parameters or 'this' — pass them as arguments, or declare the binding at module scope) are not supported yet (instantiating 'outerGeneric' with ) - 36 | function outerGeneric(seed: U): string { - 37 | const inner = (x: T): string => `${String(x)}${String(seed)}`; + 35 | function outerGeneric(seed: U): string { + 36 | const inner = (x: T): string => `${String(x)}${String(seed)}`; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 38 | return inner(3); + 37 | return inner(3); -generic-value-bindings.ts:38:10 - error SC2004: uses of 'inner' inherit the blocker on its declaration (instantiating 'outerGeneric' with ) +generic-value-bindings.ts:37:10 - error SC2004: uses of 'inner' inherit the blocker on its declaration (instantiating 'outerGeneric' with ) - 37 | const inner = (x: T): string => `${String(x)}${String(seed)}`; - 38 | return inner(3); + 36 | const inner = (x: T): string => `${String(x)}${String(seed)}`; + 37 | return inner(3); | ^~~~~ - 39 | } + 38 | } hint: the declaration of 'inner' did not compile — fix the diagnostic reported there and these sites clear with it -generic-value-bindings.ts:47:10 - error SC1090: generic arrow/function-expression bindings declared inside functions as values (each call of the enclosing function makes a new 'same' function object, while its compiled instances are shared module functions — call 'same' directly, or declare it at module scope) are not supported yet +generic-value-bindings.ts:46:10 - error SC1090: generic arrow/function-expression bindings declared inside functions as values (each call of the enclosing function makes a new 'same' function object, while its compiled instances are shared module functions — call 'same' directly, or declare it at module scope) are not supported yet - 46 | const same = (x: T): T => x; - 47 | return same; + 45 | const same = (x: T): T => x; + 46 | return same; | ^~~~~~~~~~~~ - 48 | } + 47 | } -generic-value-bindings.ts:60:13 - error SC1090: results that are themselves generic functions ('(y: U) => string' keeps its type parameters — no call-site instantiation pins them, and the returned value would need the producing call's frame; restructure to one generic function taking all arguments) are not supported yet +generic-value-bindings.ts:59:13 - error SC1090: results that are themselves generic functions ('(y: U) => string' keeps its type parameters — no call-site instantiation pins them, and the returned value would need the producing call's frame; restructure to one generic function taking all arguments) are not supported yet - 59 | const curried = (x: T) => (y: U): string => `${x}|${y}`; - 60 | console.log(curried(1)("a")); + 58 | const curried = (x: T) => (y: U): string => `${x}|${y}`; + 59 | console.log(curried(1)("a")); | ^~~~~~~~~~ - 61 | + 60 | -generic-value-bindings.ts:72:15 - error SC1090: calls of the generic method 'm' through this receiver (no compiled declaration with a body resolves statically here — ambient 'declare class' and interface-only methods are signature-only, and only class, static, and object-literal generic methods with bodies monomorphize) is not supported yet +generic-value-bindings.ts:71:15 - error SC1090: calls of the generic method 'm' through this receiver (no compiled declaration with a body resolves statically here — ambient 'declare class' and interface-only methods are signature-only, and only class, static, and object-literal generic methods with bodies monomorphize) is not supported yet - 71 | function throughParam(a: Amb): void { - 72 | console.log(a.m(4)); + 70 | function throughParam(a: Amb): void { + 71 | console.log(a.m(4)); | ^~~~~~ - 73 | } + 72 | } -generic-value-bindings.ts:83:32 - error SC1090: generic arrow/function-expression bindings declared inside functions that code can call before their declaration runs (the binding 'picked' is a 'var' in a branch or block, or is called from a place that can run first — declare it with const before its uses) are not supported yet +generic-value-bindings.ts:82:32 - error SC1090: generic arrow/function-expression bindings declared inside functions that code can call before their declaration runs (the binding 'picked' is a 'var' in a branch or block, or is called from a place that can run first — declare it with const before its uses) are not supported yet - 82 | } - 83 | const invoke = (): number => picked(1); + 81 | } + 82 | const invoke = (): number => picked(1); | ^~~~~~ - 84 | return invoke(); \ No newline at end of file + 83 | return invoke(); \ No newline at end of file diff --git a/tests/harness/corpus-costs/generic-arrow-mutable-captures.ts.json b/tests/harness/corpus-costs/generic-arrow-mutable-captures.ts.json new file mode 100644 index 000000000..50963cb4d --- /dev/null +++ b/tests/harness/corpus-costs/generic-arrow-mutable-captures.ts.json @@ -0,0 +1 @@ +{ "plain": 3793, "san": 7466 } From d540a649e6a3563357d66d73addd7c9d2628e3c2 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 19:41:17 -0700 Subject: [PATCH 10/70] fs.watch: hand listeners the changed entry name and an EventEmitter surface An fs.watch listener may now take (), (eventType), or (eventType, filename). On a directory watch the filename is the entry that changed; a file watch names the watched file's basename. The parameter must admit null (Node passes null when the platform names nothing). macOS directory watches that ask for the name are refused with SC2020 until the FSEvents slice lands. FSWatcher gains its EventEmitter surface for "change", "error" and "close" (on, once, addListener, prependListener, prependOnceListener, off, removeListener). The name and error adapters are emitted per program because each builds the listener's own parameter shapes, and the erased `string | null` nullable union takes the payload directly instead of a union box. The runtime keeps one listener list per event with the once flag and prepend ordering; a once listener's registration reference leaves when it fires. Co-Authored-By: Claude Opus 5.5 --- .../compiler/ambient/node-fallback/fs.d.ts | 28 +- packages/compiler/src/backend/llvm/emitter.ts | 10 + .../src/backend/llvm/expr-stream-callbacks.ts | 149 +++++++++- .../src/backend/llvm/lib-filesystem.ts | 83 +++++- .../frontend/lowering/builtins/filesystem.ts | 128 +++++++-- packages/compiler/src/ir/builtin-effects.ts | 2 + .../compiler/src/ir/builtin-signatures.ts | 7 + packages/runtime/src/scr_watch.c | 270 ++++++++++++++++-- packages/runtime/src/scr_watch.h | 41 ++- 9 files changed, 649 insertions(+), 69 deletions(-) diff --git a/packages/compiler/ambient/node-fallback/fs.d.ts b/packages/compiler/ambient/node-fallback/fs.d.ts index 534fad940..46c38a315 100644 --- a/packages/compiler/ambient/node-fallback/fs.d.ts +++ b/packages/compiler/ambient/node-fallback/fs.d.ts @@ -555,11 +555,29 @@ declare module "node:fs" { ): void; /* fs.watch — the file-watching slice (scr_watch.c, kqueue EVFILT_VNODE): * "rename"/"change" events on ONE path, watcher.close() to stop. The - * listener takes zero parameters or the eventType string (the filename - * parameter has no lowering); an open watcher keeps the loop alive. A - * path that cannot be opened THROWS Node's fs error synchronously. */ + * listener takes (), (eventType) or (eventType, filename); an open + * watcher keeps the loop alive. A path that cannot be opened THROWS + * Node's fs error synchronously. */ + export type WatchEventType = "rename" | "change"; + /* (eventType, filename): filename is the changed entry's name (null when + * the platform does not provide one). */ + export type WatchListener = (event: WatchEventType, filename: T | null) => void; export interface FSWatcher { close(): void; + on(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; + on(event: "error", listener: (error: Error) => void): this; + on(event: "close", listener: () => void): this; + once(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; + once(event: "error", listener: (error: Error) => void): this; + once(event: "close", listener: () => void): this; + addListener( + event: "change", + listener: (eventType: string, filename: string | Buffer) => void, + ): this; + addListener(event: "error", listener: (error: Error) => void): this; + addListener(event: "close", listener: () => void): this; + off(event: "change" | "error" | "close", listener: (...args: any[]) => void): this; + removeListener(event: "change" | "error" | "close", listener: (...args: any[]) => void): this; } /* The options-record stance (see http.RequestOptions): persistent: true, * recursive: false, and encoding: "utf8" state the lowered behavior and @@ -571,11 +589,11 @@ declare module "node:fs" { encoding?: string; [option: string]: unknown; } - export function watch(path: string, listener?: (eventType: string) => void): FSWatcher; + export function watch(path: string, listener?: WatchListener): FSWatcher; export function watch( path: string, options: WatchOptions, - listener?: (eventType: string) => void, + listener?: WatchListener, ): FSWatcher; /* fs.promises IS the fs/promises module (Node's rule) — the namespace- * import form `fs.promises.readFile(...)` lowers through the same diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 6980c8f45..1addb5cb7 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -285,6 +285,8 @@ import { cryptoIntThunkFor, type BytesThunkValue, fsRenameThunkFor, + watchNameThunkFor, + watchErrorThunkFor, streamCbThunkFor, zlibBytesThunkFor, } from "./expr-stream-callbacks.js"; @@ -8157,6 +8159,14 @@ export class LlEmitter { return fsRenameThunkFor(this, cbT); } + watchNameThunkFor(cbT: IrType & { kind: "func" }): string { + return watchNameThunkFor(this, cbT); + } + + watchErrorThunkFor(cbT: IrType & { kind: "func" }): string { + return watchErrorThunkFor(this, cbT); + } + cryptoBytesThunkFor( cbT: IrType & { kind: "func" }, arrayBuffer = false, diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 3d9d8da76..00ee4d636 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -1,7 +1,7 @@ /* Focused LLVM expression emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; -import { type IrType, isRefCounted, typeKey } from "../../ir/ir.js"; -import { FN_ATTRS, releaseSym, retainSym, traceArg } from "./shapes.js"; +import { type IrType, isRefCounted, STRING, typeKey } from "../../ir/ir.js"; +import { FN_ATTRS, releaseSym, retainSym, traceArg, vAdapters } from "./shapes.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { unionPeekLines, unionTagLines } from "./union-repr.js"; import { registerReplay } from "./emit-journal.js"; @@ -966,6 +966,151 @@ registerReplay("streamDoneFnFor", (host, args) => registerReplay("fsRenameThunkFor", (host, args) => fsRenameThunkFor(host, args[0] as IrType & { kind: "func" }), ); +/** The (eventType, filename) fs.watch listener adapter: the runtime hands + * the borrowed C strings, the thunk builds what the listener declared. The + * listener owns its +1 arguments (the universal convention + * scr_watch_thunk_event follows). */ +export function watchNameThunkFor( + host: LlvmEmitterContext, + cbT: IrType & { kind: "func" }, +): string { + const key = `watchname:${typeKey(cbT)}`; + let sym = host.resolveThunks.get(key); + if (sym) return sym; + sym = host.contentSymbol("sc_watchname_", key); + host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "watchNameThunkFor", [cbT]); + const S = host.sizeType; + host.declare(`declare ${S} @strlen(ptr)`); + host.declare(`declare ptr @scr_str_new(ptr, ${S})`); + host.declare(`declare void @scr_str_release(ptr)`); + const d: string[] = [ + `define internal void @${sym}(ptr %cb, ptr %event, ptr %name) ${FN_ATTRS} { ; fs.watch listener ${typeKey(cbT)}`, + `entry:`, + ` %evlen = call ${S} @strlen(ptr %event)`, + ` %evstr = call ptr @scr_str_new(ptr %event, ${S} %evlen)`, + ` %slot = alloca ptr`, + ` %has = icmp ne ptr %name, null`, + ` br i1 %has, label %yes, label %no`, + `yes:`, + ]; + const nameParam = cbT.params[1]!; + if (nameParam.kind === "dyn") { + host.declare(`declare ptr @scr_dyn_new_null()`); + host.declare(`declare ptr @scr_dyn_new_str(ptr)`); + d.push( + ` %nlen = call ${S} @strlen(ptr %name)`, + ` %nstr = call ptr @scr_str_new(ptr %name, ${S} %nlen)`, + ` %ndyn = call ptr @scr_dyn_new_str(ptr %nstr)`, + ` call void @scr_str_release(ptr %nstr)`, + ` store ptr %ndyn, ptr %slot`, + ` br label %go`, + `no:`, + ` %dn = call ptr @scr_dyn_new_null()`, + ` store ptr %dn, ptr %slot`, + ); + } else { + if (nameParam.kind !== "union") + throw new InternalCompilerError("llvm emitter bug: fs.watch filename param is not a union"); + const def = host.unionsById.get(nameParam.unionId); + const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; + const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; + if (strTag < 0 || nullTag < 0) + throw new InternalCompilerError("llvm emitter bug: fs.watch filename union lacks its arms"); + if (host.nullableUnions.has(nameParam.unionId)) { + // `string | null` erases: the owned payload IS the union's value. + d.push( + ` %nlen = call ${S} @strlen(ptr %name)`, + ` %nstr = call ptr @scr_str_new(ptr %name, ${S} %nlen)`, + ` store ptr %nstr, ptr %slot`, + ` br label %go`, + `no:`, + ` store ptr null, ptr %slot`, + ); + } else { + const rc = vAdapters(host.shapeHost, STRING); + host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`); + d.push( + ` %nlen = call ${S} @strlen(ptr %name)`, + ` %nstr = call ptr @scr_str_new(ptr %name, ${S} %nlen)`, + ` %nu = call ptr @scr_union_new_ref(i32 ${strTag}, ptr %nstr, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, STRING)})`, + ` store ptr %nu, ptr %slot`, + ` br label %go`, + `no:`, + ` store ptr ${host.unitInstanceRef(nameParam.unionId, nullTag)}, ptr %slot`, + ); + } + } + let ev: string; + if (cbT.params[0]?.kind === "dyn") { + host.declare(`declare ptr @scr_dyn_new_str(ptr)`); + d.push(` br label %go`, `go:`, ` %evd = call ptr @scr_dyn_new_str(ptr %evstr)`); + ev = "%evd"; + } else { + d.push(` br label %go`, `go:`); + ev = "%evstr"; + } + d.push( + ` %arg = load ptr, ptr %slot`, + ` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`, + ` %fn = load ptr, ptr %fnp`, + ` call void %fn(ptr %cb, ptr ${ev}, ptr %arg)`, + ` ret void`, + `}`, + ``, + ); + host.resolveThunkDefs.push(...d); + host.journal?.built(); + return sym; +} + +/** The FSWatcher 'error' listener adapter: the runtime hands a borrowed + * ScrError, the listener receives the declared dyn value or an Error + * object it owns. A zero-parameter listener uses the runtime's + * scr_watch_err_thunk0 and never reaches here. */ +export function watchErrorThunkFor( + host: LlvmEmitterContext, + cbT: IrType & { kind: "func" }, +): string { + const key = `watcherr:${typeKey(cbT)}`; + let sym = host.resolveThunks.get(key); + if (sym) return sym; + sym = host.contentSymbol("sc_watcherr_", key); + host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "watchErrorThunkFor", [cbT]); + const param = cbT.params[0]!; + const d: string[] = [ + `define internal void @${sym}(ptr %cb, ptr %err) ${FN_ATTRS} { ; FSWatcher error listener ${typeKey(cbT)}`, + `entry:`, + ]; + if (param.kind === "dyn") { + host.declare(`declare ptr @scr_dyn_from_error(ptr)`); + d.push(` %de = call ptr @scr_dyn_from_error(ptr %err)`); + } else if (param.kind === "object" && param.className === "%Error") { + host.declare(`declare ptr @scr_error_retain(ptr)`); + d.push(` %de = call ptr @scr_error_retain(ptr %err)`); + } else { + throw new InternalCompilerError("llvm emitter bug: FSWatcher error param is not an Error"); + } + d.push( + ` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`, + ` %fn = load ptr, ptr %fnp`, + ` call void %fn(ptr %cb, ptr %de)`, + ` ret void`, + `}`, + ``, + ); + host.resolveThunkDefs.push(...d); + host.journal?.built(); + return sym; +} + +registerReplay("watchNameThunkFor", (host, args) => + watchNameThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("watchErrorThunkFor", (host, args) => + watchErrorThunkFor(host, args[0] as IrType & { kind: "func" }), +); registerReplay("cryptoIntThunkFor", (host, args) => cryptoIntThunkFor(host, args[0] as IrType & { kind: "func" }), ); diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 9174f4775..310f2cb57 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -448,17 +448,19 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): // loop (usesTimers). host.usesTimers = true; const path = host.emitExpr(e.args[0]!); - host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr)`); + host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr, i1 zeroext)`); const t = B.tmp(); - B.line(`${t} = call ptr @scr_fs_watch(ptr ${path.name}, ptr null, ptr null)`); + B.line(`${t} = call ptr @scr_fs_watch(ptr ${path.name}, ptr null, ptr null, i1 false)`); const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(); return out; } if (e.fn === "fs.watchCb") { // The callback MOVES into the watcher's registry; the adapter is - // runtime-provided per listener shape (zero-param, or the eventType - // string). May throw (ENOENT) — the standard pending check. + // runtime-provided per listener shape (zero-param, the eventType + // string, or the compiler's (eventType, filename) adapter). May + // throw (ENOENT, or the macOS directory-names refusal) — the + // standard pending check. host.usesTimers = true; const cbT = e.args[1]!.type; if (cbT.kind !== "func") @@ -466,15 +468,80 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const path = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); host.toHeap(cb); - const adapter = cbT.params.length === 0 ? "scr_watch_thunk0" : "scr_watch_thunk_event"; - host.declare(`declare void @${adapter}(ptr, ptr)`); - host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr)`); + const adapter = + cbT.params.length === 0 + ? "scr_watch_thunk0" + : cbT.params.length === 1 + ? "scr_watch_thunk_event" + : host.watchNameThunkFor(cbT); + // The named form is a definition this module emits; only the runtime's + // fixed adapters need a declaration. + if (cbT.params.length < 2) host.declare(`declare void @${adapter}(ptr, ptr, ptr)`); + host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr, i1 zeroext)`); const t = B.tmp(); - B.line(`${t} = call ptr @scr_fs_watch(ptr ${path.name}, ptr ${cb.name}, ptr @${adapter})`); + B.line( + `${t} = call ptr @scr_fs_watch(ptr ${path.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${cbT.params.length === 2 ? "true" : "false"})`, + ); const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(); return out; } + if (e.fn === "watcher.on") { + // (recv, event, flags, listener) — flags: 1 once, 2 prepend. The + // listener MOVES in; the adapter follows the event (0 change, 1 + // error, 2 close — CLOSE takes no adapter, a plain zero-arg call). + // Registration can throw (the macOS directory-names refusal). + const flagsArg = e.args[2]!; + if (flagsArg.kind !== "numLit") + throw new InternalCompilerError("llvm emitter bug: watcher.on flags not a literal"); + const flags = flagsArg.value; + const codeArg = e.args[1]!; + if (codeArg.kind !== "numLit") + throw new InternalCompilerError("llvm emitter bug: watcher.on event not a literal"); + const code = codeArg.value; + const cbT = e.args[3]!.type; + if (cbT.kind !== "func") + throw new InternalCompilerError("llvm emitter bug: watcher.on listener not a func"); + const w = host.emitExpr(e.args[0]!); + const event = host.emitExpr(e.args[1]!); + const cb = host.emitExpr(e.args[3]!); + host.toHeap(cb); + let adapter = "null"; + let wantsName = false; + if (code === 0) { + adapter = + cbT.params.length === 0 + ? "scr_watch_thunk0" + : cbT.params.length === 1 + ? "scr_watch_thunk_event" + : host.watchNameThunkFor(cbT); + wantsName = cbT.params.length === 2; + // The named form is a definition this module emits; only the runtime's + // fixed adapters need a declaration. + if (cbT.params.length < 2) host.declare(`declare void @${adapter}(ptr, ptr, ptr)`); + } else if (code === 1) { + adapter = cbT.params.length === 0 ? "scr_watch_err_thunk0" : host.watchErrorThunkFor(cbT); + if (cbT.params.length === 0) host.declare(`declare void @scr_watch_err_thunk0(ptr, ptr)`); + } + host.declare( + `declare void @scr_watcher_on(ptr, double, ptr, ptr, i1 zeroext, i1 zeroext, i1 zeroext)`, + ); + B.line( + `call void @scr_watcher_on(ptr ${w.name}, double ${event.name}, ptr ${cb.name}, ptr ${adapter === "null" ? "null" : `@${adapter}`}, i1 ${flags & 1 ? "true" : "false"}, i1 ${flags & 2 ? "true" : "false"}, i1 ${wantsName ? "true" : "false"})`, + ); + host.emitPendingCheck(); + return { name: "", type: e.type }; + } + if (e.fn === "watcher.off") { + // Removal is by pointer identity: the listener stays borrowed + // (never heap-moved), exactly like process.offExit's. + const w = host.emitExpr(e.args[0]!); + const event = host.emitExpr(e.args[1]!); + const cb = host.emitExpr(e.args[2]!); + host.declare(`declare void @scr_watcher_off(ptr, double, ptr)`); + B.line(`call void @scr_watcher_off(ptr ${w.name}, double ${event.name}, ptr ${cb.name})`); + return { name: "", type: e.type }; + } if (e.fn === "watcher.close") { // Idempotent; never throws. const w = host.emitExpr(e.args[0]!); diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index d8272c795..d248caa5e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -1110,13 +1110,37 @@ export function lowerStatsMethodCall( ); } +/** The watch-listener shapes: () , (eventType: string), and + * (eventType: string, filename: string | null) — the filename parameter + * must admit null (Node passes null when the platform names nothing), or + * be the checked dynamic. Returns the parameter count, or null when the + * shape has no lowering. */ +function watchListenerArity(lowerer: Lowerer, cb: IrExpr): 0 | 1 | 2 | null { + if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 2) + return null; + const [event, name] = cb.type.params; + if (event !== undefined && event.kind !== "string" && event.kind !== "dyn") return null; + if (name !== undefined && name.kind !== "dyn") { + const def = name.kind === "union" ? lowerer.unions.get(name.unionId) : undefined; + if ( + !def || + !def.arms.some((a) => a.kind === "nullT") || + !def.arms.some((a) => a.kind === "string") || + !def.arms.every((a) => a.kind === "nullT" || a.kind === "undefinedT" || a.kind === "string") + ) + return null; + } + return cb.type.params.length as 0 | 1 | 2; +} + /** `fs.watch(path[, options][, listener])` → the fs.watch libCall * (scr_watch.c — kqueue EVFILT_VNODE on the opened path; an unopenable * path THROWS Node's fs error synchronously, the polling-fallback catch * shape). The listener fires with "rename"/"change" and takes zero - * parameters or the eventType string — the filename parameter has no - * lowering (kqueue watches the inode, not the directory entry; you - * watched one path). The options record follows the options-record + * parameters, the eventType string, or (eventType, filename: string | + * null) — the filename is the entry name on a directory watch and the + * watched file's basename otherwise (Linux inotify names both; a macOS + * directory watch has no name source until F3). The options record follows the options-record * stance: persistent: true, recursive: false, and encoding: "utf8" * state the lowered behavior and are accepted; persistent: false * (a watcher that does NOT hold the loop), recursive: true (kqueue @@ -1187,7 +1211,7 @@ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc: lowerer.noLowering( "fs.watch with a non-utf8 encoding", prop, - "the encoding applies to the filename argument, which has no lowering — utf8 (the default) is accepted", + "the filename argument is a string — utf8 (the default) is the accepted encoding", ); } if (key === "signal") { @@ -1219,19 +1243,11 @@ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc: : null; if (listenerArg !== null) { const cb = lowerer.lowerExpr(listenerArg); - if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 1) { + if (watchListenerArity(lowerer, cb) === null) { lowerer.noLowering( "fs.watch with this listener shape", listenerArg, - "the listener takes () or (eventType: string) — the filename parameter has no lowering (you watched one path)", - ); - } - const param = cb.type.params[0]; - if (param !== undefined && param.kind !== "string") { - lowerer.unsupported( - "SC1090", - listenerArg, - `watch listeners whose parameter is not the eventType string (got '${lowerer.fmt(param)}')`, + "the listener takes (), (eventType: string), or (eventType: string, filename: string | null)", ); } args.push(cb); @@ -1245,10 +1261,12 @@ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc: }; } -/** Method calls on FSWatcher receivers: close() — idempotent, statement - * position (Node returns void there too). Everything else @types/node - * declares (ref/unref, the EventEmitter surface) fences member- - * qualified. Null for non-watcher receivers. */ +/** Method calls on FSWatcher receivers: close() (idempotent, statement + * position — Node returns void there too) and the EventEmitter surface for + * 'change', 'error' and 'close' (on/once/addListener/prependListener/ + * prependOnceListener/off/removeListener, registered as their own + * statement). Everything else @types/node declares (ref/unref, other + * events) fences member-qualified. Null for non-watcher receivers. */ export function lowerWatcherMethodCall( lowerer: Lowerer, call: ts.CallExpression, @@ -1263,10 +1281,82 @@ export function lowerWatcherMethodCall( const receiver = lowerer.lowerExpr(access.expression); return { kind: "libCall", fn: "watcher.close", args: [receiver], type: VOID, loc }; } + // The EventEmitter surface for the three events the watcher emits: + // "change" (the listener shapes fs.watch takes), "error", and "close". + const ON = ["on", "once", "addListener", "prependListener", "prependOnceListener"]; + if ( + (ON.includes(name) || name === "off" || name === "removeListener") && + call.arguments.nodes.length === 2 + ) { + const evT = lowerer.typeOf(call.arguments.nodes[0]!); + const event = evT.isStringLiteralType() ? evT.value : null; + const code = event === "change" ? 0 : event === "error" ? 1 : event === "close" ? 2 : -1; + if (code < 0) { + lowerer.noLowering( + `FSWatcher.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, + call.arguments.nodes[0]!, + '"change", "error", and "close" are the supported FSWatcher events (as literals)', + ); + } + const parent = call.parent; + if ( + !ts.isExpressionStatement(parent) && + !(ts.isArrowFunction(parent) && parent.body === call) + ) { + lowerer.unsupported( + "SC1090", + call, + `using the result of FSWatcher.${name} (register it as its own statement)`, + ); + } + const receiver = lowerer.lowerExpr(access.expression); + const cb = lowerer.lowerExpr(call.arguments.nodes[1]!); + if (name === "off" || name === "removeListener") { + return { + kind: "libCall", + fn: "watcher.off", + args: [receiver, numLit(code, loc), cb], + type: VOID, + loc, + }; + } + const ok = + code === 0 + ? watchListenerArity(lowerer, cb) !== null + : code === 1 + ? cb.type.kind === "func" && + cb.type.ret.kind === "void" && + cb.type.params.length <= 1 && + (cb.type.params[0] === undefined || + cb.type.params[0].kind === "dyn" || + (cb.type.params[0].kind === "object" && cb.type.params[0].className === "%Error")) + : cb.type.kind === "func" && cb.type.ret.kind === "void" && cb.type.params.length === 0; + if (!ok) { + lowerer.noLowering( + `FSWatcher.${name}("${event}", ...) with this listener shape`, + call.arguments.nodes[1]!, + code === 0 + ? "the listener takes (), (eventType: string), or (eventType: string, filename: string | null)" + : code === 1 + ? "the listener takes () or (error: Error)" + : "the listener takes ()", + ); + } + const flags = + (name === "once" || name === "prependOnceListener" ? 1 : 0) | + (name.startsWith("prepend") ? 2 : 0); + return { + kind: "libCall", + fn: "watcher.on", + args: [receiver, numLit(code, loc), numLit(flags, loc), cb], + type: VOID, + loc, + }; + } lowerer.noLowering( `FSWatcher.${name}`, call, - "close() is the supported FSWatcher member", + 'close(), and on/once/addListener/prependListener/prependOnceListener/off/removeListener for "change", "error", and "close" are the supported FSWatcher members', lowerer.checker.getSymbolAtLocation(access.name), ); } diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 60f46e7ee..48580f4fe 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -1088,6 +1088,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fs.closeSync", "fs.watch", "fs.watchCb", + "watcher.on", + "watcher.off", "crypto.x509Fingerprint", "crypto.x509FingerprintStr", "crypto.x509ValidFrom", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 024f11277..47514c561 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1304,6 +1304,13 @@ function buildLibFnSignatures() { "fs.watch": { argTypes: [STRING], result: FSWATCHER_T }, "fs.watchCb": { argTypes: [STRING, null], result: FSWATCHER_T }, "watcher.close": { argTypes: [FSWATCHER_T], result: VOID }, + // FSWatcher's EventEmitter surface: (receiver, event code, once|prepend + // flags, listener) for on/once/addListener/prependListener/ + // prependOnceListener, and (receiver, event code, listener) for + // off/removeListener. The listener's func type is program-dependent; + // the null slot pins the arity and the receiver. + "watcher.on": { argTypes: [FSWATCHER_T, F64, F64, null], result: VOID }, + "watcher.off": { argTypes: [FSWATCHER_T, F64, null], 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 diff --git a/packages/runtime/src/scr_watch.c b/packages/runtime/src/scr_watch.c index f86adb219..0d5087961 100644 --- a/packages/runtime/src/scr_watch.c +++ b/packages/runtime/src/scr_watch.c @@ -57,12 +57,21 @@ #include #include #include +#include #include #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || \ defined(__OpenBSD__) || defined(__DragonFly__) #define SCR_HAVE_KQUEUE 1 #include +#include +/* Darwin directory watches deliver through FSEvents in libuv, which names + * entries; the kqueue arm watches the directory inode and cannot. Until + * slice F3 lands FSEvents, a listener that reads the filename has no + * lowering there. */ +#if defined(__APPLE__) +#define SCR_WATCH_DIR_NAMES_REFUSED 1 +#endif #elif defined(__linux__) #define SCR_HAVE_INOTIFY 1 #include @@ -92,14 +101,35 @@ static void scr_watch_oom(void) { abort(); } +enum { SCR_WATCH_EV_CHANGE = 0, SCR_WATCH_EV_ERROR = 1, SCR_WATCH_EV_CLOSE = 2 }; + +/* One registered listener: the event it answers, its adapter, and whether + * it is one-shot (once/prependOnceListener — removed before it runs). */ typedef struct { - ScrClosure *cb; /* owned */ - ScrWatchFn fn; /* adapter: event name borrowed */ + ScrClosure *cb; /* owned */ + void *fn; /* CHANGE: ScrWatchFn; ERROR: ScrWatchErrFn; CLOSE: NULL (zero-arg call) */ + uint8_t event; + bool once; } ScrWatchL; +/* The 'close' emission: the moved-out listeners, called once on the next + * tick in registration order. The batch owns them, so a watcher released + * before the tick leaves nothing dangling. */ +typedef struct { + size_t rc; + ScrWatchL *ls; + size_t n; +} ScrWatchCloseBatch; + +static void *scr_watch_batch_retain(void *p); +static void scr_watch_batch_release(void *p); + struct ScrWatcher { size_t rc; int fd; /* -1 once closed, or after a failed reopen (no further events) */ +#ifdef SCR_WATCH_DIR_NAMES_REFUSED + bool is_dir; /* the opened path is a directory (the F3 gap) */ +#endif #ifdef SCR_HAVE_INOTIFY int wd; /* the inotify watch descriptor; -1 once dead (IN_IGNORED * arrived or close() removed it) */ @@ -124,6 +154,24 @@ struct ScrWatcher { static SCR_TL ScrWatcher *scr_watchers = NULL; static SCR_TL size_t scr_watchers_open = 0; /* liveness: open (unclosed) watchers */ +static void scr_watch_close_tick(ScrClosure *cb); + +#if defined(SCR_HAVE_INOTIFY) || defined(SCR_HAVE_KQUEUE) +/* libuv's uv__basename_r: the part after the last '/', or the whole path. */ +static const char *scr_watch_basename(const char *path) { + const char *slash = strrchr(path, '/'); + return slash ? slash + 1 : path; +} +#endif + +#if defined(SCR_WATCH_DIR_NAMES_REFUSED) +static void scr_watch_refuse_dir_names(void) { + const char *m = + "fs.watch of a directory with a listener that reads the filename has no lowering on macOS yet [SC2020]"; + scr_throw_error_msg_code(SCR_ERR_ERROR, m, strlen(m), "SC2020"); +} +#endif + #ifdef SCR_HAVE_KQUEUE static SCR_TL int scr_watch_kq = -1; /* created with the first watch; lives forever */ @@ -238,7 +286,8 @@ void scr_watcher_release_v(void *p) { scr_watcher_release((ScrWatcher *)p); } /* fs.watch(path, cb?) — opens the path NOW (throwing Node's fs error on * failure), registers the watcher with the loop, and arms the vnode * filter. cb (nullable) moves in as the first listener. Result +1. */ -ScrWatcher *scr_fs_watch(ScrStr *path, ScrClosure *cb /*moves, nullable*/, ScrWatchFn fn) { +ScrWatcher *scr_fs_watch(ScrStr *path, ScrClosure *cb /*moves, nullable*/, ScrWatchFn fn, + bool wants_name) { #ifdef SCR_HAVE_INOTIFY /* The wd IS the validation: Node's Linux throw comes straight from * inotify_add_watch's errno, the same message shape as the open below. */ @@ -295,6 +344,22 @@ ScrWatcher *scr_fs_watch(ScrStr *path, ScrClosure *cb /*moves, nullable*/, ScrWa scr_fs_throw(errno, "watch", path); return NULL; /* exception pending */ } +#endif +#ifdef SCR_WATCH_DIR_NAMES_REFUSED + /* The path opened: a DIRECTORY watch on macOS has no name source before + * F3, so a listener that reads the filename is refused HERE, before the + * watcher is registered. */ + bool is_dir = false; + { + struct stat st; + if (fstat(fd, &st) == 0 && S_ISDIR(st.st_mode)) is_dir = true; + } + if (wants_name && is_dir) { + close(fd); + if (cb != NULL) scr_closure_release(cb); + scr_watch_refuse_dir_names(); + return NULL; /* exception pending */ + } #endif ScrWatcher *w = calloc(1, sizeof *w); if (!w) scr_watch_oom(); @@ -305,8 +370,13 @@ ScrWatcher *scr_fs_watch(ScrStr *path, ScrClosure *cb /*moves, nullable*/, ScrWa if (!w->ls) scr_watch_oom(); w->ls[0].cb = cb; w->ls[0].fn = fn; + w->ls[0].event = SCR_WATCH_EV_CHANGE; + w->ls[0].once = false; w->n = w->cap = 1; } +#ifdef SCR_WATCH_DIR_NAMES_REFUSED + w->is_dir = is_dir; +#endif #ifdef SCR_HAVE_INOTIFY w->fd = -1; /* inotify holds the watch; there is no per-watcher fd */ w->wd = wd; @@ -348,6 +418,21 @@ ScrWatcher *scr_fs_watch(ScrStr *path, ScrClosure *cb /*moves, nullable*/, ScrWa void scr_watcher_close(ScrWatcher *w) { if (w->closed) return; w->closed = true; + /* The 'close' listeners leave the LIVE list and run on the next tick, + * in registration order, exactly once (Node emits 'close' after the + * first close() returns). */ + ScrWatchL *closes = NULL; + size_t ncloses = 0; + if (w->n > 0) { + closes = malloc(w->n * sizeof *closes); + if (!closes) scr_watch_oom(); + size_t kept = 0; + for (size_t i = 0; i < w->n; i++) { + if (w->ls[i].event == SCR_WATCH_EV_CLOSE) closes[ncloses++] = w->ls[i]; + else w->ls[kept++] = w->ls[i]; + } + w->n = kept; + } #ifdef SCR_HAVE_INOTIFY if (w->wd >= 0) { /* The queued IN_IGNORED this rm produces finds no open watcher and is @@ -366,6 +451,20 @@ void scr_watcher_close(ScrWatcher *w) { } scr_watcher_drop_listeners(w); scr_watchers_open--; + if (ncloses > 0) { + ScrWatchCloseBatch *batch = malloc(sizeof *batch); + if (!batch) scr_watch_oom(); + batch->rc = 1; /* the box owns it */ + batch->ls = closes; + batch->n = ncloses; + ScrClosure *tick = scr_closure_new((void *)scr_watch_close_tick, 1); + tick->caps[0] = + scr_box_new_obj(scr_watch_batch_retain, scr_watch_batch_release, NULL); + scr_box_set_ref(tick->caps[0], batch); + scr_next_tick(tick); + } else { + free(closes); + } } /* Unlinks and releases closed watchers — called only where no fetched @@ -385,30 +484,118 @@ static void scr_watch_sweep(void) { } /* The runtime-provided listener adapters. */ -void scr_watch_thunk0(ScrClosure *cb, const char *event) { +void scr_watch_thunk0(ScrClosure *cb, const char *event, const char *filename) { (void)event; + (void)filename; ((void (*)(ScrClosure *))cb->fn)(cb); } -void scr_watch_thunk_event(ScrClosure *cb, const char *event) { +void scr_watch_thunk_event(ScrClosure *cb, const char *event, const char *filename) { + (void)filename; /* The listener owns its +1 param per the universal convention. */ ScrStr *e = scr_str_new(event, strlen(event)); ((void (*)(ScrClosure *, ScrStr *))cb->fn)(cb, e); } +/* The zero-parameter 'error' listener: the error itself is dropped. */ +void scr_watch_err_thunk0(ScrClosure *cb, ScrError *err) { + (void)err; + ((void (*)(ScrClosure *))cb->fn)(cb); +} -/* Fires one watcher's listeners with the event name (snapshot — a - * listener closing the watcher mid-emit still finishes this delivery, - * and close() drops only the LIVE list). */ -static void scr_watch_fire(ScrWatcher *w, const char *event) { - size_t n = w->n; - if (n == 0) return; - ScrWatchL *snap = malloc(n * sizeof *snap); - if (!snap) scr_watch_oom(); - for (size_t i = 0; i < n; i++) { - snap[i] = w->ls[i]; - scr_closure_retain(snap[i].cb); +/* watcher.on/once/addListener/prependListener(event, listener). A closed + * watcher can never fire again, so the listener leaves at once. */ +void scr_watcher_on(ScrWatcher *w, double event, ScrClosure *cb, void *fn, bool once, bool prepend, + bool wants_name) { + if (w->closed) { + scr_closure_release(cb); + return; } +#ifdef SCR_WATCH_DIR_NAMES_REFUSED + if (event == SCR_WATCH_EV_CHANGE && wants_name && w->is_dir) { + scr_closure_release(cb); + scr_watch_refuse_dir_names(); + return; + } +#else + (void)wants_name; +#endif + if (w->n == w->cap) { + size_t cap = w->cap == 0 ? 1 : w->cap * 2; + ScrWatchL *grown = realloc(w->ls, cap * sizeof *grown); + if (!grown) scr_watch_oom(); + w->ls = grown; + w->cap = cap; + } + ScrWatchL entry = {cb, fn, (uint8_t)event, once}; + if (prepend) { + memmove(w->ls + 1, w->ls, w->n * sizeof *w->ls); + w->ls[0] = entry; + } else { + w->ls[w->n] = entry; + } + w->n++; +} + +/* watcher.off/removeListener(event, listener): the most recently added + * listener of that event with the same closure identity leaves. */ +void scr_watcher_off(ScrWatcher *w, double event, ScrClosure *cb) { + for (size_t i = w->n; i > 0; i--) { + ScrWatchL *l = &w->ls[i - 1]; + if (l->event != (uint8_t)event || l->cb != cb) continue; + scr_closure_release(l->cb); + memmove(l + 1, l, (w->n - i) * sizeof *w->ls); + w->n--; + return; + } +} + +static void *scr_watch_batch_retain(void *p) { + ScrWatchCloseBatch *b = p; + if (b->rc != SIZE_MAX) b->rc++; + return b; +} + +static void scr_watch_batch_release(void *p) { + ScrWatchCloseBatch *b = p; + if (!b || b->rc == SIZE_MAX) return; + if (--b->rc != 0) return; + for (size_t i = 0; i < b->n; i++) scr_closure_release(b->ls[i].cb); + free(b->ls); + free(b); +} + +static void scr_watch_close_tick(ScrClosure *cb) { + ScrWatchCloseBatch *b = scr_box_get_ref(cb->caps[0]); + if (b == NULL) return; + for (size_t i = 0; i < b->n; i++) { + if (!scr_exc_pending()) ((void (*)(ScrClosure *))b->ls[i].cb->fn)(b->ls[i].cb); + } + scr_watch_batch_release(b); +} + +/* Fires one watcher's CHANGE listeners with the event name and the entry + * name (NULL when the platform named nothing). The snapshot follows the + * existing shape — a listener closing the watcher mid-emit still finishes + * this delivery — and a one-shot listener leaves the LIVE list before it + * runs (EventEmitter's rule). */ +static void scr_watch_fire(ScrWatcher *w, const char *event, const char *filename) { + if (w->n == 0) return; + ScrWatchL *snap = malloc(w->n * sizeof *snap); + if (!snap) scr_watch_oom(); + size_t n = 0, kept = 0; + for (size_t i = 0; i < w->n; i++) { + ScrWatchL l = w->ls[i]; + if (l.event != SCR_WATCH_EV_CHANGE) { + w->ls[kept++] = l; + continue; + } + snap[n++] = l; + scr_closure_retain(l.cb); + if (!l.once) w->ls[kept++] = l; + else scr_closure_release(l.cb); /* the live list's own reference leaves now */ + } + w->n = kept; for (size_t i = 0; i < n; i++) { - if (!scr_exc_pending()) snap[i].fn(snap[i].cb, event); + if (!scr_exc_pending()) ((ScrWatchFn)snap[i].fn)(snap[i].cb, event, filename); scr_closure_release(snap[i].cb); } free(snap); @@ -448,6 +635,16 @@ static void scr_watch_dispatch(void) { if (w == NULL || w->closed || w->fd < 0) continue; uint32_t ff = evs[i].fflags; bool rename = (ff & (NOTE_RENAME | NOTE_DELETE | NOTE_REVOKE)) != 0; + /* The entry name BEFORE the reopen dance closes the old fd: kqueue + * names the watched inode by its CURRENT path (libuv's + * F_GETPATH rule). A directory watch names the directory itself — + * only the zero/one-parameter listeners, which ignore it, see it. */ + const char *name = NULL; +#ifdef F_GETPATH + char pathbuf[MAXPATHLEN]; + if (w->fd >= 0 && fcntl(w->fd, F_GETPATH, pathbuf) == 0) + name = scr_watch_basename(pathbuf); +#endif if (rename) { /* The inode left the name: reopen best-effort so the atomic * replace-by-rename idiom keeps delivering on the new file. The @@ -459,9 +656,9 @@ static void scr_watch_dispatch(void) { close(w->fd); w->fd = -1; } - scr_watch_fire(w, "rename"); + scr_watch_fire(w, "rename", name); } else if (ff & (NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB)) { - scr_watch_fire(w, "change"); + scr_watch_fire(w, "change", name); } if (scr_exc_pending()) return; /* the loop surfaces it */ } @@ -489,7 +686,10 @@ static void scr_watch_dispatch(void) { bool change = (e->mask & (IN_ATTRIB | IN_MODIFY)) != 0; if (e->mask & IN_IGNORED) w->wd = -1; /* the kernel dropped the watch: * no further events (no reopen), like Node */ - scr_watch_fire(w, change ? "change" : "rename"); + /* libuv's filename rule: the entry name for a directory event, the + * watched path's basename otherwise (uv__basename_r). */ + const char *name = e->len ? e->name : scr_watch_basename(w->path->data); + scr_watch_fire(w, change ? "change" : "rename", name); if (scr_exc_pending()) return; /* the loop surfaces it */ } if (scr_loop_has_ready()) return; /* microtasks interleave first */ @@ -510,18 +710,36 @@ static void scr_watch_dispatch(void) { * overflow notification, delivered as a filterless "rename" burst * signal the same way libuv degrades. */ if (ok && bytes == 0) { - scr_watch_fire(w, "rename"); + scr_watch_fire(w, "rename", NULL); /* the overflow notification names nothing */ if (scr_exc_pending()) return; } else if (ok) { char *p = w->buf; for (;;) { FILE_NOTIFY_INFORMATION *e = (FILE_NOTIFY_INFORMATION *)p; + size_t wlen = e->FileNameLength / sizeof(WCHAR); /* not NUL-terminated */ bool match = w->filter == NULL || - CompareStringOrdinal(e->FileName, (int)(e->FileNameLength / sizeof(WCHAR)), - w->filter, -1, TRUE) == CSTR_EQUAL; + CompareStringOrdinal(e->FileName, (int)wlen, w->filter, -1, TRUE) == CSTR_EQUAL; if (match) { - scr_watch_fire(w, e->Action == FILE_ACTION_MODIFIED ? "change" : "rename"); + char stackbuf[1024]; + char *heap = NULL; + char *namebuf = stackbuf; + size_t namecap = sizeof stackbuf; + if (wlen * 4 + 1 > namecap) { + heap = malloc(wlen * 4 + 1); + if (!heap) scr_watch_oom(); + namebuf = heap; + namecap = wlen * 4 + 1; + } + int nbytes = WideCharToMultiByte(CP_UTF8, 0, e->FileName, (int)wlen, namebuf, + (int)namecap - 1, NULL, NULL); + char *name = NULL; + if (nbytes > 0) { + namebuf[nbytes] = '\0'; + name = namebuf; + } + scr_watch_fire(w, e->Action == FILE_ACTION_MODIFIED ? "change" : "rename", name); + free(heap); if (scr_exc_pending()) return; if (w->closed) break; /* a listener closed its own watcher */ } @@ -530,6 +748,10 @@ static void scr_watch_dispatch(void) { } } if (!w->closed && w->dh != INVALID_HANDLE_VALUE && !scr_watch_issue(w)) { + /* TODO(windows): EPERM 'error' event — Node emits one when the + * watched directory disappears (GetOverlappedResult fails with + * ERROR_ACCESS_DENIED, libuv's UV_EPERM). Unverified on Windows; + * the watcher stops emitting, like the kqueue exhaustion stance. */ scr_watch_backend_drop(w); /* reissue refused: no further events, like kqueue exhaustion */ } } diff --git a/packages/runtime/src/scr_watch.h b/packages/runtime/src/scr_watch.h index c038dbe55..c2118d1cd 100644 --- a/packages/runtime/src/scr_watch.h +++ b/packages/runtime/src/scr_watch.h @@ -1,9 +1,12 @@ -/* scriptc runtime — fs.watch (scr_watch.c). Included by scr_runtime.h, which - * says where new declarations go. */ +/* scriptc runtime — fs.watch (scr_watch.c) and fs.watchFile + * (scr_watch_file.c). Included by scr_runtime.h, which says where new + * declarations go. */ #ifndef SCR_WATCH_H #define SCR_WATCH_H #include "scr_bytes.h" +#include "scr_error.h" +#include "scr_lib.h" /* ── fs.watch (scr_watch.c — compiled only when the program uses it; * design note atop the file). FSWatcher handles over the unit's own @@ -12,9 +15,12 @@ * close/exit — the child ownership story), an open watcher keeps the * loop alive until close() (Node's persistent default). */ typedef struct ScrWatcher ScrWatcher; -/* The event adapter: the name ("rename"/"change") arrives BORROWED - * (static storage); adapters build what their listener declared. */ -typedef void (*ScrWatchFn)(ScrClosure *cb, const char *event); +/* The event adapter: the event name ("rename"/"change") and the entry + * name (NULL when the platform names nothing) arrive BORROWED (static + * storage); adapters build what their listener declared. */ +typedef void (*ScrWatchFn)(ScrClosure *cb, const char *event, const char *filename); +/* The 'error' adapter: the ScrError arrives borrowed. */ +typedef void (*ScrWatchErrFn)(ScrClosure *cb, ScrError *err); ScrWatcher *scr_watcher_retain(ScrWatcher *w); void scr_watcher_release(ScrWatcher *w); @@ -23,13 +29,26 @@ void scr_watcher_release_v(void *p); /* fs.watch(path, listener?) — path borrowed, the callback (nullable) * MOVES. Opens the path NOW and THROWS Node's fs error when it cannot - * ("ENOENT: ..., watch 'x'" — the polling-fallback catch shape). +1. */ -ScrWatcher *scr_fs_watch(ScrStr *path, ScrClosure *cb, ScrWatchFn fn); + * ("ENOENT: ..., watch 'x'" — the polling-fallback catch shape). A + * CHANGE listener that reads the filename (wants_name) on a macOS + * directory watch has no lowering yet and THROWS [SC2020]. +1. */ +ScrWatcher *scr_fs_watch(ScrStr *path, ScrClosure *cb, ScrWatchFn fn, bool wants_name); void scr_watcher_close(ScrWatcher *w); /* idempotent */ -/* The runtime-provided listener adapters (zero-param, and the one-param - * (eventType: string) shape). */ -void scr_watch_thunk0(ScrClosure *cb, const char *event); -void scr_watch_thunk_event(ScrClosure *cb, const char *event); +/* watcher.on/once/addListener/prependListener(event, listener): cb MOVES + * in. On a closed watcher the listener is released at once (it can never + * run). Directory watches on macOS cannot name entries (slice F3), so a + * CHANGE listener that reads the filename (wants_name) THROWS there. */ +void scr_watcher_on(ScrWatcher *w, double event, ScrClosure *cb, void *fn, bool once, bool prepend, + bool wants_name); +/* watcher.off/removeListener(event, listener): removes the most recently + * added listener of that event whose closure is `cb` (pointer identity, + * EventEmitter's rule); cb is borrowed. */ +void scr_watcher_off(ScrWatcher *w, double event, ScrClosure *cb); +/* The runtime-provided listener adapters: the zero- and one-parameter + * fs.watch shapes, and the zero-parameter 'error' listener. */ +void scr_watch_thunk0(ScrClosure *cb, const char *event, const char *filename); +void scr_watch_thunk_event(ScrClosure *cb, const char *event, const char *filename); +void scr_watch_err_thunk0(ScrClosure *cb, ScrError *err); void scr_watch_install(void); /* The loop-side registration (scr_async.c, always linked) — the net * hook's exact shape, one more nullable slot set. */ From 7176257136cc294d12db5431b22b3caf5ced36a9 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 19:41:23 -0700 Subject: [PATCH 11/70] fs.watchFile: poll a path from its own gated runtime unit fs.watchFile(path[, options], listener) polls the resolved path every `interval` milliseconds (5007 by default, `persistent` by default) and calls its listener with (current, previous) Stats when a poll sees a difference. The first poll only records the baseline, a path that cannot be stat'd polls as all-zero Stats, and a stat failure that repeats for the same errno is not re-reported. One poller per RESOLVED path is shared by every listener on it, and fs.unwatchFile(path[, listener]) removes the last listener registered with that callback, or every listener when called without one. The call is a statement, since Node's StatWatcher has no lowering, and bigint: true is refused. The implementation is scr_watch_file.c, a new gated runtime unit switched by moduleUsesFsWatchFile, so programs that do not poll link none of it. The feature travels the usual chain (runtime-features, executable-features, the native link info, the executable cache keys, the runtime-pack matrix) and the validator learns that fs.unwatchFile's trailing listener slot may be omitted. Evidence: - tests/corpus/fs-watch-file-names.ts and fs-watch-file-polling.ts match Node on stdout, stderr and exit status in the plain and sanitized lanes - 1564-fs-watch.ts, 1751-options-record-stance.ts and the `fs` corpus stay green; the new order baselines and corpus cost files are recorded Co-Authored-By: Claude Opus 5.5 --- docs/content/docs/limitations.mdx | 2 + .../builtin-apis/file-watching.mdx | 5 + .../src/generated/node-v24-compatibility.json | 18 +- .../generated/node-v24-internal.json | 30 +- internal/compatibility/static-support.json | 3 +- .../compiler/ambient/node-fallback/fs.d.ts | 22 ++ .../src/backend/executable-features.ts | 1 + packages/compiler/src/backend/library-pack.ts | 1 + .../compiler/src/backend/llvm/emit-module.ts | 8 + .../src/backend/llvm/lib-filesystem.ts | 34 ++ .../src/backend/native-link-info-core.ts | 2 + .../compiler/src/backend/native/contracts.ts | 5 + .../compiler/src/backend/native/executable.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/builtin-calls.ts | 8 + .../frontend/lowering/builtins/filesystem.ts | 123 +++++++ .../src/frontend/lowering/surfaces.ts | 7 + packages/compiler/src/index.ts | 1 + packages/compiler/src/ir/api.ts | 1 + packages/compiler/src/ir/builtin-effects.ts | 2 + .../compiler/src/ir/builtin-signatures.ts | 10 + packages/compiler/src/ir/runtime-features.ts | 13 + packages/compiler/src/ir/validate.ts | 9 +- packages/compiler/src/native/cli.ts | 1 + packages/compiler/src/native/prepare.ts | 1 + .../compiler/src/native/sanitizer.test.ts | 1 + .../tests/corpus/fs-watch-file-names.ts.json | 6 + .../corpus/fs-watch-file-polling.ts.json | 6 + .../runtime-pack-matrix.mjs | 1 + packages/runtime/src/scr_lib.c | 37 +- packages/runtime/src/scr_lib.h | 5 + packages/runtime/src/scr_watch.h | 19 + packages/runtime/src/scr_watch_file.c | 331 ++++++++++++++++++ tests/corpus/fs-watch-file-names.ts | 60 ++++ tests/corpus/fs-watch-file-polling.ts | 49 +++ .../corpus-costs/fs-watch-file-names.ts.json | 1 + .../fs-watch-file-polling.ts.json | 1 + 41 files changed, 811 insertions(+), 22 deletions(-) create mode 100644 docs/content/limitations/builtin-apis/file-watching.mdx create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-watch-file-names.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-watch-file-polling.ts.json create mode 100644 packages/runtime/src/scr_watch_file.c create mode 100644 tests/corpus/fs-watch-file-names.ts create mode 100644 tests/corpus/fs-watch-file-polling.ts create mode 100644 tests/harness/corpus-costs/fs-watch-file-names.ts.json create mode 100644 tests/harness/corpus-costs/fs-watch-file-polling.ts.json diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index 88b2ca5fc..ad8912a40 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -100,6 +100,8 @@ title: "Limitations" ../limitations/builtin-apis/file-streams.mdx +../limitations/builtin-apis/file-watching.mdx + ../limitations/builtin-apis/crypto.mdx ../limitations/builtin-apis/assertions.mdx diff --git a/docs/content/limitations/builtin-apis/file-watching.mdx b/docs/content/limitations/builtin-apis/file-watching.mdx new file mode 100644 index 000000000..2d2b386f0 --- /dev/null +++ b/docs/content/limitations/builtin-apis/file-watching.mdx @@ -0,0 +1,5 @@ +- **File watching:** `fs.watch` returns an `FSWatcher` whose `on`, `once`, `off`, and `close` take `change`, `rename`, `error`, and `close` listeners. + A listener may take the event name and the entry name, but the name arrives as a string (Node's default encoding), so a listener annotated with the `Buffer` arm of `@types/node`'s `change` overload has no lowering. + Watching a directory on macOS with a listener that reads the entry name is refused with `SC2020` because FSEvents is not implemented yet; file watches and listeners that ignore the name work on every target. +- **Stat polling:** `fs.watchFile` polls the resolved path every `interval` milliseconds (default 5007) and calls its listener with `(current, previous)` `Stats`; `fs.unwatchFile` removes the last listener registered with that callback, or every listener when called without one. + The call must be a statement of its own, because the `StatWatcher` it returns in Node.js is not lowered, and `bigint: true` is refused. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 387f5b21c..391d7a5f8 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -30842,9 +30842,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" } }, { @@ -30911,9 +30911,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" } }, { @@ -32659,9 +32659,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" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 00d6f8db3..a93e7b34c 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -44292,6 +44292,7 @@ "evidence": "derived:descendants", "tests": [ "tests/corpus/1403-buffer-fs.ts", + "tests/corpus/1564-fs-watch.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2686-fs-file-handle.ts", "tests/corpus/2844-node-disposable-resources.ts", @@ -44328,6 +44329,8 @@ "tests/corpus/6207-fs-constants-object.ts", "tests/corpus/6208-fs-constants-require.cjs", "tests/corpus/fs-native-realpath.ts", + "tests/corpus/fs-watch-file-names.ts", + "tests/corpus/fs-watch-file-polling.ts", "tests/corpus/node24-fs-links-callbacks.cjs", "tests/corpus/node24-fs-links-errors.cjs", "tests/corpus/node24-fs-links-options.cjs", @@ -47429,8 +47432,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:fs" + "status": "partial", + "evidence": "compiler-dedicated:fs.unwatchFile", + "tests": [ + "tests/corpus/1564-fs-watch.ts", + "tests/corpus/fs-watch-file-names.ts", + "tests/corpus/fs-watch-file-polling.ts" + ] }, "anchorSource": "exact" }, @@ -47601,8 +47609,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:fs" + "status": "partial", + "evidence": "compiler-dedicated:fs.watchFile", + "tests": [ + "tests/corpus/1564-fs-watch.ts", + "tests/corpus/fs-watch-file-names.ts", + "tests/corpus/fs-watch-file-polling.ts" + ] }, "anchorSource": "exact" }, @@ -49669,8 +49682,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:fs" + "status": "partial", + "evidence": "compiler-dedicated:fs.FSWatcher", + "tests": [ + "tests/corpus/1564-fs-watch.ts", + "tests/corpus/fs-watch-file-names.ts", + "tests/corpus/fs-watch-file-polling.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 1242ad9a7..8afe8af22 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -485,6 +485,7 @@ { "symbols": ["SocketAddress.parse", "BlockList.isBlockList", "blockList.addAddress", "blockList.addAddresses", "blockList.addCIDR", "blockList.addCIDRs", "blockList.addRange", "blockList.addSubnet", "blockList.check", "blockList.clear", "blockList.fromJSON", "blockList.removeAddress", "blockList.removeCIDR", "blockList.removeRange", "blockList.removeSubnet", "blockList.toJSON"], "status": "supported", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] }, { "symbols": ["readline.createInterface", "rl.close"], "status": "partial", "evidence": ["tests/corpus/1475-readline-closed-stdin.ts", "tests/corpus/5878-readline-dynamic-streams.mjs"] }, { "symbols": ["test", "it", "test.skip", "test.todo", "test.only", "describe", "suite", "before", "after", "beforeEach", "afterEach", "context.test", "context.skip", "context.todo", "context.assert"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] }, - { "symbols": ["http.request", "http.get", "http.createServer", "https.request", "https.get", "https.createServer", "request.end", "request.destroy", "request.setTimeout", "request.write", "response.end", "response.getHeader", "response.hasHeader", "response.removeHeader", "response.setHeader", "response.write", "response.writeHead", "headersTimeout", "requestTimeout", "timeout", "keepAliveTimeout", "keepAliveTimeoutBuffer"], "status": "partial", "evidence": ["tests/corpus/1780-http-res-surface.ts", "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2689-http-server-timeout-properties.ts"] } + { "symbols": ["http.request", "http.get", "http.createServer", "https.request", "https.get", "https.createServer", "request.end", "request.destroy", "request.setTimeout", "request.write", "response.end", "response.getHeader", "response.hasHeader", "response.removeHeader", "response.setHeader", "response.write", "response.writeHead", "headersTimeout", "requestTimeout", "timeout", "keepAliveTimeout", "keepAliveTimeoutBuffer"], "status": "partial", "evidence": ["tests/corpus/1780-http-res-surface.ts", "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2689-http-server-timeout-properties.ts"] }, + { "symbols": ["fs.FSWatcher", "fs.watchFile", "fs.unwatchFile"], "status": "partial", "evidence": ["tests/corpus/1564-fs-watch.ts", "tests/corpus/fs-watch-file-names.ts", "tests/corpus/fs-watch-file-polling.ts"] } ] } diff --git a/packages/compiler/ambient/node-fallback/fs.d.ts b/packages/compiler/ambient/node-fallback/fs.d.ts index 46c38a315..70172f384 100644 --- a/packages/compiler/ambient/node-fallback/fs.d.ts +++ b/packages/compiler/ambient/node-fallback/fs.d.ts @@ -595,6 +595,28 @@ declare module "node:fs" { options: WatchOptions, listener?: WatchListener, ): FSWatcher; + /* fs.watchFile — stat polling (scr_watch_file.c, libuv's uv_fs_poll + * shape): the listener runs with (current, previous) Stats when a poll + * sees a difference, the first poll only records the baseline, and a + * path that cannot be stat'd polls as all-zero Stats. One poller per + * resolved path is shared by every listener on it. interval defaults to + * 5007 ms, persistent defaults to true; bigint: true (BigIntStats) has + * no lowering. The call is a statement: Node's StatWatcher has no + * lowering. */ + export interface StatWatcher {} + export function watchFile( + filename: string, + options: { interval?: number; persistent?: boolean; bigint?: false }, + listener: (current: Stats, previous: Stats) => void, + ): StatWatcher; + export function watchFile( + filename: string, + listener: (current: Stats, previous: Stats) => void, + ): StatWatcher; + export function unwatchFile( + filename: string, + listener?: (current: Stats, previous: Stats) => void, + ): void; /* fs.promises IS the fs/promises module (Node's rule) — the namespace- * import form `fs.promises.readFile(...)` lowers through the same * fs/promises table as `import { readFile } from "node:fs/promises"`. */ diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index f4290a15a..bb4542573 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -66,6 +66,7 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ dgramDyn: features.dgramDyn, textCodecDyn: features.textCodecDyn, watch: features.fsWatch, + watchFile: features.fsWatchFile, foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []), nodeTest: features.nodeTest, tls: features.tls, diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index 14ffdff28..7b74dc808 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -56,6 +56,7 @@ export async function compilePackedLibrary( dgramDyn: false, textCodecDyn: false, watch: false, + watchFile: false, foreignFfi: false, nodeTest: false, tls: false, diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 6f261d4bb..bcdb985f4 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -478,6 +478,10 @@ export function emitModuleParts(emitter: LlEmitter): string[] { const usesEvents = runtimeFeatures.processEvents; const usesChildProcess = runtimeFeatures.childProcess; const usesFsWatch = runtimeFeatures.fsWatch; + // fs.watchFile programs register the poller unit's exit cleanup — the + // same one-line gating (native/executable.ts links scr_watch_file.c on + // the same predicate). + const usesFsWatchFile = runtimeFeatures.fsWatchFile; // Programs that call into Node record the libnode pack and its Node // version before %main (scr_node_island.c). const usesNodeIsland = runtimeFeatures.nodeIsland; @@ -541,6 +545,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { if (workerStdioBox || workerStdinBox) emitter.declare(`declare void @scr_worker_stdio_install(ptr, ptr, ptr)`); if (usesFsWatch) emitter.declare(`declare void @scr_watch_install()`); + if (usesFsWatchFile) emitter.declare(`declare void @scr_watch_file_install()`); if (usesNodeIsland) emitter.declare(`declare void @scr_node_island_install(ptr, ptr)`); // Interned now: the C-string constants flush before main assembles. const nodeIslandConfig = usesNodeIsland @@ -1226,6 +1231,9 @@ export function emitModuleParts(emitter: LlEmitter): string[] { // fs.watch programs fill the loop's watch hooks the same way — // scr_watch.c links only when this line is emitted. ...(usesFsWatch ? [` call void @scr_watch_install()`] : []), + // fs.watchFile programs register the poller unit's atexit cleanup the + // same way — scr_watch_file.c links only when this line is emitted. + ...(usesFsWatchFile ? [` call void @scr_watch_file_install()`] : []), ...(nodeIslandConfig ? [ ` call void @scr_node_island_install(ptr ${nodeIslandConfig[0]}, ptr ${nodeIslandConfig[1]})`, diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 310f2cb57..dc9c5c876 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -549,6 +549,40 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): B.line(`call void @scr_watcher_close(ptr ${w.name})`); return { name: "", type: e.type }; } + if (e.fn === "fs.watchFile") { + // (path, interval, persistent, listener) — the listener MOVES into + // the poller's registry and the adapter is runtime-provided per + // listener shape (zero, one or two Stats parameters). The poller + // re-arms a timer every interval, so this is a timer program. + host.usesTimers = true; + const cbT = e.args[3]!.type; + if (cbT.kind !== "func") + throw new InternalCompilerError("llvm emitter bug: fs.watchFile listener not a func"); + const path = host.emitExpr(e.args[0]!); + const interval = host.emitExpr(e.args[1]!); + const persistent = host.emitExpr(e.args[2]!); + const cb = host.emitExpr(e.args[3]!); + host.toHeap(cb); + const adapter = `scr_watch_file_thunk${cbT.params.length}`; + host.declare(`declare void @${adapter}(ptr, ptr, ptr)`); + host.declare(`declare void @scr_fs_watch_file(ptr, double, i1 zeroext, ptr, ptr)`); + B.line( + `call void @scr_fs_watch_file(ptr ${path.name}, double ${interval.name}, i1 ${persistent.name}, ptr ${cb.name}, ptr @${adapter})`, + ); + return { name: "", type: e.type }; + } + if (e.fn === "fs.unwatchFile") { + // Removal is by closure identity: the listener stays borrowed + // (never heap-moved), exactly like watcher.off's; an absent one is + // the NULL that removes every listener on the path. + const path = host.emitExpr(e.args[0]!); + const cb = e.args[1] === undefined ? null : host.emitExpr(e.args[1]!); + host.declare(`declare void @scr_fs_unwatch_file(ptr, ptr)`); + B.line( + `call void @scr_fs_unwatch_file(ptr ${path.name}, ptr ${cb === null ? "null" : cb.name})`, + ); + return { name: "", type: e.type }; + } if (e.fn === "fs.globTypesSync" || e.fn === "fsp.globTypes") { // Dirent rows from one glob snapshot (scr_glob.c): mode 1 is the // globSync form, 3 the fs/promises form with resolved parent paths. diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 6a40a1be7..10d665790 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -116,6 +116,8 @@ export interface NativeLinkFeatures { * scr_text_codec_dyn.c. */ textCodecDyn: boolean; watch: boolean; + /** fs.watchFile/fs.unwatchFile: scr_watch_file.c. */ + watchFile: boolean; foreignFfi: boolean; nodeTest: boolean; tls: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index f093d6483..aca6000b4 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -255,6 +255,11 @@ export interface CcOptions { * scr_watch.c into the binary — the net gating precedent, so watch-free * binaries keep their exact link line. */ watch?: boolean; + /** The program uses fs.watchFile or fs.unwatchFile (moduleUsesFsWatchFile + * on the IR): compiles scr_watch_file.c into the binary — its own gated + * unit under the same precedent, so polling-free binaries keep their + * exact link line. */ + watchFile?: boolean; /** The executable manifest has a format-5 foreign callback descriptor: * compiles the MPSC queue/self-pipe unit. Other FFI and non-FFI binaries * keep their existing runtime size class. */ diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 3515bc387..e8ae31067 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -336,6 +336,9 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.dgram && u.opts.dgramDyn, "scr_dgram_dyn.c"), unit((u) => u.opts.textCodecDyn, "scr_text_codec_dyn.c"), unit((u) => u.opts.watch, "scr_watch.c"), + // fs.watchFile's stat poller: its own gated unit (no loop hook — the + // re-armed timers are the liveness), linked only by programs that poll. + unit((u) => u.opts.watchFile, "scr_watch_file.c"), unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"), unit((u) => u.opts.nodeTest, "scr_test.c"), unit((u) => u.opts.webcrypto, "scr_webcrypto.c"), diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index fcbe3f08e..510e046b6 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -120,6 +120,7 @@ const features = { dgramDyn: false, textCodecDyn: false, watch: false, + watchFile: false, foreignFfi: false, nodeTest: false, tls: false, diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index a12c137b2..0fc279d20 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -61,6 +61,7 @@ const BASE: NativeLinkFeatures = { dgramDyn: false, textCodecDyn: false, watch: false, + watchFile: false, foreignFfi: false, nodeTest: false, tls: false, diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index c42ceb79e..25254934f 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -63,6 +63,7 @@ export async function warmNativeCaches( dgramDyn: false, textCodecDyn: false, watch: false, + watchFile: false, foreignFfi: false, nodeTest: false, tls: false, diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index 1f5625fb8..9c81ccc19 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -60,6 +60,7 @@ const native: EarlyExecutableNativeFeatures = { dgramDyn: false, textCodecDyn: false, watch: false, + watchFile: false, foreignFfi: false, nodeTest: false, tls: false, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 25a77c10d..c7daf3f68 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -109,6 +109,7 @@ export interface EarlyExecutableNativeFeatures { dgramDyn: boolean; textCodecDyn: boolean; watch: boolean; + watchFile: boolean; foreignFfi: boolean; nodeTest: boolean; tls: boolean; @@ -295,6 +296,7 @@ const BOOLEAN_NATIVE_KEYS = [ "dgramDyn", "textCodecDyn", "watch", + "watchFile", "foreignFfi", "nodeTest", "tls", diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts index d2e4b53d5..33c4502bc 100644 --- a/packages/compiler/src/frontend/lowering/builtin-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts @@ -1,4 +1,5 @@ import { lowerReadlineCursorCall } from "./builtins/readline.js"; +import { lowerFsWatchFileCall } from "./builtins/filesystem.js"; import { lowerMonitorEventLoopDelayCall } from "./builtins/performance.js"; import { lowerEnvironmentData, @@ -134,6 +135,13 @@ export function lowerBuiltinCall( if (streamServed) return streamServed; const fsTs = lowerer.lowerFsToUnixTimestampCall(expr, bi, loc); if (fsTs) return fsTs; + // fs.watchFile/fs.unwatchFile have no table entry (their checked-dynamic + // form is the fs.watchFileChk ladder below), so the typed polling forms + // dispatch here; anything else falls through to the ladder and the fence. + if (bi.module === "fs" && (bi.member === "watchFile" || bi.member === "unwatchFile")) { + const polling = lowerFsWatchFileCall(lowerer, expr, loc, bi.member === "unwatchFile"); + if (polling) return polling; + } const fsLadder = lowerer.lowerFsLadderCall(expr, bi, loc); if (fsLadder) return fsLadder; const cryptoServed = lowerer.lowerCryptoModuleCall(expr, bi, loc); diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index d248caa5e..318d58ee0 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -8,6 +8,7 @@ import { builtinModuleFnOf, FS_READDIR_DOCUMENTED_OPTIONS, FS_WATCH_DOCUMENTED_OPTIONS, + FS_WATCH_FILE_DOCUMENTED_OPTIONS, fenceOrDropOptionKey, } from "../surfaces.js"; import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "../optional-arguments.js"; @@ -1261,6 +1262,128 @@ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc: }; } +/** `fs.watchFile(path[, options], listener)` and + * `fs.unwatchFile(path[, listener])` → the polling libCalls + * (scr_watch_file.c: libuv's uv_fs_poll shape — one poller per RESOLVED + * path, shared by every listener on it, re-armed every `interval` ms, + * 5007 by default, persistent by default). The listener takes + * (current: Stats, previous: Stats) — zero, one or both — and runs when a + * poll sees a difference; the first poll only records the baseline, and a + * file that cannot be stat'd polls as all-zero Stats. The options record + * follows the options-record stance: interval, persistent: true/false and + * bigint: false are accepted; bigint: true fences (BigIntStats); other + * documented keys fence by name; undocumented keys drop like Node. + * Statement position only — Node returns a StatWatcher, which has no + * lowering. Returns null for any untyped shape so the checked-dynamic + * ladder (fs.watchFileChk) keeps its current IR. */ +export function lowerFsWatchFileCall( + lowerer: Lowerer, + expr: ts.CallExpression, + loc: SrcLoc, + unwatch: boolean, +): IrExpr | null { + const nodes = expr.arguments.nodes; + if (nodes.length < 1 || nodes.length > 3 || nodes.some(ts.isSpreadElement)) return null; + if (lowerer.mapTypeOf(lowerer.typeOf(nodes[0]!))?.kind !== "string") return null; + const path = lowerer.lowerExprExpecting(nodes[0]!, STRING); + if (unwatch) { + if (nodes.length > 2) return null; + const args: IrExpr[] = [path]; + if (nodes.length === 2) { + if (lowerer.mapTypeOf(lowerer.typeOf(nodes[1]!))?.kind !== "func") return null; + args.push(lowerer.lowerExpr(nodes[1]!)); + } + return { kind: "libCall", fn: "fs.unwatchFile", args, type: VOID, loc }; + } + const hasOptions = nodes.length >= 2 && ts.isObjectLiteralExpression(nodes[1]!); + if (nodes.length === 3 && !hasOptions) return null; + if (nodes.length === 2 && hasOptions) return null; /* no listener */ + const listenerArg = nodes[nodes.length - 1]!; + if (lowerer.mapTypeOf(lowerer.typeOf(listenerArg))?.kind !== "func") return null; + const parent = expr.parent; + if (!ts.isExpressionStatement(parent) && !(ts.isArrowFunction(parent) && parent.body === expr)) { + lowerer.unsupported( + "SC1090", + expr, + "using the returned StatWatcher (register the fs.watchFile call as its own statement)", + ); + } + let interval: IrExpr | null = null; + let persistent: IrExpr | null = null; + if (hasOptions) { + for (const prop of (nodes[1] as ts.ObjectLiteralExpression).properties.nodes) { + if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) { + lowerer.noLowering( + "fs.watchFile options with computed keys or spreads", + prop, + "each option must be a plain `name: value` entry with a literal key", + ); + } + if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) { + lowerer.noLowering( + "fs.watchFile options with computed keys", + prop, + "each option must be a plain `name: value` entry with a literal key", + ); + } + const key = prop.name.text; + const value = ts.isPropertyAssignment(prop) + ? prop.initializer + : ts.isShorthandPropertyAssignment(prop) + ? prop.name + : null; + if (key === "interval" && value !== null) { + interval = lowerer.lowerExprExpecting(value, F64); + continue; + } + if (key === "persistent" && value !== null) { + persistent = lowerer.lowerExprExpecting(value, BOOL); + continue; + } + if (key === "bigint") { + // The fallback's option record declares `bigint: false`, so the + // false literal (or `false` as a value) states the lowered Stats + // shape; BigIntStats has no lowering. + if (value !== null && value.kind === ts.SyntaxKind.FalseKeyword) continue; + lowerer.noLowering( + "fs.watchFile with bigint: true", + prop, + "the listener receives Stats (double integers); BigIntStats has no lowering", + ); + } + fenceOrDropOptionKey( + lowerer, + prop, + key, + "fs.watchFile", + FS_WATCH_FILE_DOCUMENTED_OPTIONS, + "interval, persistent: true/false, and bigint: false are the accepted options", + ); + // An undocumented key, dropped like Node drops it. + } + } + const cb = lowerer.lowerExpr(listenerArg); + if ( + cb.type.kind !== "func" || + cb.type.ret.kind !== "void" || + cb.type.params.length > 2 || + !cb.type.params.every((p) => p.kind === "stats") + ) { + lowerer.noLowering( + "fs.watchFile with this listener shape", + listenerArg, + "the listener takes (current: Stats, previous: Stats)", + ); + } + return { + kind: "libCall", + fn: "fs.watchFile", + args: [path, interval ?? numLit(5007, loc), persistent ?? boolLit(true, loc), cb], + type: VOID, + loc, + }; +} + /** Method calls on FSWatcher receivers: close() (idempotent, statement * position — Node returns void there too) and the EventEmitter surface for * 'change', 'error' and 'close' (on/once/addListener/prependListener/ diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 03b65bd54..6a06fbaad 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -443,6 +443,13 @@ export const FS_WATCH_DOCUMENTED_OPTIONS: ReadonlySet = new Set([ "signal", ]); +/** fs.watchFile's documented option keys. */ +export const FS_WATCH_FILE_DOCUMENTED_OPTIONS: ReadonlySet = new Set([ + "interval", + "persistent", + "bigint", +]); + /** fs.readdirSync's documented option keys. */ export const FS_READDIR_DOCUMENTED_OPTIONS: ReadonlySet = new Set([ "encoding", diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 0719112cc..fdf2335a8 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -745,6 +745,7 @@ async function compileExecutableNative( dgramDyn: features.dgramDyn, textCodecDyn: features.textCodecDyn, watch: features.watch, + watchFile: features.watchFile, foreignFfi: features.foreignFfi, ...(features.workers ? { workers: true } : {}), ...(features.youngNursery ? { youngNursery: true } : {}), diff --git a/packages/compiler/src/ir/api.ts b/packages/compiler/src/ir/api.ts index 600cdb051..8ea02cfe6 100644 --- a/packages/compiler/src/ir/api.ts +++ b/packages/compiler/src/ir/api.ts @@ -28,6 +28,7 @@ export { moduleUsesQs, moduleUsesParseArgs, moduleUsesFsWatch, + moduleUsesFsWatchFile, moduleUsesNodeTest, moduleUsesDgram, moduleUsesHttpServer, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 48580f4fe..5733525cd 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -1090,6 +1090,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fs.watchCb", "watcher.on", "watcher.off", + "fs.watchFile", + "fs.unwatchFile", "crypto.x509Fingerprint", "crypto.x509FingerprintStr", "crypto.x509ValidFrom", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 47514c561..622abfd94 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1311,6 +1311,16 @@ function buildLibFnSignatures() { // the null slot pins the arity and the receiver. "watcher.on": { argTypes: [FSWATCHER_T, F64, F64, null], result: VOID }, "watcher.off": { argTypes: [FSWATCHER_T, F64, null], result: VOID }, + /** fs.watchFile(path, interval, persistent, listener) → void + * (scr_watch_file.c — its own gated unit; moduleUsesFsWatchFile is the + * switch, and scr_watch_file_install() is emitted with it). One + * libuv-shaped stat poller per RESOLVED path, shared by every listener + * on it; the listener's (current, previous) Stats func type is + * program-dependent, so the slot pins the arity. Statement position + * only (Node returns a StatWatcher). fs.unwatchFile(path[, listener]) + * takes a borrowed listener; the null slot is the absent one. */ + "fs.watchFile": { argTypes: [STRING, F64, BOOL, null], result: VOID }, + "fs.unwatchFile": { argTypes: [STRING, null], 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 diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index da4f11623..d583ea62a 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -1069,6 +1069,9 @@ const RUNTIME_FEATURE_RULES = { fetch: rule({ prefixes: ["fetch."] }), fileHandle: rule({ types: ["fileHandle"] }), fsWatch: rule({ prefixes: ["fs.watch", "watcher."], types: ["fsWatcher"] }), + /** fs.watchFile / fs.unwatchFile (scr_watch_file.c): the stat-polling + * half of the watching surface, gated on its own unit. */ + fsWatchFile: rule({ fns: ["fs.watchFile", "fs.unwatchFile"] }), /** A socket 'lookup' listener: the C main installs the net unit's * lookup records (scr_net_lookup_install). */ netLookup: rule({ fns: ["net.sockOnLookup"] }), @@ -1286,6 +1289,7 @@ function noRuntimeFeatures(): RuntimeFeatures { fetch: false, fileHandle: false, fsWatch: false, + fsWatchFile: false, http2: false, http: false, inspect: false, @@ -1631,6 +1635,15 @@ export function moduleUsesFsWatch(mod: IrModule): boolean { return scanRuntimeFeatures(mod, "fsWatch").fsWatch; } +/** True when the module contains an fs.watchFile/fs.unwatchFile libCall — + * the link switch that pulls scr_watch_file.c into the binary and has the + * emitted main call scr_watch_file_install (the moduleUsesFsWatch + * precedent, one unit of its own). Programs that never poll pay zero bytes + * and keep their exact link line. */ +export function moduleUsesFsWatchFile(mod: IrModule): boolean { + return scanRuntimeFeatures(mod, "fsWatchFile").fsWatchFile; +} + /** True when the module contains any test.* libCall or a testCtx handle * type — the link switch that pulls scr_test.c into the binary and has * the emitted main return scr_test_exit_code() after the loop drains diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 995603bcd..f5312713c 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4187,7 +4187,14 @@ function validateFunction( e.fn === "readable.unpipe" || e.fn === "rl.create" || e.fn === "rl.createSignal"; - if (variadic ? e.args.length < sig.argTypes.length : e.args.length !== sig.argTypes.length) { + // fs.unwatchFile's listener is optional, so its last slot may be + // omitted (1 or 2 args) rather than extended. + const optionalTail = e.fn === "fs.unwatchFile" ? 1 : 0; + const arityOk = variadic + ? e.args.length >= sig.argTypes.length + : e.args.length === sig.argTypes.length || + (optionalTail > 0 && e.args.length === sig.argTypes.length - optionalTail); + if (!arityOk) { err(`libCall ${e.fn}: ${e.args.length} args, expected ${sig.argTypes.length}`, e.loc); } e.args.forEach((a, i) => { diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index bc33e683a..5c8a91022 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -194,6 +194,7 @@ async function main(): Promise { dgramDyn: false, textCodecDyn: false, watch: false, + watchFile: false, foreignFfi: false, nodeTest: false, tls: false, diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index bf496aa99..009818cd7 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -268,6 +268,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "dgramDyn", "textCodecDyn", "watch", + "watchFile", "foreignFfi", "nodeTest", "tls", diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index b1d03b52d..509602d64 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -66,6 +66,7 @@ const features = { dgramDyn: false, textCodecDyn: false, watch: false, + watchFile: false, foreignFfi: false, nodeTest: false, tls: false, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-watch-file-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-watch-file-names.ts.json new file mode 100644 index 000000000..88ec75941 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-watch-file-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fs-watch-file-names.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-watch-file-polling.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-watch-file-polling.ts.json new file mode 100644 index 000000000..7f11fb9bc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-watch-file-polling.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fs-watch-file-polling.ts" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 72e888dce..d4dff88c3 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -177,6 +177,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [ // TextEncoder and TextDecoder records boxed as untyped values. ["scr_text_codec_dyn.c", "textCodecDyn"], ["scr_watch.c", "watch"], + ["scr_watch_file.c", "watchFile"], ["scr_ffi_queue.c", "foreignFfi"], ["scr_test.c", "nodeTest"], ["scr_tls_ca.c", "tlsCaEffective"], diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 67f18c0d8..8a5f46a10 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -5557,7 +5557,7 @@ static bool scr_stats_link_target(HANDLE h, DWORD tag, double *size_out, ScrStr return true; } -static ScrStats *scr_stats_of_path(const ScrStr *path, const char *op, +static ScrStats *scr_stats_win_handle_stats(const ScrStr *path, const char *op, bool no_follow) { WCHAR *wide = scr_fs_win_wide(path); if (!wide) return scr_stats_crt_fallback(path, op); @@ -5603,7 +5603,7 @@ static ScrStats *scr_stats_of_path(const ScrStr *path, const char *op, /* Reparse points are a general interception mechanism. Node/libuv * follows non-link tags even for lstat, so reopen the resolved target. */ CloseHandle(h); - return scr_stats_of_path(path, op, false); + return scr_stats_win_handle_stats(path, op, false); } if (!scr_stats_link_target(h, tagged.ReparseTag, &link_size, NULL)) { /* A mount-point tag may name a mounted volume rather than a junction. @@ -5611,7 +5611,7 @@ static ScrStats *scr_stats_of_path(const ScrStr *path, const char *op, * ordinary reparse points and retries with the final component * followed. Do not classify from the tag alone. */ CloseHandle(h); - return scr_stats_of_path(path, op, false); + return scr_stats_win_handle_stats(path, op, false); } is_link = true; } @@ -6054,7 +6054,7 @@ ScrBytes *scr_fs_readlink_bytes(ScrStr *path) { ScrStats *scr_fs_stat(ScrStr *path) { if (!scr_fs_path_ok(path, "path")) return NULL; #ifdef _WIN32 - return scr_stats_of_path(path, "stat", false); + return scr_stats_win_handle_stats(path, "stat", false); #else int error = 0; ScrStats *s = scr_stats_posix(path->data, -1, false, &error); /* follows symlinks */ @@ -6066,7 +6066,7 @@ ScrStats *scr_fs_stat(ScrStr *path) { ScrStats *scr_fs_lstat(ScrStr *path) { if (!scr_fs_path_ok(path, "path")) return NULL; #ifdef _WIN32 - return scr_stats_of_path(path, "lstat", true); + return scr_stats_win_handle_stats(path, "lstat", true); #else int error = 0; ScrStats *s = scr_stats_posix(path->data, -1, true, &error); /* no follow */ @@ -6108,6 +6108,33 @@ ScrStats *scr_fs_fstat(double fd) { return s; } +/* fs.watchFile's poll (scr_watch_file.c calls this): stat(2) following + * symlinks, libuv's uv_fs_stat — NULL with *error = errno on failure, + * never throwing, because a failed poll IS a reportable event. +1. */ +ScrStats *scr_stats_of_path(ScrStr *path, int *error) { + if (!scr_fs_path_ok(path, "path")) { + *error = EINVAL; + return NULL; + } +#ifdef _WIN32 + /* The CRT snapshot states what the probe can see without opening a + * handle: the poll needs the errno, not Win32's extended attributes. */ + struct stat st; + if (stat(path->data, &st) != 0) { + *error = errno; + return NULL; + } + return scr_stats_of_crt(&st); +#else + return scr_stats_posix(path->data, -1, false, error); +#endif +} + +/* The all-zero Stats a failed poll reports (Node's kZeroStats): every + * field 0, so isFile()/isDirectory() and the size/date reads all answer + * zero/false. +1. */ +ScrStats *scr_stats_zero(void) { return scr_stats_new(); } + /* ── Stats objects ─────────────────────────────────────────────────── * Node's Stats and BigIntStats over a snapshot. The integers and the *Ms * fields (and BigIntStats' *Ns fields) are own properties in the binding's diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index fa88873af..1a099c84d 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -479,6 +479,11 @@ double scr_stats_ctime(ScrStats *s); double scr_stats_birthtime(ScrStats *s); /* fstat without throwing: +1, or NULL with *error set to the errno. */ ScrStats *scr_stats_of_fd(int fd, int *error); +/* stat(2) following symlinks without throwing (the fs.watchFile poll): + * +1, or NULL with *error set to the errno. */ +ScrStats *scr_stats_of_path(ScrStr *path, int *error); +/* The all-zero snapshot a failed poll reports (Node's kZeroStats). +1. */ +ScrStats *scr_stats_zero(void); /* Node's Stats (or BigIntStats) object for the snapshot: own fields in the * binding's order, lazy Date accessors and the type tests on the * prototypes. Borrows s; +1. */ diff --git a/packages/runtime/src/scr_watch.h b/packages/runtime/src/scr_watch.h index c2118d1cd..83f02c917 100644 --- a/packages/runtime/src/scr_watch.h +++ b/packages/runtime/src/scr_watch.h @@ -50,6 +50,25 @@ void scr_watch_thunk0(ScrClosure *cb, const char *event, const char *filename); void scr_watch_thunk_event(ScrClosure *cb, const char *event, const char *filename); void scr_watch_err_thunk0(ScrClosure *cb, ScrError *err); void scr_watch_install(void); + +/* ── fs.watchFile (scr_watch_file.c — same gating precedent: linked only + * when the program uses it). Stat polling in Node's shape: one poller per + * RESOLVED path, shared by every listener on it. */ +typedef void (*ScrWatchFileFn)(ScrClosure *cb, ScrStats *current, ScrStats *previous); +/* fs.watchFile(path, { interval, persistent }, listener) — cb MOVES in; + * the adapter calls the listener with (current, previous) Stats (+1 each). + * The path is resolved (path.resolve) and keys the poller; a second call + * on the same resolved path adds the listener and ignores its options. */ +void scr_fs_watch_file(ScrStr *path, double interval, bool persistent, ScrClosure *cb, + ScrWatchFileFn fn); +/* fs.unwatchFile(path[, listener]) — cb borrowed, NULL removes every + * listener; with none left the poller stops. */ +void scr_fs_unwatch_file(ScrStr *path, ScrClosure *cb); +void scr_watch_file_thunk0(ScrClosure *cb, ScrStats *current, ScrStats *previous); +void scr_watch_file_thunk1(ScrClosure *cb, ScrStats *current, ScrStats *previous); +void scr_watch_file_thunk2(ScrClosure *cb, ScrStats *current, ScrStats *previous); +void scr_watch_file_install(void); + /* The loop-side registration (scr_async.c, always linked) — the net * hook's exact shape, one more nullable slot set. */ void scr_loop_set_watch(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void)); diff --git a/packages/runtime/src/scr_watch_file.c b/packages/runtime/src/scr_watch_file.c new file mode 100644 index 000000000..6b40e5ed0 --- /dev/null +++ b/packages/runtime/src/scr_watch_file.c @@ -0,0 +1,331 @@ +/* fs.watchFile / fs.unwatchFile — the stat-polling half of the OPTIONAL + * file-watching surface: one libuv-shaped poller (uv_fs_poll) per RESOLVED + * path, shared by every listener registered on it, re-arming a one-shot + * timer every `interval` ms. This translation unit links ONLY into binaries + * whose IR uses fs.watchFile/fs.unwatchFile (the scr_watch.c gating + * precedent), and the emitted main calls scr_watch_file_install() before + * %main, which registers the exit cleanup. Polling-free programs pay zero + * bytes and keep their exact link line. + * + * Semantics, matched against Node (libuv's poll_cb + Node's StatWatcher) + * and pinned by corpus: + * - The FIRST poll only records the baseline: a successful first stat + * emits nothing (libuv's busy_polling == 0), a failing first stat emits + * ONE change with both sides all-zero (Node's kZeroStats). + * - Thereafter a stat failure emits once per distinct errno; a success + * after a failure, or a success whose snapshot differs (ctime, mtime, + * birthtime, size, mode, uid, gid, ino, dev), emits (current, + * previous), where previous is the last SUCCESSFUL snapshot. + * - Node's onchange suppression: with no status reported yet, a status of + * -1 whose nlink matches the previous snapshot reports nothing. + * - watchFile on a path that already has a poller ADDS the listener and + * ignores its options; the first call's interval and persistent stand. + * - unwatchFile(path, listener) removes the LAST listener registered with + * that closure (pointer identity, Node's rule); unwatchFile(path) + * removes every listener. The last removal stops the poller (its timer + * clears), which is what lets the process exit. + * - An emit works on a snapshot of the listeners with retained closures, + * so a listener may unwatch itself or others mid-emit — the corpus's + * own shape. + * - persistent: false unrefs the poll timer: the poller still fires while + * the loop runs for something else, but never holds the process open. */ +#include "scr_runtime.h" + +#include +#include +#include +#include + +static void scr_watch_file_oom(void) { + fputs("scriptc: out of memory\n", stderr); + abort(); +} + +/* One registered listener: the closure (owned) and its (current, + * previous) adapter. */ +typedef struct { + ScrClosure *cb; /* owned */ + ScrWatchFileFn fn; +} ScrWatchFileL; + +struct ScrWatchFiler { + size_t rc; + ScrStr *path; /* the resolved path (+1): the registry key and the stat target */ + double interval; /* the first call's interval, reused for every poll */ + bool persistent; + int state; /* libuv's busy_polling: 0 before the first poll, 1 after + * a success, -errno after a failure */ + int node_status; /* Node's kOldStatus: -1 until a change is reported */ + ScrStats *last; /* the last successful snapshot (+1), NULL = none yet */ + ScrWatchFileL *ls; + size_t n, cap; + double timer; /* the armed poll timer's handle; 0 when none is armed */ + ScrClosure *pollCb; /* the re-armed poll closure (+1); NULL once stopped */ + struct ScrWatchFiler *next; /* the poller registry (+1 each) */ +}; + +typedef struct ScrWatchFiler ScrWatchFiler; + +static SCR_TL ScrWatchFiler *scr_watch_filers = NULL; /* the registry (+1 each) */ + +static ScrWatchFiler *scr_watch_file_retain(ScrWatchFiler *p) { + if (p->rc != SIZE_MAX) p->rc++; + return p; +} + +static void *scr_watch_file_retain_v(void *p) { return scr_watch_file_retain((ScrWatchFiler *)p); } + +static void scr_watch_file_release(ScrWatchFiler *p) { + if (!p || p->rc == SIZE_MAX) return; + if (--p->rc == 0) { + for (size_t i = 0; i < p->n; i++) scr_closure_release(p->ls[i].cb); + free(p->ls); + if (p->timer != 0) { + scr_clear_interval(p->timer); + p->timer = 0; + } + scr_closure_release(p->pollCb); + scr_stats_release(p->last); + scr_str_release(p->path); + free(p); + } +} + +static void scr_watch_file_release_v(void *p) { scr_watch_file_release((ScrWatchFiler *)p); } + +/* path.resolve over the one argument: the poller's key AND the stat target, + * so `./x` and `/abs/x` share one poller, exactly like Node's + * resolve-and-match. */ +static ScrStr *scr_watch_file_resolve(ScrStr *path) { + ScrArr *parts = scr_arr_new(SCR_ELEM_STR, 1); + scr_arr_push_ref(parts, scr_str_retain(path)); + ScrStr *resolved = scr_path_resolve(parts); + scr_arr_release(parts); + return resolved; +} + +static ScrWatchFiler *scr_watch_file_find(ScrStr *resolved) { + for (ScrWatchFiler *p = scr_watch_filers; p != NULL; p = p->next) { + if (scr_str_eq(p->path, resolved)) return p; + } + return NULL; +} + +/* Arms the next poll: the first one at 0 (libuv stats on a later loop + * turn, never synchronously), then one per interval. */ +static void scr_watch_file_arm(ScrWatchFiler *p, double ms) { + p->timer = scr_set_timeout_handle(scr_closure_retain(p->pollCb), ms); + if (!p->persistent) scr_timer_unref(p->timer); +} + +/* libuv's poll comparison: ctime, mtime and birthtime (the *_ms doubles + * carry the nanosecond fraction, so a sub-millisecond change still + * differs), size, mode, uid, gid, ino and dev. `uv_stat_t`'s flags and gen + * have no ScrStats field, so they are not compared (libuv does). A NULL + * side (no successful snapshot yet) never compares equal. */ +static bool scr_stats_same(ScrStats *a, ScrStats *b) { + if (a == NULL || b == NULL) return false; + return scr_stats_ctime_ms(a) == scr_stats_ctime_ms(b) && + scr_stats_mtime_ms(a) == scr_stats_mtime_ms(b) && + scr_stats_birthtime_ms(a) == scr_stats_birthtime_ms(b) && + scr_stats_size(a) == scr_stats_size(b) && scr_stats_mode(a) == scr_stats_mode(b) && + scr_stats_uid(a) == scr_stats_uid(b) && scr_stats_gid(a) == scr_stats_gid(b) && + scr_stats_ino(a) == scr_stats_ino(b) && scr_stats_dev(a) == scr_stats_dev(b); +} + +/* Emits one change: `cur` is the fresh snapshot (NULL = all-zero), + * `prev` the last successful one (NULL = all-zero), `new_status` Node's + * status for this poll (0 success, -errno failure). Listeners run in + * registration order off a snapshot, each with its own +1 Stats refs. */ +static void scr_watch_file_emit(ScrWatchFiler *p, ScrStats *cur, ScrStats *prev, int new_status) { + double ncur = cur != NULL ? scr_stats_nlink(cur) : 0; + double nprev = prev != NULL ? scr_stats_nlink(prev) : 0; + if (p->node_status == -1 && new_status == -1 && ncur == nprev) return; + p->node_status = new_status; + if (p->n == 0) return; + ScrStats *c = cur != NULL ? scr_stats_retain(cur) : scr_stats_zero(); + ScrStats *v = prev != NULL ? scr_stats_retain(prev) : scr_stats_zero(); + ScrWatchFileL *snap = malloc(p->n * sizeof *snap); + if (!snap) { + scr_stats_release(c); + scr_stats_release(v); + scr_watch_file_oom(); + } + memcpy(snap, p->ls, p->n * sizeof *snap); + size_t n = p->n; + for (size_t i = 0; i < n; i++) scr_closure_retain(snap[i].cb); + for (size_t i = 0; i < n; i++) { + if (!scr_exc_pending()) snap[i].fn(snap[i].cb, scr_stats_retain(c), scr_stats_retain(v)); + scr_closure_release(snap[i].cb); + } + free(snap); + scr_stats_release(c); + scr_stats_release(v); +} + +/* The timer's body: stat, decide (libuv's poll_cb then Node's onchange), + * and re-arm unless a listener stopped the poller. */ +static void scr_watch_file_poll(ScrClosure *cb) { + ScrWatchFiler *p = scr_box_get_ref(cb->caps[0]); + if (p == NULL) return; + p->timer = 0; /* the loop consumed this arm */ + int err = 0; + ScrStats *cur = scr_stats_of_path(p->path, &err); + if (cur == NULL) { + if (p->state != -err) { + scr_watch_file_emit(p, NULL, p->last, -err); + p->state = -err; + } + } else { + if (p->state != 0 && (p->state < 0 || !scr_stats_same(p->last, cur))) + scr_watch_file_emit(p, cur, p->last, 0); + scr_stats_release(p->last); + p->last = scr_stats_retain(cur); + p->state = 1; + } + scr_stats_release(cur); /* NULL-tolerant */ + /* A listener may have unwatched the last listener (unwatchFile stops + * the poller): only a poller with listeners re-arms. */ + if (p->n > 0 && p->pollCb != NULL && !scr_exc_pending()) scr_watch_file_arm(p, p->interval); + scr_watch_file_release(p); +} + +static void scr_watch_file_add(ScrWatchFiler *p, ScrClosure *cb /*moves*/, ScrWatchFileFn fn) { + if (p->n == p->cap) { + size_t cap = p->cap != 0 ? p->cap * 2 : 4; + ScrWatchFileL *grown = realloc(p->ls, cap * sizeof *grown); + if (!grown) { + scr_closure_release(cb); + scr_watch_file_oom(); + } + p->ls = grown; + p->cap = cap; + } + p->ls[p->n].cb = cb; + p->ls[p->n].fn = fn; + p->n++; +} + +/* Stops a poller: clears its timer, unlinks it from the registry, and + * breaks the poll-closure cycle (closure → box → poller) so the last + * external reference frees the poller. Safe from inside a listener: the + * poll frame's own reference keeps `p` alive. */ +static void scr_watch_file_stop(ScrWatchFiler *p) { + scr_watch_file_retain(p); /* the frame's reference */ + if (p->timer != 0) { + scr_clear_interval(p->timer); + p->timer = 0; + } + for (ScrWatchFiler **pp = &scr_watch_filers; *pp != NULL; pp = &(*pp)->next) { + if (*pp == p) { + *pp = p->next; + p->next = NULL; + scr_watch_file_release(p); /* the registry's reference */ + break; + } + } + ScrClosure *pollCb = p->pollCb; + p->pollCb = NULL; + scr_closure_release(pollCb); /* drops the box's reference */ + scr_watch_file_release(p); +} + +/* fs.watchFile(path, interval, persistent, listener) — the listener MOVES + * in. A second call on the same resolved path adds the listener to the + * existing poller and ignores its options (Node's rule). */ +void scr_fs_watch_file(ScrStr *path, double interval, bool persistent, ScrClosure *cb, + ScrWatchFileFn fn) { + ScrStr *resolved = scr_watch_file_resolve(path); + ScrWatchFiler *existing = scr_watch_file_find(resolved); + if (existing != NULL) { + scr_str_release(resolved); + scr_watch_file_add(existing, cb, fn); + return; + } + ScrWatchFiler *p = malloc(sizeof *p); + if (!p) { + scr_str_release(resolved); + scr_closure_release(cb); + scr_watch_file_oom(); + } + p->rc = 1; + p->path = resolved; + p->interval = interval; + p->persistent = persistent; + p->state = 0; + p->node_status = -1; + p->last = NULL; + p->ls = NULL; + p->n = p->cap = 0; + p->timer = 0; + /* The poll closure captures the poller through a box, which retains it + * from here on (the cycle scr_watch_file_stop breaks). */ + p->pollCb = scr_closure_new((void *)scr_watch_file_poll, 1); + p->pollCb->caps[0] = + scr_box_new_obj(scr_watch_file_retain_v, scr_watch_file_release_v, NULL); + scr_box_set_ref(p->pollCb->caps[0], p); + scr_watch_file_add(p, cb, fn); + p->next = scr_watch_filers; + scr_watch_filers = p; + scr_watch_file_retain(p); /* the registry's reference */ + scr_watch_file_arm(p, 0); + /* The creating reference moved into the box (scr_box_set_ref moves, it + * does not retain): the box and the registry are the two owners. */ +} + +/* fs.unwatchFile(path[, listener]) — `cb` is borrowed (pointer identity); + * NULL removes every listener on the resolved path. Removing the last one + * stops the poller. */ +void scr_fs_unwatch_file(ScrStr *path, ScrClosure *cb) { + ScrStr *resolved = scr_watch_file_resolve(path); + ScrWatchFiler *p = scr_watch_file_find(resolved); + scr_str_release(resolved); + if (p == NULL) return; + if (cb != NULL) { + /* The LAST listener registered with this closure goes (Node's rule). */ + for (size_t i = p->n; i-- > 0;) { + if (p->ls[i].cb == cb) { + scr_closure_release(p->ls[i].cb); + memmove(&p->ls[i], &p->ls[i + 1], (p->n - i - 1) * sizeof *p->ls); + p->n--; + break; + } + } + } else { + for (size_t i = 0; i < p->n; i++) scr_closure_release(p->ls[i].cb); + p->n = 0; + } + if (p->n == 0) scr_watch_file_stop(p); +} + +/* ── the runtime-provided listener adapters ─────────────────────────── + * The listener owns its +1 arguments (the universal convention + * scr_watch_thunk_event follows). */ +void scr_watch_file_thunk0(ScrClosure *cb, ScrStats *current, ScrStats *previous) { + (void)current; + (void)previous; + ((void (*)(ScrClosure *))cb->fn)(cb); +} + +void scr_watch_file_thunk1(ScrClosure *cb, ScrStats *current, ScrStats *previous) { + (void)previous; + ((void (*)(ScrClosure *, ScrStats *))cb->fn)(cb, current); +} + +void scr_watch_file_thunk2(ScrClosure *cb, ScrStats *current, ScrStats *previous) { + ((void (*)(ScrClosure *, ScrStats *, ScrStats *))cb->fn)(cb, current, previous); +} + +/* Exit-time registry cleanup (the scr_watch.c precedent): pollers a program + * legitimately leaves running at exit release their listeners and timers, + * so the RC audit sees a clean heap. */ +static void scr_watch_file_cleanup_atexit(void) { + while (scr_watch_filers != NULL) scr_watch_file_stop(scr_watch_filers); +} + +void scr_watch_file_install(void) { + static SCR_TL bool installed = false; + if (installed) return; + installed = true; + scr_atexit(scr_watch_file_cleanup_atexit); +} diff --git a/tests/corpus/fs-watch-file-names.ts b/tests/corpus/fs-watch-file-names.ts new file mode 100644 index 000000000..61c824332 --- /dev/null +++ b/tests/corpus/fs-watch-file-names.ts @@ -0,0 +1,60 @@ +// fs.watch listeners that take the filename, on a FILE watch: the +// filename is the watched file's basename. The listener can be a runtime +// value typed WatchListener, and the watcher's EventEmitter +// surface takes more 'change' listeners (on), a one-shot one (once), an +// 'error' listener (never called here), and a 'close' listener, which +// runs on the next tick after close() (before immediates) and only once +// however often close() is called. Triggers repeat on an interval until +// the event lands; event counts and coalescing are deliberately +// unobserved (they differ between inotify, kqueue and FSEvents). +import * as fs from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +const dir = fs.mkdtempSync(join(tmpdir(), "scr-watch-names-")); +const head = join(dir, "HEAD"); +fs.writeFileSync(head, "ref: main\n"); + +let firstName: string | null = "unset"; +let secondName = "unset"; +let onceCalls = 0; +const listener: fs.WatchListener = (eventType, filename) => { + if (firstName !== "unset") return; + firstName = filename; + console.log("listener value names", filename, eventType === "change" || eventType === "rename"); +}; +const watcher = fs.watch(head, listener); +// 'change' listener named as any: the @types/node overload declares +// filename string | Buffer, whose bytes arm has no lowering (Node passes a +// string with the default encoding), so the checked name arrives as dyn. +watcher.on("change", (_eventType: string, filename: any) => { + if (secondName !== "unset") return; + secondName = String(filename); + console.log("on('change') names", secondName); +}); +watcher.once("change", () => { + onceCalls++; +}); +watcher.on("error", (error: Error) => console.log("error", error.message)); +watcher.on("close", () => { + console.log("close event", onceCalls); + setImmediate(() => { + console.log("immediate after close"); + fs.rmSync(dir, { recursive: true, force: true }); + }); +}); + +const bound = setTimeout(() => { + console.log("timed out"); + process.exit(1); +}, 10000); +const touch = setInterval(() => fs.appendFileSync(head, "x"), 40); +const poll = setInterval(() => { + if (firstName === "unset" || secondName === "unset") return; + clearInterval(poll); + clearInterval(touch); + clearTimeout(bound); + watcher.close(); + watcher.close(); + console.log("closed"); +}, 10); diff --git a/tests/corpus/fs-watch-file-polling.ts b/tests/corpus/fs-watch-file-polling.ts new file mode 100644 index 000000000..87cddf867 --- /dev/null +++ b/tests/corpus/fs-watch-file-polling.ts @@ -0,0 +1,49 @@ +// fs.watchFile / fs.unwatchFile: stat polling at the given interval. The +// listener receives (current, previous) Stats whenever a poll sees a +// difference; the first poll only records the baseline. A missing file +// polls as all-zero Stats: the first poll reports one change with both +// sides zeroed, and the creation reports previous.size 0 and previous.mtimeMs 0. Two watchFile calls on one path share one poller; +// unwatchFile(path, listener) removes one listener, unwatchFile(path) +// removes the rest, and once no listener is left the poller stops and +// the process can exit. +import * as fs from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +const dir = fs.mkdtempSync(join(tmpdir(), "scr-watchfile-")); +const existing = join(dir, "existing.txt"); +const later = join(dir, "later.txt"); +fs.writeFileSync(existing, "a"); + +let firstCalls = 0; +const first = (current: fs.Stats, previous: fs.Stats): void => { + firstCalls++; + if (firstCalls === 1) console.log("existing grew", previous.size, "->", current.size); +}; +const second = (current: fs.Stats, previous: fs.Stats): void => { + console.log("second listener", previous.size, "->", current.size, current.isFile()); + fs.unwatchFile(existing, first); + fs.unwatchFile(existing); + startMissingPhase(); +}; +fs.watchFile(existing, { interval: 20 }, first); +fs.watchFile(existing, { interval: 1000 }, second); +setTimeout(() => fs.writeFileSync(existing, "abcd"), 150); + +function startMissingPhase(): void { + let events = 0; + const listener = (current: fs.Stats, previous: fs.Stats): void => { + events++; + if (events === 1) { + // The first poll fails (ENOENT): one change with both sides zeroed. + console.log("missing", previous.size, previous.mtimeMs, previous.isFile(), current.size, current.isFile()); + setTimeout(() => fs.writeFileSync(later, "hello"), 100); + return; + } + console.log("created", previous.size, previous.mtimeMs, "->", current.size, current.isFile()); + fs.unwatchFile(later, listener); + fs.rmSync(dir, { recursive: true, force: true }); + console.log("unwatched"); + }; + fs.watchFile(later, { interval: 20, persistent: true }, listener); +} diff --git a/tests/harness/corpus-costs/fs-watch-file-names.ts.json b/tests/harness/corpus-costs/fs-watch-file-names.ts.json new file mode 100644 index 000000000..91f0b7565 --- /dev/null +++ b/tests/harness/corpus-costs/fs-watch-file-names.ts.json @@ -0,0 +1 @@ +{ "plain": 1300, "san": 3300 } diff --git a/tests/harness/corpus-costs/fs-watch-file-polling.ts.json b/tests/harness/corpus-costs/fs-watch-file-polling.ts.json new file mode 100644 index 000000000..376e9a4e6 --- /dev/null +++ b/tests/harness/corpus-costs/fs-watch-file-polling.ts.json @@ -0,0 +1 @@ +{ "plain": 1300, "san": 3800 } From 7dbcf07fe03383be1f0813b11c2c70649524d6cb Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 19:12:05 -0700 Subject: [PATCH 12/70] Pass console lines and byte chunks to a patched stdio write Node's console writes each formatted line with stream.write(line, errorHandler), so a replaced process.stdout.write sees a string and a function; a Buffer write arrives as a Buffer. The stdio write hook carried only a `wide` bit, so every intercepted chunk became a JavaScript string and console lines carried a single argument. Replace that bit with a `flags` int (SCR_STDIO_HOOK_WIDE, _CONSOLE, _BYTES) and keep the hook signature at four parameters, so the console write path and the byte paths differ only in what they pass. scr_stdio_intercept now builds a chunk instead of a string, boxes SCR_STDIO_HOOK_BYTES as a Buffer, and appends a console error handler for SCR_STDIO_HOOK_CONSOLE. Text callers of the old string entry point move to a new scr_stdio_write_text. Corpus: stdout-patch-console-arguments.js. Co-Authored-By: Claude Code Co-Authored-By: Claude 5.5 --- .../stdout-patch-console-arguments.js.json | 6 ++++ packages/runtime/src/scr_console.c | 18 +++++++--- packages/runtime/src/scr_console_native.c | 4 +-- packages/runtime/src/scr_events.c | 33 ++++++++++++++++--- packages/runtime/src/scr_lib.h | 13 ++++++-- .../corpus/stdout-patch-console-arguments.js | 29 ++++++++++++++++ .../stdout-patch-console-arguments.js.json | 1 + 7 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-console-arguments.js.json create mode 100644 tests/corpus/stdout-patch-console-arguments.js create mode 100644 tests/harness/corpus-costs/stdout-patch-console-arguments.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-console-arguments.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-console-arguments.js.json new file mode 100644 index 000000000..d8798d615 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-console-arguments.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/stdout-patch-console-arguments.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index f1fefa626..040cb2234 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -203,7 +203,7 @@ void scr_init(void) { * the stream differs. */ /* A real definition lets relocatable library links localize this hook; * a tentative common symbol stays externally visible on Mach-O. */ -SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len, bool wide) = NULL; +SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len, int flags) = NULL; SCR_TL bool (*scr_stdio_capture)(int fd, const void *data, size_t len) = NULL; SCR_TL bool scr_stdin_fed = false; @@ -420,7 +420,7 @@ static void scr_console_write(int fd, size_t n, const ScrLogArg *args, bool glob } if (scr_stdio_write_hook) { /* A patched write receives the JavaScript string, lone surrogates kept. */ - scr_stdio_write_hook(fd, line, used, wide); + scr_stdio_write_hook(fd, line, used, (wide ? SCR_STDIO_HOOK_WIDE : 0) | SCR_STDIO_HOOK_CONSOLE); if (scr_exc_pending()) scr_exc_clear(); } else { if (wide) scr_wtf8_to_utf8(line, used); @@ -481,8 +481,16 @@ int scr_stdio_write_raw(int fd, const void *data, size_t len) { #endif } +/* A byte chunk (process.stdout.write(buffer), a stream piped to stdout). */ bool scr_stdio_write(int fd, const void *data, size_t len) { - if (scr_stdio_write_hook) return scr_stdio_write_hook(fd, data, len, false); + if (scr_stdio_write_hook) return scr_stdio_write_hook(fd, data, len, SCR_STDIO_HOOK_BYTES); + (void)scr_stdio_write_raw(fd, data, len); + return true; +} + +/* A string chunk that is plain UTF-8. */ +bool scr_stdio_write_text(int fd, const void *data, size_t len) { + if (scr_stdio_write_hook) return scr_stdio_write_hook(fd, data, len, 0); (void)scr_stdio_write_raw(fd, data, len); return true; } @@ -490,7 +498,7 @@ bool scr_stdio_write(int fd, const void *data, size_t len) { /* A string chunk: a patched write receives the string itself; the stream * receives its UTF-8, each lone surrogate as U+FFFD. */ static __attribute__((noinline)) bool scr_stdio_write_wide(int fd, const ScrStr *s) { - if (scr_stdio_write_hook) return scr_stdio_write_hook(fd, s->data, s->len, true); + if (scr_stdio_write_hook) return scr_stdio_write_hook(fd, s->data, s->len, SCR_STDIO_HOOK_WIDE); char *owned; const char *bytes = scr_str_utf8(s, &owned); (void)scr_stdio_write_raw(fd, bytes, s->len); @@ -500,5 +508,5 @@ static __attribute__((noinline)) bool scr_stdio_write_wide(int fd, const ScrStr bool scr_stdio_write_str(int fd, const ScrStr *s) { if (__builtin_expect(scr_wtf8_any(), 0) && !s->ascii) return scr_stdio_write_wide(fd, s); - return scr_stdio_write(fd, s->data, s->len); + return scr_stdio_write_text(fd, s->data, s->len); } diff --git a/packages/runtime/src/scr_console_native.c b/packages/runtime/src/scr_console_native.c index e11ffd3af..82dc476a1 100644 --- a/packages/runtime/src/scr_console_native.c +++ b/packages/runtime/src/scr_console_native.c @@ -489,8 +489,8 @@ void scr_console_clear(void) { if (!scr_process_is_tty(1)) return; const char *term = getenv("TERM"); if (term && strcmp(term, "dumb") == 0) return; - scr_stdio_write(1, "\x1b[1;1H", 6); - scr_stdio_write(1, "\x1b[0J", 4); + scr_stdio_write_text(1, "\x1b[1;1H", 6); + scr_stdio_write_text(1, "\x1b[0J", 4); } /* console.dir over a dyn value: inspect with custom inspection off. The diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index 78b7bb0f1..de1d12105 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -1507,9 +1507,19 @@ static ScrDyn *scr_stdio_invoke(void *ptr, ScrDyn *self, const char *name, return result; } -static bool scr_stdio_intercept(int fd, const void *data, size_t length, bool wide) { +/* Node's console error handler for a stream write (createWriteErrorHandler): + * console swallows write errors, so it ignores its argument. */ +static ScrDyn *scr_stdio_console_error_handler(ScrClosure *cb, ScrDyn *const *args, size_t argc) { + (void)cb; (void)args; (void)argc; + return scr_dyn_retain(scr_dyn_undefined()); +} + +static SCR_TL ScrDyn *scr_stdio_console_handler = NULL; + +static bool scr_stdio_intercept(int fd, const void *data, size_t length, int flags) { ScrStdio *stream = &scr_stdio_streams[fd == 2 ? 2 : 1]; if (!stream->write || scr_dyn_strict_eq(stream->write, scr_stdio_methods[STDIO_WRITE])) { + bool wide = (flags & SCR_STDIO_HOOK_WIDE) != 0; char *owned = NULL; if (wide && scr_wtf8_find_surrogate(data, length)) { owned = malloc(length ? length : 1); @@ -1522,12 +1532,22 @@ static bool scr_stdio_intercept(int fd, const void *data, size_t length, bool wi free(owned); return true; } - ScrStr *text = scr_str_new(data, length); - ScrDyn *chunk = scr_dyn_new_str(text); - scr_str_release(text); + ScrDyn *chunk; + if (flags & SCR_STDIO_HOOK_BYTES) { + ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)length); + memcpy(b->data, data, length); + chunk = scr_dyn_new_chunk(b); + scr_bytes_release(b); + } else { + ScrStr *text = scr_str_new(data, length); + chunk = scr_dyn_new_str(text); + scr_str_release(text); + } + ScrDyn *argv[2] = {chunk, scr_stdio_console_handler}; + size_t argc = (flags & SCR_STDIO_HOOK_CONSOLE) ? 2 : 1; scr_dyn_this_push(stream, SCR_DYNH_STDIO); ScrDyn *fn = scr_dyn_retain(stream->write); - ScrDyn *result = scr_dyn_call(fn, &chunk, 1, "write"); + ScrDyn *result = scr_dyn_call(fn, argv, argc, "write"); scr_dyn_release(fn); scr_dyn_this_pop(); bool accepted = result && scr_dyn_truthy(result); @@ -1562,6 +1582,8 @@ static void scr_stdio_cleanup(void) { scr_dyn_release(scr_stdio_methods[i]); scr_stdio_methods[i] = NULL; } + scr_dyn_release(scr_stdio_console_handler); + scr_stdio_console_handler = NULL; scr_stdio_initialized = false; } @@ -1582,6 +1604,7 @@ ScrDyn *scr_process_stdio(double fd) { scr_box_set_f64(cb->caps[0], i); scr_stdio_methods[i] = scr_dyn_new_func(cb, scr_stdio_method, i == STDIO_WRITE ? 3 : 1, "", scr_stdio_names[i]); } + scr_stdio_console_handler = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_stdio_console_error_handler, 1, "", ""); scr_stdio_write_hook = scr_stdio_intercept; } return scr_dyn_new_handle(&scr_stdio_streams[fd == 0 ? 0 : fd == 2 ? 2 : 1], SCR_DYNH_STDIO); diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index fa88873af..324a4c041 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -71,9 +71,16 @@ ScrStr *scr_process_cwd(void); /* +1 fresh (getcwd) */ bool scr_stdio_write(int fd, const void *data, size_t len); bool scr_stdio_write_str(int fd, const ScrStr *s); int scr_stdio_write_raw(int fd, const void *data, size_t len); -/* `wide` marks string bytes that may hold lone surrogates, which become - * U+FFFD when they reach the stream itself. */ -extern SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len, bool wide); +/* `flags` describes the chunk a patched write receives: SCR_STDIO_HOOK_WIDE + * marks string bytes that may hold lone surrogates (they become U+FFFD when + * they reach the stream itself), SCR_STDIO_HOOK_CONSOLE a console line + * (Node's console passes its error handler as the second argument), and + * SCR_STDIO_HOOK_BYTES a byte chunk (the replacement receives a Buffer). */ +enum { SCR_STDIO_HOOK_WIDE = 1, SCR_STDIO_HOOK_CONSOLE = 2, SCR_STDIO_HOOK_BYTES = 4 }; +extern SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len, int flags); +/* A string chunk given as bytes (UTF-8, no lone surrogates): what + * scr_stdio_write was for callers that write text. */ +bool scr_stdio_write_text(int fd, const void *data, size_t len); /* A worker created with the stdout or stderr option: answers true when it * took the bytes for fd 1 or 2 instead of the process descriptor. */ extern SCR_TL bool (*scr_stdio_capture)(int fd, const void *data, size_t len); diff --git a/tests/corpus/stdout-patch-console-arguments.js b/tests/corpus/stdout-patch-console-arguments.js new file mode 100644 index 000000000..7f73d785f --- /dev/null +++ b/tests/corpus/stdout-patch-console-arguments.js @@ -0,0 +1,29 @@ +// What a replaced process.stdout.write receives from console: Node's +// console writes each formatted line with stream.write(line, errorHandler), +// so the replacement sees a string and a function as its second argument +// for console.log, info, debug, dir, count, table and grouped lines alike; +// a direct write passes exactly its own arguments. The error handler accepts +// a null error. +const original = process.stdout.write; +const seen = []; +process.stdout.write = function (chunk, encodingOrCallback, callback) { + const kind = typeof chunk === "string" ? "string" : Buffer.isBuffer(chunk) ? "Buffer" : chunk.constructor.name; + seen.push(`${kind}:${JSON.stringify(String(chunk))}:${typeof encodingOrCallback}:${typeof callback}`); + if (typeof encodingOrCallback === "function") encodingOrCallback(null); + return true; +}; +console.log("line", 1); +console.info("info"); +console.debug("debug"); +console.log("%s=%d", "x", 2); +console.dir({ a: 1 }); +console.count(); +console.group("group"); +console.log("inside"); +console.groupEnd(); +console.table([{ a: 1 }]); +process.stdout.write("direct"); +process.stdout.write("hex", "utf8"); +process.stdout.write(Buffer.from("bytes")); +process.stdout.write = original; +console.log(seen.join("\n")); diff --git a/tests/harness/corpus-costs/stdout-patch-console-arguments.js.json b/tests/harness/corpus-costs/stdout-patch-console-arguments.js.json new file mode 100644 index 000000000..b5d3a2f90 --- /dev/null +++ b/tests/harness/corpus-costs/stdout-patch-console-arguments.js.json @@ -0,0 +1 @@ +{ "plain": 5460, "san": 7830 } From 22357e8bedc5b5d2ab27c7a9573457366cec064a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 28 Oct 2026 22:18:11 -0500 Subject: [PATCH 13/70] Lower ReadableStream tee, pipeTo and pipeThrough in TypeScript In TypeScript, tee(), pipeTo() and pipeThrough() on a ReadableStream were refused with SC2020, and the WritableStream constructor and a TransformStream with a transformer had no lowering, while JavaScript programs reach all of them through the web stream classes (scr_webstreams.c). A TypeScript program that uses one of them now builds its ReadableStream, WritableStream and TransformStream values with those classes, as JavaScript does, and %main installs them, so a native stream such as a Response body answers the three methods through its web view. A program that uses only getReader(), cancel(), locked and the identity transform keeps the native streams and builds to the same binary. The fallback declarations gain UnderlyingSink and Transformer, the WritableStream constructor takes a sink and the TransformStream constructor a transformer and queuing strategies, and their controllers map to checked-dynamic handles. The fetch profile lists the three methods as static with the new static-web-streams fetch fixture as their differential evidence, and keeps them in the set that links the classes. The surface manifest is regenerated, and the surface-manifest probe that kept tee() and pipeTo() fenced now fences values() and the async iterator. Adds the static-web-streams fixture and the corpus program web-streams-typed-pipes.ts, with its corpus cost and order baseline. --- .../builtin-apis/streams-and-readline.mdx | 6 +- .../ambient/scriptc-node-fallback.d.ts | 31 +++- packages/compiler/src/compat/fetch-profile.ts | 51 +++--- .../lowering/builtins/web-streams-dyn.ts | 85 +++++++++- .../src/frontend/lowering/lower-stdlib.ts | 7 +- .../compiler/src/frontend/lowering/lowerer.ts | 4 + packages/compiler/src/frontend/type-mapper.ts | 2 + packages/compiler/surface-manifest.json | 15 +- .../corpus/web-streams-typed-pipes.ts.json | 6 + tests/corpus/web-streams-typed-pipes.ts | 158 ++++++++++++++++++ .../fetch/static-web-streams/main.mts | 96 +++++++++++ .../__snapshots__/node-types-fenced.txt | 26 +-- .../web-streams-typed-pipes.ts.json | 1 + tests/harness/fetch.test.ts | 1 + tests/harness/surface-manifest.test.ts | 2 +- 15 files changed, 419 insertions(+), 72 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-streams-typed-pipes.ts.json create mode 100644 tests/corpus/web-streams-typed-pipes.ts create mode 100644 tests/fixtures/fetch/static-web-streams/main.mts create mode 100644 tests/harness/corpus-costs/web-streams-typed-pipes.ts.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index 52bf534e8..01cd9eb9d 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -35,7 +35,9 @@ `stream.compose` accepts streams and async generator functions in any position, an async function as the last stage, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it. A web `ReadableStream` is read as an async iterable by `Readable.from` and as the first stage of `compose` or `pipeline`, and through its reader by `Readable.fromWeb` (with `objectMode`, `highWaterMark`, `encoding`, and `signal` in an options object literal) and `Duplex.from`. In TypeScript, `new TransformStream()` without arguments builds the identity transform: its readable side is a web `ReadableStream`, its writable side supports `getWriter`, `abort`, `close`, and `locked`, and the writer supports `write`, `close`, `abort`, `releaseLock`, `ready`, `closed`, and `desiredSize`, with Node's backpressure (a write completes when the readable side reads its chunk). - A web stream in a later compose stage and `Readable.toWeb` are unsupported, and in TypeScript, so are a `TransformStream` with a transformer or queuing strategies, the `WritableStream` constructor, `pipeTo`, and `pipeThrough`. + In TypeScript, `tee`, `pipeTo`, and `pipeThrough`, the `WritableStream` constructor with an underlying sink, and a `TransformStream` with a transformer or queuing strategies compile: a program that uses one of them builds its `ReadableStream`, `WritableStream`, and `TransformStream` values with the same classes as JavaScript, and a `Response` or `Request` body has those members too. + A TypeScript program that uses only `getReader`, `cancel`, `locked`, and the identity `TransformStream` keeps the smaller native streams. + A web stream in a later compose stage and `Readable.toWeb` are unsupported. The `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. `readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read. `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. @@ -60,7 +62,7 @@ The `readline/promises` `Readline` class writes to the same outputs. `readline.emitKeypressEvents` emits `'keypress'` events on `process.stdin` or a readable stream, shared with terminal interfaces on that stream as in Node; a listener's first parameter must admit `undefined`, which it receives for escape sequences. Chained listener registration is unsupported. - `node:stream/web` exports the same classes as the globals; in TypeScript, only `ReadableStream` and the identity `TransformStream` have lowerings. + `node:stream/web` exports the same classes as the globals; in TypeScript, `ReadableStream`, `WritableStream`, and `TransformStream` have lowerings. In JavaScript, `ReadableStream`, `WritableStream`, and `TransformStream` with their default readers, writers, and controllers, `CountQueuingStrategy`, `ByteLengthQueuingStrategy`, `TextEncoderStream`, and `TextDecoderStream`, as globals and as `node:stream/web` exports, are constructor values with Node's prototypes, brand checks, and errors. Their underlying sources, sinks, and transformers, queuing strategies with a `size` function, readers and writers, `tee`, `pipeTo` and `pipeThrough` with their options and an abort signal, `ReadableStream.from`, and async iteration settle promises in Node's order, and the streams take own properties. `TextDecoderStream` decodes UTF-8; its other encodings are unsupported. diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index edb391f7d..7a7b61fd7 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -1115,9 +1115,9 @@ declare var ReadableStream: { from(iterable: string): ReadableStream; readonly prototype: ReadableStream; }; -/* The writable side of a TransformStream and its writer. The - * WritableStream constructor has no native lowering; writable streams come - * from `new TransformStream()`. */ +/* The writable side of a TransformStream and its writer. A program that + * constructs a WritableStream, or a TransformStream with a transformer, + * builds its streams with the web stream classes (scr_webstreams.c). */ interface WritableStreamDefaultWriter { readonly closed: Promise; readonly desiredSize: number | null; @@ -1133,14 +1133,25 @@ interface WritableStream { close(): Promise; getWriter(): WritableStreamDefaultWriter; } -/* The identity transform: chunks written to `writable` are read from - * `readable`, and a write completes when its chunk is read. */ +/* Without a transformer, the identity transform: chunks written to + * `writable` are read from `readable`, and a write completes when its chunk + * is read. */ interface TransformStream { readonly readable: ReadableStream; readonly writable: WritableStream; } +interface Transformer { + start?(controller: TransformStreamDefaultController): unknown; + transform?(chunk: I, controller: TransformStreamDefaultController): unknown; + flush?(controller: TransformStreamDefaultController): unknown; + cancel?(reason?: unknown): unknown; +} declare var TransformStream: { - new (): TransformStream; + new ( + transformer?: Transformer, + writableStrategy?: QueuingStrategy, + readableStrategy?: QueuingStrategy, + ): TransformStream; readonly prototype: TransformStream; }; /* The other web stream interfaces. JavaScript sources construct and read @@ -1158,8 +1169,14 @@ interface WritableStreamDefaultController { readonly signal: AbortSignal; error(reason?: unknown): void; } +interface UnderlyingSink { + start?(controller: WritableStreamDefaultController): unknown; + write?(chunk: W, controller: WritableStreamDefaultController): unknown; + close?(): unknown; + abort?(reason?: unknown): unknown; +} declare var WritableStream: { - new (sink?: object, strategy?: QueuingStrategy): WritableStream; + new (sink?: UnderlyingSink, strategy?: QueuingStrategy): WritableStream; readonly prototype: WritableStream; }; declare var WritableStreamDefaultWriter: { diff --git a/packages/compiler/src/compat/fetch-profile.ts b/packages/compiler/src/compat/fetch-profile.ts index 07f5f09c8..e723d2df6 100644 --- a/packages/compiler/src/compat/fetch-profile.ts +++ b/packages/compiler/src/compat/fetch-profile.ts @@ -155,6 +155,12 @@ const typedInterfaceUnsupported = "typed source has no compiler bridge for this const metadataExclusion = "WebIDL brand metadata is observable reflection, not an executable compatibility operation"; +/** ReadableStream methods a typed program reaches through the web stream + * classes (scr_webstreams.c): on a stream it constructs with them, and on + * the native stream through its web view (scr_fetch.c, sf_stream_web_view). + * A program that calls one links the classes. */ +export const READABLE_STREAM_WEB_VIEW_CALLS = ["pipeThrough", "pipeTo", "tee"] as const; + function buildFetchCompatProfile() { return { schemaVersion: 1, @@ -233,7 +239,7 @@ function buildFetchCompatProfile() { ], responseCalls: ["json", "text", "bytes", "clone"], readableStreamReads: ["locked"], - readableStreamCalls: ["cancel", "getReader"], + readableStreamCalls: ["cancel", "getReader", ...READABLE_STREAM_WEB_VIEW_CALLS], }, operations: [ { @@ -447,6 +453,13 @@ function buildFetchCompatProfile() { facets: ["webidl-conversion", "identity", "state-machine", "error-shape"], evidence: [generated("stream-traces"), fixture("static-stream")], }, + ...READABLE_STREAM_WEB_VIEW_CALLS.map((member): FetchCompatOperation => ({ + id: `stdlib.readable-stream.${member}`, + name: `ReadableStream.${member}`, + kind: "method", + facets: ["identity", "promise-settlement", "state-machine", "error-shape"], + evidence: [fixture("static-web-streams")], + })), { id: "stdlib.readable-stream-default-reader.closed", name: "ReadableStreamDefaultReader.closed", @@ -757,25 +770,15 @@ function buildFetchCompatProfile() { "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", - ), + ...READABLE_STREAM_WEB_VIEW_CALLS.map((member) => + staticEntry(`stdlib.readable-stream.${member}`, "ReadableStream", member, "prototype"), ), - ...["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.values", + "ReadableStream", + "values", + "prototype", + "direct for-await consumption compiles statically; storing or driving the first-class iterator handle is unsupported", ), unsupportedEntry( "stdlib.readable-stream.symbol.asyncIterator", @@ -954,7 +957,8 @@ export interface StaticFetchMembers { readableStreamReads: ReadonlySet; readableStreamCalls: ReadonlySet; /** ReadableStream prototype members the native stream does not - * implement, symbol-keyed ones as `[Symbol.name]`. */ + * implement itself, symbol-keyed ones as `[Symbol.name]`: the unsupported + * ones, and the calls it answers through its web view. */ readableStreamUnsupported: ReadonlySet; } @@ -979,8 +983,8 @@ export function staticFetchMembers(): StaticFetchMembers { ), readableStreamReads: new Set(profile.members.readableStreamReads), readableStreamCalls: new Set(profile.members.readableStreamCalls), - readableStreamUnsupported: new Set( - profile.inventory.entries + readableStreamUnsupported: new Set([ + ...profile.inventory.entries .filter( (entry) => entry.owner === "ReadableStream" && @@ -988,7 +992,8 @@ export function staticFetchMembers(): StaticFetchMembers { entry.status === "unsupported", ) .map((entry) => entry.member), - ), + ...READABLE_STREAM_WEB_VIEW_CALLS, + ]), }; return staticFetchMemberSets; } diff --git a/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts b/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts index 765d3beae..67caec84a 100644 --- a/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts +++ b/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts @@ -1,4 +1,7 @@ -import { staticFetchMembers } from "../../../compat/fetch-profile.js"; +import { + READABLE_STREAM_WEB_VIEW_CALLS, + staticFetchMembers, +} from "../../../compat/fetch-profile.js"; import { numLit } from "../../../ir/build.js"; import { everyStmt } from "../../../ir/traverse.js"; import { @@ -16,6 +19,7 @@ import { constituentTypes } from "../../ts7/checker.js"; import { ENTRY_NAME, type Lowerer } from "../lowerer.js"; import { stdlibGlobalNameOf } from "../surfaces.js"; import { streamWebImportOf } from "./module-bindings.js"; +import { lowerDynObjectLiteral } from "../expressions/object-literals.js"; /* The web stream classes in JavaScript sources: the globals and the * node:stream/web exports, which are the same constructors. Each reference @@ -92,6 +96,63 @@ export function programNamesWebStreamClass(lowerer: Lowerer): boolean { return found; } +/** The classes a TypeScript source constructs through the web stream + * classes once it uses an operation the native streams lack. */ +const TYPED_CLASSES: ReadonlySet = new Set([ + "ReadableStream", + "WritableStream", + "TransformStream", +]); +const WEB_VIEW_CALLS: ReadonlySet = new Set(READABLE_STREAM_WEB_VIEW_CALLS); + +/** Whether a TypeScript source of the program uses a web stream operation + * the native streams lack: a ReadableStream tee(), pipeTo() or + * pipeThrough() call, a WritableStream constructor, or a TransformStream + * constructor with arguments (a transformer or queuing strategies). Such a + * program builds its ReadableStream, WritableStream and TransformStream + * values with the web stream classes, as JavaScript does; a program that + * uses only getReader(), cancel() and the identity transform keeps the + * native streams. */ +export function programUsesTypedWebStreams(lowerer: Lowerer): boolean { + if (lowerer.typedWebStreams !== undefined) return lowerer.typedWebStreams; + let found = false; + for (const file of lowerer.program.getSourceFiles()) { + if (file.isDeclarationFile || isJsSourceFile(file)) continue; + ts.walkPreorder(file, (node) => { + if ( + ts.isPropertyAccessExpression(node) && + WEB_VIEW_CALLS.has(node.name.text) && + isReadableStreamMember(lowerer, node) + ) + found = true; + else if (ts.isNewExpression(node)) { + const name = stdlibGlobalNameOf(lowerer, unparenthesized(node.expression)); + if ( + name === "WritableStream" || + (name === "TransformStream" && (node.arguments?.nodes.length ?? 0) > 0) + ) + found = true; + } + return found ? "stop" : undefined; + }); + if (found) break; + } + lowerer.typedWebStreams = found; + return found; +} + +/** The class a TypeScript `new` expression constructs through the web + * stream classes (programUsesTypedWebStreams), as its index in + * WEB_STREAM_CLASSES; null otherwise. */ +function typedWebStreamClassKindOf(lowerer: Lowerer, node: ts.Expression): number | null { + const expr = unparenthesized(node); + if (!ts.isIdentifier(expr) || isJsSourceFile(ts.getSourceFileOfNode(expr))) return null; + const name = streamWebImportOf(lowerer, expr) ?? stdlibGlobalNameOf(lowerer, expr); + if (name === null || !TYPED_CLASSES.has(name) || !programUsesTypedWebStreams(lowerer)) + return null; + return CLASS_NAMES.indexOf(name); +} + export function webStreamClassValue(kind: number, loc: SrcLoc): IrExpr { return { kind: "libCall", fn: "webstream.class", args: [numLit(kind, loc)], type: DYN, loc }; } @@ -102,13 +163,24 @@ export function lowerWebStreamClassRead(lowerer: Lowerer, node: ts.Expression): return kind === null ? null : webStreamClassValue(kind, locOf(node)); } -/** `new X(...args)` on one of the classes in JavaScript. */ +/** `new X(...args)` on one of the classes in JavaScript, and on + * ReadableStream, WritableStream and TransformStream in a TypeScript + * program that uses the web stream classes (programUsesTypedWebStreams). + * A TypeScript source's object literal argument (a source, sink, + * transformer or strategy) becomes a plain object with its methods. */ export function lowerWebStreamNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { - const kind = webStreamClassKindOf(lowerer, expr.expression); + const typed = webStreamClassKindOf(lowerer, expr.expression) === null; + const kind = typed + ? typedWebStreamClassKindOf(lowerer, expr.expression) + : webStreamClassKindOf(lowerer, expr.expression); if (kind === null) return null; const nodes = expr.arguments?.nodes ?? []; if (nodes.some(ts.isSpreadElement)) return null; const loc = locOf(expr); + const argument = (arg: ts.Expression): IrExpr => + typed && ts.isObjectLiteralExpression(arg) + ? lowerDynObjectLiteral(lowerer, arg) + : lowerer.lowerExprExpecting(arg, DYN); return { kind: "libCall", fn: "dyn.construct", @@ -116,7 +188,7 @@ export function lowerWebStreamNew(lowerer: Lowerer, expr: ts.NewExpression): IrE webStreamClassValue(kind, loc), { kind: "dynArrLit", - elems: nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + elems: nodes.map(argument), type: DYN, loc, }, @@ -285,6 +357,7 @@ function programLinksFetch(functions: IrFunction[]): boolean { * count in a program that links the fetch unit, since the value can be a * fetch body. */ function programUsesWebStreamOnlyMembers(lowerer: Lowerer, functions: IrFunction[]): boolean { + if (programUsesTypedWebStreams(lowerer)) return true; const members = staticFetchMembers().readableStreamUnsupported; const iterates = members.has("[Symbol.asyncIterator]"); let untyped = false; @@ -310,7 +383,9 @@ function programUsesWebStreamOnlyMembers(lowerer: Lowerer, functions: IrFunction } /** Links the web stream classes into a program whose JavaScript uses a - * ReadableStream member the fetch unit's own stream lacks: a fetch body's + * ReadableStream member the fetch unit's own stream lacks, or whose + * TypeScript uses the web stream classes (programUsesTypedWebStreams): a + * fetch body's or a native stream's * handle stands for a web ReadableStream once such a member is used * (scr_fetch.c, sf_stream_web_view). %main installs the classes first. A * program that names one of the classes links them already. */ diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 44f2b7370..f1db4bcf1 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -1425,7 +1425,7 @@ export function fenceStaticReadableStreamMember( lowerer.noLowering( `ReadableStream.${member} in a static build`, access, - "the native static ReadableStream surface is locked plus cancel() and getReader(); values()/[Symbol.asyncIterator]() compile when consumed directly by for await, while stored iterator handles remain unsupported", + "ReadableStream's locked compiles as a read, and getReader(), cancel(), tee(), pipeTo() and pipeThrough() as direct calls; values()/[Symbol.asyncIterator]() compile when consumed directly by for await, while method values and stored iterator handles remain unsupported", sym, ); } @@ -1965,8 +1965,9 @@ export function lowerStaticReadableStreamNew( /** `new TransformStream()`: the identity transform, a native handle whose * readable side is a native ReadableStream and whose writable side is the - * runtime's WritableStream. A transformer or queuing strategies have no - * native lowering yet and keep the SC2020 refusal. */ + * runtime's WritableStream. A TransformStream with a transformer or + * queuing strategies is built with the web stream classes instead + * (web-streams-dyn.ts, lowerWebStreamNew). */ export function lowerStaticTransformStreamNew( lowerer: Lowerer, expr: ts.NewExpression, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 6ae1ea9f5..c3a5ec42f 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3783,6 +3783,9 @@ export class Lowerer { /** web-streams-dyn.ts's answer: whether a JavaScript source names a web * stream class; undefined until asked. */ namesWebStreamClass: boolean | undefined = undefined; + /** web-streams-dyn.ts's answer: whether a TypeScript source uses a web + * stream operation the native streams lack; undefined until asked. */ + typedWebStreams: boolean | undefined = undefined; /** The first lifted function of each name (liftedFnNamed), for the * liftedFns indexed so far. liftedFns only grows. */ private readonly liftedFnsByName = new Map(); @@ -4413,6 +4416,7 @@ export class Lowerer { "sloppyThisGlobalReads", "globalLookupNames", "namesWebStreamClass", + "typedWebStreams", "timeValueDateDecisions", "timeValueDateWrites", "dynamicOverloadAnswers", diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 36dae9b10..12aa672f4 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -220,8 +220,10 @@ export const FETCH_AMBIENT_TYPES = [ "ReadableStreamReadValueResult", "ReadableStreamReadDoneResult", "TransformStream", + "TransformStreamDefaultController", "WritableStream", "WritableStreamDefaultWriter", + "WritableStreamDefaultController", "Blob", "File", "BlobPropertyBag", diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index ea8295406..480a18409 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -5039,17 +5039,15 @@ "id": "stdlib.readable-stream.pipeThrough", "kind": "stdlib", "name": "ReadableStream.pipeThrough", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.22.0 / Undici 7.29.1; the wider Web Streams graph is outside the native readable-stream slice" + "status": "static", + "note": "Node 24.22.0 / Undici 7.29.1; facets: identity, promise-settlement, state-machine, error-shape; differential evidence: fixture:static-web-streams" }, { "id": "stdlib.readable-stream.pipeTo", "kind": "stdlib", "name": "ReadableStream.pipeTo", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.22.0 / Undici 7.29.1; this operation is outside the native readable-stream implementation" + "status": "static", + "note": "Node 24.22.0 / Undici 7.29.1; facets: identity, promise-settlement, state-machine, error-shape; differential evidence: fixture:static-web-streams" }, { "id": "stdlib.readable-stream.symbol.asyncIterator", @@ -5063,9 +5061,8 @@ "id": "stdlib.readable-stream.tee", "kind": "stdlib", "name": "ReadableStream.tee", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.22.0 / Undici 7.29.1; this operation is outside the native readable-stream implementation" + "status": "static", + "note": "Node 24.22.0 / Undici 7.29.1; facets: identity, promise-settlement, state-machine, error-shape; differential evidence: fixture:static-web-streams" }, { "id": "stdlib.readable-stream.values", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-streams-typed-pipes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-streams-typed-pipes.ts.json new file mode 100644 index 000000000..8455ba03b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-streams-typed-pipes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/web-streams-typed-pipes.ts" + ], + "diags": [] +} diff --git a/tests/corpus/web-streams-typed-pipes.ts b/tests/corpus/web-streams-typed-pipes.ts new file mode 100644 index 000000000..ab5564baa --- /dev/null +++ b/tests/corpus/web-streams-typed-pipes.ts @@ -0,0 +1,158 @@ +// ReadableStream tee(), pipeTo() and pipeThrough() in TypeScript, with +// WritableStream sinks and TransformStream transformers: chunk order, close +// and flush, cancel through a tee branch, the pipe options, Node's +// TypeErrors for locked streams, error propagation through a pipe, and a +// Response body piped through a transform. + +const describe = (e: unknown): string => `${(e as Error).name}: ${(e as Error).message}`; + +function source(chunks: string[]): ReadableStream { + return new ReadableStream({ + start(controller) { + for (const chunk of chunks) controller.enqueue(chunk); + controller.close(); + }, + }); +} + +async function drain(stream: ReadableStream): Promise { + const out: T[] = []; + const reader = stream.getReader(); + for (;;) { + const result = await reader.read(); + if (result.done) break; + out.push(result.value); + } + return out; +} + +async function tees(): Promise { + const [left, right] = source(["a", "b", "c"]).tee(); + console.log("tee", (await drain(left)).join(""), (await drain(right)).join("")); + + const log: string[] = []; + const watched = new ReadableStream({ + start(controller) { + controller.enqueue(1); + controller.enqueue(2); + }, + cancel(reason) { + log.push(`source cancel ${String(reason)}`); + }, + }); + const [a, b] = watched.tee(); + // The source is cancelled once both branches are, with both reasons. + const leftDone = a.cancel("left done").then(() => log.push("left cancelled")); + log.push("left requested"); + await Promise.all([leftDone, b.cancel("right done").then(() => log.push("right cancelled"))]); + console.log("tee cancel", log.join(" | "), watched.locked); + + const locked = source(["x"]); + locked.getReader(); + try { + locked.tee(); + } catch (e) { + console.log("tee locked", describe(e)); + } +} + +async function pipes(): Promise { + const written: string[] = []; + const sink = new WritableStream({ + write(chunk) { + written.push(chunk); + }, + close() { + written.push(""); + }, + }); + await source(["one", "two"]).pipeTo(sink); + console.log("pipeTo", written.join(","), sink.locked); + + const kept: string[] = []; + const open = new WritableStream({ + write(chunk) { + kept.push(chunk); + }, + close() { + kept.push(""); + }, + }); + await source(["p"]).pipeTo(open, { preventClose: true }); + const writer = open.getWriter(); + await writer.write("after"); + await writer.close(); + console.log("preventClose", kept.join(",")); + + const failing = new ReadableStream({ + start(controller) { + controller.enqueue("ok"); + controller.error(new RangeError("source failed")); + }, + }); + const aborted: string[] = []; + try { + await failing.pipeTo( + new WritableStream({ + write(chunk) { + aborted.push(chunk); + }, + abort(reason) { + aborted.push(`abort ${describe(reason)}`); + }, + }), + ); + } catch (e) { + console.log("pipeTo error", describe(e), aborted.join(" | ")); + } + + const busy = source(["q"]); + busy.getReader(); + try { + await busy.pipeTo(new WritableStream()); + } catch (e) { + console.log("pipeTo locked", describe(e)); + } +} + +async function transforms(): Promise { + const upper = new TransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk.toUpperCase()); + }, + flush(controller) { + controller.enqueue("!"); + }, + }); + console.log("pipeThrough", (await drain(source(["ab", "cd"]).pipeThrough(upper))).join("")); + + const lengths = new TransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk.length); + }, + }); + console.log("lengths", (await drain(source(["a", "bbb", "cc"]).pipeThrough(lengths))).join(" ")); + + const identity = new TransformStream(); + console.log("identity", (await drain(source(["i", "j"]).pipeThrough(identity))).join("")); + + const body = new Response("fetch body").body!; + const decoded = body.pipeThrough( + new TransformStream({ + transform(chunk, controller) { + controller.enqueue(new TextDecoder().decode(chunk)); + }, + }), + ); + console.log("response body", (await drain(decoded)).join("")); + + const [first, second] = new Response("split").body!.tee(); + const decoder = new TextDecoder(); + const text = async (s: ReadableStream): Promise => + (await drain(s)).map((chunk) => decoder.decode(chunk)).join(""); + console.log("response tee", await text(first), await text(second)); +} + +await tees(); +await pipes(); +await transforms(); diff --git a/tests/fixtures/fetch/static-web-streams/main.mts b/tests/fixtures/fetch/static-web-streams/main.mts new file mode 100644 index 000000000..e8e171d66 --- /dev/null +++ b/tests/fixtures/fetch/static-web-streams/main.mts @@ -0,0 +1,96 @@ +// ReadableStream tee(), pipeTo() and pipeThrough() in a static build: a +// fetched body (whole and chunked) tee'd, piped into a WritableStream and +// through a TransformStream, constructed streams through a transformer, +// and Node's TypeErrors for locked streams and errors through a pipe. +const base = process.argv[2]!; +const describe = (e: unknown): string => (e instanceof Error ? `${e.name}: ${e.message}` : String(e)); +const decoder = new TextDecoder(); + +async function collect(stream: ReadableStream): Promise { + const reader = stream.getReader(); + let text = ""; + for (;;) { + const result = await reader.read(); + if (result.done) break; + text += decoder.decode(result.value, { stream: true }); + } + return text + decoder.decode(); +} + +const fetched = await fetch(`${base}/text`); +const [left, right] = fetched.body!.tee(); +console.log("tee", await collect(left), await collect(right), fetched.bodyUsed); + +const chunked = await fetch(`${base}/chunked`); +const parts: string[] = []; +await chunked.body!.pipeTo( + new WritableStream({ + write(chunk) { + parts.push(decoder.decode(chunk, { stream: true })); + }, + close() { + parts.push(""); + }, + }), +); +console.log("pipeTo", parts.join("").length > 0, parts[parts.length - 1], chunked.bodyUsed); + +const upper = await fetch(`${base}/text`); +const shouted = upper.body!.pipeThrough( + new TransformStream({ + transform(chunk, controller) { + controller.enqueue(new TextEncoder().encode(decoder.decode(chunk).toUpperCase())); + }, + }), +); +console.log("pipeThrough", await collect(shouted)); + +const words = new ReadableStream({ + start(controller) { + controller.enqueue("alpha"); + controller.enqueue("beta"); + controller.close(); + }, +}); +const counted = words.pipeThrough( + new TransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk.length); + }, + flush(controller) { + controller.enqueue(-1); + }, + }), +); +const counts: number[] = []; +const reader = counted.getReader(); +for (;;) { + const result = await reader.read(); + if (result.done) break; + counts.push(result.value); +} +console.log("constructed", counts.join(" ")); + +const locked = await fetch(`${base}/text`); +locked.body!.getReader(); +try { + locked.body!.tee(); +} catch (e) { + console.log("tee locked", describe(e)); +} +try { + await locked.body!.pipeTo(new WritableStream()); +} catch (e) { + console.log("pipeTo locked", describe(e)); +} + +const failing = new ReadableStream({ + start(controller) { + controller.error(new TypeError("source failed")); + }, +}); +try { + await failing.pipeTo(new WritableStream()); +} catch (e) { + console.log("pipeTo error", describe(e)); +} diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index ada6231d4..073a031e5 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -142,41 +142,23 @@ fenced.ts:80:3 - error SC2020: 'RequestInit option 'integrity' in a static build hint: the compiler does not preserve this RequestInit member's conversion or transport behavior -fenced.ts:84:3 - error SC2020: 'ReadableStream.tee' is typed by @types/node but has no scriptc lowering yet - - 83 | const body = (await fetch(url)).body!; - 84 | body.tee(); - | ^~~~~~~~ - 85 | const tee = body.tee; - - hint: this operation is outside the native readable-stream implementation - -fenced.ts:85:15 - error SC2020: 'ReadableStream.tee' is typed by @types/node but has no scriptc lowering yet +fenced.ts:85:15 - error SC2020: 'ReadableStream.tee in a static build' is typed by @types/node but has no scriptc lowering yet 84 | body.tee(); 85 | const tee = body.tee; | ^~~~~~~~ 86 | body["tee"](); - hint: this operation is outside the native readable-stream implementation + hint: ReadableStream's locked compiles as a read, and getReader(), cancel(), tee(), pipeTo() and pipeThrough() as direct calls; values()/[Symbol.asyncIterator]() compile when consumed directly by for await, while method values and stored iterator handles remain unsupported -fenced.ts:86:3 - error SC2020: 'ReadableStream.tee' is typed by @types/node but has no scriptc lowering yet - - 85 | const tee = body.tee; - 86 | body["tee"](); - | ^~~~~~~~~~~ - 87 | const bracketTee = body["tee"]; - - hint: this operation is outside the native readable-stream implementation - -fenced.ts:87:22 - error SC2020: 'ReadableStream.tee' is typed by @types/node but has no scriptc lowering yet +fenced.ts:87:22 - error SC2020: 'ReadableStream.tee in a static build' is typed by @types/node but has no scriptc lowering yet 86 | body["tee"](); 87 | const bracketTee = body["tee"]; | ^~~~~~~~~~~ 88 | } - hint: this operation is outside the native readable-stream implementation + hint: ReadableStream's locked compiles as a read, and getReader(), cancel(), tee(), pipeTo() and pipeThrough() as direct calls; values()/[Symbol.asyncIterator]() compile when consumed directly by for await, while method values and stored iterator handles remain unsupported fenced.ts:93:23 - error SC2020: 'util.callbackify into a callback whose error parameter excludes null' is part of the standard library types but has no scriptc lowering yet diff --git a/tests/harness/corpus-costs/web-streams-typed-pipes.ts.json b/tests/harness/corpus-costs/web-streams-typed-pipes.ts.json new file mode 100644 index 000000000..5cce6d4a6 --- /dev/null +++ b/tests/harness/corpus-costs/web-streams-typed-pipes.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 6000 } diff --git a/tests/harness/fetch.test.ts b/tests/harness/fetch.test.ts index 1df7965f6..acd93af81 100644 --- a/tests/harness/fetch.test.ts +++ b/tests/harness/fetch.test.ts @@ -165,6 +165,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { "static-dispatch-throw", "static-response-json", "static-clone", + "static-web-streams", ] as const; test.for( staticCases.flatMap((name) => (["llvm"] as const).map((backend) => [name, backend] as const)), diff --git a/tests/harness/surface-manifest.test.ts b/tests/harness/surface-manifest.test.ts index 710cc529b..c4650151f 100644 --- a/tests/harness/surface-manifest.test.ts +++ b/tests/harness/surface-manifest.test.ts @@ -450,7 +450,7 @@ test("unimplemented Response and ReadableStream calls stay fenced", () => { " void value.blob(); void value.formData();\n" + "}\n" + "function stream(value: ReadableStream): void {\n" + - " void value.tee(); void value.pipeTo(null as never);\n" + + " void value.values(); void value[Symbol.asyncIterator]();\n" + "}\n" + "void response; void stream;\n", }); From 6d07a49cbfc4ef88852f173681c43a4994a3a8ed Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 28 Oct 2026 23:24:06 -0500 Subject: [PATCH 14/70] Return freed memory sooner: chunked shared writes, a bounded string spare, idle trims Three runtime changes that lower peak memory without changing behavior: - In programs with workers, process.stdout.write and process.stderr.write of a view into a SharedArrayBuffer read it through a 64 KiB buffer instead of copying the whole view first, so writing a 32 MB shared view no longer holds a second 32 MB. Programs without workers keep the direct write. - The one-slot string spare keeps freed blocks of 512 bytes to 1 MiB only. A larger block stayed resident until a string of about its size came along, which a program that replaces its input with an edited copy never does. - The main thread trims its arena when its event loop waits with nothing ready, once it has carved 16 MB more since its last trim, as a parking thread does: pages that hold only free blocks go back to the system while the program waits on its workers. --- .../corpus/shared-bytes-stdio-write.ts.json | 6 +++ packages/runtime/src/scr_alloc.c | 22 +++++++++- packages/runtime/src/scr_alloc.test.c | 44 +++++++++++++++++++ packages/runtime/src/scr_bytes_io.c | 29 ++++++++++-- packages/runtime/src/scr_string.c | 8 +++- tests/corpus/shared-bytes-stdio-write.ts | 27 ++++++++++++ .../shared-bytes-stdio-write.ts.json | 1 + 7 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/shared-bytes-stdio-write.ts.json create mode 100644 tests/corpus/shared-bytes-stdio-write.ts create mode 100644 tests/harness/corpus-costs/shared-bytes-stdio-write.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/shared-bytes-stdio-write.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/shared-bytes-stdio-write.ts.json new file mode 100644 index 000000000..33c91a4c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/shared-bytes-stdio-write.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/shared-bytes-stdio-write.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index 13b83a396..11e675542 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -1599,10 +1599,30 @@ static void scr_sa_group_starting(void) { * waits. Only that thread writes it. */ static _Atomic(unsigned long) scr_sa_loop_turns; +/* The carved bytes of the main thread's arena at its last idle trim. */ +static size_t scr_sa_idle_carved; + +/* The main thread returns its free pages when its loop waits with nothing + * ready, as a parking thread does (scr_sa_trim), once it has carved at least + * SCR_SA_TRIM_MIN more since its last trim: memory it freed (a large teardown + * before it starts workers, say) then stops counting while the workers + * allocate their own. Measuring the carved bytes costs one pass over the + * classes; the trim itself only runs when the frontier has grown. */ +static void scr_sa_idle_trim(void) { + if (scr_sa_arena < 0) return; + size_t carved = scr_sa_carved(); + if (carved - scr_sa_idle_carved < SCR_SA_TRIM_MIN) return; + scr_sa_trim(); + scr_sa_idle_carved = scr_sa_carved(); +} + void scr_sa_loop_waiting(bool waiting) { if (scr_sa_worker) return; unsigned long turns = atomic_load_explicit(&scr_sa_loop_turns, memory_order_relaxed); - if ((bool)(turns & 1) != waiting) atomic_store_explicit(&scr_sa_loop_turns, turns + 1, memory_order_relaxed); + if ((bool)(turns & 1) != waiting) { + atomic_store_explicit(&scr_sa_loop_turns, turns + 1, memory_order_relaxed); + if (waiting) scr_sa_idle_trim(); + } } /* Waits out the delay first, and stops if a worker starts meanwhile: the diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c index bf9fc7e64..ce57aa48c 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -923,6 +923,49 @@ static void check_discard_waits_for_loop(void) { free(discard_blocks); } +/* The thread that is not a worker returns its free pages when its loop + * waits with nothing ready, once it has carved SCR_SA_TRIM_MIN more since + * its last trim; blocks allocated afterwards come from the returned range, + * zeroed. The test's thread plays the loop's. IDLE_SIZE is a class the + * other checks leave unused. */ +enum { IDLE_BLOCKS = 1 << 20, IDLE_KEEP = 4096, IDLE_SIZE = 88 }; +static void check_idle_trim(void) { + unsigned char **blocks = malloc(IDLE_BLOCKS * sizeof *blocks); + unsigned char *kept[IDLE_BLOCKS / IDLE_KEEP]; + assert(blocks); + uintptr_t lo = UINTPTR_MAX, hi = 0; + for (size_t i = 0; i < IDLE_BLOCKS; i++) { + blocks[i] = scr_mem_alloc(IDLE_SIZE); + assert(blocks[i] && owned(blocks[i])); + fill(blocks[i], IDLE_SIZE, (unsigned char)i); + if ((uintptr_t)blocks[i] < lo) lo = (uintptr_t)blocks[i]; + if ((uintptr_t)blocks[i] + IDLE_SIZE > hi) hi = (uintptr_t)blocks[i] + IDLE_SIZE; + } + for (size_t i = 0; i < IDLE_BLOCKS; i++) { + if (i % IDLE_KEEP == 0) kept[i / IDLE_KEEP] = blocks[i]; + else scr_mem_free(blocks[i]); + } + size_t total = (hi - lo) / (size_t)sysconf(_SC_PAGESIZE); + assert(resident_pages(lo, hi) > total / 2); + scr_sa_loop_waiting(true); + scr_sa_loop_waiting(false); + /* One page per kept block stays, give or take the pages at run ends. */ + assert(resident_pages(lo, hi) < total / 8); + for (size_t k = 0; k < IDLE_BLOCKS / IDLE_KEEP; k++) verify(kept[k], IDLE_SIZE, (unsigned char)(k * IDLE_KEEP)); + size_t outside = 0; + for (size_t i = 0; i < IDLE_BLOCKS / 2; i++) { + unsigned char *q = scr_mem_calloc(IDLE_SIZE); + assert(q && owned(q)); + for (size_t k = 0; k < IDLE_SIZE; k++) assert(q[k] == 0); + if ((uintptr_t)q < lo || (uintptr_t)q >= hi) outside++; + blocks[i] = q; + } + assert(outside < 256); + for (size_t i = 0; i < IDLE_BLOCKS / 2; i++) scr_mem_free(blocks[i]); + for (size_t k = 0; k < IDLE_BLOCKS / IDLE_KEEP; k++) scr_mem_free(kept[k]); + free(blocks); +} + static void check_discard(void) { discard_blocks = malloc(DISCARD_BLOCKS * sizeof *discard_blocks); assert(discard_blocks); @@ -1172,6 +1215,7 @@ int main(void) { check_discard(); check_discard_delay_grows(); check_discard_waits_for_loop(); + check_idle_trim(); #endif printf("allocator checks passed: size-class workers\n"); #else diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 0c4bee4ee..2459d2e05 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1226,16 +1226,37 @@ ScrBytes *scr_crypto_random_bytes(double n) { /* ── process.stdout/stderr.write(buf) ──────────────────────────────────── */ +/* Where another thread can write shared bytes (where SCR_BYTES_SNAPSHOT + * copies them), they are written through a bounded local buffer, read with + * scr_shared_load, so no shared raw memory reaches the system call and a + * large shared view is not copied whole first. */ +static bool scr_stdio_write_bytes(int fd, const ScrBytes *b) { + size_t n = b->len * scr_bytes_elem_size(b->elem); +#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) + if (!b->shared) return scr_stdio_write(fd, b->data, n); + enum { CHUNK = 64 * 1024 }; + unsigned char *chunk = scr_mem_alloc(n < CHUNK ? (n ? n : 1) : CHUNK); + bool ok = true; + for (size_t off = 0; ok && off < n; off += CHUNK) { + size_t k = n - off < CHUNK ? n - off : CHUNK; + scr_shared_load(chunk, b->data + off, k); + ok = scr_stdio_write(fd, chunk, k); + } + scr_mem_free(chunk); + return ok; +#else + return scr_stdio_write(fd, b->data, n); +#endif +} + bool scr_process_stdout_write_bytes(const ScrBytes *b, const ScrStr *encoding) { - SCR_BYTES_SNAPSHOT(b); (void)encoding; - return scr_stdio_write(1, b->data, b->len * scr_bytes_elem_size(b->elem)); + return scr_stdio_write_bytes(1, b); } bool scr_process_stderr_write_bytes(const ScrBytes *b, const ScrStr *encoding) { - SCR_BYTES_SNAPSHOT(b); (void)encoding; - return scr_stdio_write(2, b->data, b->len * scr_bytes_elem_size(b->elem)); + return scr_stdio_write_bytes(2, b); } /* Buffer.from over checked-native input. Strings use the existing codec; diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index d712ebb84..14203cf96 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -432,9 +432,13 @@ static ScrStr *scr_str_new_ascii(const char *bytes, size_t len) { #ifndef SCR_RC_AUDIT static SCR_TL ScrStr *scr_str_spare; #endif +/* The largest block the spare keeps. A larger one stays resident until a + * string of about its size comes along, which may be never: a program that + * replaces a 50 MB input with its edited copy would otherwise hold both. */ +#define SCR_STR_SPARE_MAX ((size_t)1 << 20) /* A spare-block reuse must not waste grossly (cap <= 4x the need) and only - * sizable blocks are worth stashing (>= 512). */ + * sizable blocks are worth stashing (512 bytes to SCR_STR_SPARE_MAX). */ static ScrStr *scr_str_take_spare(size_t len) { #ifndef SCR_RC_AUDIT ScrStr *s = scr_str_spare; @@ -483,7 +487,7 @@ void scr_str_release(ScrStr *s) { scr_live_strings--; #endif #ifndef SCR_RC_AUDIT - if (s->cap >= 512) { + if (s->cap >= 512 && s->cap <= SCR_STR_SPARE_MAX) { ScrStr *old = scr_str_spare; scr_str_spare = s; if (!old) return; diff --git a/tests/corpus/shared-bytes-stdio-write.ts b/tests/corpus/shared-bytes-stdio-write.ts new file mode 100644 index 000000000..0a138fb77 --- /dev/null +++ b/tests/corpus/shared-bytes-stdio-write.ts @@ -0,0 +1,27 @@ +// process.stdout.write and process.stderr.write of views into a SharedArrayBuffer that a worker +// filled: a view larger than one write chunk (64 KiB), at a byte offset, one shorter than a chunk, +// and an empty one. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +type Fill = { buffer: SharedArrayBuffer }; + +if (isMainThread) { + const sab = new SharedArrayBuffer(200_000); + const worker = new Worker(new URL(import.meta.url), { workerData: { buffer: sab } }); + worker.on("message", () => { + process.stdout.write(new Uint8Array(sab, 1000, 150_000)); + process.stdout.write("\n"); + process.stdout.write(new Uint8Array(sab, 0, 0)); + + process.stdout.write(new Uint8Array(sab, 160_000, 20_000)); + process.stdout.write("\n"); + + process.stderr.write(new Uint8Array(sab, 63, 70_000)); + process.stderr.write("\n"); + console.log("done"); + }); +} else { + const bytes = new Uint8Array((workerData as Fill).buffer); + for (let i = 0; i < bytes.length; i++) bytes[i] = i % 64 === 63 ? 10 : 65 + (i % 26); + parentPort!.postMessage("filled"); +} diff --git a/tests/harness/corpus-costs/shared-bytes-stdio-write.ts.json b/tests/harness/corpus-costs/shared-bytes-stdio-write.ts.json new file mode 100644 index 000000000..46d9e66e9 --- /dev/null +++ b/tests/harness/corpus-costs/shared-bytes-stdio-write.ts.json @@ -0,0 +1 @@ +{ "plain": 8136, "san": 68380 } From 8c79a0f6b8fcdee8a7b361e4c32edad476764868 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 28 Oct 2026 23:42:43 -0500 Subject: [PATCH 15/70] Pass only the line to a patched stdio write without ignoreErrors A Console instance over process stdio passed a patched write the line and an error handler, and discarded what the write threw, whatever its ignoreErrors setting. Node's kWriteToConsole does that only with ignoreErrors (the default, and the global console). With ignoreErrors false, from the options or the third constructor argument, it calls stream.write(line) alone and lets a throw propagate out of the console method. The console write path now takes the instance's ignoreErrors, so such an instance passes only the line and keeps the write's exception pending. Instances bound to other streams already followed the setting. --- ...stance-ignore-errors-stdout-patch.cjs.json | 6 +++ packages/runtime/src/scr_console.c | 26 ++++++----- packages/runtime/src/scr_console.h | 4 +- packages/runtime/src/scr_console_native.c | 2 +- ...le-instance-ignore-errors-stdout-patch.cjs | 43 +++++++++++++++++++ ...stance-ignore-errors-stdout-patch.cjs.json | 1 + 6 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/console-instance-ignore-errors-stdout-patch.cjs.json create mode 100644 tests/corpus/console-instance-ignore-errors-stdout-patch.cjs create mode 100644 tests/harness/corpus-costs/console-instance-ignore-errors-stdout-patch.cjs.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/console-instance-ignore-errors-stdout-patch.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/console-instance-ignore-errors-stdout-patch.cjs.json new file mode 100644 index 000000000..8fa6ef329 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/console-instance-ignore-errors-stdout-patch.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/console-instance-ignore-errors-stdout-patch.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 040cb2234..d28f058fa 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -387,9 +387,12 @@ SCR_TL char *(*scr_console_indenter)(const char *line, size_t *used) = NULL; /* Console methods submit one formatted chunk to their backing stream before * returning, as Node's console does: a caller observing a live child sees - * this line before the next JS turn. Errors are ignored (the global Node - * console uses ignoreErrors=true for stream writes). */ -static void scr_console_write(int fd, size_t n, const ScrLogArg *args, bool global) { + * this line before the next JS turn. With ignoreErrors (the global console, + * and Console instances by default) a patched write receives the line and + * an error handler, and what it throws is discarded; without it the write + * receives only the line and its throw propagates, as in Node's + * kWriteToConsole. */ +static void scr_console_write(int fd, size_t n, const ScrLogArg *args, bool global, bool ignore_errors) { #if SCR_STDIO_DIRECT /* A large single string goes out without a copy: string + newline in one * vectored write. */ @@ -420,8 +423,9 @@ static void scr_console_write(int fd, size_t n, const ScrLogArg *args, bool glob } if (scr_stdio_write_hook) { /* A patched write receives the JavaScript string, lone surrogates kept. */ - scr_stdio_write_hook(fd, line, used, (wide ? SCR_STDIO_HOOK_WIDE : 0) | SCR_STDIO_HOOK_CONSOLE); - if (scr_exc_pending()) scr_exc_clear(); + scr_stdio_write_hook(fd, line, used, + (wide ? SCR_STDIO_HOOK_WIDE : 0) | (ignore_errors ? SCR_STDIO_HOOK_CONSOLE : 0)); + if (ignore_errors && scr_exc_pending()) scr_exc_clear(); } else { if (wide) scr_wtf8_to_utf8(line, used); (void)scr_stdio_write_raw(fd, line, used); @@ -430,7 +434,7 @@ static void scr_console_write(int fd, size_t n, const ScrLogArg *args, bool glob } void scr_console_log(size_t n, const ScrLogArg *args) { - scr_console_write(1, n, args, true); + scr_console_write(1, n, args, true, true); } /* console.error and console.warn. Runtime-internal stdout output that is @@ -438,21 +442,21 @@ void scr_console_log(size_t n, const ScrLogArg *args) { * merged redirection (2>&1); JavaScript-visible stdout writes have already * been submitted. */ void scr_console_error(size_t n, const ScrLogArg *args) { - scr_console_write(2, n, args, true); + scr_console_write(2, n, args, true, true); } void scr_console_log_parts(size_t n, const ScrLogArg *args) { - scr_console_write(1, n, args, true); + scr_console_write(1, n, args, true, true); } void scr_console_error_parts(size_t n, const ScrLogArg *args) { - scr_console_write(2, n, args, true); + scr_console_write(2, n, args, true, true); } /* A Console instance's line on process stdout (fd 1) or stderr (fd 2): * the global console's group indentation does not apply. */ -void scr_console_write_fd(int fd, size_t n, const ScrLogArg *args) { - scr_console_write(fd == 2 ? 2 : 1, n, args, false); +void scr_console_write_fd(int fd, size_t n, const ScrLogArg *args, bool ignore_errors) { + scr_console_write(fd == 2 ? 2 : 1, n, args, false, ignore_errors); } /* One JavaScript-visible raw write. Node's global console and process stream diff --git a/packages/runtime/src/scr_console.h b/packages/runtime/src/scr_console.h index 10491d053..5b21d7462 100644 --- a/packages/runtime/src/scr_console.h +++ b/packages/runtime/src/scr_console.h @@ -41,8 +41,8 @@ void scr_console_error(size_t n, const ScrLogArg *args); void scr_console_log_parts(size_t n, const ScrLogArg *args); void scr_console_error_parts(size_t n, const ScrLogArg *args); /* A Console instance's line on fd 1 or 2, without the global console's - * group indentation. */ -void scr_console_write_fd(int fd, size_t n, const ScrLogArg *args); + * group indentation, under the instance's ignoreErrors. */ +void scr_console_write_fd(int fd, size_t n, const ScrLogArg *args, bool ignore_errors); /* The global console's line rewrite while a group is open (scr_console.c; * installed by scr_console_group): the indented heap copy of a rendered * line, its length in *used. */ diff --git a/packages/runtime/src/scr_console_native.c b/packages/runtime/src/scr_console_native.c index 82dc476a1..7590f1ace 100644 --- a/packages/runtime/src/scr_console_native.c +++ b/packages/runtime/src/scr_console_native.c @@ -120,7 +120,7 @@ static void scr_console_inst_print(ScrConsoleInst *inst, int which, ScrStr *text } ScrLogArg arg = {.tag = SCR_ARG_STR}; arg.v.s = text; - scr_console_write_fd(inst->fd[which] == 2 ? 2 : 1, 1, &arg); + scr_console_write_fd(inst->fd[which] == 2 ? 2 : 1, 1, &arg, inst->ignore_errors); } /* 0 for the stdout members, 1 for the stderr members, -1 otherwise. */ diff --git a/tests/corpus/console-instance-ignore-errors-stdout-patch.cjs b/tests/corpus/console-instance-ignore-errors-stdout-patch.cjs new file mode 100644 index 000000000..907c4b07b --- /dev/null +++ b/tests/corpus/console-instance-ignore-errors-stdout-patch.cjs @@ -0,0 +1,43 @@ +// What a replaced process.stdout.write receives from a Console instance over +// process.stdout: with ignoreErrors (the default) the line and an error +// handler, and a throw from the write is discarded; with ignoreErrors false, +// as an option or the third constructor argument, only the line, and the +// throw propagates out of the console method. +const { Console } = require("node:console"); +const original = process.stdout.write; +const seen = []; +let fail = false; +process.stdout.write = function (chunk, encodingOrCallback, callback) { + seen.push(`${typeof chunk}:${JSON.stringify(String(chunk))}:${typeof encodingOrCallback}:${typeof callback}`); + if (fail) throw new Error("write failed"); + if (typeof encodingOrCallback === "function") encodingOrCallback(null); + return true; +}; +const strict = new Console({ stdout: process.stdout, ignoreErrors: false }); +const lenient = new Console({ stdout: process.stdout }); +const positional = new Console(process.stdout, process.stderr, false); +strict.log("strict", 1); +lenient.log("lenient"); +positional.info("positional"); +fail = true; +try { + strict.log("throws"); + seen.push("strict: no throw"); +} catch (e) { + seen.push(`strict threw ${e.message}`); +} +try { + lenient.log("swallowed"); + seen.push("lenient: no throw"); +} catch (e) { + seen.push(`lenient threw ${e.message}`); +} +try { + console.log("global"); + seen.push("global: no throw"); +} catch (e) { + seen.push(`global threw ${e.message}`); +} +fail = false; +process.stdout.write = original; +console.log(seen.join("\n")); diff --git a/tests/harness/corpus-costs/console-instance-ignore-errors-stdout-patch.cjs.json b/tests/harness/corpus-costs/console-instance-ignore-errors-stdout-patch.cjs.json new file mode 100644 index 000000000..5e13b3a7c --- /dev/null +++ b/tests/harness/corpus-costs/console-instance-ignore-errors-stdout-patch.cjs.json @@ -0,0 +1 @@ +{ "plain": 3440, "san": 4500 } From 16ea77108bb9fb5958dfdde56baff833b40410f4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 28 Oct 2026 23:57:15 -0500 Subject: [PATCH 16/70] Reuse lowered units across builds behind SCRIPTC_INCR_LOWERING=1 --- .github/workflows/ci.yml | 2 + package.json | 1 + packages/compiler/src/executable/prepare.ts | 10 +- .../compiler/src/frontend/lowering/lowerer.ts | 201 +- .../frontend/lowering/unit-journal.test.ts | 87 +- .../src/frontend/lowering/unit-journal.ts | 434 +- .../src/frontend/lowering/unit-reuse.ts | 1030 ++++ packages/compiler/src/frontend/type-mapper.ts | 38 +- packages/compiler/src/index.ts | 39 + packages/compiler/src/ir/codec.generated.ts | 5099 +++++++++++++++++ packages/compiler/src/ir/codec.test.ts | 235 + packages/compiler/src/ir/codec.ts | 376 ++ packages/compiler/src/ir/fields.generated.ts | 34 +- packages/compiler/src/ir/ir.ts | 84 +- packages/compiler/src/ir/serialize.ts | 7 +- packages/compiler/src/native/cache.ts | 2 + packages/compiler/src/native/prepare.ts | 61 +- packages/compiler/test/unit-reuse.test.ts | 315 + scripts/generate-ir-codec.mjs | 440 ++ 19 files changed, 8361 insertions(+), 134 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/unit-reuse.ts create mode 100644 packages/compiler/src/ir/codec.generated.ts create mode 100644 packages/compiler/src/ir/codec.test.ts create mode 100644 packages/compiler/src/ir/codec.ts create mode 100644 packages/compiler/test/unit-reuse.test.ts create mode 100644 scripts/generate-ir-codec.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23936e46a..4fbd624fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,6 +136,8 @@ jobs: run: pnpm generate:ts7-ast --check - name: Verify the generated IR field readers run: pnpm generate:ir-fields --check + - name: Verify the generated IR codec + run: pnpm generate:ir-codec --check native_macos: needs: [validation_scope, lanes] diff --git a/package.json b/package.json index d049a4ecb..5540010f2 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "test:ts7": "node scripts/test-ts7.mjs", "test:checker-identity": "node scripts/checker-identity.mjs", "generate:ts7-ast": "node scripts/generate-ts7-ast-schema.mjs", + "generate:ir-codec": "node scripts/generate-ir-codec.mjs", "generate:ir-fields": "node scripts/generate-ir-fields.mjs", "test:fetch-conformance": "vitest run tests/harness/fetch-conformance.test.ts", "test:test262": "tsx tests/test262/run.ts", diff --git a/packages/compiler/src/executable/prepare.ts b/packages/compiler/src/executable/prepare.ts index 2629a65d5..614e60155 100644 --- a/packages/compiler/src/executable/prepare.ts +++ b/packages/compiler/src/executable/prepare.ts @@ -20,6 +20,7 @@ import { } from "../backend/target-diagnostics.js"; import type { LowerResult, LowerStats } from "../frontend/lowering/lowerer.js"; import type { FrontendFactory } from "../frontend/pipeline.js"; +import type { UnitStore } from "../frontend/lowering/unit-reuse.js"; import type { CompilationTiming } from "../timing.js"; import { readProjectConfig } from "../project-config.js"; import { nodeIslandPolicy } from "../node-islands/policy.js"; @@ -42,7 +43,9 @@ export interface PreparedExecutableModule { warnings: ScrDiagnostic[]; } -/** Release the parser/checker before native object generation begins. */ +/** Release the parser/checker before native object generation begins. + * `unitStore` keeps the lowered units of this build for the next one + * (unit-reuse.ts). */ export function prepareExecutableModule( entryPath: string, opts: CompileRequestOptions, @@ -52,6 +55,7 @@ export function prepareExecutableModule( timing: CompilationTiming = () => {}, lowerIslands: ReadonlyMap = new Map(), retryFences = true, + unitStore: UnitStore | null = null, ): PreparedExecutableModule | CompileFailure { // The island decision reads the detector's scan unless islands are off. const islandScan = @@ -115,6 +119,7 @@ export function prepareExecutableModule( timing, grown, retryFences, + unitStore, ); } return fail(fe.preflight); @@ -127,6 +132,7 @@ export function prepareExecutableModule( targetPlatform: buildPlatform, ...(ffi !== null ? { ffiImports: ffi.functions } : {}), ...(opts.tier !== undefined ? { tierMode: opts.tier } : {}), + ...(unitStore !== null ? { unitStore } : {}), }, (module) => validateModule(module).length === 0, ); @@ -165,6 +171,7 @@ export function prepareExecutableModule( timing, grown, retryFences, + unitStore, ); } // A failed program whose compiled packages need the runtime or a @@ -267,6 +274,7 @@ export function prepareExecutableModule( timing, islands, false, + unitStore, ); if (islanded.ok) return islanded; firstError ??= islanded.diagnostics[0] ?? null; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index b928055eb..e04e37953 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -30,7 +30,9 @@ import { irDataTable, journalDump, journalMode, + HeldTables, lowerWaveRecorded, + type WaveReuse, mapTable, setTable, waveStats, @@ -41,6 +43,7 @@ import { type UnitEvents, type WaveHost, } from "./unit-journal.js"; +import { UnitReuse, unitReuseVerify, verifyStoredUnit, type UnitStore } from "./unit-reuse.js"; import { moduleNamespaceDynValue, moduleNamespaceObject, @@ -961,6 +964,9 @@ export interface LowerOptions { nodeIslands?: NodeIslandSet | null; /** Parser/checker services owned by the load; required for npm graph analysis. */ frontendServices?: FrontendServices | undefined; + /** Where reachable emit stores its units' records and finds an earlier + * build's (unit-reuse.ts). */ + unitStore?: UnitStore | null; /** The project's tsconfig sets `checkJs` (see isCheckedJsSourceFile). */ projectCheckJs?: boolean; /** Coverage: additionally lower the unreached remainder (bodies nothing @@ -1360,7 +1366,7 @@ function lowerToIrWithIslands( }); for (const d of extraModuleCycleDiags) reachableEmit.pushDiag(d); for (const d of ffiValidation.diagnostics) reachableEmit.pushDiag(d); - const emitted = reachableEmit.emitReachable(options.libRoots); + const emitted = reachableEmit.emitReachable(options.libRoots, options.unitStore ?? null); let { reachable } = emitted; let result = emitted.result; let resultLowerer = reachableEmit; @@ -9940,7 +9946,10 @@ export class Lowerer { * demand-driven) and lifted lambdas lower inline with their enclosing * body; both fire edges through the same hooks and are not units * themselves. */ - emitReachable(extraRoots?: readonly string[]): { reachable: Set; result: LowerResult } { + emitReachable( + extraRoots?: readonly string[], + unitStore: UnitStore | null = null, + ): { reachable: Set; result: LowerResult } { // Frontend passes below rewrite function bodies through the IR mapper // (caught-value and class dispatch); key-order notes follow the record // literals they rebuild until the witness pass has read them. @@ -9998,12 +10007,16 @@ export class Lowerer { // Every lowerable body, by emitted-function name. The names double as // retained-function keys and are deterministic by construction // (qualified declaration names). + // `source` is the declaration whose text a unit lowers and `owners` the + // declarations around it (unit-reuse.ts). const units = new Map< string, { order: number; roots: readonly ts.Node[]; lower: () => IrFunction | null; + source?: ts.Node | undefined; + owners?: readonly ts.Node[]; } >(); // Roots are collected with plain pushes: spread arguments into a rest @@ -10047,6 +10060,7 @@ export class Lowerer { order: unitOrder++, roots: functionRoots(decl), lower: () => this.lowerFunction(decl), + source: decl, }); } } @@ -10067,6 +10081,7 @@ export class Lowerer { roots: classCtorRoots(info), lower: () => info.localClass && !info.localClass.ready ? null : this.lowerClassCtor(info), + source: info.decl ?? undefined, }); // The descriptor helpers of a class that extends Array, reached // from its constructor. @@ -10075,11 +10090,13 @@ export class Lowerer { order: unitOrder++, roots: [], lower: () => arrayClassFieldsFn(this, info), + source: info.decl ?? undefined, }); units.set(`%${cName}.species`, { order: unitOrder++, roots: [], lower: () => arrayClassSpeciesFn(this, info), + source: info.decl ?? undefined, }); } for (const { mName, member } of this.classMethodMembers(info)) { @@ -10090,6 +10107,8 @@ export class Lowerer { info.localClass && !info.localClass.ready ? null : this.lowerClassMethodMember(info, member), + source: member, + owners: info.decl ? [info.decl] : [], }); } for (const prop of info.throwingSetters) { @@ -10097,6 +10116,7 @@ export class Lowerer { order: unitOrder++, roots: [], lower: () => this.throwingSetterFn(info, prop), + source: info.decl ?? undefined, }); } } @@ -10106,6 +10126,8 @@ export class Lowerer { order: unitOrder++, roots: functionRoots(entry.member), lower: () => lowerStaticMethod(this, info, name), + source: entry.member, + owners: info.decl ? [info.decl] : [], }); } } @@ -10420,17 +10442,94 @@ export class Lowerer { ), ); }; - const recorded = journalMode() === "serial" ? waveStats() : null; - const host = recorded === null ? null : this.waveHost(loweredUnits, lowerUnitBody); + // With a unit store, every unit is recorded as in the serial mode, and + // a unit applies the record an earlier build stored for it where that + // record still holds (unit-reuse.ts). + const reuse = + unitStore === null + ? null + : UnitReuse.open( + unitStore, + this.program, + parts.map((fp) => fp.sf), + { + facts: (declaration) => this.declarationFacts(declaration), + exportsOf: (specifier) => { + const symbol = this.checker.getSymbolAtLocation(specifier); + if (!symbol) return null; + return this.checker + .getExportsOfModule(symbol) + .map((exported) => ts.symbolName(exported)) + .sort() + .join("\n"); + }, + isLocal: (identifier, container) => { + const declarations = this.checker.getSymbolAtLocation(identifier)?.declarations; + if (declarations === undefined || declarations.length === 0) return false; + const sf = ts.getSourceFileOfNode(container); + return declarations.every( + (declaration) => + ts.getSourceFileOfNode(declaration) === sf && + declaration.pos >= container.pos && + declaration.end <= container.end, + ); + }, + }, + ); + const recorded = + journalMode() === "serial" || (reuse !== null && reuse.records) ? waveStats() : null; + const host = + recorded === null + ? null + : this.waveHost(loweredUnits, lowerUnitBody, (name) => { + const priority = metadataPriority.get(name)!; + return { owner: demandOwner(priority), priority }; + }); const dump = recorded === null ? null : journalDump(); + const fileOfUnit = (name: string): string | null => { + const source = units.get(name)?.source; + return source === undefined ? null : ts.getSourceFileOfNode(source).fileName; + }; + const waveReuse: WaveReuse | null = + reuse === null || !reuse.records + ? null + : { + verify: unitReuseVerify(), + stored: (name) => { + // A unit without a declared rank takes the next expression + // rank when it lowers, which applying a record would not + // advance. + const unit = units.get(name); + const file = fileOfUnit(name); + if (unit === undefined || file === null || !metadataPriority.has(name)) return null; + return reuse.candidate(name, file, unit.source ?? null, unit.owners ?? []); + }, + check: (name, stored, pending, unit) => + verifyStoredUnit(name, stored, pending, unit.record, unit.reasons), + note: (name, record, applied, files) => { + const file = fileOfUnit(name); + if (file !== null) reuse.note(name, file, record, applied, files); + }, + }; const recordLines: string[] = []; const drainUnits = (): void => { + // The waves of one drain have only checker prefetches between them, so + // they share the content their units start from (HeldTables). + const held = + host !== null && recorded !== null ? new HeldTables(host.tables, recorded) : null; while (queue.length > 0) { const wave = queue.splice(0); shuffle?.(wave); this.checker.prefetchRoots(wave.flatMap((name) => units.get(name)!.roots)); if (host !== null && recorded !== null) { - lowerWaveRecorded(host, wave, recorded, dump === null ? null : recordLines); + lowerWaveRecorded( + host, + wave, + recorded, + dump === null ? null : recordLines, + waveReuse, + held, + ); continue; } for (const name of wave) { @@ -10447,6 +10546,7 @@ export class Lowerer { } } } + held?.check(); }; drainUnits(); this.restoreGenericInstanceOrder(); @@ -10491,6 +10591,13 @@ export class Lowerer { } classDispatch.finalize(this); if (dump !== null) writeRecords(dump, recordLines); + if (reuse !== null) { + reuse.finish(); + if (process.env["SCRIPTC_TIMING"] === "1") + process.stderr.write( + `scriptc lowering ${JSON.stringify({ phase: "unit-reuse", ...reuse.stats })}\n`, + ); + } if (recorded !== null && process.env["SCRIPTC_TIMING"] === "1") process.stderr.write( `scriptc lowering ${JSON.stringify({ phase: "unit-records", ...recorded })}\n`, @@ -10539,11 +10646,57 @@ export class Lowerer { return { reachable, result: this.finishModule(functions) }; } + /** What the steps before the worklist settled for a top-level + * declaration, as text without source positions (unit-reuse.ts): a + * function's signature, a class's layout and the signatures of its + * methods and statics, and the globals of a variable statement. */ + private declarationFacts(declaration: ts.Node): string { + const positionFree = (key: string, value: unknown): unknown => + key === "loc" || key === "scope" ? undefined : value; + if (ts.isFunctionDeclaration(declaration)) { + const symbol = declSymbolOf(this, declaration); + const sig = symbol ? this.fnSigsBySymbol.get(symbol) : undefined; + return sig === undefined ? "" : JSON.stringify(sig, positionFree); + } + if (ts.isClassDeclaration(declaration)) { + const info = this.classes.get(this.classNamer(declaration)); + if (info === undefined) return ""; + const staticMethods: { name: string; params: ParamShape[]; ret: IrType }[] = []; + if (info.staticMethods) + for (const [name, method] of info.staticMethods) + staticMethods.push({ name, params: method.params, ret: method.ret }); + return JSON.stringify( + { + def: info.def, + fields: [...info.fields], + methods: [...info.methods], + statics: info.staticFields.map((field) => ({ + name: field.name, + type: field.type, + runtimeOptional: field.runtimeOptional ?? false, + globalId: field.globalId, + readonly: field.readonly, + })), + staticMethods, + }, + positionFree, + ); + } + if (ts.isVariableStatement(declaration)) { + const globals: (IrGlobal | null)[] = []; + for (const decl of declaration.declarationList.declarations.nodes) + globals.push(this.globalsByDeclNode.get(decl) ?? null); + return JSON.stringify(globals, positionFree); + } + return ""; + } + /** The lowerer's side of a recorded wave (unit-journal.ts). * `lowerUnitBody` lowers a unit as reachable emit does. */ private waveHost( loweredUnits: Map, lowerUnitBody: (name: string) => IrFunction | null, + ownerOf: (name: string) => { owner: GenericDemandOwner; priority: readonly [number, number] }, ): WaveHost { const scalars = this.journalScalars(); const flags = new Map(scalars.map((scalar) => [scalar.name, scalar])); @@ -10574,6 +10727,35 @@ export class Lowerer { return undefined; } }; + // Makes a recorded call again (resolves has answered true for it). + const replay = (event: JournalEvent): void => { + switch (event.kind) { + case "edge": + this.noteEdge(event.name); + break; + case "superConstructorEdge": + this.noteSuperConstructorEdge(event.name); + break; + case "virtualEdge": + this.noteVirtualEdge(classNamed(event.name)!, event.detail!); + break; + case "functionDemand": + this.noteGenericInstanceDemand(instanceNamed(event.name)!); + break; + case "classDemand": + this.noteGenericClassInstanceDemand(classNamed(event.name)!); + break; + case "statement": + this.countStatement(statementNode(event)!, event.detail ?? undefined); + break; + case "failedStatement": + this.countStatementFailed(statementNode(event)!, event.detail ?? undefined); + break; + case "flag": + flags.get(event.name)!.set!(); + break; + } + }; return { tables: this.journalTables(), scalars, @@ -10614,6 +10796,15 @@ export class Lowerer { return true; } }, + keep: (name, fn, events) => { + if (fn) loweredUnits.set(name, fn); + const { owner, priority } = ownerOf(name); + this.withGenericDemandOwner(owner, () => + this.shapes.withDeclaredOrderPriority(priority, () => { + for (const event of events) replay(event); + }), + ); + }, }; } diff --git a/packages/compiler/src/frontend/lowering/unit-journal.test.ts b/packages/compiler/src/frontend/lowering/unit-journal.test.ts index 3b9599d7b..55b030c2f 100644 --- a/packages/compiler/src/frontend/lowering/unit-journal.test.ts +++ b/packages/compiler/src/frontend/lowering/unit-journal.test.ts @@ -4,6 +4,7 @@ import { Lowerer } from "./lowerer.js"; import { checkedTable, contentDigest, + decodeRecordFunction, encodeUnitFunction, flagScalar, functionTable, @@ -12,7 +13,9 @@ import { recordUnit, setTable, waveStats, + type UnitRecord, type WaveHost, + type WaveReuse, } from "./unit-journal.js"; import type { IrFunction } from "../../ir/ir.js"; @@ -225,6 +228,7 @@ function host(lower: (name: string, state: HostState) => IrFunction | null): { }, statementCounts: () => ({ total: 0, failed: 0, other: 0 }), resolves: () => true, + keep: () => {}, }, }; } @@ -276,7 +280,10 @@ test("a unit's record lists its events, flags and added entries", () => { ["flag", "flag"], ]); expect(record!.tables).toEqual({ helpers: [`"key.a"\n"%h.a"`] }); - expect(JSON.parse(JSON.stringify(record))).toEqual(record); + // The function is in the binary encoding, and the rest is JSON data. + const { fn: bytes, ...rest } = record!; + expect(bytes).toBeInstanceOf(Uint8Array); + expect(JSON.parse(JSON.stringify(rest))).toEqual(rest); }); test("a unit that changes an entry it did not add has no record", () => { @@ -303,6 +310,7 @@ test("a recorded wave throws when a unit changes a held function in place", () = store: () => {}, statementCounts: () => ({ total: 0, failed: 0, other: 0 }), resolves: () => true, + keep: () => {}, }; lowerWaveRecorded(subject, ["a"], waveStats()); expect(() => lowerWaveRecorded(subject, ["a", "b"], waveStats())).toThrow( @@ -310,6 +318,47 @@ test("a recorded wave throws when a unit changes a held function in place", () = ); }); +test("a recorded wave holds the entries an applied record adds after its first lowering unit", () => { + // `first` lowers, which takes the wave's content; `a` applies a stored + // record that adds `%added`; `b` changes `%added` in place without naming + // it, which the end of the wave must see. + const functions: IrFunction[] = []; + const subject: WaveHost = { + tables: [functionTable("lifted", functions, (name) => functions.some((f) => f.name === name))], + scalars: [], + lowerRecorded: (name) => { + if (name === "b") + functions + .find((f) => f.name === "%added")! + .locals.push({ id: "x.0", name: "x", type: { kind: "f64" } } as never); + return { fn: fn(`%${name}`), poisoned: false }; + }, + store: () => {}, + statementCounts: () => ({ total: 0, failed: 0, other: 0 }), + resolves: () => true, + keep: () => {}, + }; + const stored: UnitRecord = { + name: "a", + fn: null, + poisoned: false, + events: [], + tables: { lifted: [encodeUnitFunction(fn("%added"))!] }, + reads: {}, + }; + const reuse: WaveReuse = { + stored: (name) => (name === "a" ? { record: stored, fn: () => null } : null), + verify: false, + check: () => {}, + note: () => {}, + }; + const stats = waveStats(); + expect(() => lowerWaveRecorded(subject, ["first", "a", "b"], stats, null, reuse)).toThrow( + "a unit changed lifted entry %added in place without naming it", + ); + expect(stats.applied).toBe(1); +}); + /** A wave host over one function table holding `%shared` and one checked * list, whose units change `%shared` in place and name it in their IR. */ function sharedHost(grows: (name: string) => boolean): { host: WaveHost; checked: string[] } { @@ -334,6 +383,7 @@ function sharedHost(grows: (name: string) => boolean): { host: WaveHost; checked store: () => {}, statementCounts: () => ({ total: 0, failed: 0, other: 0 }), resolves: () => true, + keep: () => {}, }, }; } @@ -352,6 +402,40 @@ test("a unit that changes a function it names in place has no record", () => { expect(stats).toMatchObject({ recorded: 0, unrecorded: 1, reasons: { "lifted:changed": 1 } }); }); +test("a record's binary function names the held functions its strings name", () => { + const functions = [fn("%shared")]; + const loc = { file: "b.ts", start: 0, end: 0 }; + const subject: WaveHost = { + tables: [functionTable("lifted", functions, (name) => functions.some((f) => f.name === name))], + scalars: [], + lowerRecorded: () => ({ + fn: { + ...fn("%a"), + body: [ + { + kind: "exprStmt", + expr: { kind: "call", callee: "%shared", args: [], type: { kind: "f64" }, loc }, + loc, + }, + ] as never, + }, + poisoned: false, + }), + store: () => {}, + statementCounts: () => ({ total: 0, failed: 0, other: 0 }), + resolves: () => true, + keep: () => {}, + }; + const unit = recordUnit(subject, "a"); + expect(unit.record!.fn).toBeInstanceOf(Uint8Array); + expect(unit.record!.reads).toEqual({ + lifted: [["%shared", contentDigest(encodeUnitFunction(functions[0]!)!)]], + }); + // The files of the function's source positions, for reuse to check. + expect([...unit.files!].sort()).toEqual(["a.ts", "b.ts"]); + expect(decodeRecordFunction(unit.record!.fn!)).toEqual(unit.fn); +}); + test("a record lists the held functions it names, with their digests", () => { const functions = [fn("%shared"), fn("%unused")]; const subject: WaveHost = { @@ -373,6 +457,7 @@ test("a record lists the held functions it names, with their digests", () => { store: () => {}, statementCounts: () => ({ total: 0, failed: 0, other: 0 }), resolves: () => true, + keep: () => {}, }; const { record } = recordUnit(subject, "a"); // The function the unit added is in its entries, not its reads. diff --git a/packages/compiler/src/frontend/lowering/unit-journal.ts b/packages/compiler/src/frontend/lowering/unit-journal.ts index e349bb2cf..68bd3e778 100644 --- a/packages/compiler/src/frontend/lowering/unit-journal.ts +++ b/packages/compiler/src/frontend/lowering/unit-journal.ts @@ -4,8 +4,9 @@ * unit's lowering reached: the unit's IR, the entries it added to the * tables that collect lowering results (Lowerer.journalTables), and the * calls it made into reachability, generic demand and statement counting - * (JournalEvent). Records hold only strings, numbers and names, so they can - * be written to a file and read by another process or another build. + * (JournalEvent). Records hold only the function's bytes (or its JSON), + * strings, numbers and names, so they can be written to a file and read by + * another process or another build. * * A unit has no record when its lowering changed state that no record * carries: a table that is not appendable grew, a field changed, an entry @@ -23,20 +24,59 @@ import { appendFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { InternalCompilerError } from "../../errors.js"; +import { decodeFunctionBinary, encodeFunctionStrings, functionDigest } from "../../ir/codec.js"; import type { IrFunction } from "../../ir/ir.js"; import { deserializeFunction, encodeFunction } from "../../ir/serialize.js"; -/** A function as records hold it, or null when it does not encode. Every - * function a record holds goes through this pair. */ +/** A function as table entries hold it, or null when it does not encode. + * Every function an entry holds goes through this pair. */ export function encodeUnitFunction(fn: IrFunction): string | null { return encodeFunction(fn) ?? null; } -/** A function from its record encoding (encodeUnitFunction). */ +/** A function from its entry encoding (encodeUnitFunction). */ export function decodeUnitFunction(text: string): IrFunction { return deserializeFunction(text); } +/** A unit's function as its record holds it: the binary IR encoding, or + * JSON (encodeUnitFunction) when an object in it would not read back with + * its own key order, which frontend passes and the IR's text depend on. */ +export type RecordFunction = string | Uint8Array; + +/** `fn` as a record holds it, or null when it does not encode. A binary + * encoding comes with its strings and the files its fields named `file` + * hold (encodeFunctionStrings). */ +export function encodeRecordFunction(fn: IrFunction): { + readonly fn: RecordFunction; + readonly strings: readonly string[] | null; + readonly files: readonly string[] | null; +} | null { + const binary = encodeFunctionStrings(fn); + if (binary !== null) return { fn: binary.bytes, strings: binary.strings, files: binary.files }; + const text = encodeUnitFunction(fn); + return text === null ? null : { fn: text, strings: null, files: null }; +} + +/** A function from its record encoding (encodeRecordFunction). */ +export function decodeRecordFunction(fn: RecordFunction): IrFunction { + return typeof fn === "string" + ? decodeUnitFunction(fn) + : decodeFunctionBinary(Buffer.from(fn.buffer, fn.byteOffset, fn.byteLength)); +} + +/** `record` as a JSON line, with a binary function in base64, marked when + * it was `applied` from an earlier build's store. */ +export function recordLine(record: UnitRecord, applied = false): string { + const fn = + record.fn === null || typeof record.fn === "string" + ? record.fn + : Buffer.from(record.fn.buffer, record.fn.byteOffset, record.fn.byteLength).toString( + "base64", + ); + return JSON.stringify(applied ? { ...record, fn, applied: true } : { ...record, fn }); +} + /** A call a unit made while it lowered, which applying its record makes * again. */ export interface JournalEvent { @@ -62,9 +102,9 @@ export interface JournalEvent { /** One unit's record. */ export interface UnitRecord { readonly name: string; - /** The unit's function in the compact IR encoding, or null when the unit + /** The unit's function (encodeRecordFunction), or null when the unit * lowered to nothing. */ - readonly fn: string | null; + readonly fn: RecordFunction | null; /** True when the unit's lowering ended in a poison: its diagnostics and * the edges it fired before poisoning stay, and it has no function. */ readonly poisoned: boolean; @@ -91,6 +131,12 @@ export interface JournalTable { readonly entries?: (from: number) => string[] | null; /** Adds the encoded entries whose keys this process's table lacks. */ readonly apply?: (entries: readonly string[]) => void; + /** Whether every encoded entry whose key this process's table holds + * holds the same value there. */ + readonly agrees?: (entries: readonly string[]) => boolean; + /** Whether this process's table holds an entry under the encoded entry's + * key. */ + readonly holds?: (entry: string) => boolean; /** A digest of the content of the entry under `key`, when the table * holds one before position `before`; null otherwise. */ readonly digest?: (key: string, before: number) => string | null; @@ -125,10 +171,20 @@ export function contentDigest(text: string): string { return `${text.length}:${(a >>> 0).toString(16)}${(b >>> 0).toString(16)}`; } -/** The names quoted in JSON `texts`, unescaped only as far as their - * delimiting quotes: the strings that start with `%` (helper, lifted and - * IR data names) and union ids (`u` and 12 hex digits), which the tables - * a unit reads and changes are keyed by. */ +/** Adds to `out` the names among `strings`: the strings that start with `%` + * (helper, lifted and IR data names) and union ids (`u` and 12 hex digits), + * which the tables a unit reads and changes are keyed by. */ +export function namedStrings(strings: Iterable, out: Set): void { + for (const text of strings) { + const first = text.charCodeAt(0); + if (first === 37 /* % */) out.add(text); + else if (first === 117 /* u */ && text.length === 13 && /^u[0-9a-f]{12}$/.test(text)) + out.add(text); + } +} + +/** The names (namedStrings) quoted in JSON `texts`, unescaped only as far + * as their delimiting quotes. */ export function quotedStrings(texts: readonly string[]): Set { const out = new Set(); const hex = (char: number): boolean => (char >= 48 && char <= 57) || (char >= 97 && char <= 102); @@ -225,6 +281,10 @@ export interface WaveHost { statementCounts(): { total: number; failed: number; other: number }; /** Whether `event` names something this process holds. */ resolves(event: JournalEvent): boolean; + /** Keeps a unit's function from its record and makes the record's events + * again, under the unit's generic demand owner and declared-order + * priority. */ + keep(name: string, fn: IrFunction | null, events: readonly JournalEvent[]): void; } /** Whether reachable emit records its units (SCRIPTC_LOWER_JOURNAL=serial, @@ -259,10 +319,69 @@ export interface WaveStats { unrecorded: number; /** Per table, field or check, the units it left without a record. */ reasons: Record; + /** Units whose stored record applied in place of lowering them + * (unit-reuse.ts). */ + applied: number; + /** Time taking and checking the tables' contents, finding stored records, + * applying them, and lowering and recording units, in milliseconds. */ + holdMs: number; + storedMs: number; + applyMs: number; + recordMs: number; + /** Of recordMs, the time the units' lowering took, and the time a + * recorded unit's results took to be replaced by its record's. */ + lowerMs: number; + replaceMs: number; } export function waveStats(): WaveStats { - return { waves: 0, units: 0, recorded: 0, unrecorded: 0, reasons: {} }; + return { + waves: 0, + units: 0, + recorded: 0, + unrecorded: 0, + reasons: {}, + applied: 0, + holdMs: 0, + storedMs: 0, + applyMs: 0, + recordMs: 0, + lowerMs: 0, + replaceMs: 0, + }; +} + +/** A wave's use of the records an earlier build stored (unit-reuse.ts). */ +/** A record an earlier build stored, with its function, which the store + * can keep apart from the record (`record.fn` is null then). */ +export interface StoredRecord { + readonly record: UnitRecord; + fn(): IrFunction | null; +} + +export interface WaveReuse { + /** The stored record of `name`, when the unit can apply it. */ + stored(name: string): StoredRecord | null; + /** Whether units with a stored record lower and are checked against it + * instead (SCRIPTC_INCR_VERIFY=1). */ + readonly verify: boolean; + /** Checks the record a unit lowered to against its stored one, whose + * entries the tables did not hold yet at the unit's turn are `pending`. */ + check( + name: string, + stored: StoredRecord, + pending: Readonly>, + unit: RecordedUnit, + ): void; + /** Notes the record a unit lowered to, with the files its binary + * function names (RecordedUnit.files), or the stored record it applied + * (`files` null). */ + note( + name: string, + record: UnitRecord | null, + applied: boolean, + files: readonly string[] | null, + ): void; } /** What lowering one unit with its events recording produced. */ @@ -275,6 +394,11 @@ export interface RecordedUnit { readonly fn: IrFunction | null; /** Each table's size before the unit. */ readonly before: readonly number[]; + /** The time the unit's lowering took. */ + readonly lowerMs: number; + /** The files the fields named `file` in the record's function hold, when + * the function is binary. */ + readonly files: readonly string[] | null; } /** Lowers `name` with its events recording, and makes its record. The unit @@ -297,7 +421,9 @@ export function recordUnit( const marks = host.scalars.map((scalar) => scalar.mark()); const counts = host.statementCounts(); const recorder = new UnitEvents(); + const lowering = performance.now(); const { fn, poisoned } = host.lowerRecorded(name, recorder); + const lowerMs = performance.now() - lowering; const reasons: string[] = []; const grown: number[] = []; tables.forEach((table, index) => { @@ -322,8 +448,8 @@ export function recordUnit( if (now.total - counts.total !== statements || now.failed - counts.failed !== failed) reasons.push("statements"); if (now.other !== counts.other) reasons.push("diagnostics"); - const text = fn === null ? null : encodeUnitFunction(fn); - if (fn !== null && text === null) reasons.push("encoding"); + const encoded = fn === null ? null : encodeRecordFunction(fn); + if (fn !== null && encoded === null) reasons.push("encoding"); const added: Record = {}; if (reasons.length === 0) for (const index of grown) { @@ -335,13 +461,14 @@ export function recordUnit( // A unit uses the entries its IR and new entries name: it reads them, and // a unit that changes one in place changes one it names. const texts: string[] = []; - if (text !== null) texts.push(text); + if (typeof encoded?.fn === "string") texts.push(encoded.fn); for (const index of grown) { const table = tables[index]!; const entries = added[table.name] ?? table.entries!(before[index]!); if (entries !== null) for (const entry of entries) texts.push(entry); } const names = quotedStrings(texts); + if (encoded?.strings != null) namedStrings(encoded.strings, names); if (own !== null) { if (reasons.length === 0) tables.forEach((table, index) => { @@ -369,36 +496,72 @@ export function recordUnit( } if (read.length > 0) reads[table.name] = read; }); - if (reasons.length > 0) return { record: null, reasons, fn, before }; + if (reasons.length > 0) return { record: null, reasons, fn, before, lowerMs, files: null }; return { - record: { name, fn: text, poisoned, events, tables: added, reads }, + record: { name, fn: encoded?.fn ?? null, poisoned, events, tables: added, reads }, reasons, fn, before, + lowerMs, + files: encoded?.files ?? null, }; } -/** Lowers `wave` with every unit recorded. A unit with a record keeps the - * results its record carries: the entries it added are taken out of their - * tables and added again from the record, and its function is the one - * decoded from the record. A unit without one keeps what lowering made. +/** Lowers `wave` with every unit recorded. Without `reuse`, a unit with a + * record keeps the results its record carries: the entries it added are + * taken out of their tables and added again from the record, and its + * function is the one decoded from the record. Otherwise, and for a unit + * without a record, a unit keeps what lowering made. * Throws an InternalCompilerError when a unit changed the content of an * entry a table held when the unit started (JournalTable.contents), which * no record carries. - * `lines`, when given, receives each unit's record as a JSON line. */ + * `lines`, when given, receives each unit's record as a JSON line. A unit + * whose stored record `reuse` gives applies it, and lowers when it does not + * apply. With `held`, the wave continues the content the waves before it + * left (HeldTables), and the check waits for HeldTables.check. */ export function lowerWaveRecorded( host: WaveHost, wave: readonly string[], stats: WaveStats, lines: string[] | null = null, + reuse: WaveReuse | null = null, + held: HeldTables | null = null, ): void { stats.waves++; const tables = host.tables; - // Units keep the content of the entries that tables held when they - // started: a record carries a unit's new entries, not changes to others. - const contents = tables.map((table) => (table.contents === undefined ? null : table.contents())); + const contents = held ?? new HeldTables(tables, stats); + let started = 0; for (const name of wave) { - const unit = recordUnit(host, name, contents); + started = performance.now(); + let stored = reuse?.stored(name) ?? null; + stats.storedMs += performance.now() - started; + let pending: Readonly> = {}; + if (stored !== null && reuse!.verify) { + // Verify mode lowers the units whose stored record would apply and + // checks the record they lower to. + if (recordApplies(host, stored.record).length === 0) + pending = pendingEntries(host, stored.record); + else stored = null; + } else if (stored !== null) { + started = performance.now(); + const reasons = applyRecord(host, stored.record, stored.fn); + stats.applyMs += performance.now() - started; + if (reasons.length === 0) { + stats.units++; + stats.recorded++; + stats.applied++; + contents.extendTaken(); + lines?.push(recordLine(stored.record, true)); + reuse!.note(name, stored.record, true, null); + continue; + } + for (const reason of reasons) + stats.reasons[`stored:${reason}`] = (stats.reasons[`stored:${reason}`] ?? 0) + 1; + } + started = performance.now(); + const unit = recordUnit(host, name, contents.take()); + stats.recordMs += performance.now() - started; + stats.lowerMs += unit.lowerMs; stats.units++; const record = unit.record; if (record === null) { @@ -406,43 +569,174 @@ export function lowerWaveRecorded( stats.unrecorded++; for (const reason of unit.reasons) stats.reasons[reason] = (stats.reasons[reason] ?? 0) + 1; host.store(name, unit.fn); + if (stored !== null && reuse!.verify) reuse!.check(name, stored, pending, unit); + reuse?.note(name, null, false, null); continue; } - lines?.push(JSON.stringify(record)); + lines?.push(recordLine(record)); stats.recorded++; - tables.forEach((table, index) => { - const entries = record.tables[table.name]; - if (entries === undefined) return; - table.truncate!(unit.before[index]!); - table.apply!(entries); - }); - host.store(name, record.fn === null ? null : decodeUnitFunction(record.fn)); + // The serial mode replaces a unit's results by its record's, so that the + // build's output is what applying the records gives. Reuse keeps what + // lowering made: its records apply in later builds, which verify mode + // and the edit-sequence tests check. + let fn = unit.fn; + if (reuse === null) { + const replacing = performance.now(); + tables.forEach((table, index) => { + const entries = record.tables[table.name]; + if (entries === undefined) return; + table.truncate!(unit.before[index]!); + table.apply!(entries); + }); + fn = record.fn === null ? null : decodeRecordFunction(record.fn); + stats.replaceMs += performance.now() - replacing; + } + host.store(name, fn); + if (stored !== null && reuse!.verify) reuse!.check(name, stored, pending, unit); + reuse?.note(name, record, false, unit.files); } - tables.forEach((table, index) => { - const key = contents[index]?.changed() ?? null; - if (key !== null) - throw new InternalCompilerError( - `a unit changed ${table.name} entry ${key} in place without naming it, which no unit record carries`, + if (held === null) contents.check(); +} + +/** The content of the tables' entries that recorded units start from + * (JournalTable.contents): units keep the content of the entries the + * tables held when they started, since a record carries a unit's new + * entries, not changes to others. Applying a stored record only adds + * entries, so the content is taken when the first unit lowers, and not at + * all when none does. Consecutive waves with nothing but checker prefetches + * between them share one: the content a wave leaves, as the check finds it, + * is the content the next one starts with. */ +export class HeldTables { + private contents: (HeldContents | null)[] | null = null; + + constructor( + private readonly tables: readonly JournalTable[], + private readonly stats: WaveStats, + ) {} + + /** The held content, taken now when it was not yet. */ + take(): (HeldContents | null)[] { + if (this.contents === null) { + const taken = performance.now(); + this.contents = this.tables.map((table) => + table.contents === undefined ? null : table.contents(), ); - }); + this.stats.holdMs += performance.now() - taken; + } + return this.contents; + } + + /** Adds the entries past the held ones, when the content is taken. */ + extendTaken(): void { + if (this.contents !== null) for (const entry of this.contents) entry?.extend(); + } + + /** Throws an InternalCompilerError when an entry a table held changed in + * place since the content was taken, other than by the units that keep + * their changes (HeldContents.refresh). */ + check(): void { + const contents = this.contents; + if (contents === null) return; + const started = performance.now(); + this.tables.forEach((table, index) => { + const key = contents[index]?.changed() ?? null; + if (key !== null) + throw new InternalCompilerError( + `a unit changed ${table.name} entry ${key} in place without naming it, which no unit record carries`, + ); + }); + this.stats.holdMs += performance.now() - started; + } +} + +/** Applies `record` in place of lowering its unit, at the unit's turn, or + * returns why the unit has to lower instead. A record applies when every + * entry it adds agrees with what the tables hold, it names only known + * tables, every event resolves and every entry it reads is held with the + * same content. Applying adds its entries, keeps its function (`stored`'s, + * when the store keeps it apart from the record) and makes its events + * again. */ +export function applyRecord( + host: WaveHost, + record: UnitRecord, + stored?: () => IrFunction | null, +): string[] { + const reasons = recordApplies(host, record); + if (reasons.length > 0) return reasons; + for (const table of host.tables) { + const entries = record.tables[table.name]; + if (entries !== undefined) table.apply!(entries); + } + const fn = record.poisoned + ? null + : stored !== undefined + ? stored() + : record.fn === null + ? null + : decodeRecordFunction(record.fn); + host.keep(record.name, fn, record.events); + return []; +} + +/** The entries `record` adds that the tables do not hold yet, by table + * name: what applying it would add at its unit's turn. */ +export function pendingEntries( + host: WaveHost, + record: UnitRecord, +): Record { + const out: Record = {}; + for (const table of host.tables) { + const entries = record.tables[table.name]; + if (entries === undefined) continue; + const pending = entries.filter((entry) => table.holds?.(entry) !== true); + if (pending.length > 0) out[table.name] = pending; + } + return out; +} + +/** Why `record` does not apply at its unit's turn (applyRecord), or no + * reasons when it does. */ +export function recordApplies(host: WaveHost, record: UnitRecord): string[] { + const tables = host.tables; + for (const table of tables) { + const entries = record.tables[table.name]; + if (entries === undefined) continue; + if (table.agrees === undefined || table.apply === undefined || !table.agrees(entries)) + return [`disagrees:${table.name}`]; + } + for (const table of Object.keys(record.tables)) + if (!tables.some((held) => held.name === table)) return [`unknown table:${table}`]; + for (const event of record.events) if (!host.resolves(event)) return [`event:${event.kind}`]; + for (const table of tables) { + const reads = record.reads[table.name]; + if (reads === undefined) continue; + if (table.digest === undefined) return [`unknown read:${table.name}`]; + const size = table.size(); + for (const [key, digest] of reads) + if (table.digest(key, size) !== digest) return [`read:${table.name}`]; + } + for (const table of Object.keys(record.reads)) + if (!tables.some((held) => held.name === table)) return [`unknown read:${table}`]; + return []; } /** The content of a keyed table's entries (JournalTable.contents): each - * entry's encoded value, and the order of the keys, since a table only adds - * entries at the end. An entry is found by its key, or by the name its value - * holds (`valueName`): a unit names the helper it got, not the key it was - * looked up by. `text` answers undefined for a key the table does not + * entry's content (`text`: its encoded value, or the value itself when it + * is a string or a number), and the order of the keys, since a table only + * adds entries at the end. An entry is found by its key, or by the name its + * value holds (`valueName`): a unit names the helper it got, not the key it + * was looked up by. `text` answers undefined for a key the table does not * hold. */ function keyedContents( size: () => number, keys: () => Iterable, - text: (key: string) => string | null | undefined, + text: (key: string) => string | number | null | undefined, valueName: (key: string) => string | null, ): HeldContents { - const texts = new Map(); + const texts = new Map(); let order: string[] = []; const byName = new Map(); - const differs = (key: string): boolean => text(key) !== texts.get(key); + const differs = (key: string): boolean => !Object.is(text(key), texts.get(key)); const keyOf = (name: string): string | undefined => (texts.has(name) ? name : byName.get(name)); const held: HeldContents = { extend: () => { @@ -499,6 +793,7 @@ export function mapTable(name: string, map: Map): JournalTable { (value) => JSON.stringify(value), (text) => JSON.parse(text) as string, (value) => value, + (value) => value, ); } @@ -509,6 +804,8 @@ export function countTable(name: string, map: Map): JournalTable map, (value) => String(value), (text) => Number(text), + null, + (value) => value, ); } @@ -530,13 +827,17 @@ export function irDataTable(name: string, map: Map): JournalTable /** An appendable table over a Map from strings to values that `encode` * writes, or answers null for, and `decode` reads back. An entry is its key * as a JSON string, a line break and its value: a JSON string holds no line - * break. A value that does not encode leaves its unit without a record. */ + * break. A value that does not encode leaves its unit without a record. + * `content` is what held contents compare (JournalTable.contents): the + * encoding, or the value itself for a table of strings or numbers, which + * only change by being replaced. */ export function codecTable( name: string, map: Map, encode: (value: V) => string | null, decode: (text: string) => V, nameOf: ((value: V) => string) | null = null, + content: (value: V) => string | number | null = encode, ): JournalTable { return { name, @@ -552,6 +853,13 @@ export function codecTable( } return out; }, + agrees: (entries) => + entries.every((entry) => { + const split = entry.indexOf("\n"); + const held = map.get(JSON.parse(entry.slice(0, split)) as string); + return held === undefined || encode(held) === entry.slice(split + 1); + }), + holds: (entry) => map.has(JSON.parse(entry.slice(0, entry.indexOf("\n"))) as string), apply: (entries) => { for (const entry of entries) { const split = entry.indexOf("\n"); @@ -569,7 +877,7 @@ export function codecTable( () => map.keys(), (key) => { const value = map.get(key); - return value === undefined ? undefined : encode(value); + return value === undefined ? undefined : content(value); }, (key) => { const value = map.get(key); @@ -585,6 +893,8 @@ export function setTable(name: string, set: Set): JournalTable { name, size: () => set.size, entries: (from) => [...set].slice(from), + agrees: () => true, + holds: (entry) => set.has(entry), apply: (entries) => { for (const entry of entries) set.add(entry); }, @@ -652,6 +962,14 @@ export function functionTable( } return out; }, + agrees: (entries) => + entries.every((entry) => { + const fn = decodeUnitFunction(entry); + if (!has(fn.name)) return true; + const index = indexOf(fn.name); + return index !== undefined && encodeUnitFunction(functions[index]!) === entry; + }), + holds: (entry) => has(decodeUnitFunction(entry).name), apply: (entries) => { for (const entry of entries) { const fn = decodeUnitFunction(entry); @@ -663,16 +981,17 @@ export function functionTable( while (functions.length > from) functions.pop(); changed(); }, + // Held functions are their content digests (heldFunction). contents: () => { const texts: (string | null)[] = []; const indexes = new Map(); const differs = (index: number): boolean => - index < functions.length && encodeUnitFunction(functions[index]!) !== texts[index]; + index < functions.length && heldFunction(functions[index]!) !== texts[index]; const held: HeldContents = { extend: () => { for (let index = texts.length; index < functions.length; index++) { const fn = functions[index]!; - texts.push(encodeUnitFunction(fn)); + texts.push(heldFunction(fn)); indexes.set(fn.name, index); } }, @@ -692,7 +1011,7 @@ export function functionTable( for (const key of keys) { const index = indexes.get(key); if (index !== undefined && index < functions.length) - texts[index] = encodeUnitFunction(functions[index]!); + texts[index] = heldFunction(functions[index]!); } }, }; @@ -702,6 +1021,17 @@ export function functionTable( }; } +/** The content a function table holds of `fn` (HeldContents): its digest + * (functionDigest), or its entry encoding when the binary encoder cannot + * write it. */ +function heldFunction(fn: IrFunction): string | null { + try { + return functionDigest(fn); + } catch { + return encodeUnitFunction(fn); + } +} + /** A table whose growth leaves the unit without a record. */ export function checkedTable(name: string, size: () => number): JournalTable { return { name, size }; diff --git a/packages/compiler/src/frontend/lowering/unit-reuse.ts b/packages/compiler/src/frontend/lowering/unit-reuse.ts new file mode 100644 index 000000000..5de2c7b0a --- /dev/null +++ b/packages/compiler/src/frontend/lowering/unit-reuse.ts @@ -0,0 +1,1030 @@ +/* Reuse of lowered units across builds (SCRIPTC_INCR_LOWERING=1). + * + * A build with a unit store records every reachable-emit unit + * (unit-journal.ts) and writes the records to the store, one entry per + * source file, beside a program entry that describes the build's program. + * The next build applies a unit's stored record in place of lowering the + * unit when nothing the unit's lowering read can have changed: + * + * - the program digest is unchanged: the IR version, the files outside the + * program's own modules (declaration files, the library), the module + * order and the statements with global effect; + * - the unit's source file has the same text, and so does every other file + * whose source positions its record holds; + * - the unit's text spells no name in the build's changed names. + * + * The changed names come from the two builds' declaration tables. A + * declaration is a top-level statement or a member of a top-level class or + * interface. Its surface is its text without the bodies of functions that + * declare their return type: what other declarations' types can depend on. + * Its facts are what the steps before the worklist settled for it: a + * function's signature, a class's layout and method signatures, a + * variable's globals. The changed names are: + * + * - the names of top-level declarations that were added or removed, or + * whose surface or facts changed, and the interfaces whose members did, + * closed over the surfaces that spell one of them (a declaration whose + * surface spells one joins); + * - the names of such class and interface members, and of declarations + * whose text alone changed; + * - the names a changed, added or removed declaration writes to outside + * itself, since analyses read a binding's or property's writes anywhere; + * - the names that refer to a module's namespace object, by a namespace + * import or an `import()` or `require()` call, when the module's exports + * changed. + * + * TypeScript resolves every dependency of a type through a name some text + * spells, so a unit whose text spells none of these sees the same types, + * the same declarations and the same facts. A unit's spelled names include + * its strings, which can name members. + * + * The first build into a cache records nothing: it writes only the + * declaration tables, and the build after it records its units. + * + * Applying a record goes through applyRecord, which still lowers the unit + * when the record disagrees with what this build holds by the unit's turn. + * SCRIPTC_INCR_VERIFY=1 lowers every unit whose record would apply and + * throws an InternalCompilerError naming the unit when the new record + * differs from the stored one. */ +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { InternalCompilerError } from "../../errors.js"; +import type { IrFunction } from "../../ir/ir.js"; +import { IrReader, IrWriter, decodeFunctionBinary } from "../../ir/codec.js"; +import { IR_CODEC_LAYOUT } from "../../ir/codec.generated.js"; +import { IR_VERSION } from "../../ir/serialize.js"; +import * as ts from "../ts7/adapter.js"; +import { isJsSourceFileName } from "../tsc-codes.js"; +import { + decodeRecordFunction, + encodeUnitFunction, + type StoredRecord, + type UnitRecord, +} from "./unit-journal.js"; + +/** Where a build's unit entries live. The store's owner keys the program + * entry by everything the lowered module depends on besides the program's + * sources: the compiler, the entry, the options and the target. */ +export interface UnitStore { + /** The program entry the last build with the same key wrote, or null. */ + readProgram(): Uint8Array | null; + writeProgram(bytes: Uint8Array): void; + /** A file entry by the name writeFile gave it, or null. */ + readFile(name: string): Uint8Array | null; + /** Writes a file entry and returns its name. */ + writeFile(bytes: Uint8Array): string; +} + +/** What reuse asks the lowerer. */ +export interface UnitReuseHooks { + /** The facts the steps before the worklist settled for a declaration (a + * top-level statement or a class's header), as text. */ + facts(declaration: ts.Node): string; + /** The names a module specifier's module exports, as text, or null when + * it names no module of the program. */ + exportsOf(specifier: ts.Node): string | null; + /** Whether every declaration of the binding `identifier` names is inside + * `container`. */ + isLocal(identifier: ts.Identifier, container: ts.Node): boolean; +} + +/** A unit store in `directory`: the program entry for `key` under + * `program/`, file entries by content digest under `files/`. Failing reads + * are misses and failing writes leave the store as it was. */ +export function directoryUnitStore(directory: string, key: string): UnitStore { + const read = (path: string): Uint8Array | null => { + try { + return readFileSync(path); + } catch { + return null; + } + }; + const write = (path: string, bytes: Uint8Array): void => { + try { + mkdirSync(join(path, ".."), { recursive: true, mode: 0o700 }); + const stage = `${path}.${process.pid}.tmp`; + writeFileSync(stage, bytes); + renameSync(stage, path); + } catch { + // A failed write only costs the next build its reuse. + } + }; + const programPath = join(directory, "program", key); + return { + readProgram: () => read(programPath), + writeProgram: (bytes) => write(programPath, bytes), + readFile: (name) => (/^[0-9a-f]{64}$/.test(name) ? read(join(directory, "files", name)) : null), + writeFile: (bytes) => { + const name = createHash("sha256").update(bytes).digest("hex"); + const path = join(directory, "files", name); + if (read(path) === null) write(path, bytes); + return name; + }, + }; +} + +/** Whether builds reuse lowered units (SCRIPTC_INCR_LOWERING=1). */ +export function unitReuseEnabled(): boolean { + return process.env["SCRIPTC_INCR_LOWERING"] === "1"; +} + +/** Whether every unit whose record would apply lowers again and is checked + * against it (SCRIPTC_INCR_VERIFY=1). */ +export function unitReuseVerify(): boolean { + return process.env["SCRIPTC_INCR_VERIFY"] === "1"; +} + +/** The first line of a program entry and of a file entry. */ +const PROGRAM_MAGIC = "scriptc-units-program-2"; +const FILE_MAGIC = "scriptc-units-file-3"; + +/** A unit's record as an entry holds it, with the files other than the + * unit's own whose source positions the record holds. The function is in + * the binary encoding (`bytes`, the record's `fn` null) when every object + * in it reads back with its own key order, which frontend passes and the + * IR's text depend on, and in the record's JSON otherwise. */ +interface StoredUnit { + readonly record: UnitRecord; + readonly files: readonly string[]; + readonly bytes: Buffer | null; +} + +/** A recorded unit as an entry holds it. `named` are the files the fields + * named `file` in its binary function hold (RecordedUnit.files). */ +function storedUnit(record: UnitRecord, named: readonly string[] | null, own: string): StoredUnit { + const files = positionFiles(record, named, own); + const fn = record.fn; + if (fn === null || typeof fn === "string") return { record, files, bytes: null }; + const bytes = Buffer.from(fn.buffer, fn.byteOffset, fn.byteLength); + return { record: { ...record, fn: null }, files, bytes }; +} + +/** The record of a stored unit, with its function. */ +function storedRecord(unit: StoredUnit): StoredRecord { + const bytes = unit.bytes; + const record = unit.record; + return { + record, + fn: () => + bytes !== null + ? decodeFunctionBinary(bytes) + : record.fn === null + ? null + : decodeRecordFunction(record.fn), + }; +} + +/** The files whose source positions `record` holds, other than `own`: + * those of its JSON function and entries, and `named` for a binary + * function. */ +function positionFiles(record: UnitRecord, named: readonly string[] | null, own: string): string[] { + const files = new Set(); + if (named !== null) for (const file of named) if (file !== own) files.add(file); + const scan = (text: string): void => { + const pattern = /"file":("(?:[^"\\]|\\.)*")/g; + for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) { + const file = JSON.parse(match[1]!) as string; + if (file !== own) files.add(file); + } + }; + if (typeof record.fn === "string") scan(record.fn); + for (const entries of Object.values(record.tables)) for (const entry of entries) scan(entry); + return [...files].sort(); +} + +/** A SHA-256 digest of `text`, in hex. */ +function digestOf(text: string): string { + return createHash("sha256").update(text).digest("hex"); +} + +/** One declaration of a module's declaration table. */ +interface Declaration { + /** The declaration's kind and name, with an ordinal for repeated ones: + * the same declaration across builds. */ + readonly key: string; + /** The names other text refers to it by. */ + readonly names: readonly string[]; + /** Whether it is a class or interface member: other text reaches it only + * through a value whose type names the owner. */ + readonly member: boolean; + /** For an interface member, the interface: another surface of the member + * is another type of its owner. A class's type as lowering sees it is in + * its header's facts instead. */ + readonly owner: string | null; + /** Digests of its text, its surface and its facts. */ + readonly text: string; + readonly surface: string; + readonly facts: string; + /** The names its text assigns. */ + readonly assigns: readonly string[]; +} + +/** A site that refers to a module's namespace object: the names that hold + * or contain it, and a digest of the module's exports. */ +interface NamespaceSite { + readonly key: string; + readonly names: readonly string[]; + readonly exports: string; +} + +/** What a build records about one of the program's own modules. */ +interface FileTable { + readonly path: string; + readonly text: string; + /** The file entry holding its units' records, or "" when it has none. */ + entry: string; + readonly declarations: readonly Declaration[]; + readonly namespaces: readonly NamespaceSite[]; +} + +/** A declaration of this build's program, for the closure over surfaces: + * the names it declares, its owner's, and its node. A class or interface + * header is its statement without the members. */ +interface DeclarationNode { + readonly names: readonly string[]; + readonly member: boolean; + readonly owner: string | null; + readonly node: ts.Node; + readonly header: boolean; + readonly js: boolean; +} + +/** The body of a function-like node whose body other declarations' types + * cannot see: it declares its return type, or it is a constructor or set + * accessor. Undefined for other nodes. */ +function opaqueBody(node: ts.Node): ts.Node | undefined { + if (!ts.isFunctionLikeDeclaration(node) || node.body === undefined) return undefined; + return node.type !== undefined || ts.isConstructorDeclaration(node) || ts.isSetAccessor(node) + ? node.body + : undefined; +} + +/** The name node of a declaration or member, if it has one. */ +function nameNodeOf(node: ts.Node): ts.Node | undefined { + if ( + ts.isFunctionDeclaration(node) || + ts.isClassDeclaration(node) || + ts.isClassExpression(node) || + ts.isInterfaceDeclaration(node) || + ts.isTypeAliasDeclaration(node) || + ts.isEnumDeclaration(node) || + ts.isModuleDeclaration(node) || + ts.isMethodDeclaration(node) || + ts.isPropertyDeclaration(node) || + ts.isGetAccessor(node) || + ts.isSetAccessor(node) || + ts.isPropertySignature(node) || + ts.isMethodSignatureDeclaration(node) + ) + return node.name ?? undefined; + return undefined; +} + +/** The kinds the name walks materialize. */ +const NAME_KINDS: ReadonlySet = new Set([ + ts.SyntaxKind.Identifier, + ts.SyntaxKind.PrivateIdentifier, +]); + +/** The kinds of string literals. */ +const STRING_KINDS: ReadonlySet = new Set([ + ts.SyntaxKind.StringLiteral, + ts.SyntaxKind.NoSubstitutionTemplateLiteral, +]); + +/** The kinds that assign to their operand. */ +const WRITE_KINDS: ReadonlySet = new Set([ + ts.SyntaxKind.BinaryExpression, + ts.SyntaxKind.PrefixUnaryExpression, + ts.SyntaxKind.PostfixUnaryExpression, + ts.SyntaxKind.DeleteExpression, +]); + +/** The operators of assignments. */ +const ASSIGNMENTS: ReadonlySet = new Set([ + ts.SyntaxKind.EqualsToken, + ts.SyntaxKind.PlusEqualsToken, + ts.SyntaxKind.MinusEqualsToken, + ts.SyntaxKind.AsteriskEqualsToken, + ts.SyntaxKind.AsteriskAsteriskEqualsToken, + ts.SyntaxKind.SlashEqualsToken, + ts.SyntaxKind.PercentEqualsToken, + ts.SyntaxKind.LessThanLessThanEqualsToken, + ts.SyntaxKind.GreaterThanGreaterThanEqualsToken, + ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, + ts.SyntaxKind.AmpersandEqualsToken, + ts.SyntaxKind.BarEqualsToken, + ts.SyntaxKind.CaretEqualsToken, + ts.SyntaxKind.BarBarEqualsToken, + ts.SyntaxKind.AmpersandAmpersandEqualsToken, + ts.SyntaxKind.QuestionQuestionEqualsToken, +]); + +/** The identifier names `node`'s text spells, all of them or only those + * outside the bodies opaqueBody names. */ +function spelledNames(node: ts.Node, surfaceOnly: boolean, out: Set): void { + if (!surfaceOnly) { + ts.walkPreorder( + node, + (child) => { + out.add((child as ts.Identifier).text); + }, + NAME_KINDS, + ); + return; + } + ts.walkPreorder(node, (child) => { + if (ts.isIdentifier(child) || ts.isPrivateIdentifier(child)) { + out.add(child.text); + return undefined; + } + const parent = child.parent; + if (parent !== undefined && opaqueBody(parent) === child) return "skip"; + return undefined; + }); +} + +/** The names `node`'s text writes to outside it: the bindings, properties + * and elements that its assignments, `++`, `--` and `delete` target, but + * not bindings declared inside `node`. A property write names the property; + * an element write names what the element's object spells. */ +function assignedNames(node: ts.Node, hooks: UnitReuseHooks): string[] { + const out = new Set(); + const spellsOutside = (expression: ts.Node): void => { + ts.walkPreorder( + expression, + (child) => { + if (ts.isIdentifier(child)) { + if (!hooks.isLocal(child, node)) out.add(child.text); + } else if (ts.isPrivateIdentifier(child)) out.add(child.text); + }, + NAME_KINDS, + ); + }; + const written = (target: ts.Node): void => { + if (ts.isParenthesizedExpression(target)) written(target.expression); + else if (ts.isIdentifier(target)) { + if (!hooks.isLocal(target, node)) out.add(target.text); + } else if (ts.isPropertyAccessExpression(target)) out.add(target.name.text); + else if (ts.isElementAccessExpression(target)) spellsOutside(target.expression); + else if (ts.isObjectLiteralExpression(target)) { + for (const property of target.properties.nodes) { + if (ts.isShorthandPropertyAssignment(property)) written(property.name); + else if (ts.isPropertyAssignment(property)) written(property.initializer); + else if (ts.isSpreadAssignment(property)) written(property.expression); + } + } else if (ts.isArrayLiteralExpression(target)) { + for (const element of target.elements.nodes) + written(ts.isSpreadElement(element) ? element.expression : element); + } else if (ts.isBinaryExpression(target)) written(target.left); + else spellsOutside(target); + }; + ts.walkPreorder( + node, + (child) => { + if (ts.isBinaryExpression(child)) { + if (ASSIGNMENTS.has(child.operatorToken.kind)) written(child.left); + } else if (ts.isPrefixUnaryExpression(child) || ts.isPostfixUnaryExpression(child)) { + if ( + child.operator === ts.SyntaxKind.PlusPlusToken || + child.operator === ts.SyntaxKind.MinusMinusToken + ) + written(child.operand); + } else if (ts.isDeleteExpression(child)) written(child.expression); + }, + WRITE_KINDS, + ); + return [...out].sort(); +} + +/** `node`'s text with the bodies opaqueBody names replaced by `{}`. */ +function surfaceText(sf: ts.SourceFile, node: ts.Node, js: boolean): string { + const text = sf.text; + const start = node.getStart(sf); + if (js) return text.slice(start, node.end); + const cuts: [number, number][] = []; + ts.walkPreorder(node, (child) => { + const body = child === node ? undefined : opaqueBody(child); + if (body === undefined) return undefined; + cuts.push([body.getStart(sf), body.end]); + return "skip"; + }); + let out = ""; + let at = start; + for (const [from, to] of cuts) { + if (from < at) continue; + out += text.slice(at, from) + "{}"; + at = to; + } + return out + text.slice(at, node.end); +} + +/** The names a statement declares. A statement without a name (an + * expression statement, a loop) declares the names it assigns, such as the + * base of an expando `f.prop = value`; in JavaScript, where such statements + * also define exports, every name and string it spells. */ +function statementNames(statement: ts.Statement, js: boolean, hooks: UnitReuseHooks): string[] { + if (ts.isVariableStatement(statement)) { + const names: string[] = []; + for (const declaration of statement.declarationList.declarations.nodes) { + const name = declaration.name; + if (ts.isIdentifier(name)) names.push(name.text); + else + ts.walkPreorder( + name, + (child) => { + names.push((child as ts.Identifier).text); + }, + NAME_KINDS, + ); + } + return names; + } + if (ts.isExportDeclaration(statement) || ts.isExportAssignment(statement)) { + const names = new Set(); + spelledNames(statement, false, names); + return [...names].sort(); + } + const name = nameNodeOf(statement); + if (name !== undefined && (ts.isIdentifier(name) || ts.isStringLiteral(name))) return [name.text]; + if (!js) return assignedNames(statement, hooks); + const names = new Set(); + spelledNames(statement, false, names); + ts.walkPreorder(statement, (child) => { + if (ts.isStringLiteral(child)) names.add(child.text); + return undefined; + }); + return [...names].sort(); +} + +/** Whether a statement of a module changes names outside the module: + * `declare global`, a module augmentation, or `export * from`. */ +function hasGlobalEffect(statement: ts.Statement): boolean { + if (ts.isModuleDeclaration(statement)) { + const name = statement.name; + return !ts.isIdentifier(name) || name.text === "global"; + } + return ts.isExportDeclaration(statement) && statement.exportClause === undefined; +} + +/** The names a class or interface member declares: its name, or the + * identifiers a computed name spells. */ +function memberNames(member: ts.Node): string[] { + const name = nameNodeOf(member); + if (name === undefined) return ts.isConstructorDeclaration(member) ? ["constructor"] : []; + if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name) || ts.isStringLiteral(name)) + return [name.text]; + const names = new Set(); + spelledNames(name, false, names); + if (names.size === 0) names.add(name.getText()); + return [...names].sort(); +} + +/** The sites in `node` that refer to a module's namespace object: a + * namespace import or re-export, `import x = require()`, and `import()` and + * `require()` calls with a string, which `names` stand for. */ +function namespaceSites( + sf: ts.SourceFile, + statement: ts.Statement, + names: readonly string[], + hooks: UnitReuseHooks, + out: NamespaceSite[], +): void { + const add = (specifier: ts.Node, holders: readonly string[]): void => { + const exports = hooks.exportsOf(specifier); + if (exports === null) return; + out.push({ + key: `${specifier.getText(sf)}\n${holders.join(",")}`, + names: holders, + exports: digestOf(exports), + }); + }; + if (ts.isImportDeclaration(statement)) { + const bindings = statement.importClause?.namedBindings; + if (bindings !== undefined && ts.isNamespaceImport(bindings)) + add(statement.moduleSpecifier, [bindings.name.text]); + return; + } + if (ts.isImportEqualsDeclaration(statement)) { + const reference = statement.moduleReference; + if (ts.isExternalModuleReference(reference)) add(reference.expression, [statement.name.text]); + return; + } + if (ts.isExportDeclaration(statement)) { + const clause = statement.exportClause; + if ( + clause !== undefined && + ts.isNamespaceExport(clause) && + statement.moduleSpecifier !== undefined + ) + add(statement.moduleSpecifier, [clause.name.getText(sf)]); + return; + } + const text = sf.text.slice(statement.pos, statement.end); + if (!text.includes("import(") && !text.includes("require(")) return; + ts.walkPreorder(statement, (child) => { + if (!ts.isCallExpression(child)) return undefined; + const callee = child.expression; + const first = child.arguments.nodes[0]; + if (first === undefined || !ts.isStringLiteralLike(first)) return undefined; + if ( + callee.kind === ts.SyntaxKind.ImportKeyword || + (ts.isIdentifier(callee) && callee.text === "require") + ) + add(first, names); + return undefined; + }); +} + +/** A module's declaration table, its declarations' nodes, and the text of + * its statements with global effect. A file that is not a module has global + * effect as a whole. */ +function fileTable( + sf: ts.SourceFile, + hooks: UnitReuseHooks, + previous: FileTable | undefined, +): { table: FileTable; nodes: DeclarationNode[]; global: string } { + // A declaration with the text it had in the previous build writes what it + // wrote then. + const before = new Map((previous?.declarations ?? []).map((d) => [d.key, d])); + const assigns = (key: string, text: string, node: ts.Node): readonly string[] => { + const then = before.get(key); + return then !== undefined && then.text === text ? then.assigns : assignedNames(node, hooks); + }; + const js = isJsSourceFileName(sf.fileName); + const declarations: Declaration[] = []; + const namespaces: NamespaceSite[] = []; + const nodes: DeclarationNode[] = []; + const keys = new Set(); + let global = ""; + const unique = (key: string): string => { + let out = key; + for (let ordinal = 1; keys.has(out); ordinal++) out = `${key}#${ordinal}`; + keys.add(out); + return out; + }; + const module = ts.isExternalModule(sf) || js; + for (const statement of sf.statements.nodes) { + if (ts.isImportDeclaration(statement) || ts.isImportEqualsDeclaration(statement)) { + namespaceSites(sf, statement, [], hooks, namespaces); + continue; + } + if (!module || hasGlobalEffect(statement)) { + global += sf.text.slice(statement.getStart(sf), statement.end) + "\n"; + continue; + } + const names = statementNames(statement, js, hooks); + namespaceSites(sf, statement, names, hooks, namespaces); + const kind = String(statement.kind); + if (ts.isClassDeclaration(statement) || ts.isInterfaceDeclaration(statement)) { + // Each member is a declaration of its own; the rest of the class or + // interface (its name, type parameters, heritage and modifiers) is the + // header, named by the class, and holds the class's facts. + const owner = names[0] ?? ""; + const seeds = ts.isInterfaceDeclaration(statement) ? owner : null; + for (const member of statement.members.nodes) { + const declared = memberNames(member); + const key = unique(`${kind}:${owner}.${declared.join(",")}`); + const text = digestOf(sf.text.slice(member.getStart(sf), member.end)); + declarations.push({ + key, + names: declared, + member: true, + owner: seeds, + text, + surface: digestOf(surfaceText(sf, member, js)), + facts: "", + assigns: assigns(key, text, member), + }); + nodes.push({ + names: declared, + member: true, + owner: seeds, + node: member, + header: false, + js, + }); + } + const members = statement.members; + const header = + sf.text.slice(statement.getStart(sf), members.pos) + + sf.text.slice(members.end, statement.end); + const digest = digestOf(header); + declarations.push({ + key: unique(`${kind}:${owner}`), + names, + member: false, + owner: null, + text: digest, + surface: digest, + facts: digestOf(hooks.facts(statement)), + assigns: [], + }); + nodes.push({ names, member: false, owner: null, node: statement, header: true, js }); + continue; + } + const digest = digestOf(sf.text.slice(statement.getStart(sf), statement.end)); + const key = unique(`${kind}:${names.join(",")}`); + declarations.push({ + key, + names, + member: false, + owner: null, + text: digest, + surface: js ? digest : digestOf(surfaceText(sf, statement, js)), + facts: digestOf(hooks.facts(statement)), + assigns: assigns(key, digest, statement), + }); + nodes.push({ names, member: false, owner: null, node: statement, header: false, js }); + } + return { + table: { + path: sf.fileName, + text: digestOf(sf.text), + entry: "", + declarations, + namespaces, + }, + nodes, + global, + }; +} + +/** The names a declaration's surface spells: a JavaScript declaration's + * whole text, a header without its members. */ +function surfaceNames(declaration: DeclarationNode): Set { + const names = new Set(); + if (!declaration.header) { + spelledNames(declaration.node, !declaration.js, names); + return names; + } + const members = new Set( + (declaration.node as ts.ClassDeclaration | ts.InterfaceDeclaration).members.nodes, + ); + ts.walkPreorder(declaration.node, (child) => { + if (members.has(child)) return "skip"; + if (ts.isIdentifier(child) || ts.isPrivateIdentifier(child)) names.add(child.text); + return undefined; + }); + return names; +} + +/** What a build knows about the units it lowers, and what it writes. */ +export class UnitReuse { + /** Whether this build records its units. A build that finds no program + * entry, the first one into a cache, records none: it lowers as a build + * without reuse does and writes only the declaration tables, so that + * recording costs the next build instead. */ + readonly records: boolean; + /** The previous build's tables by path, when its program digest matches. */ + private readonly previous: Map | null; + /** The names whose declarations changed since the previous build. */ + private readonly changed: Set; + /** The decoded file entries by path. */ + private readonly decoded = new Map | null>(); + /** The records this build made or applied, by file path, in unit order. */ + private readonly written = new Map>(); + /** Files whose units this build lowered. */ + private readonly rewritten = new Set(); + /** Counts, and the time opening (tables and changed names) and finishing + * (writing entries) took, in milliseconds. */ + readonly stats = { + candidates: 0, + hits: 0, + misses: 0, + changedNames: 0, + openMs: 0, + readMs: 0, + finishMs: 0, + /** Functions this build stored in the binary encoding, and as JSON, and + * the time making their units' entries took. */ + binary: 0, + json: 0, + storeMs: 0, + }; + + private constructor( + private readonly store: UnitStore, + private readonly program: string, + private readonly tables: Map, + previous: { program: string; tables: Map } | null, + nodes: readonly DeclarationNode[], + ) { + this.previous = previous !== null && previous.program === program ? previous.tables : null; + this.records = previous !== null; + this.changed = this.previous === null ? new Set() : changedNames(this.previous, tables, nodes); + this.stats.changedNames = this.changed.size; + } + + /** Opens reuse for a build of `files`, the program's own modules in + * module order, within `program`. */ + static open( + store: UnitStore, + program: ts.Program, + files: readonly ts.SourceFile[], + hooks: UnitReuseHooks, + ): UnitReuse { + const started = performance.now(); + const own = new Set(files); + const tables = new Map(); + const nodes: DeclarationNode[] = []; + const previous = readProgram(store); + const digest = createHash("sha256").update( + `${PROGRAM_MAGIC}\n${IR_VERSION}\n${IR_CODEC_LAYOUT}\n`, + ); + for (const sf of files) { + const file = fileTable(sf, hooks, previous?.tables.get(sf.fileName)); + tables.set(sf.fileName, file.table); + for (const node of file.nodes) nodes.push(node); + digest.update(`${sf.fileName}\n${digestOf(file.global)}\n`); + } + const others = program + .getSourceFiles() + .filter((sf) => !own.has(sf)) + .map((sf) => `${sf.fileName}\n${digestOf(sf.text)}\n`) + .sort(); + for (const other of others) digest.update(other); + const reuse = new UnitReuse(store, digest.digest("hex"), tables, previous, nodes); + reuse.stats.openMs = performance.now() - started; + return reuse; + } + + /** The stored record of unit `name`, whose text is `source` in `file` + * inside the declarations `owners`, when the unit can apply it. */ + candidate( + name: string, + file: string, + source: ts.Node | null, + owners: readonly ts.Node[], + ): StoredRecord | null { + if (this.previous === null || source === null) return null; + const before = this.previous.get(file); + const now = this.tables.get(file); + if ( + before === undefined || + now === undefined || + before.text !== now.text || + before.entry === "" + ) + return null; + let units = this.decoded.get(file); + if (units === undefined) { + const started = performance.now(); + units = readFileEntry(this.store, before.entry); + this.decoded.set(file, units); + this.stats.readMs += performance.now() - started; + } + const stored = units?.get(name); + if (stored === undefined) return null; + this.stats.candidates++; + for (const other of stored.files) { + const then = this.previous.get(other); + if (then === undefined || then.text !== this.tables.get(other)?.text) return null; + } + if (this.changed.size > 0) { + const names = new Set(); + spelledNames(source, false, names); + // A string can name a member too (`value["name"]`, `keyof` types). + ts.walkPreorder( + source, + (child) => { + names.add((child as ts.StringLiteral).text); + }, + STRING_KINDS, + ); + for (const owner of owners) { + const ownerName = nameNodeOf(owner); + if (ownerName !== undefined && ts.isIdentifier(ownerName)) names.add(ownerName.text); + } + for (const spelled of names) if (this.changed.has(spelled)) return null; + } + return storedRecord(stored); + } + + /** Notes the record a unit of `file` lowered to, with the files its + * binary function names (RecordedUnit.files), or the stored record it + * applied (`applied`). A unit without a record is noted with null. */ + note( + name: string, + file: string, + record: UnitRecord | null, + applied: boolean, + files: readonly string[] | null, + ): void { + let units = this.written.get(file); + if (units === undefined) { + units = new Map(); + this.written.set(file, units); + } + if (record !== null) { + let unit = applied ? this.decoded.get(file)?.get(name) : undefined; + if (unit === undefined) { + const started = performance.now(); + unit = storedUnit(record, files, file); + this.stats.storeMs += performance.now() - started; + if (unit.bytes !== null) this.stats.binary++; + else if (unit.record.fn !== null) this.stats.json++; + } + units.set(name, unit); + } + if (applied) this.stats.hits++; + else { + this.stats.misses++; + this.rewritten.add(file); + } + } + + /** Writes the file entries of files whose units changed and the program + * entry. A file none of whose units this build lowered or applied keeps + * no entry. */ + finish(): void { + const started = performance.now(); + for (const [path, table] of this.tables) { + const units = this.written.get(path); + if (units === undefined || units.size === 0) continue; + const before = this.previous?.get(path); + const unchanged = + before !== undefined && + before.text === table.text && + before.entry !== "" && + !this.rewritten.has(path) && + this.decoded.get(path)?.size === units.size; + table.entry = unchanged ? before.entry : this.store.writeFile(encodeFileEntry(units)); + } + this.store.writeProgram(encodeProgramEntry(this.program, this.tables)); + this.stats.finishMs = performance.now() - started; + } +} + +/** The changed names of a build (see the comment at the top) from the + * previous build's tables and this build's. */ +function changedNames( + before: Map, + now: Map, + nodes: readonly DeclarationNode[], +): Set { + const surfaces = new Set(); + const direct = new Set(); + // Text reaches a member through a value whose type names its owner, so a + // member with another type stops the units that spell it, and an + // interface's member gives the interface another type. A class's own type + // changes with its header's facts. + const typeChanged = (declaration: { + readonly names: readonly string[]; + readonly member: boolean; + readonly owner: string | null; + }): void => { + for (const name of declaration.names) (declaration.member ? direct : surfaces).add(name); + if (declaration.owner !== null) surfaces.add(declaration.owner); + }; + const compare = (from: FileTable | undefined, to: FileTable | undefined): void => { + const old = new Map((from?.declarations ?? []).map((d) => [d.key, d])); + const seen = new Set(); + for (const declaration of to?.declarations ?? []) { + seen.add(declaration.key); + const previous = old.get(declaration.key); + if ( + previous === undefined || + previous.surface !== declaration.surface || + previous.facts !== declaration.facts + ) + typeChanged(declaration); + else if (previous.text !== declaration.text) + for (const name of declaration.names) direct.add(name); + else continue; + for (const name of declaration.assigns) direct.add(name); + for (const name of previous?.assigns ?? []) direct.add(name); + } + for (const declaration of from?.declarations ?? []) + if (!seen.has(declaration.key)) { + typeChanged(declaration); + for (const name of declaration.assigns) direct.add(name); + } + const sites = new Map((from?.namespaces ?? []).map((site) => [site.key, site.exports])); + for (const site of to?.namespaces ?? []) + if (sites.get(site.key) !== site.exports) for (const name of site.names) surfaces.add(name); + }; + for (const [path, table] of now) compare(before.get(path), table); + for (const [path, table] of before) if (!now.has(path)) compare(table, undefined); + if (surfaces.size > 0) { + // A declaration whose surface spells a changed name can have another + // type now (typeChanged); repeat until no surface adds a name. + const pending = nodes.map((declaration) => ({ + declaration, + spells: surfaceNames(declaration), + })); + for (let grew = true; grew;) { + grew = false; + for (let index = 0; index < pending.length; index++) { + const entry = pending[index]!; + let hit = false; + for (const name of entry.spells) + if (surfaces.has(name)) { + hit = true; + break; + } + if (!hit) continue; + typeChanged(entry.declaration); + // Each declaration joins once. + pending[index] = pending[pending.length - 1]!; + pending.pop(); + index--; + grew = true; + } + } + } + for (const name of direct) surfaces.add(name); + return surfaces; +} + +function encodeProgramEntry(program: string, tables: Map): Uint8Array { + const lines = [PROGRAM_MAGIC, program]; + for (const table of tables.values()) lines.push(JSON.stringify(table)); + return Buffer.from(lines.join("\n"), "utf8"); +} + +function readProgram(store: UnitStore): { program: string; tables: Map } | null { + const bytes = store.readProgram(); + if (bytes === null) return null; + try { + const lines = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength) + .toString("utf8") + .split("\n"); + if (lines[0] !== PROGRAM_MAGIC || lines.length < 2) return null; + const tables = new Map(); + for (const line of lines.slice(2)) { + const table = JSON.parse(line) as FileTable; + tables.set(table.path, table); + } + return { program: lines[1]!, tables }; + } catch { + // An unreadable entry is a miss. + return null; + } +} + +/** A file entry: per stored unit, the unit without its function's bytes as + * JSON, then the bytes when it has them. */ +function encodeFileEntry(units: Map): Uint8Array { + const w = new IrWriter(); + w.str(FILE_MAGIC); + w.length(units.size); + for (const unit of units.values()) { + w.str(JSON.stringify({ record: unit.record, files: unit.files })); + if (unit.bytes === null) w.byte(0); + else { + w.byte(1); + w.blob(unit.bytes); + } + } + return w.finish(); +} + +function readFileEntry(store: UnitStore, name: string): Map | null { + const bytes = store.readFile(name); + if (bytes === null) return null; + try { + const r = new IrReader(Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)); + if (r.str() !== FILE_MAGIC) return null; + const units = new Map(); + for (let count = r.length(); count > 0; count--) { + const unit = JSON.parse(r.str()) as { record: UnitRecord; files: string[] }; + const fn = r.byte() === 0 ? null : r.blob(); + units.set(unit.record.name, { record: unit.record, files: unit.files, bytes: fn }); + } + return r.done() ? units : null; + } catch { + return null; + } +} + +/** Throws when a unit lowered to another record than its stored one + * (SCRIPTC_INCR_VERIFY=1), naming the unit and the first part that + * differs. `pending` is the stored record's entries the tables did not hold + * at the unit's turn. */ +export function verifyStoredUnit( + name: string, + storedRecord: StoredRecord, + pending: Readonly>, + lowered: UnitRecord | null, + reasons: readonly string[], +): void { + const stored = storedRecord.record; + const fail = (what: string): never => { + throw new InternalCompilerError( + `SCRIPTC_INCR_VERIFY: unit ${name} lowered to another ${what} than its stored record`, + ); + }; + if (lowered === null) return fail(`result (no record: ${reasons.join(", ")})`); + if (lowered.poisoned !== stored.poisoned) fail("poison"); + // As JSON text, which keeps key order, whichever encoding each record has. + const text = (fn: IrFunction | null): string | null => + fn === null ? null : (encodeUnitFunction(fn) ?? ""); + const loweredFn = lowered.fn === null ? null : decodeRecordFunction(lowered.fn); + if (text(loweredFn) !== text(storedRecord.fn())) fail("function"); + if (JSON.stringify(lowered.events) !== JSON.stringify(stored.events)) fail("event list"); + // A stored entry the tables hold already is one an earlier unit of this + // build added: applying the record leaves it as it is. + if (JSON.stringify(lowered.tables) !== JSON.stringify(pending)) fail("table entries"); +} diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 02539b610..0fd084979 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1,6 +1,6 @@ import { InternalCompilerError } from "../errors.js"; import { keyedHelperName, type KeyedNameOwner } from "./lowering/stable-names.js"; -import type { HeldContents, JournalTable } from "./lowering/unit-journal.js"; +import { contentDigest, type HeldContents, type JournalTable } from "./lowering/unit-journal.js"; import { mockCallRecordType, mockContextType } from "./mock-shapes.js"; import * as ts from "./ts7/adapter.js"; import { bodyReadsArguments } from "./arguments-usage.js"; @@ -716,9 +716,39 @@ export class UnionRegistry { * the key its arms give. A unit that adds a recursive union changes * journalSizes, so the unions past a unit's start are interned ones. */ journalTable(): JournalTable { + // The position of each union by id, for the unions indexed so far. + let indexes = new Map(); + let indexed = 0; + const indexOf = (id: string): number | undefined => { + if (indexed > this.unions.length) { + indexes = new Map(); + indexed = 0; + } + for (; indexed < this.unions.length; indexed++) + indexes.set(this.unions[indexed]!.id, indexed); + const index = indexes.get(id); + return index !== undefined && this.unions[index]?.id === id ? index : undefined; + }; return { name: "unions", size: () => this.unions.length, + digest: (key, before) => { + if (!this.byId.has(key)) return null; + let index = indexOf(key); + if (index === undefined) { + // A list that was cut and grown again holds other unions at the + // positions indexed before. + indexes = new Map(); + indexed = 0; + index = indexOf(key); + } + if (index === undefined || index >= before) return null; + return contentDigest(JSON.stringify(this.unions[index]!)); + }, + holds: (entry) => { + const def = JSON.parse(entry) as IrUnionDef; + return this.byKey.has(unionKey(def.arms, def.discriminant)); + }, entries: (from) => { const out: string[] = []; for (const def of this.unions.slice(from)) { @@ -729,6 +759,12 @@ export class UnionRegistry { } return out; }, + agrees: (entries) => + entries.every((entry) => { + const def = JSON.parse(entry) as IrUnionDef; + const held = this.byKey.get(unionKey(def.arms, def.discriminant)); + return held === undefined ? !this.byId.has(def.id) : held === def.id; + }), apply: (entries) => { for (const entry of entries) { const def = JSON.parse(entry) as IrUnionDef; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index b837b74fd..4ed209262 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -23,6 +23,11 @@ import { checkerCacheIdentity } from "./frontend/ts7/checker-backend.js"; import { setLibDiagnosticsStore } from "./frontend/ts7/lib-diagnostics.js"; import { analyzeWithVerdictCache, fileVerdictStore } from "./coverage/verdict-cache.js"; import { coverageCompilerIdentity } from "./coverage/verdict-startup.js"; +import { + directoryUnitStore, + unitReuseEnabled, + type UnitStore, +} from "./frontend/lowering/unit-reuse.js"; import { commentOnlyReplay } from "./coverage/verdict-semantic.js"; import { llvmRefusalDiag, targetRefusalDiag } from "./backend/target-diagnostics.js"; import type { @@ -914,6 +919,37 @@ function runtimePackDiagnostic(error: RuntimePackError, entryPath: string): ScrD ); } +/** The unit store of a Node-hosted build (unit-reuse.ts), keyed by the + * compiler, the entry, the options that reach lowering and the target + * platform. */ +async function nodeUnitStore( + entryPath: string, + opts: CompileRequestOptions, + ffi: FfiProfile | null, + buildPlatform: string, +): Promise { + if (!unitReuseEnabled() || provenanceSources() !== null) return null; + const root = await prepareBuildCacheRoot(buildCacheRoot()); + if (root === null) return null; + const directory = join(root, "units-v1"); + const implementation = await coverageCompilerIdentity(directory); + if (implementation === null) return null; + const { outPath: _outPath, outDir: _outDir, ...lowering } = opts; + const key = createHash("sha256") + .update( + JSON.stringify({ + schema: 1, + entry: resolve(entryPath), + options: lowering, + ffi, + platform: buildPlatform, + compiler: implementation.digest, + }), + ) + .digest("hex"); + return directoryUnitStore(directory, key); +} + interface PreparedExecutable { llvmSource: LlvmInput; /** The kept LLVM text beside the executable, installed when the build ends. */ @@ -945,6 +981,9 @@ async function prepareExecutableInput( buildPlatform, nodeFrontend, timing, + new Map(), + true, + await nodeUnitStore(entryPath, opts, ffi, buildPlatform), ); if (!prepared.ok) return prepared; const { mod, sourceTexts } = prepared; diff --git a/packages/compiler/src/ir/codec.generated.ts b/packages/compiler/src/ir/codec.generated.ts new file mode 100644 index 000000000..40d5fe3d2 --- /dev/null +++ b/packages/compiler/src/ir/codec.generated.ts @@ -0,0 +1,5099 @@ +// Generated from packages/compiler/src/ir/ir.ts by scripts/generate-ir-codec.mjs. +// Run `pnpm generate:ir-codec` after changing the IR's types. +import type { IrBindingSource, IrClassDef, IrContracts, IrExpr, IrFfiCallbackParam, IrFfiContextParam, IrFfiImport, IrFfiParam, IrFfiReleaseParam, IrFunction, IrGlobal, IrLibCallback, IrLibExport, IrLibIdentity, IrLibSection, IrLibTrapOverlay, IrLocal, IrModule, IrNodeIslandModules, IrParam, IrRecordShape, IrStmt, IrType, IrUnionDef, IrUnionDiscriminant, IrWasmModule, IrWasmUntranslated, SrcLoc } from "./ir.js"; +import type { IrReader, IrWriter } from "./codec.js"; + +/** A digest of the coders below: an encoding written with other coders is + * refused instead of misread, also after a change that keeps IR_VERSION. */ +export const IR_CODEC_LAYOUT = "978da33223a8447d"; + +/** Writes `mod` field by field (codec.ts describes the bytes). */ +export function encodeIrModule(w: IrWriter, mod: IrModule): void { + encIrModule(w, mod); +} +/** Reads a module encodeIrModule wrote. */ +export function decodeIrModule(r: IrReader): IrModule { + return decIrModule(r); +} +/** Writes one function. */ +export function encodeIrFunction(w: IrWriter, fn: IrFunction): void { + encIrFunction(w, fn); +} +/** Reads a function encodeIrFunction wrote. */ +export function decodeIrFunction(r: IrReader): IrFunction { + return decIrFunction(r); +} + +const KEYS0: readonly string[] = ["irVersion","sourceFile","functions","classes","globals","records","unions","entry","workers","declBoundary","inferredFactFiles","declOrigins","lifetimeRegions","foreignElementKeys","threadSlotKeys","freezableArrays","sloppyDynWrites","untypedIntegrity","commonJsEntry","nodeIsland","ffiImports","lib","contracts","int32ArrayCandidates","wasmModules","wasmUntranslated","wasmCustomSections"]; +const KEYS1: readonly string[] = ["name","sourceName","ownsPrototype","params","returnType","locals","captures","classCaptures","body","loc","tier","speculativeDispatch","fastRead","async","generator","asyncCacheGlobal","asyncCycleCacheGlobal"]; +const KEYS2: readonly string[] = ["libnode","nodeVersion","modules"]; +const KEYS3: readonly string[] = ["profileName","prefix","initSymbol","sinkRegisterSymbol","collectSymbol","resultResetSymbol","threadInstances","callbackRegisterSymbol","callbacks","exports","trapOverlays","identity"]; +const KEYS4: readonly string[] = ["int32Fields","int32Params","int32Returns","int32Locals","int32Elements","noThrow","acyclic","immortalGlobals","programLifetime","inlineArrays"]; +const KEYS5: readonly string[] = ["kind","elem","optionalElements"]; +const KEYS6: readonly string[] = ["kind"]; +const KEYS7: readonly string[] = ["kind"]; +const KEYS8: readonly string[] = ["kind","elem"]; +const KEYS9: readonly string[] = ["kind"]; +const KEYS10: readonly string[] = ["kind"]; +const KEYS11: readonly string[] = ["kind"]; +const KEYS12: readonly string[] = ["kind"]; +const KEYS13: readonly string[] = ["kind","className"]; +const KEYS14: readonly string[] = ["kind"]; +const KEYS15: readonly string[] = ["kind"]; +const KEYS16: readonly string[] = ["kind"]; +const KEYS17: readonly string[] = ["kind"]; +const KEYS18: readonly string[] = ["kind"]; +const KEYS19: readonly string[] = ["kind"]; +const KEYS20: readonly string[] = ["kind"]; +const KEYS21: readonly string[] = ["kind"]; +const KEYS22: readonly string[] = ["kind"]; +const KEYS23: readonly string[] = ["kind","params","ret","rest","restAbi","argumentsAll"]; +const KEYS24: readonly string[] = ["kind","async","yieldT","retT","nextT"]; +const KEYS25: readonly string[] = ["kind"]; +const KEYS26: readonly string[] = ["kind"]; +const KEYS27: readonly string[] = ["kind"]; +const KEYS28: readonly string[] = ["kind"]; +const KEYS29: readonly string[] = ["kind"]; +const KEYS30: readonly string[] = ["kind"]; +const KEYS31: readonly string[] = ["kind","key","value"]; +const KEYS32: readonly string[] = ["kind","moduleId"]; +const KEYS33: readonly string[] = ["kind"]; +const KEYS34: readonly string[] = ["kind"]; +const KEYS35: readonly string[] = ["kind"]; +const KEYS36: readonly string[] = ["kind","className"]; +const KEYS37: readonly string[] = ["kind"]; +const KEYS38: readonly string[] = ["kind","inner"]; +const KEYS39: readonly string[] = ["kind","shapeId"]; +const KEYS40: readonly string[] = ["kind"]; +const KEYS41: readonly string[] = ["kind"]; +const KEYS42: readonly string[] = ["kind"]; +const KEYS43: readonly string[] = ["kind","elem"]; +const KEYS44: readonly string[] = ["kind"]; +const KEYS45: readonly string[] = ["kind"]; +const KEYS46: readonly string[] = ["kind"]; +const KEYS47: readonly string[] = ["kind"]; +const KEYS48: readonly string[] = ["kind"]; +const KEYS49: readonly string[] = ["kind"]; +const KEYS50: readonly string[] = ["kind","unionId"]; +const KEYS51: readonly string[] = ["kind"]; +const KEYS52: readonly string[] = ["kind"]; +const KEYS53: readonly string[] = ["file","start","end"]; +const KEYS54: readonly string[] = ["yieldT","nextT","resultType","closesSource","tag","inheritsTag"]; +const KEYS55: readonly string[] = ["name","localCaptures","localBaseCapture","baseValueGlobal","jsName","jsLength","ownStaticFields","prototypeDataHelper","instancePrototypeHelper","stringTagHelper","tracksOwnFields","errorNameAfter","errorBagOrder","symbolFields","runtime","arrayElem","arrayFields","arraySpecies","base","fields","methods","prototypeNames","abstract","abstractMethods","genericOf","loc"]; +const KEYS56: readonly string[] = ["id","name","type","mutable","tdz","initFlag","source"]; +const KEYS57: readonly string[] = ["id","fields","tuple","indexValue","freezable","declaredOrder","accessorOrder","orderVariants"]; +const KEYS58: readonly string[] = ["id","arms","discriminant"]; +const KEYS59: readonly string[] = ["file","pkg"]; +const KEYS60: readonly string[] = ["root","files","links","location"]; +const KEYS61: readonly string[] = ["name","symbol","library","callbackOperation","callbackTarget","params","returns"]; +const KEYS62: readonly string[] = ["buildIdSymbol","abiVersionSymbol","buildId","abiVersion"]; +const KEYS63: readonly string[] = ["sha256","bytes"]; +const KEYS64: readonly string[] = ["sha256","size","reason"]; +const KEYS65: readonly string[] = ["localId","name","type","owned"]; +const KEYS66: readonly string[] = ["id","name","type","mutable","source","boxed","tdz"]; +const KEYS67: readonly string[] = ["localId","name","type","owned","slot"]; +const KEYS68: readonly string[] = ["kind","arr","index","loc"]; +const KEYS69: readonly string[] = ["kind","arr","index","value","loc"]; +const KEYS70: readonly string[] = ["kind","arr","length","loc","pop","reserve"]; +const KEYS71: readonly string[] = ["kind","arr","index","loc"]; +const KEYS72: readonly string[] = ["kind","localId","value","initializes","loc"]; +const KEYS73: readonly string[] = ["kind","labels","body","loc"]; +const KEYS74: readonly string[] = ["kind","label","loc"]; +const KEYS75: readonly string[] = ["kind","arr","index","value","loc"]; +const KEYS76: readonly string[] = ["kind","label","loc"]; +const KEYS77: readonly string[] = ["kind","body","cond","labels","loc"]; +const KEYS78: readonly string[] = ["kind","expr","loc"]; +const KEYS79: readonly string[] = ["kind","obj","className","field","value","sloppy","define","attributesChecked","loc"]; +const KEYS80: readonly string[] = ["kind","init","cond","update","body","labels","loc"]; +const KEYS81: readonly string[] = ["kind","localId","iterable","body","labels","loc"]; +const KEYS82: readonly string[] = ["kind","cond","then","else_","loc"]; +const KEYS83: readonly string[] = ["kind","obj","shapeId","key","loc"]; +const KEYS84: readonly string[] = ["kind","obj","shapeId","key","value","overflowOnly","loc"]; +const KEYS85: readonly string[] = ["kind","obj","shapeId","field","value","loc"]; +const KEYS86: readonly string[] = ["kind","localId","loc"]; +const KEYS87: readonly string[] = ["kind","value","awaited","loc"]; +const KEYS88: readonly string[] = ["kind","code","message","loc"]; +const KEYS89: readonly string[] = ["kind","disc","cases","labels","loc"]; +const KEYS90: readonly string[] = ["kind","value","loc"]; +const KEYS91: readonly string[] = ["kind","tryBody","catchBody","catchLocalId","dropFinallyErrors","finallyBody","suppressFinallyErrors","loc"]; +const KEYS92: readonly string[] = ["kind","localId","init","loc"]; +const KEYS93: readonly string[] = ["kind","cond","body","labels","loc"]; +const KEYS94: readonly string[] = ["kind","shapeId"]; +const KEYS95: readonly string[] = ["loc","scope"]; +const KEYS96: readonly string[] = ["field","cases"]; +const KEYS97: readonly string[] = ["name","slot","params","returns","unregisteredTrap"]; +const KEYS98: readonly string[] = ["symbol","fnName","params","returns","inboundBytesTrap","inboundIntTrap","inboundCstringTrap"]; +const KEYS99: readonly string[] = ["code","teaching","remediation"]; +const KEYS100: readonly string[] = ["kind","method","receiver","args","type","loc"]; +const KEYS101: readonly string[] = ["kind","arr","index","type","loc"]; +const KEYS102: readonly string[] = ["kind","arr","index","type","loc"]; +const KEYS103: readonly string[] = ["kind","elems","spreads","capacity","type","loc"]; +const KEYS104: readonly string[] = ["kind","length","type","loc"]; +const KEYS105: readonly string[] = ["kind","arr","index","type","loc"]; +const KEYS106: readonly string[] = ["kind","localId","value","type","loc"]; +const KEYS107: readonly string[] = ["kind","value","type","adopt","loc"]; +const KEYS108: readonly string[] = ["kind","value","promiseTag","type","loc"]; +const KEYS109: readonly string[] = ["kind","op","left","right","type","loc"]; +const KEYS110: readonly string[] = ["kind","value","type","loc"]; +const KEYS111: readonly string[] = ["kind","method","receiver","args","type","loc","invalidNaN"]; +const KEYS112: readonly string[] = ["kind","source","from","type","loc"]; +const KEYS113: readonly string[] = ["kind","callee","args","type","loc"]; +const KEYS114: readonly string[] = ["kind","callee","receiver","args","type","loc"]; +const KEYS115: readonly string[] = ["kind","value","className","type","loc"]; +const KEYS116: readonly string[] = ["kind","value","type","loc"]; +const KEYS117: readonly string[] = ["kind","value","test","className","negated","type","loc"]; +const KEYS118: readonly string[] = ["kind","value","type","loc"]; +const KEYS119: readonly string[] = ["kind","id","type","loc"]; +const KEYS120: readonly string[] = ["kind","className","type","loc","captures"]; +const KEYS121: readonly string[] = ["kind","fnName","captures","adapts","type","loc"]; +const KEYS122: readonly string[] = ["kind","value","type","loc"]; +const KEYS123: readonly string[] = ["kind","elems","type","loc"]; +const KEYS124: readonly string[] = ["kind","callee","receiver","calleeName","calleeNameValue","args","spreads","type","loc"]; +const KEYS125: readonly string[] = ["kind","value","preserveRefs","adopt","exact","inferredSite","origin","type","loc"]; +const KEYS126: readonly string[] = ["kind","value","spelling","firstProp","type","loc"]; +const KEYS127: readonly string[] = ["kind","value","type","loc","liveRef","fnName","fnLength"]; +const KEYS128: readonly string[] = ["kind","key","negated","value","type","loc"]; +const KEYS129: readonly string[] = ["kind","recv","method","calleeName","calleeNameValue","args","type","loc"]; +const KEYS130: readonly string[] = ["kind","value","count","notIterableMessage","type","loc"]; +const KEYS131: readonly string[] = ["kind","key","optional","value","type","loc"]; +const KEYS132: readonly string[] = ["kind","fields","type","loc"]; +const KEYS133: readonly string[] = ["kind","left","right","negated","type","loc"]; +const KEYS134: readonly string[] = ["kind","test","bytesElem","negated","value","type","loc"]; +const KEYS135: readonly string[] = ["kind","import","args","type","loc"]; +const KEYS136: readonly string[] = ["kind","unionId","type","loc"]; +const KEYS137: readonly string[] = ["kind","obj","className","field","type","loc"]; +const KEYS138: readonly string[] = ["kind","op","prefix","obj","className","field","fieldDyn","type","loc"]; +const KEYS139: readonly string[] = ["kind","mode","gen","arg","type","loc"]; +const KEYS140: readonly string[] = ["kind","op","prefix","localId","type","loc"]; +const KEYS141: readonly string[] = ["kind","value","className","type","loc"]; +const KEYS142: readonly string[] = ["kind","value","classValue","type","loc"]; +const KEYS143: readonly string[] = ["kind","name","args","type","loc"]; +const KEYS144: readonly string[] = ["kind","value","indent","circular","type","loc"]; +const KEYS145: readonly string[] = ["kind","fn","args","type","loc","prototypeIdentityOnly","realTimer","assertOptions"]; +const KEYS146: readonly string[] = ["kind","op","left","right","type","loc"]; +const KEYS147: readonly string[] = ["kind","method","receiver","args","type","loc"]; +const KEYS148: readonly string[] = ["kind","seed","type","loc"]; +const KEYS149: readonly string[] = ["kind","moduleId","type","loc"]; +const KEYS150: readonly string[] = ["kind","className","args","type","loc"]; +const KEYS151: readonly string[] = ["kind","executor","type","loc"]; +const KEYS152: readonly string[] = ["kind","callee","args","type","loc"]; +const KEYS153: readonly string[] = ["kind","left","right","type","loc"]; +const KEYS154: readonly string[] = ["kind","value","spelling","type","loc"]; +const KEYS155: readonly string[] = ["kind","id","receiver","body","type","loc"]; +const KEYS156: readonly string[] = ["kind","left","right","retag","type","loc"]; +const KEYS157: readonly string[] = ["kind","value","type","loc"]; +const KEYS158: readonly string[] = ["kind","type","loc"]; +const KEYS159: readonly string[] = ["kind","source","overrides","type","loc"]; +const KEYS160: readonly string[] = ["kind","obj","shapeId","field","type","loc"]; +const KEYS161: readonly string[] = ["kind","obj","shapeId","field","type","loc"]; +const KEYS162: readonly string[] = ["kind","obj","shapeId","key","overflowOnly","missNaN","type","loc"]; +const KEYS163: readonly string[] = ["kind","fields","nullProto","type","loc"]; +const KEYS164: readonly string[] = ["kind","obj","shapeId","type","loc"]; +const KEYS165: readonly string[] = ["kind","obj","shapeId","key","type","loc"]; +const KEYS166: readonly string[] = ["kind","obj","shapeId","type","loc"]; +const KEYS167: readonly string[] = ["kind","method","receiver","args","type","loc"]; +const KEYS168: readonly string[] = ["kind","pattern","flags","type","loc"]; +const KEYS169: readonly string[] = ["kind","type","loc"]; +const KEYS170: readonly string[] = ["kind","stmts","result","generatorDelegate","diverges","type","loc"]; +const KEYS171: readonly string[] = ["kind","method","receiver","args","type","loc"]; +const KEYS172: readonly string[] = ["kind","seed","type","loc"]; +const KEYS173: readonly string[] = ["kind","op","left","right","type","loc"]; +const KEYS174: readonly string[] = ["kind","left","right","type","loc"]; +const KEYS175: readonly string[] = ["kind","negated","left","right","type","loc"]; +const KEYS176: readonly string[] = ["kind","method","receiver","args","type","loc"]; +const KEYS177: readonly string[] = ["kind","value","type","loc"]; +const KEYS178: readonly string[] = ["kind","key","cooked","type","loc"]; +const KEYS179: readonly string[] = ["kind","cond","then","else_","type","loc"]; +const KEYS180: readonly string[] = ["kind","operand","type","loc"]; +const KEYS181: readonly string[] = ["kind","operand","type","loc"]; +const KEYS182: readonly string[] = ["kind","op","operand","type","loc"]; +const KEYS183: readonly string[] = ["kind","unionId","field","value","type","loc"]; +const KEYS184: readonly string[] = ["kind","unionId","negated","sameValue","left","right","type","loc"]; +const KEYS185: readonly string[] = ["kind","unionId","tag","union","func","negated","type","loc"]; +const KEYS186: readonly string[] = ["kind","unionId","tag","negated","value","type","loc"]; +const KEYS187: readonly string[] = ["kind","unionId","key","value","type","loc"]; +const KEYS188: readonly string[] = ["kind","unionId","tag","value","type","loc"]; +const KEYS189: readonly string[] = ["kind","fromId","unionId","tags","value","type","loc"]; +const KEYS190: readonly string[] = ["kind","unionId","tag","value","type","loc"]; +const KEYS191: readonly string[] = ["kind","unit","type","loc"]; +const KEYS192: readonly string[] = ["kind","value","type","loc"]; +const KEYS193: readonly string[] = ["kind","localId","type","loc"]; +const KEYS194: readonly string[] = ["kind","className","method","args","type","loc","targets"]; +const KEYS195: readonly string[] = ["kind","value","awaited","delegated","captureCompletion","type","loc"]; +const KEYS196: readonly string[] = ["name","type"]; +const KEYS197: readonly string[] = ["name","type"]; +const KEYS198: readonly string[] = ["path","size","sha256","placeholder"]; +const KEYS199: readonly string[] = ["path","target"]; +const KEYS200: readonly string[] = ["diff","skipPrototype"]; +const KEYS201: readonly string[] = ["returnType"]; +const KEYS202: readonly string[] = ["test","body"]; +const KEYS203: readonly string[] = ["field","globalId"]; +const KEYS204: readonly string[] = ["field","wellKnown"]; +const KEYS205: readonly string[] = ["tag","values"]; +const KEYS206: readonly string[] = ["context"]; +const KEYS207: readonly string[] = ["callback"]; +const KEYS208: readonly string[] = ["callback"]; +const KEYS209: readonly string[] = ["arg","what"]; +const KEYS210: readonly string[] = ["key","value"]; +const KEYS211: readonly string[] = ["key","value"]; +const KEYS212: readonly string[] = ["name","value"]; +const KEYS213: readonly string[] = ["name","value","overflow"]; +const KEYS214: readonly string[] = ["id","params","returns","lifetime","invoke"]; +const KEYS215: readonly string[] = ["release","params","returns"]; + +function encIrModule(w: IrWriter, v: IrModule): void { + w.keys(Object.keys(v), KEYS0); + w.num(v.irVersion); + w.str(v.sourceFile); + encT2(w, v.functions); + if (v.classes === undefined) w.byte(0); + else { + w.byte(1); + encT3(w, v.classes); + } + if (v.globals === undefined) w.byte(0); + else { + w.byte(1); + encT4(w, v.globals); + } + if (v.records === undefined) w.byte(0); + else { + w.byte(1); + encT5(w, v.records); + } + if (v.unions === undefined) w.byte(0); + else { + w.byte(1); + encT6(w, v.unions); + } + w.str(v.entry); + if (v.workers === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.workers); + } + if (v.declBoundary === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.declBoundary); + } + if (v.inferredFactFiles === undefined) w.byte(0); + else { + w.byte(1); + encT7(w, v.inferredFactFiles); + } + if (v.declOrigins === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.declOrigins); + } + if (v.lifetimeRegions === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.lifetimeRegions); + } + if (v.foreignElementKeys === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.foreignElementKeys); + } + if (v.threadSlotKeys === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.threadSlotKeys); + } + if (v.freezableArrays === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.freezableArrays); + } + if (v.sloppyDynWrites === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.sloppyDynWrites); + } + if (v.untypedIntegrity === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.untypedIntegrity); + } + if (v.commonJsEntry === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.commonJsEntry); + } + if (v.nodeIsland === undefined) w.byte(0); + else { + w.byte(1); + encT9(w, v.nodeIsland); + } + if (v.ffiImports === undefined) w.byte(0); + else { + w.byte(1); + encT10(w, v.ffiImports); + } + if (v.lib === undefined) w.byte(0); + else { + w.byte(1); + encIrLibSection(w, v.lib); + } + if (v.contracts === undefined) w.byte(0); + else { + w.byte(1); + encIrContracts(w, v.contracts); + } + if (v.int32ArrayCandidates === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.int32ArrayCandidates); + } + if (v.wasmModules === undefined) w.byte(0); + else { + w.byte(1); + encT13(w, v.wasmModules); + } + if (v.wasmUntranslated === undefined) w.byte(0); + else { + w.byte(1); + encT14(w, v.wasmUntranslated); + } + if (v.wasmCustomSections === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.wasmCustomSections); + } +} +function decIrModule(r: IrReader): IrModule { + const f0 = (r.num() as NonNullable); + const f1 = r.str(); + const f2 = decT2(r); + const f3 = r.tag(2, "IrModule.classes") === 0 ? undefined : decT3(r); + const f4 = r.tag(2, "IrModule.globals") === 0 ? undefined : decT4(r); + const f5 = r.tag(2, "IrModule.records") === 0 ? undefined : decT5(r); + const f6 = r.tag(2, "IrModule.unions") === 0 ? undefined : decT6(r); + const f7 = r.str(); + const f8 = r.tag(2, "IrModule.workers") === 0 ? undefined : r.bool(); + const f9 = r.tag(2, "IrModule.declBoundary") === 0 ? undefined : (r.bool() as NonNullable); + const f10 = r.tag(2, "IrModule.inferredFactFiles") === 0 ? undefined : decT7(r); + const f11 = r.tag(2, "IrModule.declOrigins") === 0 ? undefined : decT8(r); + const f12 = r.tag(2, "IrModule.lifetimeRegions") === 0 ? undefined : (r.bool() as NonNullable); + const f13 = r.tag(2, "IrModule.foreignElementKeys") === 0 ? undefined : decT8(r); + const f14 = r.tag(2, "IrModule.threadSlotKeys") === 0 ? undefined : decT8(r); + const f15 = r.tag(2, "IrModule.freezableArrays") === 0 ? undefined : decT8(r); + const f16 = r.tag(2, "IrModule.sloppyDynWrites") === 0 ? undefined : decT8(r); + const f17 = r.tag(2, "IrModule.untypedIntegrity") === 0 ? undefined : (r.bool() as NonNullable); + const f18 = r.tag(2, "IrModule.commonJsEntry") === 0 ? undefined : r.bool(); + const f19 = r.tag(2, "IrModule.nodeIsland") === 0 ? undefined : decT9(r); + const f20 = r.tag(2, "IrModule.ffiImports") === 0 ? undefined : decT10(r); + const f21 = r.tag(2, "IrModule.lib") === 0 ? undefined : decIrLibSection(r); + const f22 = r.tag(2, "IrModule.contracts") === 0 ? undefined : decIrContracts(r); + const f23 = r.tag(2, "IrModule.int32ArrayCandidates") === 0 ? undefined : decT8(r); + const f24 = r.tag(2, "IrModule.wasmModules") === 0 ? undefined : decT13(r); + const f25 = r.tag(2, "IrModule.wasmUntranslated") === 0 ? undefined : decT14(r); + const f26 = r.tag(2, "IrModule.wasmCustomSections") === 0 ? undefined : r.bool(); + const o: IrModule = { irVersion: f0, sourceFile: f1, functions: f2, ...(f3 !== undefined ? { classes: f3 } : {}), ...(f4 !== undefined ? { globals: f4 } : {}), ...(f5 !== undefined ? { records: f5 } : {}), ...(f6 !== undefined ? { unions: f6 } : {}), entry: f7, ...(f8 !== undefined ? { workers: f8 } : {}), ...(f9 !== undefined ? { declBoundary: f9 } : {}), ...(f10 !== undefined ? { inferredFactFiles: f10 } : {}), ...(f11 !== undefined ? { declOrigins: f11 } : {}), ...(f12 !== undefined ? { lifetimeRegions: f12 } : {}), ...(f13 !== undefined ? { foreignElementKeys: f13 } : {}), ...(f14 !== undefined ? { threadSlotKeys: f14 } : {}), ...(f15 !== undefined ? { freezableArrays: f15 } : {}), ...(f16 !== undefined ? { sloppyDynWrites: f16 } : {}), ...(f17 !== undefined ? { untypedIntegrity: f17 } : {}), ...(f18 !== undefined ? { commonJsEntry: f18 } : {}), ...(f19 !== undefined ? { nodeIsland: f19 } : {}), ...(f20 !== undefined ? { ffiImports: f20 } : {}), ...(f21 !== undefined ? { lib: f21 } : {}), ...(f22 !== undefined ? { contracts: f22 } : {}), ...(f23 !== undefined ? { int32ArrayCandidates: f23 } : {}), ...(f24 !== undefined ? { wasmModules: f24 } : {}), ...(f25 !== undefined ? { wasmUntranslated: f25 } : {}), ...(f26 !== undefined ? { wasmCustomSections: f26 } : {}) }; + return o; +} + +function encIrFunction(w: IrWriter, v: IrFunction): void { + w.keys(Object.keys(v), KEYS1); + w.str(v.name); + if (v.sourceName === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.sourceName); + } + if (v.ownsPrototype === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.ownsPrototype); + } + encT15(w, v.params); + encIrType(w, v.returnType); + encT17(w, v.locals); + if (v.captures === undefined) w.byte(0); + else { + w.byte(1); + encT15(w, v.captures); + } + if (v.classCaptures === undefined) w.byte(0); + else { + w.byte(1); + encT18(w, v.classCaptures); + } + encT19(w, v.body); + encSrcLoc(w, v.loc); + if (v.tier === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.tier); + } + if (v.speculativeDispatch === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.speculativeDispatch); + } + if (v.fastRead === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.fastRead); + } + if (v.async === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.async); + } + if (v.generator === undefined) w.byte(0); + else { + w.byte(1); + encT21(w, v.generator); + } + if (v.asyncCacheGlobal === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.asyncCacheGlobal); + } + if (v.asyncCycleCacheGlobal === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.asyncCycleCacheGlobal); + } +} +function decIrFunction(r: IrReader): IrFunction { + const f0 = r.str(); + const f1 = r.tag(2, "IrFunction.sourceName") === 0 ? undefined : r.str(); + const f2 = r.tag(2, "IrFunction.ownsPrototype") === 0 ? undefined : (r.bool() as NonNullable); + const f3 = decT15(r); + const f4 = decIrType(r); + const f5 = decT17(r); + const f6 = r.tag(2, "IrFunction.captures") === 0 ? undefined : decT15(r); + const f7 = r.tag(2, "IrFunction.classCaptures") === 0 ? undefined : decT18(r); + const f8 = decT19(r); + const f9 = decSrcLoc(r); + const f10 = r.tag(2, "IrFunction.tier") === 0 ? undefined : (r.str() as NonNullable); + const f11 = r.tag(2, "IrFunction.speculativeDispatch") === 0 ? undefined : r.bool(); + const f12 = r.tag(2, "IrFunction.fastRead") === 0 ? undefined : (r.str() as NonNullable); + const f13 = r.tag(2, "IrFunction.async") === 0 ? undefined : (r.bool() as NonNullable); + const f14 = r.tag(2, "IrFunction.generator") === 0 ? undefined : decT21(r); + const f15 = r.tag(2, "IrFunction.asyncCacheGlobal") === 0 ? undefined : r.str(); + const f16 = r.tag(2, "IrFunction.asyncCycleCacheGlobal") === 0 ? undefined : r.str(); + const o: IrFunction = { name: f0, ...(f1 !== undefined ? { sourceName: f1 } : {}), ...(f2 !== undefined ? { ownsPrototype: f2 } : {}), params: f3, returnType: f4, locals: f5, ...(f6 !== undefined ? { captures: f6 } : {}), ...(f7 !== undefined ? { classCaptures: f7 } : {}), body: f8, loc: f9, ...(f10 !== undefined ? { tier: f10 } : {}), ...(f11 !== undefined ? { speculativeDispatch: f11 } : {}), ...(f12 !== undefined ? { fastRead: f12 } : {}), ...(f13 !== undefined ? { async: f13 } : {}), ...(f14 !== undefined ? { generator: f14 } : {}), ...(f15 !== undefined ? { asyncCacheGlobal: f15 } : {}), ...(f16 !== undefined ? { asyncCycleCacheGlobal: f16 } : {}) }; + return o; +} + +function encT2(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrFunction(w, item); + } +} +function decT2(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrFunction(r)); + return out; +} + +function encT3(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrClassDef(w, item); + } +} +function decT3(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrClassDef(r)); + return out; +} + +function encT4(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrGlobal(w, item); + } +} +function decT4(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrGlobal(r)); + return out; +} + +function encT5(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrRecordShape(w, item); + } +} +function decT5(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrRecordShape(r)); + return out; +} + +function encT6(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrUnionDef(w, item); + } +} +function decT6(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrUnionDef(r)); + return out; +} + +function encT7(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encT26(w, item); + } +} +function decT7(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decT26(r)); + return out; +} + +function encT8(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + w.str(item); + } +} +function decT8(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(r.str()); + return out; +} + +function encT9(w: IrWriter, v: NonNullable): void { + w.keys(Object.keys(v), KEYS2); + w.str(v.libnode); + w.str(v.nodeVersion); + if (v.modules === undefined) w.byte(0); + else { + w.byte(1); + encIrNodeIslandModules(w, v.modules); + } +} +function decT9(r: IrReader): NonNullable { + const f0 = r.str(); + const f1 = r.str(); + const f2 = r.tag(2, "IrModule.nodeIsland.modules") === 0 ? undefined : decIrNodeIslandModules(r); + const o: NonNullable = { libnode: f0, nodeVersion: f1, ...(f2 !== undefined ? { modules: f2 } : {}) }; + return o; +} + +function encT10(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrFfiImport(w, item); + } +} +function decT10(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrFfiImport(r)); + return out; +} + +function encIrLibSection(w: IrWriter, v: IrLibSection): void { + w.keys(Object.keys(v), KEYS3); + w.str(v.profileName); + w.str(v.prefix); + w.str(v.initSymbol); + w.str(v.sinkRegisterSymbol); + if (v.collectSymbol === null) w.byte(0); + else { + w.byte(1); + w.str(v.collectSymbol); + } + if (v.resultResetSymbol === null) w.byte(0); + else { + w.byte(1); + w.str(v.resultResetSymbol); + } + w.bool(v.threadInstances); + if (v.callbackRegisterSymbol === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.callbackRegisterSymbol); + } + if (v.callbacks === undefined) w.byte(0); + else { + w.byte(1); + encT29(w, v.callbacks); + } + encT30(w, v.exports); + encT31(w, v.trapOverlays); + if (v.identity === undefined) w.byte(0); + else { + w.byte(1); + encIrLibIdentity(w, v.identity); + } +} +function decIrLibSection(r: IrReader): IrLibSection { + const f0 = r.str(); + const f1 = r.str(); + const f2 = r.str(); + const f3 = r.str(); + const f4 = r.tag(2, "IrModule.lib.collectSymbol") === 0 ? null : r.str(); + const f5 = r.tag(2, "IrModule.lib.resultResetSymbol") === 0 ? null : r.str(); + const f6 = r.bool(); + const f7 = r.tag(2, "IrModule.lib.callbackRegisterSymbol") === 0 ? undefined : r.str(); + const f8 = r.tag(2, "IrModule.lib.callbacks") === 0 ? undefined : decT29(r); + const f9 = decT30(r); + const f10 = decT31(r); + const f11 = r.tag(2, "IrModule.lib.identity") === 0 ? undefined : decIrLibIdentity(r); + const o: IrLibSection = { profileName: f0, prefix: f1, initSymbol: f2, sinkRegisterSymbol: f3, collectSymbol: f4, resultResetSymbol: f5, threadInstances: f6, ...(f7 !== undefined ? { callbackRegisterSymbol: f7 } : {}), ...(f8 !== undefined ? { callbacks: f8 } : {}), exports: f9, trapOverlays: f10, ...(f11 !== undefined ? { identity: f11 } : {}) }; + return o; +} + +function encIrContracts(w: IrWriter, v: IrContracts): void { + w.keys(Object.keys(v), KEYS4); + if (v.int32Fields === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.int32Fields); + } + if (v.int32Params === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.int32Params); + } + if (v.int32Returns === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.int32Returns); + } + if (v.int32Locals === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.int32Locals); + } + if (v.int32Elements === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.int32Elements); + } + if (v.noThrow === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.noThrow); + } + if (v.acyclic === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.acyclic); + } + if (v.immortalGlobals === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.immortalGlobals); + } + if (v.programLifetime === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.programLifetime); + } + if (v.inlineArrays === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.inlineArrays); + } +} +function decIrContracts(r: IrReader): IrContracts { + const f0 = r.tag(2, "IrModule.contracts.int32Fields") === 0 ? undefined : decT8(r); + const f1 = r.tag(2, "IrModule.contracts.int32Params") === 0 ? undefined : decT8(r); + const f2 = r.tag(2, "IrModule.contracts.int32Returns") === 0 ? undefined : decT8(r); + const f3 = r.tag(2, "IrModule.contracts.int32Locals") === 0 ? undefined : decT8(r); + const f4 = r.tag(2, "IrModule.contracts.int32Elements") === 0 ? undefined : decT8(r); + const f5 = r.tag(2, "IrModule.contracts.noThrow") === 0 ? undefined : decT8(r); + const f6 = r.tag(2, "IrModule.contracts.acyclic") === 0 ? undefined : decT8(r); + const f7 = r.tag(2, "IrModule.contracts.immortalGlobals") === 0 ? undefined : decT8(r); + const f8 = r.tag(2, "IrModule.contracts.programLifetime") === 0 ? undefined : decT8(r); + const f9 = r.tag(2, "IrModule.contracts.inlineArrays") === 0 ? undefined : decT8(r); + const o: IrContracts = { ...(f0 !== undefined ? { int32Fields: f0 } : {}), ...(f1 !== undefined ? { int32Params: f1 } : {}), ...(f2 !== undefined ? { int32Returns: f2 } : {}), ...(f3 !== undefined ? { int32Locals: f3 } : {}), ...(f4 !== undefined ? { int32Elements: f4 } : {}), ...(f5 !== undefined ? { noThrow: f5 } : {}), ...(f6 !== undefined ? { acyclic: f6 } : {}), ...(f7 !== undefined ? { immortalGlobals: f7 } : {}), ...(f8 !== undefined ? { programLifetime: f8 } : {}), ...(f9 !== undefined ? { inlineArrays: f9 } : {}) }; + return o; +} + +function encT13(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrWasmModule(w, item); + } +} +function decT13(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrWasmModule(r)); + return out; +} + +function encT14(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrWasmUntranslated(w, item); + } +} +function decT14(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrWasmUntranslated(r)); + return out; +} + +function encT15(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrParam(w, item); + } +} +function decT15(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrParam(r)); + return out; +} + +function encIrType(w: IrWriter, v: IrType): void { + switch (v.kind) { + case "array": { + w.length(0); + w.keys(Object.keys(v), KEYS5); + encIrType(w, v.elem); + if (v.optionalElements === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.optionalElements); + } + return; + } + case "bigint": { + w.length(1); + w.keys(Object.keys(v), KEYS6); + return; + } + case "bool": { + w.length(2); + w.keys(Object.keys(v), KEYS7); + return; + } + case "bytes": { + w.length(3); + w.keys(Object.keys(v), KEYS8); + w.str(v.elem); + return; + } + case "caught": { + w.length(4); + w.keys(Object.keys(v), KEYS9); + return; + } + case "child": { + w.length(5); + w.keys(Object.keys(v), KEYS10); + return; + } + case "childStream": { + w.length(6); + w.keys(Object.keys(v), KEYS11); + return; + } + case "childWriter": { + w.length(7); + w.keys(Object.keys(v), KEYS12); + return; + } + case "classval": { + w.length(8); + w.keys(Object.keys(v), KEYS13); + w.str(v.className); + return; + } + case "cryptoHash": { + w.length(9); + w.keys(Object.keys(v), KEYS14); + return; + } + case "cryptoHmac": { + w.length(10); + w.keys(Object.keys(v), KEYS15); + return; + } + case "date": { + w.length(11); + w.keys(Object.keys(v), KEYS16); + return; + } + case "dgramSocket": { + w.length(12); + w.keys(Object.keys(v), KEYS17); + return; + } + case "dir": { + w.length(13); + w.keys(Object.keys(v), KEYS18); + return; + } + case "dyn": { + w.length(14); + w.keys(Object.keys(v), KEYS19); + return; + } + case "f64": { + w.length(15); + w.keys(Object.keys(v), KEYS20); + return; + } + case "fileHandle": { + w.length(16); + w.keys(Object.keys(v), KEYS21); + return; + } + case "fsWatcher": { + w.length(17); + w.keys(Object.keys(v), KEYS22); + return; + } + case "func": { + w.length(18); + w.keys(Object.keys(v), KEYS23); + encT36(w, v.params); + encIrType(w, v.ret); + if (v.rest === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.rest); + } + if (v.restAbi === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.restAbi); + } + if (v.argumentsAll === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.argumentsAll); + } + return; + } + case "generator": { + w.length(19); + w.keys(Object.keys(v), KEYS24); + if (v.async === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.async); + } + encIrType(w, v.yieldT); + encIrType(w, v.retT); + encIrType(w, v.nextT); + return; + } + case "http2Session": { + w.length(20); + w.keys(Object.keys(v), KEYS25); + return; + } + case "http2Stream": { + w.length(21); + w.keys(Object.keys(v), KEYS26); + return; + } + case "httpClientReq": { + w.length(22); + w.keys(Object.keys(v), KEYS27); + return; + } + case "httpReq": { + w.length(23); + w.keys(Object.keys(v), KEYS28); + return; + } + case "httpRes": { + w.length(24); + w.keys(Object.keys(v), KEYS29); + return; + } + case "jsval": { + w.length(25); + w.keys(Object.keys(v), KEYS30); + return; + } + case "map": { + w.length(26); + w.keys(Object.keys(v), KEYS31); + encIrType(w, v.key); + encIrType(w, v.value); + return; + } + case "moduleNs": { + w.length(27); + w.keys(Object.keys(v), KEYS32); + w.str(v.moduleId); + return; + } + case "netServer": { + w.length(28); + w.keys(Object.keys(v), KEYS33); + return; + } + case "netSocket": { + w.length(29); + w.keys(Object.keys(v), KEYS34); + return; + } + case "nullT": { + w.length(30); + w.keys(Object.keys(v), KEYS35); + return; + } + case "object": { + w.length(31); + w.keys(Object.keys(v), KEYS36); + w.str(v.className); + return; + } + case "procStream": { + w.length(32); + w.keys(Object.keys(v), KEYS37); + return; + } + case "promise": { + w.length(33); + w.keys(Object.keys(v), KEYS38); + encIrType(w, v.inner); + return; + } + case "record": { + w.length(34); + w.keys(Object.keys(v), KEYS39); + w.str(v.shapeId); + return; + } + case "regex": { + w.length(35); + w.keys(Object.keys(v), KEYS40); + return; + } + case "searchParams": { + w.length(36); + w.keys(Object.keys(v), KEYS41); + return; + } + case "secureCtx": { + w.length(37); + w.keys(Object.keys(v), KEYS42); + return; + } + case "set": { + w.length(38); + w.keys(Object.keys(v), KEYS43); + encIrType(w, v.elem); + return; + } + case "spawnRes": { + w.length(39); + w.keys(Object.keys(v), KEYS44); + return; + } + case "stats": { + w.length(40); + w.keys(Object.keys(v), KEYS45); + return; + } + case "string": { + w.length(41); + w.keys(Object.keys(v), KEYS46); + return; + } + case "symbol": { + w.length(42); + w.keys(Object.keys(v), KEYS47); + return; + } + case "testCtx": { + w.length(43); + w.keys(Object.keys(v), KEYS48); + return; + } + case "undefinedT": { + w.length(44); + w.keys(Object.keys(v), KEYS49); + return; + } + case "union": { + w.length(45); + w.keys(Object.keys(v), KEYS50); + w.str(v.unionId); + return; + } + case "url": { + w.length(46); + w.keys(Object.keys(v), KEYS51); + return; + } + case "void": { + w.length(47); + w.keys(Object.keys(v), KEYS52); + return; + } + } + w.refuse("IrType kind " + String((v as { kind: unknown }).kind)); +} +function decIrType(r: IrReader): IrType { + switch (r.length()) { + case 0: { + const f0 = decIrType(r); + const f1 = r.tag(2, "IrFunction.returnType.optionalElements") === 0 ? undefined : (r.bool() as NonNullable["optionalElements"]>); + const o: Extract = { kind: "array", elem: f0, ...(f1 !== undefined ? { optionalElements: f1 } : {}) }; + return o; + } + case 1: { + const o: Extract = { kind: "bigint" }; + return o; + } + case 2: { + const o: Extract = { kind: "bool" }; + return o; + } + case 3: { + const f0 = (r.str() as NonNullable["elem"]>); + const o: Extract = { kind: "bytes", elem: f0 }; + return o; + } + case 4: { + const o: Extract = { kind: "caught" }; + return o; + } + case 5: { + const o: Extract = { kind: "child" }; + return o; + } + case 6: { + const o: Extract = { kind: "childStream" }; + return o; + } + case 7: { + const o: Extract = { kind: "childWriter" }; + return o; + } + case 8: { + const f0 = r.str(); + const o: Extract = { kind: "classval", className: f0 }; + return o; + } + case 9: { + const o: Extract = { kind: "cryptoHash" }; + return o; + } + case 10: { + const o: Extract = { kind: "cryptoHmac" }; + return o; + } + case 11: { + const o: Extract = { kind: "date" }; + return o; + } + case 12: { + const o: Extract = { kind: "dgramSocket" }; + return o; + } + case 13: { + const o: Extract = { kind: "dir" }; + return o; + } + case 14: { + const o: Extract = { kind: "dyn" }; + return o; + } + case 15: { + const o: Extract = { kind: "f64" }; + return o; + } + case 16: { + const o: Extract = { kind: "fileHandle" }; + return o; + } + case 17: { + const o: Extract = { kind: "fsWatcher" }; + return o; + } + case 18: { + const f0 = decT36(r); + const f1 = decIrType(r); + const f2 = r.tag(2, "IrFunction.returnType.rest") === 0 ? undefined : (r.bool() as NonNullable["rest"]>); + const f3 = r.tag(2, "IrFunction.returnType.restAbi") === 0 ? undefined : (r.str() as NonNullable["restAbi"]>); + const f4 = r.tag(2, "IrFunction.returnType.argumentsAll") === 0 ? undefined : (r.bool() as NonNullable["argumentsAll"]>); + const o: Extract = { kind: "func", params: f0, ret: f1, ...(f2 !== undefined ? { rest: f2 } : {}), ...(f3 !== undefined ? { restAbi: f3 } : {}), ...(f4 !== undefined ? { argumentsAll: f4 } : {}) }; + return o; + } + case 19: { + const f0 = r.tag(2, "IrFunction.returnType.async") === 0 ? undefined : (r.bool() as NonNullable["async"]>); + const f1 = decIrType(r); + const f2 = decIrType(r); + const f3 = decIrType(r); + const o: Extract = { kind: "generator", ...(f0 !== undefined ? { async: f0 } : {}), yieldT: f1, retT: f2, nextT: f3 }; + return o; + } + case 20: { + const o: Extract = { kind: "http2Session" }; + return o; + } + case 21: { + const o: Extract = { kind: "http2Stream" }; + return o; + } + case 22: { + const o: Extract = { kind: "httpClientReq" }; + return o; + } + case 23: { + const o: Extract = { kind: "httpReq" }; + return o; + } + case 24: { + const o: Extract = { kind: "httpRes" }; + return o; + } + case 25: { + const o: Extract = { kind: "jsval" }; + return o; + } + case 26: { + const f0 = decIrType(r); + const f1 = decIrType(r); + const o: Extract = { kind: "map", key: f0, value: f1 }; + return o; + } + case 27: { + const f0 = r.str(); + const o: Extract = { kind: "moduleNs", moduleId: f0 }; + return o; + } + case 28: { + const o: Extract = { kind: "netServer" }; + return o; + } + case 29: { + const o: Extract = { kind: "netSocket" }; + return o; + } + case 30: { + const o: Extract = { kind: "nullT" }; + return o; + } + case 31: { + const f0 = r.str(); + const o: Extract = { kind: "object", className: f0 }; + return o; + } + case 32: { + const o: Extract = { kind: "procStream" }; + return o; + } + case 33: { + const f0 = decIrType(r); + const o: Extract = { kind: "promise", inner: f0 }; + return o; + } + case 34: { + const f0 = r.str(); + const o: Extract = { kind: "record", shapeId: f0 }; + return o; + } + case 35: { + const o: Extract = { kind: "regex" }; + return o; + } + case 36: { + const o: Extract = { kind: "searchParams" }; + return o; + } + case 37: { + const o: Extract = { kind: "secureCtx" }; + return o; + } + case 38: { + const f0 = decIrType(r); + const o: Extract = { kind: "set", elem: f0 }; + return o; + } + case 39: { + const o: Extract = { kind: "spawnRes" }; + return o; + } + case 40: { + const o: Extract = { kind: "stats" }; + return o; + } + case 41: { + const o: Extract = { kind: "string" }; + return o; + } + case 42: { + const o: Extract = { kind: "symbol" }; + return o; + } + case 43: { + const o: Extract = { kind: "testCtx" }; + return o; + } + case 44: { + const o: Extract = { kind: "undefinedT" }; + return o; + } + case 45: { + const f0 = r.str(); + const o: Extract = { kind: "union", unionId: f0 }; + return o; + } + case 46: { + const o: Extract = { kind: "url" }; + return o; + } + case 47: { + const o: Extract = { kind: "void" }; + return o; + } + } + throw new Error("IR codec: invalid IrType kind"); +} + +function encT17(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrLocal(w, item); + } +} +function decT17(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrLocal(r)); + return out; +} + +function encT18(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encT38(w, item); + } +} +function decT18(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decT38(r)); + return out; +} + +function encT19(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrStmt(w, item); + } +} +function decT19(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrStmt(r)); + return out; +} + +function encSrcLoc(w: IrWriter, v: SrcLoc): void { + w.keys(Object.keys(v), KEYS53); + w.file(v.file); + w.num(v.start); + w.num(v.end); +} +function decSrcLoc(r: IrReader): SrcLoc { + const f0 = r.str(); + const f1 = r.num(); + const f2 = r.num(); + const o: SrcLoc = { file: f0, start: f1, end: f2 }; + return o; +} + +function encT21(w: IrWriter, v: NonNullable): void { + w.keys(Object.keys(v), KEYS54); + encIrType(w, v.yieldT); + encIrType(w, v.nextT); + encT40(w, v.resultType); + if (v.closesSource === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.closesSource); + } + if (v.tag === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.tag); + } + if (v.inheritsTag === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.inheritsTag); + } +} +function decT21(r: IrReader): NonNullable { + const f0 = decIrType(r); + const f1 = decIrType(r); + const f2 = decT40(r); + const f3 = r.tag(2, "IrFunction.generator.closesSource") === 0 ? undefined : (r.bool() as NonNullable["closesSource"]>); + const f4 = r.tag(2, "IrFunction.generator.tag") === 0 ? undefined : r.str(); + const f5 = r.tag(2, "IrFunction.generator.inheritsTag") === 0 ? undefined : (r.bool() as NonNullable["inheritsTag"]>); + const o: NonNullable = { yieldT: f0, nextT: f1, resultType: f2, ...(f3 !== undefined ? { closesSource: f3 } : {}), ...(f4 !== undefined ? { tag: f4 } : {}), ...(f5 !== undefined ? { inheritsTag: f5 } : {}) }; + return o; +} + +function encIrClassDef(w: IrWriter, v: IrClassDef): void { + w.keys(Object.keys(v), KEYS55); + w.str(v.name); + if (v.localCaptures === undefined) w.byte(0); + else { + w.byte(1); + encT15(w, v.localCaptures); + } + if (v.localBaseCapture === undefined) w.byte(0); + else { + w.byte(1); + w.num(v.localBaseCapture); + } + if (v.baseValueGlobal === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.baseValueGlobal); + } + if (v.jsName === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.jsName); + } + if (v.jsLength === undefined) w.byte(0); + else { + w.byte(1); + w.num(v.jsLength); + } + if (v.ownStaticFields === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.ownStaticFields); + } + if (v.prototypeDataHelper === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.prototypeDataHelper); + } + if (v.instancePrototypeHelper === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.instancePrototypeHelper); + } + if (v.stringTagHelper === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.stringTagHelper); + } + if (v.tracksOwnFields === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.tracksOwnFields); + } + if (v.errorNameAfter === undefined) w.byte(0); + else if (v.errorNameAfter === null) w.byte(1); + else { + w.byte(2); + w.str(v.errorNameAfter); + } + if (v.errorBagOrder === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.errorBagOrder); + } + if (v.symbolFields === undefined) w.byte(0); + else { + w.byte(1); + encT41(w, v.symbolFields); + } + if (v.runtime === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.runtime); + } + if (v.arrayElem === undefined) w.byte(0); + else { + w.byte(1); + encIrType(w, v.arrayElem); + } + if (v.arrayFields === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.arrayFields); + } + if (v.arraySpecies === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.arraySpecies); + } + if (v.base === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.base); + } + encT42(w, v.fields); + if (v.methods === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.methods); + } + if (v.prototypeNames === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.prototypeNames); + } + if (v.abstract === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.abstract); + } + if (v.abstractMethods === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.abstractMethods); + } + if (v.genericOf === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.genericOf); + } + encSrcLoc(w, v.loc); +} +function decIrClassDef(r: IrReader): IrClassDef { + const f0 = r.str(); + const f1 = r.tag(2, "IrModule.classes[].localCaptures") === 0 ? undefined : decT15(r); + const f2 = r.tag(2, "IrModule.classes[].localBaseCapture") === 0 ? undefined : r.num(); + const f3 = r.tag(2, "IrModule.classes[].baseValueGlobal") === 0 ? undefined : r.str(); + const f4 = r.tag(2, "IrModule.classes[].jsName") === 0 ? undefined : r.str(); + const f5 = r.tag(2, "IrModule.classes[].jsLength") === 0 ? undefined : r.num(); + const f6 = r.tag(2, "IrModule.classes[].ownStaticFields") === 0 ? undefined : (r.bool() as NonNullable); + const f7 = r.tag(2, "IrModule.classes[].prototypeDataHelper") === 0 ? undefined : r.str(); + const f8 = r.tag(2, "IrModule.classes[].instancePrototypeHelper") === 0 ? undefined : r.str(); + const f9 = r.tag(2, "IrModule.classes[].stringTagHelper") === 0 ? undefined : r.str(); + const f10 = r.tag(2, "IrModule.classes[].tracksOwnFields") === 0 ? undefined : r.bool(); + const f11 = r.tag(3, "IrModule.classes[].errorNameAfter") === 0 ? undefined : r.lastTag === 1 ? null : r.str(); + const f12 = r.tag(2, "IrModule.classes[].errorBagOrder") === 0 ? undefined : r.bool(); + const f13 = r.tag(2, "IrModule.classes[].symbolFields") === 0 ? undefined : decT41(r); + const f14 = r.tag(2, "IrModule.classes[].runtime") === 0 ? undefined : (r.bool() as NonNullable); + const f15 = r.tag(2, "IrModule.classes[].arrayElem") === 0 ? undefined : decIrType(r); + const f16 = r.tag(2, "IrModule.classes[].arrayFields") === 0 ? undefined : r.str(); + const f17 = r.tag(2, "IrModule.classes[].arraySpecies") === 0 ? undefined : r.str(); + const f18 = r.tag(2, "IrModule.classes[].base") === 0 ? undefined : r.str(); + const f19 = decT42(r); + const f20 = r.tag(2, "IrModule.classes[].methods") === 0 ? undefined : decT8(r); + const f21 = r.tag(2, "IrModule.classes[].prototypeNames") === 0 ? undefined : decT8(r); + const f22 = r.tag(2, "IrModule.classes[].abstract") === 0 ? undefined : (r.bool() as NonNullable); + const f23 = r.tag(2, "IrModule.classes[].abstractMethods") === 0 ? undefined : decT8(r); + const f24 = r.tag(2, "IrModule.classes[].genericOf") === 0 ? undefined : r.str(); + const f25 = decSrcLoc(r); + const o: IrClassDef = { name: f0, ...(f1 !== undefined ? { localCaptures: f1 } : {}), ...(f2 !== undefined ? { localBaseCapture: f2 } : {}), ...(f3 !== undefined ? { baseValueGlobal: f3 } : {}), ...(f4 !== undefined ? { jsName: f4 } : {}), ...(f5 !== undefined ? { jsLength: f5 } : {}), ...(f6 !== undefined ? { ownStaticFields: f6 } : {}), ...(f7 !== undefined ? { prototypeDataHelper: f7 } : {}), ...(f8 !== undefined ? { instancePrototypeHelper: f8 } : {}), ...(f9 !== undefined ? { stringTagHelper: f9 } : {}), ...(f10 !== undefined ? { tracksOwnFields: f10 } : {}), ...(f11 !== undefined ? { errorNameAfter: f11 } : {}), ...(f12 !== undefined ? { errorBagOrder: f12 } : {}), ...(f13 !== undefined ? { symbolFields: f13 } : {}), ...(f14 !== undefined ? { runtime: f14 } : {}), ...(f15 !== undefined ? { arrayElem: f15 } : {}), ...(f16 !== undefined ? { arrayFields: f16 } : {}), ...(f17 !== undefined ? { arraySpecies: f17 } : {}), ...(f18 !== undefined ? { base: f18 } : {}), fields: f19, ...(f20 !== undefined ? { methods: f20 } : {}), ...(f21 !== undefined ? { prototypeNames: f21 } : {}), ...(f22 !== undefined ? { abstract: f22 } : {}), ...(f23 !== undefined ? { abstractMethods: f23 } : {}), ...(f24 !== undefined ? { genericOf: f24 } : {}), loc: f25 }; + return o; +} + +function encIrGlobal(w: IrWriter, v: IrGlobal): void { + w.keys(Object.keys(v), KEYS56); + w.str(v.id); + w.str(v.name); + encIrType(w, v.type); + w.bool(v.mutable); + if (v.tdz === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.tdz); + } + if (v.initFlag === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.initFlag); + } + if (v.source === undefined) w.byte(0); + else { + w.byte(1); + encIrBindingSource(w, v.source); + } +} +function decIrGlobal(r: IrReader): IrGlobal { + const f0 = r.str(); + const f1 = r.str(); + const f2 = decIrType(r); + const f3 = r.bool(); + const f4 = r.tag(2, "IrModule.globals[].tdz") === 0 ? undefined : (r.bool() as NonNullable); + const f5 = r.tag(2, "IrModule.globals[].initFlag") === 0 ? undefined : r.str(); + const f6 = r.tag(2, "IrModule.globals[].source") === 0 ? undefined : decIrBindingSource(r); + const o: IrGlobal = { id: f0, name: f1, type: f2, mutable: f3, ...(f4 !== undefined ? { tdz: f4 } : {}), ...(f5 !== undefined ? { initFlag: f5 } : {}), ...(f6 !== undefined ? { source: f6 } : {}) }; + return o; +} + +function encIrRecordShape(w: IrWriter, v: IrRecordShape): void { + w.keys(Object.keys(v), KEYS57); + w.str(v.id); + encT44(w, v.fields); + if (v.tuple === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.tuple); + } + if (v.indexValue === undefined) w.byte(0); + else { + w.byte(1); + encIrType(w, v.indexValue); + } + if (v.freezable === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.freezable); + } + if (v.declaredOrder === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.declaredOrder); + } + if (v.accessorOrder === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.accessorOrder); + } + if (v.orderVariants === undefined) w.byte(0); + else { + w.byte(1); + encT45(w, v.orderVariants); + } +} +function decIrRecordShape(r: IrReader): IrRecordShape { + const f0 = r.str(); + const f1 = decT44(r); + const f2 = r.tag(2, "IrModule.records[].tuple") === 0 ? undefined : (r.bool() as NonNullable); + const f3 = r.tag(2, "IrModule.records[].indexValue") === 0 ? undefined : decIrType(r); + const f4 = r.tag(2, "IrModule.records[].freezable") === 0 ? undefined : (r.bool() as NonNullable); + const f5 = r.tag(2, "IrModule.records[].declaredOrder") === 0 ? undefined : decT8(r); + const f6 = r.tag(2, "IrModule.records[].accessorOrder") === 0 ? undefined : decT8(r); + const f7 = r.tag(2, "IrModule.records[].orderVariants") === 0 ? undefined : decT45(r); + const o: IrRecordShape = { id: f0, fields: f1, ...(f2 !== undefined ? { tuple: f2 } : {}), ...(f3 !== undefined ? { indexValue: f3 } : {}), ...(f4 !== undefined ? { freezable: f4 } : {}), ...(f5 !== undefined ? { declaredOrder: f5 } : {}), ...(f6 !== undefined ? { accessorOrder: f6 } : {}), ...(f7 !== undefined ? { orderVariants: f7 } : {}) }; + return o; +} + +function encIrUnionDef(w: IrWriter, v: IrUnionDef): void { + w.keys(Object.keys(v), KEYS58); + w.str(v.id); + encT36(w, v.arms); + if (v.discriminant === undefined) w.byte(0); + else { + w.byte(1); + encIrUnionDiscriminant(w, v.discriminant); + } +} +function decIrUnionDef(r: IrReader): IrUnionDef { + const f0 = r.str(); + const f1 = decT36(r); + const f2 = r.tag(2, "IrModule.unions[].discriminant") === 0 ? undefined : decIrUnionDiscriminant(r); + const o: IrUnionDef = { id: f0, arms: f1, ...(f2 !== undefined ? { discriminant: f2 } : {}) }; + return o; +} + +function encT26(w: IrWriter, v: NonNullable[number]>): void { + w.keys(Object.keys(v), KEYS59); + w.file(v.file); + w.str(v.pkg); +} +function decT26(r: IrReader): NonNullable[number]> { + const f0 = r.str(); + const f1 = r.str(); + const o: NonNullable[number]> = { file: f0, pkg: f1 }; + return o; +} + +function encIrNodeIslandModules(w: IrWriter, v: IrNodeIslandModules): void { + w.keys(Object.keys(v), KEYS60); + w.str(v.root); + encT47(w, v.files); + encT48(w, v.links); + if (v.location === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.location); + } +} +function decIrNodeIslandModules(r: IrReader): IrNodeIslandModules { + const f0 = r.str(); + const f1 = decT47(r); + const f2 = decT48(r); + const f3 = r.tag(2, "IrModule.nodeIsland.modules.location") === 0 ? undefined : r.str(); + const o: IrNodeIslandModules = { root: f0, files: f1, links: f2, ...(f3 !== undefined ? { location: f3 } : {}) }; + return o; +} + +function encIrFfiImport(w: IrWriter, v: IrFfiImport): void { + w.keys(Object.keys(v), KEYS61); + w.str(v.name); + w.str(v.symbol); + if (v.library === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.library); + } + if (v.callbackOperation === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.callbackOperation); + } + if (v.callbackTarget === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.callbackTarget); + } + encT49(w, v.params); + w.str(v.returns); +} +function decIrFfiImport(r: IrReader): IrFfiImport { + const f0 = r.str(); + const f1 = r.str(); + const f2 = r.tag(2, "IrModule.ffiImports[].library") === 0 ? undefined : r.str(); + const f3 = r.tag(2, "IrModule.ffiImports[].callbackOperation") === 0 ? undefined : (r.str() as NonNullable); + const f4 = r.tag(2, "IrModule.ffiImports[].callbackTarget") === 0 ? undefined : r.str(); + const f5 = decT49(r); + const f6 = (r.str() as NonNullable); + const o: IrFfiImport = { name: f0, symbol: f1, ...(f2 !== undefined ? { library: f2 } : {}), ...(f3 !== undefined ? { callbackOperation: f3 } : {}), ...(f4 !== undefined ? { callbackTarget: f4 } : {}), params: f5, returns: f6 }; + return o; +} + +function encT29(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrLibCallback(w, item); + } +} +function decT29(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrLibCallback(r)); + return out; +} + +function encT30(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrLibExport(w, item); + } +} +function decT30(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrLibExport(r)); + return out; +} + +function encT31(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrLibTrapOverlay(w, item); + } +} +function decT31(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrLibTrapOverlay(r)); + return out; +} + +function encIrLibIdentity(w: IrWriter, v: IrLibIdentity): void { + w.keys(Object.keys(v), KEYS62); + w.str(v.buildIdSymbol); + w.str(v.abiVersionSymbol); + w.str(v.buildId); + w.num(v.abiVersion); +} +function decIrLibIdentity(r: IrReader): IrLibIdentity { + const f0 = r.str(); + const f1 = r.str(); + const f2 = r.str(); + const f3 = r.num(); + const o: IrLibIdentity = { buildIdSymbol: f0, abiVersionSymbol: f1, buildId: f2, abiVersion: f3 }; + return o; +} + +function encIrWasmModule(w: IrWriter, v: IrWasmModule): void { + w.keys(Object.keys(v), KEYS63); + w.str(v.sha256); + w.str(v.bytes); +} +function decIrWasmModule(r: IrReader): IrWasmModule { + const f0 = r.str(); + const f1 = r.str(); + const o: IrWasmModule = { sha256: f0, bytes: f1 }; + return o; +} + +function encIrWasmUntranslated(w: IrWriter, v: IrWasmUntranslated): void { + w.keys(Object.keys(v), KEYS64); + w.str(v.sha256); + w.num(v.size); + w.str(v.reason); +} +function decIrWasmUntranslated(r: IrReader): IrWasmUntranslated { + const f0 = r.str(); + const f1 = r.num(); + const f2 = r.str(); + const o: IrWasmUntranslated = { sha256: f0, size: f1, reason: f2 }; + return o; +} + +function encIrParam(w: IrWriter, v: IrParam): void { + w.keys(Object.keys(v), KEYS65); + w.str(v.localId); + w.str(v.name); + encIrType(w, v.type); + if (v.owned === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.owned); + } +} +function decIrParam(r: IrReader): IrParam { + const f0 = r.str(); + const f1 = r.str(); + const f2 = decIrType(r); + const f3 = r.tag(2, "IrFunction.params[].owned") === 0 ? undefined : (r.bool() as NonNullable); + const o: IrParam = { localId: f0, name: f1, type: f2, ...(f3 !== undefined ? { owned: f3 } : {}) }; + return o; +} + +function encT36(w: IrWriter, v: NonNullable["params"]>): void { + w.length(v.length); + for (const item of v) { + encIrType(w, item); + } +} +function decT36(r: IrReader): NonNullable["params"]> { + const length = r.length(); + const out: NonNullable["params"]>[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrType(r)); + return out; +} + +function encIrLocal(w: IrWriter, v: IrLocal): void { + w.keys(Object.keys(v), KEYS66); + w.str(v.id); + w.str(v.name); + encIrType(w, v.type); + w.bool(v.mutable); + if (v.source === undefined) w.byte(0); + else { + w.byte(1); + encIrBindingSource(w, v.source); + } + if (v.boxed === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.boxed); + } + if (v.tdz === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.tdz); + } +} +function decIrLocal(r: IrReader): IrLocal { + const f0 = r.str(); + const f1 = r.str(); + const f2 = decIrType(r); + const f3 = r.bool(); + const f4 = r.tag(2, "IrFunction.locals[].source") === 0 ? undefined : decIrBindingSource(r); + const f5 = r.tag(2, "IrFunction.locals[].boxed") === 0 ? undefined : (r.bool() as NonNullable); + const f6 = r.tag(2, "IrFunction.locals[].tdz") === 0 ? undefined : (r.bool() as NonNullable); + const o: IrLocal = { id: f0, name: f1, type: f2, mutable: f3, ...(f4 !== undefined ? { source: f4 } : {}), ...(f5 !== undefined ? { boxed: f5 } : {}), ...(f6 !== undefined ? { tdz: f6 } : {}) }; + return o; +} + +function encT38(w: IrWriter, v: NonNullable[number]>): void { + w.keys(Object.keys(v), KEYS67); + w.str(v.localId); + w.str(v.name); + encIrType(w, v.type); + if (v.owned === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.owned); + } + w.num(v.slot); +} +function decT38(r: IrReader): NonNullable[number]> { + const f0 = r.str(); + const f1 = r.str(); + const f2 = decIrType(r); + const f3 = r.tag(2, "IrFunction.classCaptures[].owned") === 0 ? undefined : (r.bool() as NonNullable[number]>["owned"]>); + const f4 = r.num(); + const o: NonNullable[number]> = { localId: f0, name: f1, type: f2, ...(f3 !== undefined ? { owned: f3 } : {}), slot: f4 }; + return o; +} + +function encIrStmt(w: IrWriter, v: IrStmt): void { + switch (v.kind) { + case "arrayDelete": { + w.length(0); + w.keys(Object.keys(v), KEYS68); + encIrExpr(w, v.arr); + encIrExpr(w, v.index); + encSrcLoc(w, v.loc); + return; + } + case "arraySet": { + w.length(1); + w.keys(Object.keys(v), KEYS69); + encIrExpr(w, v.arr); + encIrExpr(w, v.index); + encIrExpr(w, v.value); + encSrcLoc(w, v.loc); + return; + } + case "arraySetLength": { + w.length(2); + w.keys(Object.keys(v), KEYS70); + encIrExpr(w, v.arr); + encIrExpr(w, v.length); + encSrcLoc(w, v.loc); + if (v.pop === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.pop); + } + if (v.reserve === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.reserve); + } + return; + } + case "arraySetUndefined": { + w.length(3); + w.keys(Object.keys(v), KEYS71); + encIrExpr(w, v.arr); + encIrExpr(w, v.index); + encSrcLoc(w, v.loc); + return; + } + case "assign": { + w.length(4); + w.keys(Object.keys(v), KEYS72); + w.str(v.localId); + encIrExpr(w, v.value); + if (v.initializes === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.initializes); + } + encSrcLoc(w, v.loc); + return; + } + case "block": { + w.length(5); + w.keys(Object.keys(v), KEYS73); + if (v.labels === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.labels); + } + encT19(w, v.body); + encSrcLoc(w, v.loc); + return; + } + case "break": { + w.length(6); + w.keys(Object.keys(v), KEYS74); + if (v.label === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.label); + } + encSrcLoc(w, v.loc); + return; + } + case "bytesSet": { + w.length(7); + w.keys(Object.keys(v), KEYS75); + encIrExpr(w, v.arr); + encIrExpr(w, v.index); + encIrExpr(w, v.value); + encSrcLoc(w, v.loc); + return; + } + case "continue": { + w.length(8); + w.keys(Object.keys(v), KEYS76); + if (v.label === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.label); + } + encSrcLoc(w, v.loc); + return; + } + case "doWhile": { + w.length(9); + w.keys(Object.keys(v), KEYS77); + encT19(w, v.body); + encIrExpr(w, v.cond); + if (v.labels === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.labels); + } + encSrcLoc(w, v.loc); + return; + } + case "exprStmt": { + w.length(10); + w.keys(Object.keys(v), KEYS78); + encIrExpr(w, v.expr); + encSrcLoc(w, v.loc); + return; + } + case "fieldSet": { + w.length(11); + w.keys(Object.keys(v), KEYS79); + encIrExpr(w, v.obj); + w.str(v.className); + w.str(v.field); + encIrExpr(w, v.value); + if (v.sloppy === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.sloppy); + } + if (v.define === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.define); + } + if (v.attributesChecked === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.attributesChecked); + } + encSrcLoc(w, v.loc); + return; + } + case "for": { + w.length(12); + w.keys(Object.keys(v), KEYS80); + if (v.init === null) w.byte(0); + else { + w.byte(1); + encIrStmt(w, v.init); + } + if (v.cond === null) w.byte(0); + else { + w.byte(1); + encIrExpr(w, v.cond); + } + if (v.update === null) w.byte(0); + else { + w.byte(1); + encIrStmt(w, v.update); + } + encT19(w, v.body); + if (v.labels === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.labels); + } + encSrcLoc(w, v.loc); + return; + } + case "forOf": { + w.length(13); + w.keys(Object.keys(v), KEYS81); + w.str(v.localId); + encIrExpr(w, v.iterable); + encT19(w, v.body); + if (v.labels === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.labels); + } + encSrcLoc(w, v.loc); + return; + } + case "if": { + w.length(14); + w.keys(Object.keys(v), KEYS82); + encIrExpr(w, v.cond); + encT19(w, v.then); + if (v.else_ === null) w.byte(0); + else { + w.byte(1); + encT19(w, v.else_); + } + encSrcLoc(w, v.loc); + return; + } + case "recordKeyDelete": { + w.length(15); + w.keys(Object.keys(v), KEYS83); + encIrExpr(w, v.obj); + w.str(v.shapeId); + encIrExpr(w, v.key); + encSrcLoc(w, v.loc); + return; + } + case "recordKeySet": { + w.length(16); + w.keys(Object.keys(v), KEYS84); + encIrExpr(w, v.obj); + w.str(v.shapeId); + encIrExpr(w, v.key); + encIrExpr(w, v.value); + if (v.overflowOnly === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.overflowOnly); + } + encSrcLoc(w, v.loc); + return; + } + case "recordSet": { + w.length(17); + w.keys(Object.keys(v), KEYS85); + encIrExpr(w, v.obj); + w.str(v.shapeId); + w.str(v.field); + encIrExpr(w, v.value); + encSrcLoc(w, v.loc); + return; + } + case "rethrow": { + w.length(18); + w.keys(Object.keys(v), KEYS86); + w.str(v.localId); + encSrcLoc(w, v.loc); + return; + } + case "return": { + w.length(19); + w.keys(Object.keys(v), KEYS87); + if (v.value === null) w.byte(0); + else { + w.byte(1); + encIrExpr(w, v.value); + } + if (v.awaited === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.awaited); + } + encSrcLoc(w, v.loc); + return; + } + case "runtimeFence": { + w.length(20); + w.keys(Object.keys(v), KEYS88); + w.str(v.code); + w.str(v.message); + encSrcLoc(w, v.loc); + return; + } + case "switch": { + w.length(21); + w.keys(Object.keys(v), KEYS89); + encIrExpr(w, v.disc); + encT54(w, v.cases); + if (v.labels === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.labels); + } + encSrcLoc(w, v.loc); + return; + } + case "throw": { + w.length(22); + w.keys(Object.keys(v), KEYS90); + encIrExpr(w, v.value); + encSrcLoc(w, v.loc); + return; + } + case "tryCatch": { + w.length(23); + w.keys(Object.keys(v), KEYS91); + encT19(w, v.tryBody); + if (v.catchBody === null) w.byte(0); + else { + w.byte(1); + encT19(w, v.catchBody); + } + if (v.catchLocalId === null) w.byte(0); + else { + w.byte(1); + w.str(v.catchLocalId); + } + if (v.dropFinallyErrors === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.dropFinallyErrors); + } + if (v.finallyBody === null) w.byte(0); + else { + w.byte(1); + encT19(w, v.finallyBody); + } + if (v.suppressFinallyErrors === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.suppressFinallyErrors); + } + encSrcLoc(w, v.loc); + return; + } + case "varDecl": { + w.length(24); + w.keys(Object.keys(v), KEYS92); + w.str(v.localId); + if (v.init === null) w.byte(0); + else { + w.byte(1); + encIrExpr(w, v.init); + } + encSrcLoc(w, v.loc); + return; + } + case "while": { + w.length(25); + w.keys(Object.keys(v), KEYS93); + encIrExpr(w, v.cond); + encT19(w, v.body); + if (v.labels === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.labels); + } + encSrcLoc(w, v.loc); + return; + } + } + w.refuse("IrStmt kind " + String((v as { kind: unknown }).kind)); +} +function decIrStmt(r: IrReader): IrStmt { + switch (r.length()) { + case 0: { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "arrayDelete", arr: f0, index: f1, loc: f2 }; + return o; + } + case 1: { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const f2 = decIrExpr(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "arraySet", arr: f0, index: f1, value: f2, loc: f3 }; + return o; + } + case 2: { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const f2 = decSrcLoc(r); + const f3 = r.tag(2, "IrFunction.body[].pop") === 0 ? undefined : r.bool(); + const f4 = r.tag(2, "IrFunction.body[].reserve") === 0 ? undefined : r.bool(); + const o: Extract = { kind: "arraySetLength", arr: f0, length: f1, loc: f2, ...(f3 !== undefined ? { pop: f3 } : {}), ...(f4 !== undefined ? { reserve: f4 } : {}) }; + return o; + } + case 3: { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "arraySetUndefined", arr: f0, index: f1, loc: f2 }; + return o; + } + case 4: { + const f0 = r.str(); + const f1 = decIrExpr(r); + const f2 = r.tag(2, "IrFunction.body[].initializes") === 0 ? undefined : (r.bool() as NonNullable["initializes"]>); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "assign", localId: f0, value: f1, ...(f2 !== undefined ? { initializes: f2 } : {}), loc: f3 }; + return o; + } + case 5: { + const f0 = r.tag(2, "IrFunction.body[].labels") === 0 ? undefined : decT8(r); + const f1 = decT19(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "block", ...(f0 !== undefined ? { labels: f0 } : {}), body: f1, loc: f2 }; + return o; + } + case 6: { + const f0 = r.tag(2, "IrFunction.body[].label") === 0 ? undefined : r.str(); + const f1 = decSrcLoc(r); + const o: Extract = { kind: "break", ...(f0 !== undefined ? { label: f0 } : {}), loc: f1 }; + return o; + } + case 7: { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const f2 = decIrExpr(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "bytesSet", arr: f0, index: f1, value: f2, loc: f3 }; + return o; + } + case 8: { + const f0 = r.tag(2, "IrFunction.body[].label") === 0 ? undefined : r.str(); + const f1 = decSrcLoc(r); + const o: Extract = { kind: "continue", ...(f0 !== undefined ? { label: f0 } : {}), loc: f1 }; + return o; + } + case 9: { + const f0 = decT19(r); + const f1 = decIrExpr(r); + const f2 = r.tag(2, "IrFunction.body[].labels") === 0 ? undefined : decT8(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "doWhile", body: f0, cond: f1, ...(f2 !== undefined ? { labels: f2 } : {}), loc: f3 }; + return o; + } + case 10: { + const f0 = decIrExpr(r); + const f1 = decSrcLoc(r); + const o: Extract = { kind: "exprStmt", expr: f0, loc: f1 }; + return o; + } + case 11: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = r.str(); + const f3 = decIrExpr(r); + const f4 = r.tag(2, "IrFunction.body[]
.sloppy") === 0 ? undefined : (r.bool() as NonNullable["sloppy"]>); + const f5 = r.tag(2, "IrFunction.body[]
.define") === 0 ? undefined : (r.bool() as NonNullable["define"]>); + const f6 = r.tag(2, "IrFunction.body[]
.attributesChecked") === 0 ? undefined : (r.bool() as NonNullable["attributesChecked"]>); + const f7 = decSrcLoc(r); + const o: Extract = { kind: "fieldSet", obj: f0, className: f1, field: f2, value: f3, ...(f4 !== undefined ? { sloppy: f4 } : {}), ...(f5 !== undefined ? { define: f5 } : {}), ...(f6 !== undefined ? { attributesChecked: f6 } : {}), loc: f7 }; + return o; + } + case 12: { + const f0 = r.tag(2, "IrFunction.body[].init") === 0 ? null : decIrStmt(r); + const f1 = r.tag(2, "IrFunction.body[].cond") === 0 ? null : decIrExpr(r); + const f2 = r.tag(2, "IrFunction.body[].update") === 0 ? null : decIrStmt(r); + const f3 = decT19(r); + const f4 = r.tag(2, "IrFunction.body[].labels") === 0 ? undefined : decT8(r); + const f5 = decSrcLoc(r); + const o: Extract = { kind: "for", init: f0, cond: f1, update: f2, body: f3, ...(f4 !== undefined ? { labels: f4 } : {}), loc: f5 }; + return o; + } + case 13: { + const f0 = r.str(); + const f1 = decIrExpr(r); + const f2 = decT19(r); + const f3 = r.tag(2, "IrFunction.body[].labels") === 0 ? undefined : decT8(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "forOf", localId: f0, iterable: f1, body: f2, ...(f3 !== undefined ? { labels: f3 } : {}), loc: f4 }; + return o; + } + case 14: { + const f0 = decIrExpr(r); + const f1 = decT19(r); + const f2 = r.tag(2, "IrFunction.body[].else_") === 0 ? null : decT19(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "if", cond: f0, then: f1, else_: f2, loc: f3 }; + return o; + } + case 15: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = decIrExpr(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "recordKeyDelete", obj: f0, shapeId: f1, key: f2, loc: f3 }; + return o; + } + case 16: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = decIrExpr(r); + const f3 = decIrExpr(r); + const f4 = r.tag(2, "IrFunction.body[].overflowOnly") === 0 ? undefined : (r.bool() as NonNullable["overflowOnly"]>); + const f5 = decSrcLoc(r); + const o: Extract = { kind: "recordKeySet", obj: f0, shapeId: f1, key: f2, value: f3, ...(f4 !== undefined ? { overflowOnly: f4 } : {}), loc: f5 }; + return o; + } + case 17: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = r.str(); + const f3 = decIrExpr(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "recordSet", obj: f0, shapeId: f1, field: f2, value: f3, loc: f4 }; + return o; + } + case 18: { + const f0 = r.str(); + const f1 = decSrcLoc(r); + const o: Extract = { kind: "rethrow", localId: f0, loc: f1 }; + return o; + } + case 19: { + const f0 = r.tag(2, "IrFunction.body[].value") === 0 ? null : decIrExpr(r); + const f1 = r.tag(2, "IrFunction.body[].awaited") === 0 ? undefined : (r.bool() as NonNullable["awaited"]>); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "return", value: f0, ...(f1 !== undefined ? { awaited: f1 } : {}), loc: f2 }; + return o; + } + case 20: { + const f0 = r.str(); + const f1 = r.str(); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "runtimeFence", code: f0, message: f1, loc: f2 }; + return o; + } + case 21: { + const f0 = decIrExpr(r); + const f1 = decT54(r); + const f2 = r.tag(2, "IrFunction.body[].labels") === 0 ? undefined : decT8(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "switch", disc: f0, cases: f1, ...(f2 !== undefined ? { labels: f2 } : {}), loc: f3 }; + return o; + } + case 22: { + const f0 = decIrExpr(r); + const f1 = decSrcLoc(r); + const o: Extract = { kind: "throw", value: f0, loc: f1 }; + return o; + } + case 23: { + const f0 = decT19(r); + const f1 = r.tag(2, "IrFunction.body[].catchBody") === 0 ? null : decT19(r); + const f2 = r.tag(2, "IrFunction.body[].catchLocalId") === 0 ? null : r.str(); + const f3 = r.tag(2, "IrFunction.body[].dropFinallyErrors") === 0 ? undefined : (r.bool() as NonNullable["dropFinallyErrors"]>); + const f4 = r.tag(2, "IrFunction.body[].finallyBody") === 0 ? null : decT19(r); + const f5 = r.tag(2, "IrFunction.body[].suppressFinallyErrors") === 0 ? undefined : (r.bool() as NonNullable["suppressFinallyErrors"]>); + const f6 = decSrcLoc(r); + const o: Extract = { kind: "tryCatch", tryBody: f0, catchBody: f1, catchLocalId: f2, ...(f3 !== undefined ? { dropFinallyErrors: f3 } : {}), finallyBody: f4, ...(f5 !== undefined ? { suppressFinallyErrors: f5 } : {}), loc: f6 }; + return o; + } + case 24: { + const f0 = r.str(); + const f1 = r.tag(2, "IrFunction.body[].init") === 0 ? null : decIrExpr(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "varDecl", localId: f0, init: f1, loc: f2 }; + return o; + } + case 25: { + const f0 = decIrExpr(r); + const f1 = decT19(r); + const f2 = r.tag(2, "IrFunction.body[].labels") === 0 ? undefined : decT8(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "while", cond: f0, body: f1, ...(f2 !== undefined ? { labels: f2 } : {}), loc: f3 }; + return o; + } + } + throw new Error("IR codec: invalid IrStmt kind"); +} + +function encT40(w: IrWriter, v: NonNullable["resultType"]>): void { + w.keys(Object.keys(v), KEYS94); + w.str(v.kind); + w.str(v.shapeId); +} +function decT40(r: IrReader): NonNullable["resultType"]> { + const f0 = (r.str() as NonNullable["resultType"]>["kind"]>); + const f1 = r.str(); + const o: NonNullable["resultType"]> = { kind: f0, shapeId: f1 }; + return o; +} + +function encT41(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encT55(w, item); + } +} +function decT41(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decT55(r)); + return out; +} + +function encT42(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encT56(w, item); + } +} +function decT42(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decT56(r)); + return out; +} + +function encIrBindingSource(w: IrWriter, v: IrBindingSource): void { + w.keys(Object.keys(v), KEYS95); + encSrcLoc(w, v.loc); + encSrcLoc(w, v.scope); +} +function decIrBindingSource(r: IrReader): IrBindingSource { + const f0 = decSrcLoc(r); + const f1 = decSrcLoc(r); + const o: IrBindingSource = { loc: f0, scope: f1 }; + return o; +} + +function encT44(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encT57(w, item); + } +} +function decT44(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decT57(r)); + return out; +} + +function encT45(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encT8(w, item); + } +} +function decT45(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decT8(r)); + return out; +} + +function encIrUnionDiscriminant(w: IrWriter, v: IrUnionDiscriminant): void { + w.keys(Object.keys(v), KEYS96); + w.str(v.field); + encT58(w, v.cases); +} +function decIrUnionDiscriminant(r: IrReader): IrUnionDiscriminant { + const f0 = r.str(); + const f1 = decT58(r); + const o: IrUnionDiscriminant = { field: f0, cases: f1 }; + return o; +} + +function encT47(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encT59(w, item); + } +} +function decT47(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decT59(r)); + return out; +} + +function encT48(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encT60(w, item); + } +} +function decT48(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decT60(r)); + return out; +} + +function encT49(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encIrFfiParam(w, item); + } +} +function decT49(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrFfiParam(r)); + return out; +} + +function encIrLibCallback(w: IrWriter, v: IrLibCallback): void { + w.keys(Object.keys(v), KEYS97); + w.str(v.name); + w.num(v.slot); + encT62(w, v.params); + w.str(v.returns); + w.str(v.unregisteredTrap); +} +function decIrLibCallback(r: IrReader): IrLibCallback { + const f0 = r.str(); + const f1 = r.num(); + const f2 = decT62(r); + const f3 = (r.str() as NonNullable); + const f4 = r.str(); + const o: IrLibCallback = { name: f0, slot: f1, params: f2, returns: f3, unregisteredTrap: f4 }; + return o; +} + +function encIrLibExport(w: IrWriter, v: IrLibExport): void { + w.keys(Object.keys(v), KEYS98); + w.str(v.symbol); + w.str(v.fnName); + encT63(w, v.params); + w.str(v.returns); + if (v.inboundBytesTrap === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.inboundBytesTrap); + } + if (v.inboundIntTrap === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.inboundIntTrap); + } + if (v.inboundCstringTrap === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.inboundCstringTrap); + } +} +function decIrLibExport(r: IrReader): IrLibExport { + const f0 = r.str(); + const f1 = r.str(); + const f2 = decT63(r); + const f3 = (r.str() as NonNullable); + const f4 = r.tag(2, "IrModule.lib.exports[].inboundBytesTrap") === 0 ? undefined : r.str(); + const f5 = r.tag(2, "IrModule.lib.exports[].inboundIntTrap") === 0 ? undefined : r.str(); + const f6 = r.tag(2, "IrModule.lib.exports[].inboundCstringTrap") === 0 ? undefined : r.str(); + const o: IrLibExport = { symbol: f0, fnName: f1, params: f2, returns: f3, ...(f4 !== undefined ? { inboundBytesTrap: f4 } : {}), ...(f5 !== undefined ? { inboundIntTrap: f5 } : {}), ...(f6 !== undefined ? { inboundCstringTrap: f6 } : {}) }; + return o; +} + +function encIrLibTrapOverlay(w: IrWriter, v: IrLibTrapOverlay): void { + w.keys(Object.keys(v), KEYS99); + w.str(v.code); + if (v.teaching === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.teaching); + } + if (v.remediation === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.remediation); + } +} +function decIrLibTrapOverlay(r: IrReader): IrLibTrapOverlay { + const f0 = r.str(); + const f1 = r.tag(2, "IrModule.lib.trapOverlays[].teaching") === 0 ? undefined : r.str(); + const f2 = r.tag(2, "IrModule.lib.trapOverlays[].remediation") === 0 ? undefined : r.str(); + const o: IrLibTrapOverlay = { code: f0, ...(f1 !== undefined ? { teaching: f1 } : {}), ...(f2 !== undefined ? { remediation: f2 } : {}) }; + return o; +} + +function encIrExpr(w: IrWriter, v: IrExpr): void { + switch (v.kind) { + case "arrIntrinsic": { + w.length(0); + w.keys(Object.keys(v), KEYS100); + w.str(v.method); + encIrExpr(w, v.receiver); + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "arrayGet": { + w.length(1); + w.keys(Object.keys(v), KEYS101); + encIrExpr(w, v.arr); + encIrExpr(w, v.index); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "arrayHas": { + w.length(2); + w.keys(Object.keys(v), KEYS102); + encIrExpr(w, v.arr); + encIrExpr(w, v.index); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "arrayLit": { + w.length(3); + w.keys(Object.keys(v), KEYS103); + encT64(w, v.elems); + if (v.spreads === undefined) w.byte(0); + else { + w.byte(1); + encT65(w, v.spreads); + } + if (v.capacity === undefined) w.byte(0); + else { + w.byte(1); + w.num(v.capacity); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "arrayNewLen": { + w.length(4); + w.keys(Object.keys(v), KEYS104); + encIrExpr(w, v.length); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "arrayState": { + w.length(5); + w.keys(Object.keys(v), KEYS105); + encIrExpr(w, v.arr); + encIrExpr(w, v.index); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "assignExpr": { + w.length(6); + w.keys(Object.keys(v), KEYS106); + w.str(v.localId); + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "awaitExpr": { + w.length(7); + w.keys(Object.keys(v), KEYS107); + encIrExpr(w, v.value); + encIrType(w, v.type); + if (v.adopt === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.adopt); + } + encSrcLoc(w, v.loc); + return; + } + case "awaitUnionExpr": { + w.length(8); + w.keys(Object.keys(v), KEYS108); + encIrExpr(w, v.value); + w.num(v.promiseTag); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "bin": { + w.length(9); + w.keys(Object.keys(v), KEYS109); + w.str(v.op); + encIrExpr(w, v.left); + encIrExpr(w, v.right); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "boolLit": { + w.length(10); + w.keys(Object.keys(v), KEYS110); + w.bool(v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "bytesIntrinsic": { + w.length(11); + w.keys(Object.keys(v), KEYS111); + w.str(v.method); + encIrExpr(w, v.receiver); + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + if (v.invalidNaN === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.invalidNaN); + } + return; + } + case "bytesNew": { + w.length(12); + w.keys(Object.keys(v), KEYS112); + if (v.source === null) w.byte(0); + else { + w.byte(1); + encIrExpr(w, v.source); + } + if (v.from === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.from); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "call": { + w.length(13); + w.keys(Object.keys(v), KEYS113); + w.str(v.callee); + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "callValue": { + w.length(14); + w.keys(Object.keys(v), KEYS114); + encIrExpr(w, v.callee); + if (v.receiver === undefined) w.byte(0); + else { + w.byte(1); + encIrExpr(w, v.receiver); + } + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "caughtCheck": { + w.length(15); + w.keys(Object.keys(v), KEYS115); + encIrExpr(w, v.value); + w.str(v.className); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "caughtNarrow": { + w.length(16); + w.keys(Object.keys(v), KEYS116); + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "caughtTest": { + w.length(17); + w.keys(Object.keys(v), KEYS117); + encIrExpr(w, v.value); + w.str(v.test); + if (v.className === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.className); + } + if (v.negated === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.negated); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "caughtToDyn": { + w.length(18); + w.keys(Object.keys(v), KEYS118); + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "chainRecv": { + w.length(19); + w.keys(Object.keys(v), KEYS119); + w.str(v.id); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "classRef": { + w.length(20); + w.keys(Object.keys(v), KEYS120); + w.str(v.className); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + if (v.captures === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.captures); + } + return; + } + case "closure": { + w.length(21); + w.keys(Object.keys(v), KEYS121); + w.str(v.fnName); + encT8(w, v.captures); + if (v.adapts === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.adapts); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "downcast": { + w.length(22); + w.keys(Object.keys(v), KEYS122); + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "dynArrLit": { + w.length(23); + w.keys(Object.keys(v), KEYS123); + encT64(w, v.elems); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "dynCall": { + w.length(24); + w.keys(Object.keys(v), KEYS124); + encIrExpr(w, v.callee); + if (v.receiver === undefined) w.byte(0); + else { + w.byte(1); + encIrExpr(w, v.receiver); + } + w.str(v.calleeName); + if (v.calleeNameValue === undefined) w.byte(0); + else { + w.byte(1); + encIrExpr(w, v.calleeNameValue); + } + encT64(w, v.args); + if (v.spreads === undefined) w.byte(0); + else { + w.byte(1); + encT66(w, v.spreads); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "dynCheck": { + w.length(25); + w.keys(Object.keys(v), KEYS125); + encIrExpr(w, v.value); + if (v.preserveRefs === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.preserveRefs); + } + if (v.adopt === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.adopt); + } + if (v.exact === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.exact); + } + if (v.inferredSite === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.inferredSite); + } + if (v.origin === undefined) w.byte(0); + else { + w.byte(1); + w.num(v.origin); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "dynDestrCheck": { + w.length(26); + w.keys(Object.keys(v), KEYS126); + encIrExpr(w, v.value); + w.str(v.spelling); + if (v.firstProp === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.firstProp); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "dynFrom": { + w.length(27); + w.keys(Object.keys(v), KEYS127); + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + if (v.liveRef === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.liveRef); + } + if (v.fnName === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.fnName); + } + if (v.fnLength === undefined) w.byte(0); + else { + w.byte(1); + w.num(v.fnLength); + } + return; + } + case "dynHasKey": { + w.length(28); + w.keys(Object.keys(v), KEYS128); + w.str(v.key); + if (v.negated === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.negated); + } + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "dynInvoke": { + w.length(29); + w.keys(Object.keys(v), KEYS129); + encIrExpr(w, v.recv); + w.str(v.method); + w.str(v.calleeName); + if (v.calleeNameValue === undefined) w.byte(0); + else { + w.byte(1); + encIrExpr(w, v.calleeNameValue); + } + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "dynIterN": { + w.length(30); + w.keys(Object.keys(v), KEYS130); + encIrExpr(w, v.value); + w.num(v.count); + if (v.notIterableMessage === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.notIterableMessage); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "dynKeyGet": { + w.length(31); + w.keys(Object.keys(v), KEYS131); + encIrExpr(w, v.key); + if (v.optional === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.optional); + } + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "dynObjLit": { + w.length(32); + w.keys(Object.keys(v), KEYS132); + if (v.fields === undefined) w.byte(0); + else { + w.byte(1); + encT67(w, v.fields); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "dynScalarEq": { + w.length(33); + w.keys(Object.keys(v), KEYS133); + encIrExpr(w, v.left); + encIrExpr(w, v.right); + if (v.negated === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.negated); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "dynTest": { + w.length(34); + w.keys(Object.keys(v), KEYS134); + w.str(v.test); + if (v.bytesElem === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.bytesElem); + } + if (v.negated === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.negated); + } + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "ffiCall": { + w.length(35); + w.keys(Object.keys(v), KEYS135); + w.str(v.import); + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "fieldAbsent": { + w.length(36); + w.keys(Object.keys(v), KEYS136); + w.str(v.unionId); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "fieldGet": { + w.length(37); + w.keys(Object.keys(v), KEYS137); + encIrExpr(w, v.obj); + w.str(v.className); + w.str(v.field); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "fieldIncDec": { + w.length(38); + w.keys(Object.keys(v), KEYS138); + w.str(v.op); + w.bool(v.prefix); + encIrExpr(w, v.obj); + w.str(v.className); + w.str(v.field); + w.bool(v.fieldDyn); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "genResume": { + w.length(39); + w.keys(Object.keys(v), KEYS139); + w.str(v.mode); + encIrExpr(w, v.gen); + if (v.arg === null) w.byte(0); + else { + w.byte(1); + encIrExpr(w, v.arg); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "incDec": { + w.length(40); + w.keys(Object.keys(v), KEYS140); + w.str(v.op); + w.bool(v.prefix); + w.str(v.localId); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "instanceOf": { + w.length(41); + w.keys(Object.keys(v), KEYS141); + encIrExpr(w, v.value); + w.str(v.className); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "instanceOfValue": { + w.length(42); + w.keys(Object.keys(v), KEYS142); + encIrExpr(w, v.value); + encIrExpr(w, v.classValue); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "intrinsic": { + w.length(43); + w.keys(Object.keys(v), KEYS143); + w.str(v.name); + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "jsonStringify": { + w.length(44); + w.keys(Object.keys(v), KEYS144); + encIrExpr(w, v.value); + if (v.indent === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.indent); + } + if (v.circular === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.circular); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "libCall": { + w.length(45); + w.keys(Object.keys(v), KEYS145); + w.str(v.fn); + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + if (v.prototypeIdentityOnly === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.prototypeIdentityOnly); + } + if (v.realTimer === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.realTimer); + } + if (v.assertOptions === undefined) w.byte(0); + else { + w.byte(1); + encT68(w, v.assertOptions); + } + return; + } + case "logical": { + w.length(46); + w.keys(Object.keys(v), KEYS146); + w.str(v.op); + encIrExpr(w, v.left); + encIrExpr(w, v.right); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "mapIntrinsic": { + w.length(47); + w.keys(Object.keys(v), KEYS147); + w.str(v.method); + encIrExpr(w, v.receiver); + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "mapNew": { + w.length(48); + w.keys(Object.keys(v), KEYS148); + if (v.seed === undefined) w.byte(0); + else { + w.byte(1); + encT69(w, v.seed); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "moduleNsRef": { + w.length(49); + w.keys(Object.keys(v), KEYS149); + w.str(v.moduleId); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "new": { + w.length(50); + w.keys(Object.keys(v), KEYS150); + w.str(v.className); + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "newPromise": { + w.length(51); + w.keys(Object.keys(v), KEYS151); + encIrExpr(w, v.executor); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "newValue": { + w.length(52); + w.keys(Object.keys(v), KEYS152); + encIrExpr(w, v.callee); + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "nullish": { + w.length(53); + w.keys(Object.keys(v), KEYS153); + encIrExpr(w, v.left); + encIrExpr(w, v.right); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "numLit": { + w.length(54); + w.keys(Object.keys(v), KEYS154); + w.num(v.value); + if (v.spelling === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.spelling); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "optChain": { + w.length(55); + w.keys(Object.keys(v), KEYS155); + w.str(v.id); + encIrExpr(w, v.receiver); + encIrExpr(w, v.body); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "orDefault": { + w.length(56); + w.keys(Object.keys(v), KEYS156); + encIrExpr(w, v.left); + encIrExpr(w, v.right); + if (v.retag === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.retag); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "promiseVoidWiden": { + w.length(57); + w.keys(Object.keys(v), KEYS157); + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "promiseWithResolvers": { + w.length(58); + w.keys(Object.keys(v), KEYS158); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "recordClone": { + w.length(59); + w.keys(Object.keys(v), KEYS159); + encIrExpr(w, v.source); + encT70(w, v.overrides); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "recordGet": { + w.length(60); + w.keys(Object.keys(v), KEYS160); + encIrExpr(w, v.obj); + w.str(v.shapeId); + w.str(v.field); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "recordHas": { + w.length(61); + w.keys(Object.keys(v), KEYS161); + encIrExpr(w, v.obj); + w.str(v.shapeId); + w.str(v.field); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "recordKeyGet": { + w.length(62); + w.keys(Object.keys(v), KEYS162); + encIrExpr(w, v.obj); + w.str(v.shapeId); + encIrExpr(w, v.key); + if (v.overflowOnly === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.overflowOnly); + } + if (v.missNaN === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.missNaN); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "recordLit": { + w.length(63); + w.keys(Object.keys(v), KEYS163); + encT71(w, v.fields); + if (v.nullProto === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.nullProto); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "recordNullProto": { + w.length(64); + w.keys(Object.keys(v), KEYS164); + encIrExpr(w, v.obj); + w.str(v.shapeId); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "recordOvfHas": { + w.length(65); + w.keys(Object.keys(v), KEYS165); + encIrExpr(w, v.obj); + w.str(v.shapeId); + encIrExpr(w, v.key); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "recordOvfKeys": { + w.length(66); + w.keys(Object.keys(v), KEYS166); + encIrExpr(w, v.obj); + w.str(v.shapeId); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "regexIntrinsic": { + w.length(67); + w.keys(Object.keys(v), KEYS167); + w.str(v.method); + encIrExpr(w, v.receiver); + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "regexLit": { + w.length(68); + w.keys(Object.keys(v), KEYS168); + w.str(v.pattern); + w.str(v.flags); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "selfRef": { + w.length(69); + w.keys(Object.keys(v), KEYS169); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "seqExpr": { + w.length(70); + w.keys(Object.keys(v), KEYS170); + encT19(w, v.stmts); + encIrExpr(w, v.result); + if (v.generatorDelegate === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.generatorDelegate); + } + if (v.diverges === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.diverges); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "setIntrinsic": { + w.length(71); + w.keys(Object.keys(v), KEYS171); + w.str(v.method); + encIrExpr(w, v.receiver); + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "setNew": { + w.length(72); + w.keys(Object.keys(v), KEYS172); + if (v.seed === undefined) w.byte(0); + else { + w.byte(1); + encIrExpr(w, v.seed); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "strCmp": { + w.length(73); + w.keys(Object.keys(v), KEYS173); + w.str(v.op); + encIrExpr(w, v.left); + encIrExpr(w, v.right); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "strConcat": { + w.length(74); + w.keys(Object.keys(v), KEYS174); + encIrExpr(w, v.left); + encIrExpr(w, v.right); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "strEq": { + w.length(75); + w.keys(Object.keys(v), KEYS175); + w.bool(v.negated); + encIrExpr(w, v.left); + encIrExpr(w, v.right); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "strIntrinsic": { + w.length(76); + w.keys(Object.keys(v), KEYS176); + w.str(v.method); + encIrExpr(w, v.receiver); + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "strLit": { + w.length(77); + w.keys(Object.keys(v), KEYS177); + w.str(v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "templateStrings": { + w.length(78); + w.keys(Object.keys(v), KEYS178); + w.str(v.key); + encT8(w, v.cooked); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "ternary": { + w.length(79); + w.keys(Object.keys(v), KEYS179); + encIrExpr(w, v.cond); + encIrExpr(w, v.then); + encIrExpr(w, v.else_); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "toBool": { + w.length(80); + w.keys(Object.keys(v), KEYS180); + encIrExpr(w, v.operand); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "toString": { + w.length(81); + w.keys(Object.keys(v), KEYS181); + encIrExpr(w, v.operand); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "unary": { + w.length(82); + w.keys(Object.keys(v), KEYS182); + w.str(v.op); + encIrExpr(w, v.operand); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "unionDisc": { + w.length(83); + w.keys(Object.keys(v), KEYS183); + w.str(v.unionId); + w.str(v.field); + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "unionEq": { + w.length(84); + w.keys(Object.keys(v), KEYS184); + w.str(v.unionId); + w.bool(v.negated); + w.bool(v.sameValue); + encIrExpr(w, v.left); + encIrExpr(w, v.right); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "unionFuncEq": { + w.length(85); + w.keys(Object.keys(v), KEYS185); + w.str(v.unionId); + w.num(v.tag); + encIrExpr(w, v.union); + encIrExpr(w, v.func); + w.bool(v.negated); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "unionIsTag": { + w.length(86); + w.keys(Object.keys(v), KEYS186); + w.str(v.unionId); + w.num(v.tag); + w.bool(v.negated); + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "unionKeyGet": { + w.length(87); + w.keys(Object.keys(v), KEYS187); + w.str(v.unionId); + encIrExpr(w, v.key); + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "unionNarrow": { + w.length(88); + w.keys(Object.keys(v), KEYS188); + w.str(v.unionId); + w.num(v.tag); + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "unionRetype": { + w.length(89); + w.keys(Object.keys(v), KEYS189); + w.str(v.fromId); + w.str(v.unionId); + encT65(w, v.tags); + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "unionWrap": { + w.length(90); + w.keys(Object.keys(v), KEYS190); + w.str(v.unionId); + w.num(v.tag); + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "unitLit": { + w.length(91); + w.keys(Object.keys(v), KEYS191); + w.str(v.unit); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "upcast": { + w.length(92); + w.keys(Object.keys(v), KEYS192); + encIrExpr(w, v.value); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "varRef": { + w.length(93); + w.keys(Object.keys(v), KEYS193); + w.str(v.localId); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + case "virtualCall": { + w.length(94); + w.keys(Object.keys(v), KEYS194); + w.str(v.className); + w.str(v.method); + encT64(w, v.args); + encIrType(w, v.type); + encSrcLoc(w, v.loc); + if (v.targets === undefined) w.byte(0); + else { + w.byte(1); + encT8(w, v.targets); + } + return; + } + case "yieldExpr": { + w.length(95); + w.keys(Object.keys(v), KEYS195); + if (v.value === null) w.byte(0); + else { + w.byte(1); + encIrExpr(w, v.value); + } + if (v.awaited === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.awaited); + } + if (v.delegated === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.delegated); + } + if (v.captureCompletion === undefined) w.byte(0); + else { + w.byte(1); + encT72(w, v.captureCompletion); + } + encIrType(w, v.type); + encSrcLoc(w, v.loc); + return; + } + } + w.refuse("IrExpr kind " + String((v as { kind: unknown }).kind)); +} +function decIrExpr(r: IrReader): IrExpr { + switch (r.length()) { + case 0: { + const f0 = (r.str() as NonNullable["method"]>); + const f1 = decIrExpr(r); + const f2 = decT64(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "arrIntrinsic", method: f0, receiver: f1, args: f2, type: f3, loc: f4 }; + return o; + } + case 1: { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "arrayGet", arr: f0, index: f1, type: f2, loc: f3 }; + return o; + } + case 2: { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "arrayHas", arr: f0, index: f1, type: f2, loc: f3 }; + return o; + } + case 3: { + const f0 = decT64(r); + const f1 = r.tag(2, "IrFunction.body[].arr.spreads") === 0 ? undefined : decT65(r); + const f2 = r.tag(2, "IrFunction.body[].arr.capacity") === 0 ? undefined : r.num(); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "arrayLit", elems: f0, ...(f1 !== undefined ? { spreads: f1 } : {}), ...(f2 !== undefined ? { capacity: f2 } : {}), type: f3, loc: f4 }; + return o; + } + case 4: { + const f0 = decIrExpr(r); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "arrayNewLen", length: f0, type: f1, loc: f2 }; + return o; + } + case 5: { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "arrayState", arr: f0, index: f1, type: f2, loc: f3 }; + return o; + } + case 6: { + const f0 = r.str(); + const f1 = decIrExpr(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "assignExpr", localId: f0, value: f1, type: f2, loc: f3 }; + return o; + } + case 7: { + const f0 = decIrExpr(r); + const f1 = decIrType(r); + const f2 = r.tag(2, "IrFunction.body[].arr.adopt") === 0 ? undefined : (r.bool() as NonNullable["adopt"]>); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "awaitExpr", value: f0, type: f1, ...(f2 !== undefined ? { adopt: f2 } : {}), loc: f3 }; + return o; + } + case 8: { + const f0 = decIrExpr(r); + const f1 = r.num(); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "awaitUnionExpr", value: f0, promiseTag: f1, type: f2, loc: f3 }; + return o; + } + case 9: { + const f0 = (r.str() as NonNullable["op"]>); + const f1 = decIrExpr(r); + const f2 = decIrExpr(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "bin", op: f0, left: f1, right: f2, type: f3, loc: f4 }; + return o; + } + case 10: { + const f0 = r.bool(); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "boolLit", value: f0, type: f1, loc: f2 }; + return o; + } + case 11: { + const f0 = (r.str() as NonNullable["method"]>); + const f1 = decIrExpr(r); + const f2 = decT64(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const f5 = r.tag(2, "IrFunction.body[].arr.invalidNaN") === 0 ? undefined : (r.bool() as NonNullable["invalidNaN"]>); + const o: Extract = { kind: "bytesIntrinsic", method: f0, receiver: f1, args: f2, type: f3, loc: f4, ...(f5 !== undefined ? { invalidNaN: f5 } : {}) }; + return o; + } + case 12: { + const f0 = r.tag(2, "IrFunction.body[].arr.source") === 0 ? null : decIrExpr(r); + const f1 = r.tag(2, "IrFunction.body[].arr.from") === 0 ? undefined : (r.bool() as NonNullable["from"]>); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "bytesNew", source: f0, ...(f1 !== undefined ? { from: f1 } : {}), type: f2, loc: f3 }; + return o; + } + case 13: { + const f0 = r.str(); + const f1 = decT64(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "call", callee: f0, args: f1, type: f2, loc: f3 }; + return o; + } + case 14: { + const f0 = decIrExpr(r); + const f1 = r.tag(2, "IrFunction.body[].arr.receiver") === 0 ? undefined : decIrExpr(r); + const f2 = decT64(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "callValue", callee: f0, ...(f1 !== undefined ? { receiver: f1 } : {}), args: f2, type: f3, loc: f4 }; + return o; + } + case 15: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "caughtCheck", value: f0, className: f1, type: f2, loc: f3 }; + return o; + } + case 16: { + const f0 = decIrExpr(r); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "caughtNarrow", value: f0, type: f1, loc: f2 }; + return o; + } + case 17: { + const f0 = decIrExpr(r); + const f1 = (r.str() as NonNullable["test"]>); + const f2 = r.tag(2, "IrFunction.body[].arr.className") === 0 ? undefined : r.str(); + const f3 = r.tag(2, "IrFunction.body[].arr.negated") === 0 ? undefined : r.bool(); + const f4 = decIrType(r); + const f5 = decSrcLoc(r); + const o: Extract = { kind: "caughtTest", value: f0, test: f1, ...(f2 !== undefined ? { className: f2 } : {}), ...(f3 !== undefined ? { negated: f3 } : {}), type: f4, loc: f5 }; + return o; + } + case 18: { + const f0 = decIrExpr(r); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "caughtToDyn", value: f0, type: f1, loc: f2 }; + return o; + } + case 19: { + const f0 = r.str(); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "chainRecv", id: f0, type: f1, loc: f2 }; + return o; + } + case 20: { + const f0 = r.str(); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const f3 = r.tag(2, "IrFunction.body[].arr.captures") === 0 ? undefined : decT8(r); + const o: Extract = { kind: "classRef", className: f0, type: f1, loc: f2, ...(f3 !== undefined ? { captures: f3 } : {}) }; + return o; + } + case 21: { + const f0 = r.str(); + const f1 = decT8(r); + const f2 = r.tag(2, "IrFunction.body[].arr.adapts") === 0 ? undefined : (r.bool() as NonNullable["adapts"]>); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "closure", fnName: f0, captures: f1, ...(f2 !== undefined ? { adapts: f2 } : {}), type: f3, loc: f4 }; + return o; + } + case 22: { + const f0 = decIrExpr(r); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "downcast", value: f0, type: f1, loc: f2 }; + return o; + } + case 23: { + const f0 = decT64(r); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "dynArrLit", elems: f0, type: f1, loc: f2 }; + return o; + } + case 24: { + const f0 = decIrExpr(r); + const f1 = r.tag(2, "IrFunction.body[].arr.receiver") === 0 ? undefined : decIrExpr(r); + const f2 = r.str(); + const f3 = r.tag(2, "IrFunction.body[].arr.calleeNameValue") === 0 ? undefined : decIrExpr(r); + const f4 = decT64(r); + const f5 = r.tag(2, "IrFunction.body[].arr.spreads") === 0 ? undefined : decT66(r); + const f6 = decIrType(r); + const f7 = decSrcLoc(r); + const o: Extract = { kind: "dynCall", callee: f0, ...(f1 !== undefined ? { receiver: f1 } : {}), calleeName: f2, ...(f3 !== undefined ? { calleeNameValue: f3 } : {}), args: f4, ...(f5 !== undefined ? { spreads: f5 } : {}), type: f6, loc: f7 }; + return o; + } + case 25: { + const f0 = decIrExpr(r); + const f1 = r.tag(2, "IrFunction.body[].arr.preserveRefs") === 0 ? undefined : (r.bool() as NonNullable["preserveRefs"]>); + const f2 = r.tag(2, "IrFunction.body[].arr.adopt") === 0 ? undefined : (r.bool() as NonNullable["adopt"]>); + const f3 = r.tag(2, "IrFunction.body[].arr.exact") === 0 ? undefined : (r.bool() as NonNullable["exact"]>); + const f4 = r.tag(2, "IrFunction.body[].arr.inferredSite") === 0 ? undefined : r.str(); + const f5 = r.tag(2, "IrFunction.body[].arr.origin") === 0 ? undefined : r.num(); + const f6 = decIrType(r); + const f7 = decSrcLoc(r); + const o: Extract = { kind: "dynCheck", value: f0, ...(f1 !== undefined ? { preserveRefs: f1 } : {}), ...(f2 !== undefined ? { adopt: f2 } : {}), ...(f3 !== undefined ? { exact: f3 } : {}), ...(f4 !== undefined ? { inferredSite: f4 } : {}), ...(f5 !== undefined ? { origin: f5 } : {}), type: f6, loc: f7 }; + return o; + } + case 26: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = r.tag(2, "IrFunction.body[].arr.firstProp") === 0 ? undefined : r.str(); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "dynDestrCheck", value: f0, spelling: f1, ...(f2 !== undefined ? { firstProp: f2 } : {}), type: f3, loc: f4 }; + return o; + } + case 27: { + const f0 = decIrExpr(r); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const f3 = r.tag(2, "IrFunction.body[].arr.liveRef") === 0 ? undefined : (r.bool() as NonNullable["liveRef"]>); + const f4 = r.tag(2, "IrFunction.body[].arr.fnName") === 0 ? undefined : r.str(); + const f5 = r.tag(2, "IrFunction.body[].arr.fnLength") === 0 ? undefined : r.num(); + const o: Extract = { kind: "dynFrom", value: f0, type: f1, loc: f2, ...(f3 !== undefined ? { liveRef: f3 } : {}), ...(f4 !== undefined ? { fnName: f4 } : {}), ...(f5 !== undefined ? { fnLength: f5 } : {}) }; + return o; + } + case 28: { + const f0 = r.str(); + const f1 = r.tag(2, "IrFunction.body[].arr.negated") === 0 ? undefined : (r.bool() as NonNullable["negated"]>); + const f2 = decIrExpr(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "dynHasKey", key: f0, ...(f1 !== undefined ? { negated: f1 } : {}), value: f2, type: f3, loc: f4 }; + return o; + } + case 29: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = r.str(); + const f3 = r.tag(2, "IrFunction.body[].arr.calleeNameValue") === 0 ? undefined : decIrExpr(r); + const f4 = decT64(r); + const f5 = decIrType(r); + const f6 = decSrcLoc(r); + const o: Extract = { kind: "dynInvoke", recv: f0, method: f1, calleeName: f2, ...(f3 !== undefined ? { calleeNameValue: f3 } : {}), args: f4, type: f5, loc: f6 }; + return o; + } + case 30: { + const f0 = decIrExpr(r); + const f1 = r.num(); + const f2 = r.tag(2, "IrFunction.body[].arr.notIterableMessage") === 0 ? undefined : r.str(); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "dynIterN", value: f0, count: f1, ...(f2 !== undefined ? { notIterableMessage: f2 } : {}), type: f3, loc: f4 }; + return o; + } + case 31: { + const f0 = decIrExpr(r); + const f1 = r.tag(2, "IrFunction.body[].arr.optional") === 0 ? undefined : (r.bool() as NonNullable["optional"]>); + const f2 = decIrExpr(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "dynKeyGet", key: f0, ...(f1 !== undefined ? { optional: f1 } : {}), value: f2, type: f3, loc: f4 }; + return o; + } + case 32: { + const f0 = r.tag(2, "IrFunction.body[].arr.fields") === 0 ? undefined : decT67(r); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "dynObjLit", ...(f0 !== undefined ? { fields: f0 } : {}), type: f1, loc: f2 }; + return o; + } + case 33: { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const f2 = r.tag(2, "IrFunction.body[].arr.negated") === 0 ? undefined : (r.bool() as NonNullable["negated"]>); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "dynScalarEq", left: f0, right: f1, ...(f2 !== undefined ? { negated: f2 } : {}), type: f3, loc: f4 }; + return o; + } + case 34: { + const f0 = (r.str() as NonNullable["test"]>); + const f1 = r.tag(2, "IrFunction.body[].arr.bytesElem") === 0 ? undefined : (r.str() as NonNullable["bytesElem"]>); + const f2 = r.tag(2, "IrFunction.body[].arr.negated") === 0 ? undefined : (r.bool() as NonNullable["negated"]>); + const f3 = decIrExpr(r); + const f4 = decIrType(r); + const f5 = decSrcLoc(r); + const o: Extract = { kind: "dynTest", test: f0, ...(f1 !== undefined ? { bytesElem: f1 } : {}), ...(f2 !== undefined ? { negated: f2 } : {}), value: f3, type: f4, loc: f5 }; + return o; + } + case 35: { + const f0 = r.str(); + const f1 = decT64(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "ffiCall", import: f0, args: f1, type: f2, loc: f3 }; + return o; + } + case 36: { + const f0 = r.str(); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "fieldAbsent", unionId: f0, type: f1, loc: f2 }; + return o; + } + case 37: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = r.str(); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "fieldGet", obj: f0, className: f1, field: f2, type: f3, loc: f4 }; + return o; + } + case 38: { + const f0 = (r.str() as NonNullable["op"]>); + const f1 = r.bool(); + const f2 = decIrExpr(r); + const f3 = r.str(); + const f4 = r.str(); + const f5 = r.bool(); + const f6 = decIrType(r); + const f7 = decSrcLoc(r); + const o: Extract = { kind: "fieldIncDec", op: f0, prefix: f1, obj: f2, className: f3, field: f4, fieldDyn: f5, type: f6, loc: f7 }; + return o; + } + case 39: { + const f0 = (r.str() as NonNullable["mode"]>); + const f1 = decIrExpr(r); + const f2 = r.tag(2, "IrFunction.body[].arr.arg") === 0 ? null : decIrExpr(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "genResume", mode: f0, gen: f1, arg: f2, type: f3, loc: f4 }; + return o; + } + case 40: { + const f0 = (r.str() as NonNullable["op"]>); + const f1 = r.bool(); + const f2 = r.str(); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "incDec", op: f0, prefix: f1, localId: f2, type: f3, loc: f4 }; + return o; + } + case 41: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "instanceOf", value: f0, className: f1, type: f2, loc: f3 }; + return o; + } + case 42: { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "instanceOfValue", value: f0, classValue: f1, type: f2, loc: f3 }; + return o; + } + case 43: { + const f0 = (r.str() as NonNullable["name"]>); + const f1 = decT64(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "intrinsic", name: f0, args: f1, type: f2, loc: f3 }; + return o; + } + case 44: { + const f0 = decIrExpr(r); + const f1 = r.tag(2, "IrFunction.body[].arr.indent") === 0 ? undefined : r.str(); + const f2 = r.tag(2, "IrFunction.body[].arr.circular") === 0 ? undefined : (r.bool() as NonNullable["circular"]>); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "jsonStringify", value: f0, ...(f1 !== undefined ? { indent: f1 } : {}), ...(f2 !== undefined ? { circular: f2 } : {}), type: f3, loc: f4 }; + return o; + } + case 45: { + const f0 = (r.str() as NonNullable["fn"]>); + const f1 = decT64(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const f4 = r.tag(2, "IrFunction.body[].arr.prototypeIdentityOnly") === 0 ? undefined : (r.bool() as NonNullable["prototypeIdentityOnly"]>); + const f5 = r.tag(2, "IrFunction.body[].arr.realTimer") === 0 ? undefined : (r.bool() as NonNullable["realTimer"]>); + const f6 = r.tag(2, "IrFunction.body[].arr.assertOptions") === 0 ? undefined : decT68(r); + const o: Extract = { kind: "libCall", fn: f0, args: f1, type: f2, loc: f3, ...(f4 !== undefined ? { prototypeIdentityOnly: f4 } : {}), ...(f5 !== undefined ? { realTimer: f5 } : {}), ...(f6 !== undefined ? { assertOptions: f6 } : {}) }; + return o; + } + case 46: { + const f0 = (r.str() as NonNullable["op"]>); + const f1 = decIrExpr(r); + const f2 = decIrExpr(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "logical", op: f0, left: f1, right: f2, type: f3, loc: f4 }; + return o; + } + case 47: { + const f0 = (r.str() as NonNullable["method"]>); + const f1 = decIrExpr(r); + const f2 = decT64(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "mapIntrinsic", method: f0, receiver: f1, args: f2, type: f3, loc: f4 }; + return o; + } + case 48: { + const f0 = r.tag(2, "IrFunction.body[].arr.seed") === 0 ? undefined : decT69(r); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "mapNew", ...(f0 !== undefined ? { seed: f0 } : {}), type: f1, loc: f2 }; + return o; + } + case 49: { + const f0 = r.str(); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "moduleNsRef", moduleId: f0, type: f1, loc: f2 }; + return o; + } + case 50: { + const f0 = r.str(); + const f1 = decT64(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "new", className: f0, args: f1, type: f2, loc: f3 }; + return o; + } + case 51: { + const f0 = decIrExpr(r); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "newPromise", executor: f0, type: f1, loc: f2 }; + return o; + } + case 52: { + const f0 = decIrExpr(r); + const f1 = decT64(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "newValue", callee: f0, args: f1, type: f2, loc: f3 }; + return o; + } + case 53: { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "nullish", left: f0, right: f1, type: f2, loc: f3 }; + return o; + } + case 54: { + const f0 = r.num(); + const f1 = r.tag(2, "IrFunction.body[].arr.spelling") === 0 ? undefined : r.str(); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "numLit", value: f0, ...(f1 !== undefined ? { spelling: f1 } : {}), type: f2, loc: f3 }; + return o; + } + case 55: { + const f0 = r.str(); + const f1 = decIrExpr(r); + const f2 = decIrExpr(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "optChain", id: f0, receiver: f1, body: f2, type: f3, loc: f4 }; + return o; + } + case 56: { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const f2 = r.tag(2, "IrFunction.body[].arr.retag") === 0 ? undefined : r.str(); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "orDefault", left: f0, right: f1, ...(f2 !== undefined ? { retag: f2 } : {}), type: f3, loc: f4 }; + return o; + } + case 57: { + const f0 = decIrExpr(r); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "promiseVoidWiden", value: f0, type: f1, loc: f2 }; + return o; + } + case 58: { + const f0 = decIrType(r); + const f1 = decSrcLoc(r); + const o: Extract = { kind: "promiseWithResolvers", type: f0, loc: f1 }; + return o; + } + case 59: { + const f0 = decIrExpr(r); + const f1 = decT70(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "recordClone", source: f0, overrides: f1, type: f2, loc: f3 }; + return o; + } + case 60: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = r.str(); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "recordGet", obj: f0, shapeId: f1, field: f2, type: f3, loc: f4 }; + return o; + } + case 61: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = r.str(); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "recordHas", obj: f0, shapeId: f1, field: f2, type: f3, loc: f4 }; + return o; + } + case 62: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = decIrExpr(r); + const f3 = r.tag(2, "IrFunction.body[].arr.overflowOnly") === 0 ? undefined : (r.bool() as NonNullable["overflowOnly"]>); + const f4 = r.tag(2, "IrFunction.body[].arr.missNaN") === 0 ? undefined : (r.bool() as NonNullable["missNaN"]>); + const f5 = decIrType(r); + const f6 = decSrcLoc(r); + const o: Extract = { kind: "recordKeyGet", obj: f0, shapeId: f1, key: f2, ...(f3 !== undefined ? { overflowOnly: f3 } : {}), ...(f4 !== undefined ? { missNaN: f4 } : {}), type: f5, loc: f6 }; + return o; + } + case 63: { + const f0 = decT71(r); + const f1 = r.tag(2, "IrFunction.body[].arr.nullProto") === 0 ? undefined : (r.bool() as NonNullable["nullProto"]>); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "recordLit", fields: f0, ...(f1 !== undefined ? { nullProto: f1 } : {}), type: f2, loc: f3 }; + return o; + } + case 64: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "recordNullProto", obj: f0, shapeId: f1, type: f2, loc: f3 }; + return o; + } + case 65: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = decIrExpr(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "recordOvfHas", obj: f0, shapeId: f1, key: f2, type: f3, loc: f4 }; + return o; + } + case 66: { + const f0 = decIrExpr(r); + const f1 = r.str(); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "recordOvfKeys", obj: f0, shapeId: f1, type: f2, loc: f3 }; + return o; + } + case 67: { + const f0 = (r.str() as NonNullable["method"]>); + const f1 = decIrExpr(r); + const f2 = decT64(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "regexIntrinsic", method: f0, receiver: f1, args: f2, type: f3, loc: f4 }; + return o; + } + case 68: { + const f0 = r.str(); + const f1 = r.str(); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "regexLit", pattern: f0, flags: f1, type: f2, loc: f3 }; + return o; + } + case 69: { + const f0 = decIrType(r); + const f1 = decSrcLoc(r); + const o: Extract = { kind: "selfRef", type: f0, loc: f1 }; + return o; + } + case 70: { + const f0 = decT19(r); + const f1 = decIrExpr(r); + const f2 = r.tag(2, "IrFunction.body[].arr.generatorDelegate") === 0 ? undefined : (r.bool() as NonNullable["generatorDelegate"]>); + const f3 = r.tag(2, "IrFunction.body[].arr.diverges") === 0 ? undefined : (r.bool() as NonNullable["diverges"]>); + const f4 = decIrType(r); + const f5 = decSrcLoc(r); + const o: Extract = { kind: "seqExpr", stmts: f0, result: f1, ...(f2 !== undefined ? { generatorDelegate: f2 } : {}), ...(f3 !== undefined ? { diverges: f3 } : {}), type: f4, loc: f5 }; + return o; + } + case 71: { + const f0 = (r.str() as NonNullable["method"]>); + const f1 = decIrExpr(r); + const f2 = decT64(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "setIntrinsic", method: f0, receiver: f1, args: f2, type: f3, loc: f4 }; + return o; + } + case 72: { + const f0 = r.tag(2, "IrFunction.body[].arr.seed") === 0 ? undefined : decIrExpr(r); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "setNew", ...(f0 !== undefined ? { seed: f0 } : {}), type: f1, loc: f2 }; + return o; + } + case 73: { + const f0 = (r.str() as NonNullable["op"]>); + const f1 = decIrExpr(r); + const f2 = decIrExpr(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "strCmp", op: f0, left: f1, right: f2, type: f3, loc: f4 }; + return o; + } + case 74: { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "strConcat", left: f0, right: f1, type: f2, loc: f3 }; + return o; + } + case 75: { + const f0 = r.bool(); + const f1 = decIrExpr(r); + const f2 = decIrExpr(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "strEq", negated: f0, left: f1, right: f2, type: f3, loc: f4 }; + return o; + } + case 76: { + const f0 = (r.str() as NonNullable["method"]>); + const f1 = decIrExpr(r); + const f2 = decT64(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "strIntrinsic", method: f0, receiver: f1, args: f2, type: f3, loc: f4 }; + return o; + } + case 77: { + const f0 = r.str(); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "strLit", value: f0, type: f1, loc: f2 }; + return o; + } + case 78: { + const f0 = r.str(); + const f1 = decT8(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "templateStrings", key: f0, cooked: f1, type: f2, loc: f3 }; + return o; + } + case 79: { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const f2 = decIrExpr(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "ternary", cond: f0, then: f1, else_: f2, type: f3, loc: f4 }; + return o; + } + case 80: { + const f0 = decIrExpr(r); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "toBool", operand: f0, type: f1, loc: f2 }; + return o; + } + case 81: { + const f0 = decIrExpr(r); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "toString", operand: f0, type: f1, loc: f2 }; + return o; + } + case 82: { + const f0 = (r.str() as NonNullable["op"]>); + const f1 = decIrExpr(r); + const f2 = decIrType(r); + const f3 = decSrcLoc(r); + const o: Extract = { kind: "unary", op: f0, operand: f1, type: f2, loc: f3 }; + return o; + } + case 83: { + const f0 = r.str(); + const f1 = r.str(); + const f2 = decIrExpr(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "unionDisc", unionId: f0, field: f1, value: f2, type: f3, loc: f4 }; + return o; + } + case 84: { + const f0 = r.str(); + const f1 = r.bool(); + const f2 = r.bool(); + const f3 = decIrExpr(r); + const f4 = decIrExpr(r); + const f5 = decIrType(r); + const f6 = decSrcLoc(r); + const o: Extract = { kind: "unionEq", unionId: f0, negated: f1, sameValue: f2, left: f3, right: f4, type: f5, loc: f6 }; + return o; + } + case 85: { + const f0 = r.str(); + const f1 = r.num(); + const f2 = decIrExpr(r); + const f3 = decIrExpr(r); + const f4 = r.bool(); + const f5 = decIrType(r); + const f6 = decSrcLoc(r); + const o: Extract = { kind: "unionFuncEq", unionId: f0, tag: f1, union: f2, func: f3, negated: f4, type: f5, loc: f6 }; + return o; + } + case 86: { + const f0 = r.str(); + const f1 = r.num(); + const f2 = r.bool(); + const f3 = decIrExpr(r); + const f4 = decIrType(r); + const f5 = decSrcLoc(r); + const o: Extract = { kind: "unionIsTag", unionId: f0, tag: f1, negated: f2, value: f3, type: f4, loc: f5 }; + return o; + } + case 87: { + const f0 = r.str(); + const f1 = decIrExpr(r); + const f2 = decIrExpr(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "unionKeyGet", unionId: f0, key: f1, value: f2, type: f3, loc: f4 }; + return o; + } + case 88: { + const f0 = r.str(); + const f1 = r.num(); + const f2 = decIrExpr(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "unionNarrow", unionId: f0, tag: f1, value: f2, type: f3, loc: f4 }; + return o; + } + case 89: { + const f0 = r.str(); + const f1 = r.str(); + const f2 = decT65(r); + const f3 = decIrExpr(r); + const f4 = decIrType(r); + const f5 = decSrcLoc(r); + const o: Extract = { kind: "unionRetype", fromId: f0, unionId: f1, tags: f2, value: f3, type: f4, loc: f5 }; + return o; + } + case 90: { + const f0 = r.str(); + const f1 = r.num(); + const f2 = decIrExpr(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const o: Extract = { kind: "unionWrap", unionId: f0, tag: f1, value: f2, type: f3, loc: f4 }; + return o; + } + case 91: { + const f0 = (r.str() as NonNullable["unit"]>); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "unitLit", unit: f0, type: f1, loc: f2 }; + return o; + } + case 92: { + const f0 = decIrExpr(r); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "upcast", value: f0, type: f1, loc: f2 }; + return o; + } + case 93: { + const f0 = r.str(); + const f1 = decIrType(r); + const f2 = decSrcLoc(r); + const o: Extract = { kind: "varRef", localId: f0, type: f1, loc: f2 }; + return o; + } + case 94: { + const f0 = r.str(); + const f1 = r.str(); + const f2 = decT64(r); + const f3 = decIrType(r); + const f4 = decSrcLoc(r); + const f5 = r.tag(2, "IrFunction.body[].arr.targets") === 0 ? undefined : decT8(r); + const o: Extract = { kind: "virtualCall", className: f0, method: f1, args: f2, type: f3, loc: f4, ...(f5 !== undefined ? { targets: f5 } : {}) }; + return o; + } + case 95: { + const f0 = r.tag(2, "IrFunction.body[].arr.value") === 0 ? null : decIrExpr(r); + const f1 = r.tag(2, "IrFunction.body[].arr.awaited") === 0 ? undefined : (r.bool() as NonNullable["awaited"]>); + const f2 = r.tag(2, "IrFunction.body[].arr.delegated") === 0 ? undefined : (r.bool() as NonNullable["delegated"]>); + const f3 = r.tag(2, "IrFunction.body[].arr.captureCompletion") === 0 ? undefined : decT72(r); + const f4 = decIrType(r); + const f5 = decSrcLoc(r); + const o: Extract = { kind: "yieldExpr", value: f0, ...(f1 !== undefined ? { awaited: f1 } : {}), ...(f2 !== undefined ? { delegated: f2 } : {}), ...(f3 !== undefined ? { captureCompletion: f3 } : {}), type: f4, loc: f5 }; + return o; + } + } + throw new Error("IR codec: invalid IrExpr kind"); +} + +function encT54(w: IrWriter, v: NonNullable["cases"]>): void { + w.length(v.length); + for (const item of v) { + encT73(w, item); + } +} +function decT54(r: IrReader): NonNullable["cases"]> { + const length = r.length(); + const out: NonNullable["cases"]>[number][] = []; + for (let i = 0; i < length; i++) out.push(decT73(r)); + return out; +} + +function encT55(w: IrWriter, v: NonNullable[number]>): void { + if (typeof v === "object" && "globalId" in v) { + w.length(0); + encT74(w, v as Extract[number]>, { globalId: unknown }>); + return; + } + if (typeof v === "object" && "wellKnown" in v) { + w.length(1); + encT75(w, v as Extract[number]>, { wellKnown: unknown }>); + return; + } + throw new Error("IR codec: no arm of T55 matches"); +} +function decT55(r: IrReader): NonNullable[number]> { + switch (r.length()) { + case 0: + return decT74(r) as NonNullable[number]>; + case 1: + return decT75(r) as NonNullable[number]>; + } + throw new Error("IR codec: invalid T55 arm"); +} + +function encT56(w: IrWriter, v: NonNullable[number]>): void { + w.keys(Object.keys(v), KEYS196); + w.str(v.name); + encIrType(w, v.type); +} +function decT56(r: IrReader): NonNullable[number]> { + const f0 = r.str(); + const f1 = decIrType(r); + const o: NonNullable[number]> = { name: f0, type: f1 }; + return o; +} + +function encT57(w: IrWriter, v: NonNullable[number]>): void { + w.keys(Object.keys(v), KEYS197); + w.str(v.name); + encIrType(w, v.type); +} +function decT57(r: IrReader): NonNullable[number]> { + const f0 = r.str(); + const f1 = decIrType(r); + const o: NonNullable[number]> = { name: f0, type: f1 }; + return o; +} + +function encT58(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + encT76(w, item); + } +} +function decT58(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push(decT76(r)); + return out; +} + +function encT59(w: IrWriter, v: NonNullable[number]>): void { + w.keys(Object.keys(v), KEYS198); + w.str(v.path); + w.num(v.size); + w.str(v.sha256); + if (v.placeholder === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.placeholder); + } +} +function decT59(r: IrReader): NonNullable[number]> { + const f0 = r.str(); + const f1 = r.num(); + const f2 = r.str(); + const f3 = r.tag(2, "IrModule.nodeIsland.modules.files[].placeholder") === 0 ? undefined : (r.bool() as NonNullable[number]>["placeholder"]>); + const o: NonNullable[number]> = { path: f0, size: f1, sha256: f2, ...(f3 !== undefined ? { placeholder: f3 } : {}) }; + return o; +} + +function encT60(w: IrWriter, v: NonNullable[number]>): void { + w.keys(Object.keys(v), KEYS199); + w.str(v.path); + w.str(v.target); +} +function decT60(r: IrReader): NonNullable[number]> { + const f0 = r.str(); + const f1 = r.str(); + const o: NonNullable[number]> = { path: f0, target: f1 }; + return o; +} + +function encIrFfiParam(w: IrWriter, v: IrFfiParam): void { + if (typeof v === "string") { + w.length(0); + w.str(v); + return; + } + if (typeof v === "object" && "context" in v) { + w.length(1); + encIrFfiContextParam(w, v as Extract); + return; + } + if (typeof v === "object" && "callback" in v && "id" in (v as { callback: object }).callback) { + w.length(2); + encIrFfiCallbackParam(w, v as Extract); + return; + } + if (typeof v === "object" && "callback" in v && "release" in (v as { callback: object }).callback) { + w.length(3); + encIrFfiReleaseParam(w, v as Extract); + return; + } + throw new Error("IR codec: no arm of IrFfiParam matches"); +} +function decIrFfiParam(r: IrReader): IrFfiParam { + switch (r.length()) { + case 0: + return r.str() as IrFfiParam; + case 1: + return decIrFfiContextParam(r) as IrFfiParam; + case 2: + return decIrFfiCallbackParam(r) as IrFfiParam; + case 3: + return decIrFfiReleaseParam(r) as IrFfiParam; + } + throw new Error("IR codec: invalid IrFfiParam arm"); +} + +function encT62(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + w.str(item); + } +} +function decT62(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push((r.str() as NonNullable[number]>)); + return out; +} + +function encT63(w: IrWriter, v: NonNullable): void { + w.length(v.length); + for (const item of v) { + w.str(item); + } +} +function decT63(r: IrReader): NonNullable { + const length = r.length(); + const out: NonNullable[number][] = []; + for (let i = 0; i < length; i++) out.push((r.str() as NonNullable[number]>)); + return out; +} + +function encT64(w: IrWriter, v: NonNullable["args"]>): void { + w.length(v.length); + for (const item of v) { + encIrExpr(w, item); + } +} +function decT64(r: IrReader): NonNullable["args"]> { + const length = r.length(); + const out: NonNullable["args"]>[number][] = []; + for (let i = 0; i < length; i++) out.push(decIrExpr(r)); + return out; +} + +function encT65(w: IrWriter, v: NonNullable["spreads"]>): void { + w.length(v.length); + for (const item of v) { + w.num(item); + } +} +function decT65(r: IrReader): NonNullable["spreads"]> { + const length = r.length(); + const out: NonNullable["spreads"]>[number][] = []; + for (let i = 0; i < length; i++) out.push(r.num()); + return out; +} + +function encT66(w: IrWriter, v: NonNullable["spreads"]>): void { + w.length(v.length); + for (const item of v) { + encT80(w, item); + } +} +function decT66(r: IrReader): NonNullable["spreads"]> { + const length = r.length(); + const out: NonNullable["spreads"]>[number][] = []; + for (let i = 0; i < length; i++) out.push(decT80(r)); + return out; +} + +function encT67(w: IrWriter, v: NonNullable["fields"]>): void { + w.length(v.length); + for (const item of v) { + encT81(w, item); + } +} +function decT67(r: IrReader): NonNullable["fields"]> { + const length = r.length(); + const out: NonNullable["fields"]>[number][] = []; + for (let i = 0; i < length; i++) out.push(decT81(r)); + return out; +} + +function encT68(w: IrWriter, v: NonNullable["assertOptions"]>): void { + w.keys(Object.keys(v), KEYS200); + if (v.diff === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.diff); + } + if (v.skipPrototype === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.skipPrototype); + } +} +function decT68(r: IrReader): NonNullable["assertOptions"]> { + const f0 = r.tag(2, "IrFunction.body[].arr.assertOptions.diff") === 0 ? undefined : (r.str() as NonNullable["assertOptions"]>["diff"]>); + const f1 = r.tag(2, "IrFunction.body[].arr.assertOptions.skipPrototype") === 0 ? undefined : (r.bool() as NonNullable["assertOptions"]>["skipPrototype"]>); + const o: NonNullable["assertOptions"]> = { ...(f0 !== undefined ? { diff: f0 } : {}), ...(f1 !== undefined ? { skipPrototype: f1 } : {}) }; + return o; +} + +function encT69(w: IrWriter, v: NonNullable["seed"]>): void { + w.length(v.length); + for (const item of v) { + encT82(w, item); + } +} +function decT69(r: IrReader): NonNullable["seed"]> { + const length = r.length(); + const out: NonNullable["seed"]>[number][] = []; + for (let i = 0; i < length; i++) out.push(decT82(r)); + return out; +} + +function encT70(w: IrWriter, v: NonNullable["overrides"]>): void { + w.length(v.length); + for (const item of v) { + encT83(w, item); + } +} +function decT70(r: IrReader): NonNullable["overrides"]> { + const length = r.length(); + const out: NonNullable["overrides"]>[number][] = []; + for (let i = 0; i < length; i++) out.push(decT83(r)); + return out; +} + +function encT71(w: IrWriter, v: NonNullable["fields"]>): void { + w.length(v.length); + for (const item of v) { + encT84(w, item); + } +} +function decT71(r: IrReader): NonNullable["fields"]> { + const length = r.length(); + const out: NonNullable["fields"]>[number][] = []; + for (let i = 0; i < length; i++) out.push(decT84(r)); + return out; +} + +function encT72(w: IrWriter, v: NonNullable["captureCompletion"]>): void { + w.keys(Object.keys(v), KEYS201); + encIrType(w, v.returnType); +} +function decT72(r: IrReader): NonNullable["captureCompletion"]> { + const f0 = decIrType(r); + const o: NonNullable["captureCompletion"]> = { returnType: f0 }; + return o; +} + +function encT73(w: IrWriter, v: NonNullable["cases"]>[number]>): void { + w.keys(Object.keys(v), KEYS202); + if (v.test === null) w.byte(0); + else { + w.byte(1); + encIrExpr(w, v.test); + } + encT19(w, v.body); +} +function decT73(r: IrReader): NonNullable["cases"]>[number]> { + const f0 = r.tag(2, "IrFunction.body[].cases[].test") === 0 ? null : decIrExpr(r); + const f1 = decT19(r); + const o: NonNullable["cases"]>[number]> = { test: f0, body: f1 }; + return o; +} + +function encT74(w: IrWriter, v: Extract[number]>, { globalId: unknown }>): void { + w.keys(Object.keys(v), KEYS203); + w.str(v.field); + w.str(v.globalId); +} +function decT74(r: IrReader): Extract[number]>, { globalId: unknown }> { + const f0 = r.str(); + const f1 = r.str(); + const o: Extract[number]>, { globalId: unknown }> = { field: f0, globalId: f1 }; + return o; +} + +function encT75(w: IrWriter, v: Extract[number]>, { wellKnown: unknown }>): void { + w.keys(Object.keys(v), KEYS204); + w.str(v.field); + w.str(v.wellKnown); +} +function decT75(r: IrReader): Extract[number]>, { wellKnown: unknown }> { + const f0 = r.str(); + const f1 = r.str(); + const o: Extract[number]>, { wellKnown: unknown }> = { field: f0, wellKnown: f1 }; + return o; +} + +function encT76(w: IrWriter, v: NonNullable[number]>): void { + w.keys(Object.keys(v), KEYS205); + w.num(v.tag); + encT85(w, v.values); +} +function decT76(r: IrReader): NonNullable[number]> { + const f0 = r.num(); + const f1 = decT85(r); + const o: NonNullable[number]> = { tag: f0, values: f1 }; + return o; +} + +function encIrFfiContextParam(w: IrWriter, v: IrFfiContextParam): void { + w.keys(Object.keys(v), KEYS206); + w.str(v.context); +} +function decIrFfiContextParam(r: IrReader): IrFfiContextParam { + const f0 = r.str(); + const o: IrFfiContextParam = { context: f0 }; + return o; +} + +function encIrFfiCallbackParam(w: IrWriter, v: IrFfiCallbackParam): void { + w.keys(Object.keys(v), KEYS207); + encT86(w, v.callback); +} +function decIrFfiCallbackParam(r: IrReader): IrFfiCallbackParam { + const f0 = decT86(r); + const o: IrFfiCallbackParam = { callback: f0 }; + return o; +} + +function encIrFfiReleaseParam(w: IrWriter, v: IrFfiReleaseParam): void { + w.keys(Object.keys(v), KEYS208); + encT87(w, v.callback); +} +function decIrFfiReleaseParam(r: IrReader): IrFfiReleaseParam { + const f0 = decT87(r); + const o: IrFfiReleaseParam = { callback: f0 }; + return o; +} + +function encT80(w: IrWriter, v: NonNullable["spreads"]>[number]>): void { + w.keys(Object.keys(v), KEYS209); + w.num(v.arg); + w.str(v.what); +} +function decT80(r: IrReader): NonNullable["spreads"]>[number]> { + const f0 = r.num(); + const f1 = r.str(); + const o: NonNullable["spreads"]>[number]> = { arg: f0, what: f1 }; + return o; +} + +function encT81(w: IrWriter, v: NonNullable["fields"]>[number]>): void { + w.keys(Object.keys(v), KEYS210); + encIrExpr(w, v.key); + encIrExpr(w, v.value); +} +function decT81(r: IrReader): NonNullable["fields"]>[number]> { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const o: NonNullable["fields"]>[number]> = { key: f0, value: f1 }; + return o; +} + +function encT82(w: IrWriter, v: NonNullable["seed"]>[number]>): void { + w.keys(Object.keys(v), KEYS211); + encIrExpr(w, v.key); + encIrExpr(w, v.value); +} +function decT82(r: IrReader): NonNullable["seed"]>[number]> { + const f0 = decIrExpr(r); + const f1 = decIrExpr(r); + const o: NonNullable["seed"]>[number]> = { key: f0, value: f1 }; + return o; +} + +function encT83(w: IrWriter, v: NonNullable["overrides"]>[number]>): void { + w.keys(Object.keys(v), KEYS212); + w.str(v.name); + encIrExpr(w, v.value); +} +function decT83(r: IrReader): NonNullable["overrides"]>[number]> { + const f0 = r.str(); + const f1 = decIrExpr(r); + const o: NonNullable["overrides"]>[number]> = { name: f0, value: f1 }; + return o; +} + +function encT84(w: IrWriter, v: NonNullable["fields"]>[number]>): void { + w.keys(Object.keys(v), KEYS213); + w.str(v.name); + encIrExpr(w, v.value); + if (v.overflow === undefined) w.byte(0); + else { + w.byte(1); + w.bool(v.overflow); + } +} +function decT84(r: IrReader): NonNullable["fields"]>[number]> { + const f0 = r.str(); + const f1 = decIrExpr(r); + const f2 = r.tag(2, "IrFunction.body[].arr.fields[].overflow") === 0 ? undefined : (r.bool() as NonNullable["fields"]>[number]>["overflow"]>); + const o: NonNullable["fields"]>[number]> = { name: f0, value: f1, ...(f2 !== undefined ? { overflow: f2 } : {}) }; + return o; +} + +function encT85(w: IrWriter, v: NonNullable[number]>["values"]>): void { + w.length(v.length); + for (const item of v) { + encT88(w, item); + } +} +function decT85(r: IrReader): NonNullable[number]>["values"]> { + const length = r.length(); + const out: NonNullable[number]>["values"]>[number][] = []; + for (let i = 0; i < length; i++) out.push(decT88(r)); + return out; +} + +function encT86(w: IrWriter, v: NonNullable): void { + w.keys(Object.keys(v), KEYS214); + w.str(v.id); + encT89(w, v.params); + w.str(v.returns); + w.str(v.lifetime); + w.str(v.invoke); +} +function decT86(r: IrReader): NonNullable { + const f0 = r.str(); + const f1 = decT89(r); + const f2 = (r.str() as NonNullable["returns"]>); + const f3 = (r.str() as NonNullable["lifetime"]>); + const f4 = (r.str() as NonNullable["invoke"]>); + const o: NonNullable = { id: f0, params: f1, returns: f2, lifetime: f3, invoke: f4 }; + return o; +} + +function encT87(w: IrWriter, v: NonNullable): void { + w.keys(Object.keys(v), KEYS215); + w.str(v.release); + encT89(w, v.params); + w.str(v.returns); +} +function decT87(r: IrReader): NonNullable { + const f0 = r.str(); + const f1 = decT89(r); + const f2 = (r.str() as NonNullable["returns"]>); + const o: NonNullable = { release: f0, params: f1, returns: f2 }; + return o; +} + +function encT88(w: IrWriter, v: NonNullable[number]>["values"]>[number]>): void { + if (typeof v === "string") { + w.length(0); + w.str(v); + return; + } + if (typeof v === "number") { + w.length(1); + w.num(v); + return; + } + if (typeof v === "boolean") { + w.length(2); + w.bool(v); + return; + } + throw new Error("IR codec: no arm of T88 matches"); +} +function decT88(r: IrReader): NonNullable[number]>["values"]>[number]> { + switch (r.length()) { + case 0: + return r.str() as NonNullable[number]>["values"]>[number]>; + case 1: + return r.num() as NonNullable[number]>["values"]>[number]>; + case 2: + return r.bool() as NonNullable[number]>["values"]>[number]>; + } + throw new Error("IR codec: invalid T88 arm"); +} + +function encT89(w: IrWriter, v: NonNullable["params"]>): void { + w.length(v.length); + for (const item of v) { + encT90(w, item); + } +} +function decT89(r: IrReader): NonNullable["params"]> { + const length = r.length(); + const out: NonNullable["params"]>[number][] = []; + for (let i = 0; i < length; i++) out.push(decT90(r)); + return out; +} + +function encT90(w: IrWriter, v: NonNullable["params"]>[number]>): void { + if (typeof v === "string") { + w.length(0); + w.str(v); + return; + } + if (true) { + w.length(1); + encIrFfiContextParam(w, v as Exclude["params"]>[number]>, string | number | boolean | readonly unknown[]>); + return; + } + throw new Error("IR codec: no arm of T90 matches"); +} +function decT90(r: IrReader): NonNullable["params"]>[number]> { + switch (r.length()) { + case 0: + return r.str() as NonNullable["params"]>[number]>; + case 1: + return decIrFfiContextParam(r) as NonNullable["params"]>[number]>; + } + throw new Error("IR codec: invalid T90 arm"); +} diff --git a/packages/compiler/src/ir/codec.test.ts b/packages/compiler/src/ir/codec.test.ts new file mode 100644 index 000000000..538bd85de --- /dev/null +++ b/packages/compiler/src/ir/codec.test.ts @@ -0,0 +1,235 @@ +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 "../index.js"; +import { + decodeFunctionBinary, + decodeModuleBinary, + encodeFunctionBinary, + encodeFunctionInOrder, + encodeFunctionStrings, + encodeModuleBinary, + functionDigest, +} from "./codec.js"; +import { IR_CODEC_LAYOUT } from "./codec.generated.js"; +import { F64, STRING, VOID, type IrFunction, type IrModule } from "./ir.js"; +import { deserializeModuleBytes } from "./serialize.js"; + +// Programs with many node kinds: classes, closures, async, generators, +// dynamic values, streams, records, regexes and lone surrogates. +const PROGRAMS = [ + "1003-json-parse-unions.ts", + "114-live-record-delete.ts", + "1200-regex-test-basics.ts", + "1631-inspect-arrays.ts", + "4900-console-instances.ts", + "4991-lone-surrogate-halves.ts", + "5612-readable-from-iterables.cjs", + "7110-als-completion-callbacks.ts", + "7321-speculative-loop-bail-sites.ts", +]; + +/** Whether `a` and `b` hold the same data: the same keys with defined + * values, in any order, and numbers equal by Object.is. */ +function sameData(a: unknown, b: unknown): boolean { + if (typeof a === "number" && typeof b === "number") return Object.is(a, b); + if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return a === b; + if (Array.isArray(a) !== Array.isArray(b)) return false; + const record = (value: object) => value as Record; + const keys = (value: object) => + Object.keys(value) + .filter((key) => record(value)[key] !== undefined) + .sort(); + const ka = keys(a); + const kb = keys(b); + return ( + ka.length === kb.length && + ka.every((key, index) => key === kb[index] && sameData(record(a)[key], record(b)[key])) + ); +} + +test.each(PROGRAMS)("%s round-trips through the binary encoding", async (program) => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-codec-")); + try { + const outPath = join(dir, "main.ir.json"); + const entry = join(import.meta.dirname, "../../../../tests/corpus", program); + 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 bytes = encodeModuleBinary(mod); + const back = decodeModuleBinary(bytes); + expect(sameData(mod, back)).toBe(true); + expect(Buffer.compare(encodeModuleBinary(back), bytes)).toBe(0); + // The JSON reader takes the binary encoding too. + expect(sameData(deserializeModuleBytes(bytes), mod)).toBe(true); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +const loc = { file: "/src/main.ts", start: 0, end: 1 }; +const fn = (body: IrFunction["body"]): IrFunction => ({ + name: "%main", + params: [], + locals: [], + body, + returnType: VOID, + loc, +}); + +test("numbers keep NaN, the infinities, -0 and integers outside int32", () => { + const values = [ + NaN, + Infinity, + -Infinity, + -0, + 0, + 1, + -1, + 2 ** 31, + -(2 ** 31) - 1, + 2 ** 53, + 0.1, + -1.5e300, + ]; + const source = fn( + values.map((value) => ({ + kind: "exprStmt", + expr: { kind: "numLit", value, type: F64, loc }, + loc, + })), + ); + const back = decodeFunctionBinary(encodeFunctionBinary(source)); + const read = back.body.map((s) => + s.kind === "exprStmt" && s.expr.kind === "numLit" ? s.expr.value : null, + ); + expect(read.length).toBe(values.length); + read.forEach((value, index) => expect(Object.is(value, values[index])).toBe(true)); +}); + +test("strings keep lone surrogates and repeat through the string table", () => { + const texts = ["plain", "\ud83d", "a\udc00b", "😀", "plain", "é"]; + const source = fn( + texts.map((value) => ({ + kind: "exprStmt", + expr: { kind: "strLit", value, type: STRING, loc }, + loc, + })), + ); + const back = decodeFunctionBinary(encodeFunctionBinary(source)); + expect( + back.body.map((s) => (s.kind === "exprStmt" && s.expr.kind === "strLit" ? s.expr.value : null)), + ).toEqual(texts); +}); + +test("absent optional fields stay absent", () => { + const source: IrModule = { + irVersion: 17, + sourceFile: "/src/main.ts", + functions: [fn([])], + entry: "%main", + }; + const back = decodeModuleBinary(encodeModuleBinary(source)); + expect(Object.keys(back).sort()).toEqual(["entry", "functions", "irVersion", "sourceFile"]); + expect(Object.keys(back.functions[0]!).sort()).toEqual(Object.keys(source.functions[0]!).sort()); +}); + +test("a function whose objects follow the declared field order reads back with its key order", () => { + // As lowering builds functions: the source name after the name, and the + // async flag set after the literal. + const source: IrFunction = { + name: "%f", + sourceName: "f", + ownsPrototype: true, + params: [], + returnType: VOID, + locals: [{ id: "x.0", name: "x", type: F64, mutable: true }], + body: [{ kind: "return", value: null, loc }], + loc, + }; + source.async = true; + const bytes = encodeFunctionInOrder(source); + expect(bytes).not.toBeNull(); + const back = decodeFunctionBinary(bytes!); + expect(JSON.stringify(back)).toBe(JSON.stringify(source)); + expect(Object.keys(back)).toEqual(Object.keys(source)); +}); + +test("a function with an object out of the declared field order has no in-order encoding", () => { + const source = fn([{ kind: "return", value: null, loc }]); + // `loc` first: spreading the rest keeps its place. + const { loc: first, ...rest } = source; + const shuffled = { loc: first, ...rest } as IrFunction; + expect(encodeFunctionInOrder(shuffled)).toBeNull(); + // An object with a key its type does not declare. + expect(encodeFunctionInOrder({ ...source, extra: 1 } as IrFunction)).toBeNull(); + // The helper above builds `locals` before `returnType`. + expect(encodeFunctionInOrder(source)).toBeNull(); + const ordered: IrFunction = { + name: source.name, + params: [], + returnType: VOID, + locals: [], + body: source.body, + loc, + }; + expect(encodeFunctionInOrder(ordered)).not.toBeNull(); +}); + +test("a node whose kind its union does not declare is refused", () => { + const source = fn([{ kind: "noSuchStatement", loc } as never]); + expect(encodeFunctionInOrder(source)).toBeNull(); + expect(() => encodeFunctionBinary(source)).toThrow(/cannot encode a IrStmt kind noSuchStatement/); +}); + +test("an encoding lists its strings and the files its fields named file hold", () => { + const other = { file: "/src/other.ts", start: 2, end: 3 }; + const source: IrFunction = { + name: "%f", + params: [], + returnType: VOID, + locals: [], + body: [ + { + kind: "exprStmt", + expr: { kind: "strLit", value: "%named", type: STRING, loc: other }, + loc, + }, + ], + loc, + }; + const encoded = encodeFunctionStrings(source)!; + expect(encoded.strings).toEqual(expect.arrayContaining(["%f", "%named", "/src/main.ts"])); + expect([...encoded.files].sort()).toEqual(["/src/main.ts", "/src/other.ts"]); + expect(Buffer.compare(encoded.bytes, encodeFunctionInOrder(source)!)).toBe(0); +}); + +test("a function's digest follows its content and not its key order", () => { + const source = fn([{ kind: "return", value: null, loc }]); + const digest = functionDigest(source); + expect(digest).toMatch(/^\d+:[0-9a-f]{16}$/); + expect(functionDigest(fn([{ kind: "return", value: null, loc }]))).toBe(digest); + // The same fields in another order. + const { loc: first, ...rest } = source; + expect(functionDigest({ loc: first, ...rest } as IrFunction)).toBe(digest); + // A change in place. + source.locals.push({ id: "x.0", name: "x", type: F64, mutable: true }); + expect(functionDigest(source)).not.toBe(digest); + // A node the encoder refuses is covered by the function's JSON text. + const refused = fn([{ kind: "noSuchStatement", loc } as never]); + const text = functionDigest(refused); + expect(text).toMatch(/^json:/); + (refused.body[0] as unknown as { extra: number }).extra = 1; + expect(functionDigest(refused)).not.toBe(text); +}); + +test("bytes another layout wrote are refused", () => { + const bytes = Buffer.from(encodeFunctionBinary(fn([]))); + // The layout digest follows the magic as the first new string. + const at = bytes.indexOf(Buffer.from(IR_CODEC_LAYOUT)); + bytes[at] = bytes[at] === 0x30 ? 0x31 : 0x30; + expect(() => decodeFunctionBinary(bytes)).toThrow(/layout/); + expect(() => decodeFunctionBinary(Buffer.from("{}"))).toThrow(/not an IR encoding/); +}); diff --git a/packages/compiler/src/ir/codec.ts b/packages/compiler/src/ir/codec.ts new file mode 100644 index 000000000..301533903 --- /dev/null +++ b/packages/compiler/src/ir/codec.ts @@ -0,0 +1,376 @@ +/* The IR's binary encoding: the fields of every node and record, by name in + * the order ir.ts declares them (codec.generated.ts), written without field + * names or JSON structure. + * + * - A length, a union's member or arm index and a string reference are + * unsigned varints (7 bits per byte, low bits first). + * - A number is one byte, 0 for an integer in the int32 range other than + * -0, followed by its zigzag varint, or 1 followed by its IEEE 754 bytes + * (little-endian), which keep NaN, the infinities and -0. + * - A string is a varint: 0 for a new string, which also gives it the next + * index in the string table, otherwise one more than the index of a + * string the encoding already has. A new string's bytes follow, after a + * varint of twice their count: plus one when they are UTF-16LE code units + * (a string with a lone surrogate, which UTF-8 cannot spell), else UTF-8. + * - A boolean is one byte. + * - A field that can be absent or null starts with a byte: 0 absent, then 1 + * for null and 2 for a value when both are possible, otherwise 1 for a + * value. + * + * An encoding starts with MAGIC and IR_CODEC_LAYOUT, a digest of the + * generated coders, so a reader refuses bytes that other coders wrote. Decoded objects carry their fields in + * declaration order, with absent optional fields left out; an object read + * back has the key order of the object written when IrWriter.inOrder held. */ +import type { IrFunction, IrModule } from "./ir.js"; +import { + IR_CODEC_LAYOUT, + decodeIrFunction, + decodeIrModule, + encodeIrFunction, + encodeIrModule, +} from "./codec.generated.js"; + +/** The first bytes of an encoding: "SCIR". */ +const MAGIC = [0x53, 0x43, 0x49, 0x52]; + +/** Whether `value` has a surrogate code unit without its pair. */ +function loneSurrogate(value: string): boolean { + for (let index = 0; index < value.length; index++) { + const unit = value.charCodeAt(index); + if (unit < 0xd800 || unit > 0xdfff) continue; + const next = index + 1 < value.length ? value.charCodeAt(index + 1) : 0; + if (unit > 0xdbff || next < 0xdc00 || next > 0xdfff) return true; + index++; + } + return false; +} + +export class IrWriter { + private bytes = Buffer.allocUnsafe(1 << 16); + private size = 0; + private readonly strings = new Map(); + /** The values of the string fields named `file` written so far. */ + readonly files = new Set(); + private readonly scratch = Buffer.allocUnsafe(8); + /** Whether every object written so far reads back with its own key order + * (`keys`). False once one held a key its type does not declare, or its + * keys in another order than the declaration's. */ + inOrder = true; + + /** Notes an object's own keys against its type's declared fields. The + * decoder builds an object with its fields in declaration order, so an + * object whose keys come in another order, or include one the type does + * not declare, does not read back as it was. */ + keys(own: readonly string[], declared: readonly string[]): void { + if (!this.inOrder) return; + let at = 0; + for (const key of own) { + while (at < declared.length && declared[at] !== key) at++; + if (at === declared.length) { + this.inOrder = false; + return; + } + at++; + } + } + + private reserve(count: number): void { + if (this.size + count <= this.bytes.length) return; + let capacity = this.bytes.length * 2; + while (capacity < this.size + count) capacity *= 2; + const grown = Buffer.allocUnsafe(capacity); + this.bytes.copy(grown, 0, 0, this.size); + this.bytes = grown; + } + + byte(value: number): void { + this.reserve(1); + this.bytes[this.size++] = value; + } + + /** A non-negative integer below 2^53. */ + length(value: number): void { + this.reserve(8); + while (value >= 0x80) { + this.bytes[this.size++] = (value % 0x80) + 0x80; + value = Math.floor(value / 0x80); + } + this.bytes[this.size++] = value; + } + + bool(value: boolean): void { + this.byte(value ? 1 : 0); + } + + num(value: number): void { + if ( + Number.isInteger(value) && + value >= -0x80000000 && + value <= 0x7fffffff && + !Object.is(value, -0) + ) { + this.byte(0); + this.length(value < 0 ? -value * 2 - 1 : value * 2); + return; + } + this.byte(1); + this.scratch.writeDoubleLE(value, 0); + this.reserve(8); + this.scratch.copy(this.bytes, this.size, 0, 8); + this.size += 8; + } + + str(value: string): void { + const known = this.strings.get(value); + if (known !== undefined) { + this.length(known + 1); + return; + } + this.strings.set(value, this.strings.size); + this.length(0); + const utf8 = Buffer.byteLength(value, "utf8"); + // UTF-8 cannot spell a lone surrogate, so a string with one is written + // as its UTF-16 code units. + const units = utf8 !== value.length && loneSurrogate(value); + const count = units ? value.length * 2 : utf8; + this.length(count * 2 + (units ? 1 : 0)); + this.reserve(count); + if (units) this.bytes.write(value, this.size, count, "utf16le"); + else this.bytes.write(value, this.size, count, "utf8"); + this.size += count; + } + + /** What the encoding could not write, such as a node whose kind its + * union does not declare, or null. Such an encoding does not decode. */ + refused: string | null = null; + + /** Notes that `what` could not be written (`refused`). */ + refuse(what: string): void { + this.inOrder = false; + this.refused ??= what; + } + + /** A string field named `file`: a source file the encoding names. */ + file(value: string): void { + this.files.add(value); + this.str(value); + } + + /** Raw bytes, after their count. */ + blob(bytes: Uint8Array): void { + this.length(bytes.length); + this.reserve(bytes.length); + Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).copy(this.bytes, this.size); + this.size += bytes.length; + } + + /** The strings written so far, each once, in the order first written. */ + written(): IterableIterator { + return this.strings.keys(); + } + + /** Starts a new encoding in the same buffer. */ + reset(): void { + this.size = 0; + this.strings.clear(); + this.files.clear(); + this.inOrder = true; + this.refused = null; + } + + /** The bytes written so far. Throws when the encoding refused a value. */ + finish(): Buffer { + if (this.refused !== null) throw new Error(`IR codec: cannot encode a ${this.refused}`); + return this.bytes.subarray(0, this.size); + } +} + +export class IrReader { + private offset = 0; + private readonly strings: string[] = []; + /** The last presence byte `tag` read. */ + lastTag = 0; + + constructor(private readonly bytes: Buffer) {} + + private fail(what: string): never { + throw new Error(`IR codec: ${what} at byte ${this.offset}`); + } + + byte(): number { + if (this.offset >= this.bytes.length) this.fail("unexpected end"); + return this.bytes[this.offset++]!; + } + + length(): number { + let value = 0; + let scale = 1; + for (;;) { + const byte = this.byte(); + value += (byte % 0x80) * scale; + if (byte < 0x80) return value; + scale *= 0x80; + if (scale > 2 ** 56) this.fail("varint too long"); + } + } + + bool(): boolean { + return this.byte() !== 0; + } + + num(): number { + const tag = this.byte(); + if (tag === 0) { + const zigzag = this.length(); + return zigzag % 2 === 0 ? zigzag / 2 : -(zigzag + 1) / 2; + } + if (tag !== 1) this.fail("invalid number tag"); + if (this.offset + 8 > this.bytes.length) this.fail("unexpected end"); + const value = this.bytes.readDoubleLE(this.offset); + this.offset += 8; + return value; + } + + str(): string { + const reference = this.length(); + if (reference > 0) { + const known = this.strings[reference - 1]; + if (known === undefined) this.fail("invalid string reference"); + return known; + } + const spelled = this.length(); + const count = Math.floor(spelled / 2); + if (this.offset + count > this.bytes.length) this.fail("unexpected end"); + const value = + spelled % 2 === 0 + ? this.bytes.toString("utf8", this.offset, this.offset + count) + : this.bytes.toString("utf16le", this.offset, this.offset + count); + this.offset += count; + this.strings.push(value); + return value; + } + + /** Raw bytes blob wrote, as a view of the bytes read. */ + blob(): Buffer { + const count = this.length(); + if (this.offset + count > this.bytes.length) this.fail("unexpected end"); + const out = this.bytes.subarray(this.offset, this.offset + count); + this.offset += count; + return out; + } + + /** A presence byte below `limit`, kept in lastTag. */ + tag(limit: number, where: string): number { + const tag = this.byte(); + if (tag >= limit) this.fail(`invalid presence byte for ${where}`); + this.lastTag = tag; + return tag; + } + + /** Whether every byte was read. */ + done(): boolean { + return this.offset === this.bytes.length; + } +} + +function header(w: IrWriter): void { + for (const byte of MAGIC) w.byte(byte); + w.str(IR_CODEC_LAYOUT); +} + +function readHeader(r: IrReader): void { + for (const byte of MAGIC) if (r.byte() !== byte) throw new Error("IR codec: not an IR encoding"); + const layout = r.str(); + if (layout !== IR_CODEC_LAYOUT) + throw new Error( + `IR codec: the encoding has layout ${layout}, this compiler reads ${IR_CODEC_LAYOUT}`, + ); +} + +/** Whether `bytes` start like an encoding this module writes. */ +export function isIrEncoding(bytes: Uint8Array): boolean { + return MAGIC.every((byte, index) => bytes[index] === byte); +} + +export function encodeModuleBinary(mod: IrModule): Buffer { + const w = new IrWriter(); + header(w); + encodeIrModule(w, mod); + return w.finish(); +} + +export function decodeModuleBinary(bytes: Buffer): IrModule { + const r = new IrReader(bytes); + readHeader(r); + const mod = decodeIrModule(r); + if (!r.done()) throw new Error("IR codec: bytes after the module"); + return mod; +} + +export function encodeFunctionBinary(fn: IrFunction): Buffer { + const w = new IrWriter(); + header(w); + encodeIrFunction(w, fn); + return w.finish(); +} + +/** The writer encodeFunctionInOrder reuses: its result is a copy. */ +const inOrderWriter = new IrWriter(); + +/** `fn`'s encoding when it reads back as `fn` is, every object with its + * own key order (IrWriter.inOrder), or null. */ +export function encodeFunctionInOrder(fn: IrFunction): Buffer | null { + return encodeFunctionStrings(fn)?.bytes ?? null; +} + +/** encodeFunctionInOrder's encoding of `fn`, with the strings it holds, + * each once, and the files its fields named `file` hold (IrWriter.file), or + * null. */ +export function encodeFunctionStrings(fn: IrFunction): { + readonly bytes: Buffer; + readonly strings: readonly string[]; + readonly files: readonly string[]; +} | null { + const w = inOrderWriter; + w.reset(); + header(w); + encodeIrFunction(w, fn); + // An encoding that refused a value is not in order either. + return w.inOrder + ? { bytes: Buffer.from(w.finish()), strings: [...w.written()], files: [...w.files] } + : null; +} + +/** A 64-bit digest of `fn`'s content, the same for functions whose fields + * hold the same values in any key order, with the byte count of the + * content it covers. It covers the binary encoding, or the JSON text when + * the encoder refuses a value (IrWriter.refuse). */ +export function functionDigest(fn: IrFunction): string { + const w = inOrderWriter; + w.reset(); + encodeIrFunction(w, fn); + if (w.refused === null) return bytesDigest(w.finish()); + return `json:${bytesDigest(Buffer.from(JSON.stringify(fn), "utf8"))}`; +} + +/** Two 32-bit multiply-xor lanes over `bytes`, FNV-1a's and one with + * another offset and MurmurHash2's multiplier, and the byte count. */ +function bytesDigest(bytes: Uint8Array): string { + let a = 0x811c9dc5; + let b = 0x050c5d1f; + for (let i = 0; i < bytes.length; i++) { + const byte = bytes[i]!; + a = Math.imul(a ^ byte, 0x01000193); + b = Math.imul(b ^ byte, 0x5bd1e995); + } + const hex = (lane: number): string => (lane >>> 0).toString(16).padStart(8, "0"); + return `${bytes.length}:${hex(a)}${hex(b)}`; +} + +export function decodeFunctionBinary(bytes: Buffer): IrFunction { + const r = new IrReader(bytes); + readHeader(r); + const fn = decodeIrFunction(r); + if (!r.done()) throw new Error("IR codec: bytes after the function"); + return fn; +} diff --git a/packages/compiler/src/ir/fields.generated.ts b/packages/compiler/src/ir/fields.generated.ts index ae45b987b..124d4a483 100644 --- a/packages/compiler/src/ir/fields.generated.ts +++ b/packages/compiler/src/ir/fields.generated.ts @@ -802,14 +802,14 @@ export function walkExprFields(node: IrExpr, walker: IrFieldWalker, skipType: bo case "classRef": { walker.string(node.kind); walker.string(node.className); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } if (node.captures !== undefined && node.captures !== null) { for (const item6 of node.captures) { walker.string(item6); } } - if (!skipType) { - walkTypeFields(node.type, walker, skipType); - } return; } case "closure": { @@ -890,12 +890,12 @@ export function walkExprFields(node: IrExpr, walker: IrFieldWalker, skipType: bo case "dynFrom": { walker.string(node.kind); walkExprFields(node.value, walker, skipType); - if (node.fnName !== undefined && node.fnName !== null) { - walker.string(node.fnName); - } if (!skipType) { walkTypeFields(node.type, walker, skipType); } + if (node.fnName !== undefined && node.fnName !== null) { + walker.string(node.fnName); + } return; } case "dynHasKey": { @@ -1635,14 +1635,14 @@ export function walkStmtFields(node: IrStmt, walker: IrFieldWalker, skipType: bo } case "block": { walker.string(node.kind); - for (const item30 of node.body) { - walkStmtFields(item30, walker, skipType); - } if (node.labels !== undefined && node.labels !== null) { - for (const item31 of node.labels) { - walker.string(item31); + for (const item30 of node.labels) { + walker.string(item30); } } + for (const item31 of node.body) { + walkStmtFields(item31, walker, skipType); + } return; } case "break": { @@ -2111,7 +2111,7 @@ export type IrFieldsCurrent = [ Unread["negated"]>>, Unread, "kind" | "value" | "type" | "loc">>, Unread, "kind" | "id" | "type" | "loc">>, - Unread, "kind" | "className" | "captures" | "type" | "loc">>, + Unread, "kind" | "className" | "type" | "loc" | "captures">>, Unread, "kind" | "fnName" | "captures" | "adapts" | "type" | "loc">>, Unread["adapts"]>>, Unread, "kind" | "value" | "type" | "loc">>, @@ -2125,9 +2125,9 @@ export type IrFieldsCurrent = [ Unread["exact"]>>, Unread["origin"]>>, Unread, "kind" | "value" | "spelling" | "firstProp" | "type" | "loc">>, - Unread, "kind" | "value" | "fnName" | "fnLength" | "liveRef" | "type" | "loc">>, - Unread["fnLength"]>>, + Unread, "kind" | "value" | "type" | "loc" | "liveRef" | "fnName" | "fnLength">>, Unread["liveRef"]>>, + Unread["fnLength"]>>, Unread, "kind" | "key" | "negated" | "value" | "type" | "loc">>, Unread["negated"]>>, Unread, "kind" | "recv" | "method" | "calleeName" | "calleeNameValue" | "args" | "type" | "loc">>, @@ -2240,7 +2240,7 @@ export type IrFieldsCurrent = [ Unread, "kind" | "arr" | "index" | "loc">>, Unread, "kind" | "localId" | "value" | "initializes" | "loc">>, Unread["initializes"]>>, - Unread, "kind" | "body" | "labels" | "loc">>, + Unread, "kind" | "labels" | "body" | "loc">>, Unread, "kind" | "label" | "loc">>, Unread, "kind" | "arr" | "index" | "value" | "loc">>, Unread, "kind" | "label" | "loc">>, @@ -2264,9 +2264,9 @@ export type IrFieldsCurrent = [ Unread["cases"]>, readonly unknown[]>[number]>, object>, "test" | "body">>, Unread, "kind" | "disc" | "cases" | "labels" | "loc">>, Unread, "kind" | "value" | "loc">>, - Unread, "kind" | "tryBody" | "catchBody" | "catchLocalId" | "finallyBody" | "suppressFinallyErrors" | "dropFinallyErrors" | "loc">>, - Unread["suppressFinallyErrors"]>>, + Unread, "kind" | "tryBody" | "catchBody" | "catchLocalId" | "dropFinallyErrors" | "finallyBody" | "suppressFinallyErrors" | "loc">>, Unread["dropFinallyErrors"]>>, + Unread["suppressFinallyErrors"]>>, Unread, "kind" | "localId" | "init" | "loc">>, Unread, "kind" | "cond" | "body" | "labels" | "loc">>, Unread, "kind" | "elem" | "optionalElements">>, diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 63814a60c..c58d287fa 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1964,14 +1964,10 @@ export interface IrFunction { /** Original TS name (mangling is a backend concern). Lifted lambdas get * synthetic '%'-prefixed names ('%' can't appear in a TS identifier). */ name: string; - /** Generated reflection dispatch contains speculative source-member edges. - * Target-only host operations behind those edges retain runtime refusals. */ - speculativeDispatch?: boolean; - /** A read helper the backend can answer directly for dyn arrays and live - * array capsules, calling the helper only when that read has no answer. - * `index`: a computed read `(value, key)` whose callers pass a number - * converted to a string as the key. */ - fastRead?: "index"; + /** Source function name for native error stack frames. */ + sourceName?: string; + /** Ordinary authored function declarations/expressions own a prototype. */ + ownsPrototype?: true; params: IrParam[]; returnType: IrType; /** All locals including params, pre-collected and scope-flat: ids are @@ -1986,22 +1982,29 @@ export interface IrFunction { * their first (`this`) parameter. The slot indexes the class object's * capture array; no closure parameter is added to the method ABI. */ classCaptures?: (IrParam & { slot: number })[]; + body: IrStmt[]; + loc: SrcLoc; + /** The function's execution tier (ir/tiers.ts). Absent means static: the + * function may use dynamic values only through boundary operations + * (dynFrom, dynCheck, kind tests) and moves, which the validator enforces + * (invariant I1). "dynamic" admits the dynamic operation vocabulary + * (dynKeyGet, dynCall, dyn.* builtins, ...). Lowering assigns the field + * from the body (assignLegacyTiers); stage S2 of the two-tier design + * keeps it out of serialized IR, so loaders derive it again. */ + tier?: "dynamic"; + /** Generated reflection dispatch contains speculative source-member edges. + * Target-only host operations behind those edges retain runtime refusals. */ + speculativeDispatch?: boolean; + /** A read helper the backend can answer directly for dyn arrays and live + * array capsules, calling the helper only when that read has no answer. + * `index`: a computed read `(value, key)` whose callers pass a number + * converted to a string as the key. */ + fastRead?: "index"; /** Async: the body runs on a fiber; `returnType` is the INNER type T (a * `return v` fulfills with v) while ordinary call sites receive * Promise. With `generator` present, call sites receive the lazy * async-generator object instead. */ async?: true; - /** Async module initializers only: a module-global Promise slot where - * the spawn wrapper caches its first evaluation promise. Every later - * static/dynamic import receives that same promise, including while the - * first evaluation is suspended. */ - asyncCacheGlobal?: string; - /** Async cyclic module initializers only: the SCC's shared Promise - * slot. Eager recursive spawning writes member promises from the inside - * out, so the member that actually initiated evaluation writes last and - * becomes the runtime cycle root. Dynamic imports wait on this shared - * completion verdict instead of a build-time-selected member. */ - asyncCycleCacheGlobal?: string; /** Generator (`function*` / `async function*`): the body runs on a fiber created SUSPENDED * (nothing runs until the first `.next()`; backends may instead compile a * synchronous body as a stackless coroutine with the same protocol); `returnType` is the @@ -2030,20 +2033,17 @@ export interface IrFunction { * that generator's tag. */ inheritsTag?: true; }; - /** Source function name for native error stack frames. */ - sourceName?: string; - /** Ordinary authored function declarations/expressions own a prototype. */ - ownsPrototype?: true; - /** The function's execution tier (ir/tiers.ts). Absent means static: the - * function may use dynamic values only through boundary operations - * (dynFrom, dynCheck, kind tests) and moves, which the validator enforces - * (invariant I1). "dynamic" admits the dynamic operation vocabulary - * (dynKeyGet, dynCall, dyn.* builtins, ...). Lowering assigns the field - * from the body (assignLegacyTiers); stage S2 of the two-tier design - * keeps it out of serialized IR, so loaders derive it again. */ - tier?: "dynamic"; - body: IrStmt[]; - loc: SrcLoc; + /** Async module initializers only: a module-global Promise slot where + * the spawn wrapper caches its first evaluation promise. Every later + * static/dynamic import receives that same promise, including while the + * first evaluation is suspended. */ + asyncCacheGlobal?: string; + /** Async cyclic module initializers only: the SCC's shared Promise + * slot. Eager recursive spawning writes member promises from the inside + * out, so the member that actually initiated evaluation writes last and + * becomes the runtime cycle root. Dynamic imports wait on this shared + * completion verdict instead of a build-time-selected member. */ + asyncCycleCacheGlobal?: string; } export interface IrParam { @@ -2280,7 +2280,7 @@ export type IrStmt = /** A bare lexical block `{ ... }` — its own scope. `labels` makes it a * labeled-break target (`lbl: { ... break lbl; ... }` and the wrapper * the frontend puts around labeled non-loop statements). */ - | { kind: "block"; body: IrStmt[]; labels?: string[]; loc: SrcLoc } + | { kind: "block"; labels?: string[]; body: IrStmt[]; loc: SrcLoc } /** `throw v`. Any non-void value type can be thrown; ownership of a * refcounted value MOVES into the runtime's exception cell. Terminates * the path like `return`: control unwinds to the innermost enclosing @@ -2324,15 +2324,15 @@ export type IrStmt = tryBody: IrStmt[]; catchBody: IrStmt[] | null; catchLocalId: string | null; - finallyBody: IrStmt[] | null; - /** Resource-management cleanup: if the guarded body and cleanup both - * throw, raise a SuppressedError instead of replacing the first error. */ - suppressFinallyErrors?: true; /** Iterator closing: if the guarded body threw and the cleanup throws * too, the cleanup's error is dropped and the first one propagates. * After a generator .return() the cleanup's error replaces it, and on * normal exits it propagates. */ dropFinallyErrors?: true; + finallyBody: IrStmt[] | null; + /** Resource-management cleanup: if the guarded body and cleanup both + * throw, raise a SuppressedError instead of replacing the first error. */ + suppressFinallyErrors?: true; loc: SrcLoc; }; @@ -3341,7 +3341,7 @@ export type IrExpr = * every classRef, so a value's construct thunk always has a constructor * to call; backends emit class objects (and thunks) for exactly the * classes some classRef in the module names. */ - | { kind: "classRef"; className: string; captures?: string[]; type: IrType; loc: SrcLoc } + | { kind: "classRef"; className: string; type: IrType; loc: SrcLoc; captures?: string[] } /** `new X(args)` through a class VALUE: call the class object's * construct thunk. `callee` is classval-typed; args are completed * against `%.constructor`'s ABI — sound because every @@ -3513,13 +3513,13 @@ export type IrExpr = | { kind: "dynFrom"; value: IrExpr; + type: IrType; + loc: SrcLoc; + liveRef?: true; 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). diff --git a/packages/compiler/src/ir/serialize.ts b/packages/compiler/src/ir/serialize.ts index 185472b89..d8ff2e273 100644 --- a/packages/compiler/src/ir/serialize.ts +++ b/packages/compiler/src/ir/serialize.ts @@ -7,6 +7,7 @@ * text parts and decoded from bytes one function at a time; neither direction * materializes the complete document as a single string. */ import type { IrExpr, IrFunction, IrModule, IrStmt } from "./ir.js"; +import { decodeModuleBinary, isIrEncoding } from "./codec.js"; // Version 17 adds the dynamic tier's jsval type and js.* builtins (two-tier // design stage S3) and serializes each function's execution tier. @@ -268,10 +269,12 @@ function functionSpans(bytes: Buffer): FunctionSpans | null { return null; } -/** Decode a UTF-8 IR artifact (compact or readable) without materializing - * it as one string: the header and each function are parsed separately. +/** Decode an IR artifact: the binary encoding (codec.ts), or UTF-8 JSON + * (compact or readable) without materializing it as one string: the header + * and each function are parsed separately. * Split points are ASCII structural bytes, so every range is valid UTF-8. */ export function deserializeModuleBytes(bytes: Buffer): IrModule { + if (isIrEncoding(bytes)) return decodeModuleBinary(bytes); const spans = functionSpans(bytes); if (spans === null) return deserializeModule(bytes.toString("utf8")); const mod = checkedVersion( diff --git a/packages/compiler/src/native/cache.ts b/packages/compiler/src/native/cache.ts index 551a151a1..d18108afb 100644 --- a/packages/compiler/src/native/cache.ts +++ b/packages/compiler/src/native/cache.ts @@ -174,6 +174,8 @@ export class NativeCache { "binary", "dsym", "coverage", + "units", + "unit-files", ]) { const directory = join(this.root, family); let names: string[]; diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index bf496aa99..753ad1281 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -32,6 +32,7 @@ import { import { compilerOwnedFileRoots } from "../frontend/dts-paths.js"; import { provenanceSources } from "../frontend/provenance-registry.js"; import { processFork } from "../frontend/process-fork.js"; +import { unitReuseEnabled, type UnitStore } from "../frontend/lowering/unit-reuse.js"; import { tokenEquivalentSource, tokenPositionEquivalentSource, @@ -196,6 +197,21 @@ function equivalentFrontendInput( }; } +/** The unit store of a build whose unit key is `key`: its program entry + * under that key, and file entries by content digest. */ +function nativeUnitStore(cache: NativeCache, key: string): UnitStore { + return { + readProgram: () => cache.read("units", key), + writeProgram: (bytes) => cache.write("units", key, bytes), + readFile: (name) => (/^[0-9a-f]{64}$/.test(name) ? cache.read("unit-files", name) : null), + writeFile: (bytes) => { + const digest = contentDigest(bytes); + if (!cache.has("unit-files", digest)) cache.write("unit-files", digest, bytes, digest); + return digest; + }, + }; +} + function implementationIdentity(toolchain: NativeToolchain): string { // Release versions alone cannot distinguish two development builds. return contentDigest( @@ -221,6 +237,9 @@ const FRONTEND_INDEPENDENT_VARIABLES = new Set([ "SCRIPTC_CHECKER_SERVER", "SCRIPTC_EMIT_WORKERS", "SCRIPTC_EMIT_WORKER_FUNCTIONS", + // Reusing lowered units leaves the lowered module as it is. + "SCRIPTC_INCR_LOWERING", + "SCRIPTC_INCR_VERIFY", "SCRIPTC_TIMING", ]); @@ -372,22 +391,28 @@ export function prepareNativeExecutable( const exclusions = { outputPaths, outputDirectories: [...directories] }; const irInput = process.env[BOOTSTRAP_IR_INPUT] ?? ""; let key: string | null = null; + // The unit store's key (unit-reuse.ts): the frontend's inputs without the + // output paths, which lowering does not read. + let unitKey: string | null = null; // The entry this build replaces: its probes give the digests of unchanged files. let previous: FrontendInputSnapshot | undefined; if (cache !== null && irInput === "" && provenanceSources() === null) { try { - key = contentDigest( - JSON.stringify({ - schema: 1, - entry, - options, - ffi, - implementation: implementationIdentity(toolchain), - target: toolchain.target.name, - declarations: toolchain.declarationsRoot ?? null, - environment: frontendEnvironment(), - }), - ); + const inputs = { + schema: 1, + entry, + options, + ffi, + implementation: implementationIdentity(toolchain), + target: toolchain.target.name, + declarations: toolchain.declarationsRoot ?? null, + environment: frontendEnvironment(), + }; + key = contentDigest(JSON.stringify(inputs)); + if (unitReuseEnabled()) { + const { outPath: _outPath, outDir: _outDir, ...lowering } = options; + unitKey = contentDigest(JSON.stringify({ ...inputs, options: lowering })); + } const cached = readFrontendCache(cache, key, exclusions); if (cached !== null && "input" in cached) { timing("frontend-cache-hit"); @@ -413,7 +438,17 @@ export function prepareNativeExecutable( irInput !== "" ? preparedFromIr(irInput, entry, options) : tracker.runSynchronous(() => - prepareExecutableModule(entry, options, ffi, toolchain.target.platform, frontend, timing), + prepareExecutableModule( + entry, + options, + ffi, + toolchain.target.platform, + frontend, + timing, + new Map(), + true, + cache !== null && unitKey !== null ? nativeUnitStore(cache, unitKey) : null, + ), ); if (!prepared.ok) return prepared; if (irInput !== "") timing("ir-input"); diff --git a/packages/compiler/test/unit-reuse.test.ts b/packages/compiler/test/unit-reuse.test.ts new file mode 100644 index 000000000..80393cf0f --- /dev/null +++ b/packages/compiler/test/unit-reuse.test.ts @@ -0,0 +1,315 @@ +import { mkdtemp, readFile, readdir, 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 VARIABLES = [ + "SCRIPTC_CACHE_DIR", + "SCRIPTC_INCR_LOWERING", + "SCRIPTC_INCR_VERIFY", + "SCRIPTC_LOWER_JOURNAL_DUMP", +]; + +afterEach(() => { + for (const name of VARIABLES) delete process.env[name]; +}); + +/** A program in a scratch directory, built with unit reuse into its own + * cache and without it. */ +class Workspace { + private builds = 0; + constructor(readonly dir: string) {} + + static async create(files: Record): Promise { + const workspace = new Workspace(await mkdtemp(join(tmpdir(), "scriptc-unit-reuse-"))); + for (const [name, text] of Object.entries(files)) await workspace.write(name, text); + return workspace; + } + + write(name: string, text: string): Promise { + return writeFile(join(this.dir, name), text); + } + + async edit(name: string, from: string, to: string): Promise { + const text = await readFile(join(this.dir, name), "utf8"); + expect(text, `${name} spells ${from}`).toContain(from); + await this.write(name, text.replace(from, to)); + } + + private async lower(label: string, reuse: boolean, verify = false): Promise { + for (const name of VARIABLES) delete process.env[name]; + if (reuse) { + process.env["SCRIPTC_CACHE_DIR"] = join(this.dir, "cache"); + process.env["SCRIPTC_INCR_LOWERING"] = "1"; + process.env["SCRIPTC_LOWER_JOURNAL_DUMP"] = join(this.dir, label); + if (verify) process.env["SCRIPTC_INCR_VERIFY"] = "1"; + } + const outPath = join(this.dir, `${label}.ir.json`); + const result = await compile(join(this.dir, "main.ts"), { + outDir: join(this.dir, "out"), + outPath, + outputKind: "ir", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return readFile(outPath, "utf8"); + } + + /** The units with a record in the last build, recorded or applied. */ + units: string[] = []; + + /** Builds with reuse, checks the IR against a build without it, and + * returns the units whose stored records applied. A build in verify mode + * follows and must give the same IR. */ + async build(): Promise { + const label = `build-${this.builds++}`; + const reused = await this.lower(label, true); + expect(reused).toBe(await this.lower(`${label}-plain`, false)); + const applied: string[] = []; + this.units = []; + for (const line of (await readFile(join(this.dir, label, "records.jsonl"), "utf8")) + .trimEnd() + .split("\n")) { + // A unit without a record has a line with its reasons. + const record = JSON.parse(line) as { name: string; applied?: boolean; reasons?: string[] }; + if (record.reasons !== undefined) continue; + this.units.push(record.name); + if (record.applied === true) applied.push(record.name); + } + this.units.sort(); + expect(await this.lower(`${label}-verify`, true, true)).toBe(reused); + return applied.sort(); + } + + dispose(): Promise { + return rm(this.dir, { recursive: true, force: true }); + } +} + +const MODULES = { + "shapes.ts": [ + "export function area(width: number, height: number): number {", + " return width * height;", + "}", + "export function label(name: string): string {", + " return `shape ${name}`;", + "}", + "export class Box {", + " constructor(readonly width: number) {}", + " scaled(by: number): number {", + " return this.width * by;", + " }", + " named(): string {", + " return label(`box ${this.width}`);", + " }", + "}", + "", + ].join("\n"), + "report.ts": [ + 'import { Box, area } from "./shapes.js";', + "export function report(sizes: number[]): number {", + " let total = 0;", + " for (const size of sizes) total += area(size, size);", + " return total;", + "}", + "export function describe(box: Box): string {", + " return `${box.named()} ${box.scaled(2)}`;", + "}", + "export function twice(value: number): number {", + " return value * 2;", + "}", + "", + ].join("\n"), + "format.ts": [ + "export function pad(text: string, width: number): string {", + " return text.padStart(width);", + "}", + "", + ].join("\n"), + "main.ts": [ + 'import { Box, label } from "./shapes.js";', + 'import { describe, report, twice } from "./report.js";', + 'import { pad } from "./format.js";', + 'console.log(report([1, 2, 3]), describe(new Box(4)), label("main"), twice(3), pad("x", 4));', + "", + ].join("\n"), +}; + +/** Whether `applied` holds a unit named `name`. */ +const has = (applied: readonly string[], name: string): boolean => + applied.some((unit) => unit.endsWith(`.${name}`) || unit === name); + +test("an unchanged rebuild applies every unit's stored record", async () => { + const workspace = await Workspace.create(MODULES); + try { + // The first build into the cache records nothing; build()'s verify build + // after it finds the tables and records. + expect(await workspace.build()).toEqual([]); + const units = workspace.units; + expect(units.length).toBeGreaterThan(4); + expect(await workspace.build()).toEqual(units); + } finally { + await workspace.dispose(); + } +}, 300_000); + +test("a body edit lowers the units of its file and the units that spell its name", async () => { + const workspace = await Workspace.create(MODULES); + try { + await workspace.build(); + await workspace.edit("shapes.ts", "return width * height;", "return width * height + 1;"); + const applied = await workspace.build(); + // report() spells area(); twice() and pad() spell nothing whose + // declaration changed. The units of shapes.ts lower again. + expect(has(applied, "report")).toBe(false); + expect(has(applied, "twice")).toBe(true); + expect(has(applied, "pad")).toBe(true); + expect(has(applied, "scaled")).toBe(false); + } finally { + await workspace.dispose(); + } +}, 300_000); + +test("a signature edit lowers the units whose text spells a name it reaches", async () => { + const workspace = await Workspace.create(MODULES); + try { + await workspace.build(); + await workspace.edit( + "shapes.ts", + " named(): string {", + ' named(prefix = ""): string {\n if (prefix !== "") return prefix;', + ); + const applied = await workspace.build(); + // The member's surface changed, and with it the type of Box: the units + // that spell named or Box lower again, the others apply. + expect(has(applied, "named")).toBe(false); + expect(has(applied, "report")).toBe(true); + expect(has(applied, "twice")).toBe(true); + } finally { + await workspace.dispose(); + } +}, 300_000); + +test("an edit above every declaration of a file keeps the other files' units", async () => { + const workspace = await Workspace.create(MODULES); + try { + await workspace.build(); + await workspace.edit( + "shapes.ts", + "export function area", + "// A comment.\nexport function area", + ); + const applied = await workspace.build(); + expect(has(applied, "report")).toBe(true); + expect(has(applied, "twice")).toBe(true); + expect(has(applied, "scaled")).toBe(false); + } finally { + await workspace.dispose(); + } +}, 300_000); + +test("an added declaration keeps the units that do not spell its name", async () => { + const workspace = await Workspace.create(MODULES); + try { + await workspace.build(); + await workspace.edit( + "report.ts", + "export function describe", + "export function unused(): number {\n return 1;\n}\nexport function describe", + ); + const applied = await workspace.build(); + expect(has(applied, "scaled")).toBe(true); + expect(has(applied, "label")).toBe(true); + expect(has(applied, "pad")).toBe(true); + expect(has(applied, "twice")).toBe(false); + } finally { + await workspace.dispose(); + } +}, 300_000); + +test("units that use a module's namespace object lower again when its exports change", async () => { + const workspace = await Workspace.create({ + "values.ts": "export let count = 1;\nexport function bump(): void {\n count += 1;\n}\n", + "main.ts": [ + 'import * as values from "./values.js";', + "function keys(): string {", + ' return Object.keys(values).join(",");', + "}", + "function current(): number {", + " return values.count;", + "}", + "values.bump();", + "console.log(keys(), current());", + "", + ].join("\n"), + }); + try { + await workspace.build(); + expect(await workspace.build()).toEqual(workspace.units); + await workspace.write( + "values.ts", + "export let count = 1;\nexport function bump(): void {\n count += 1;\n}\nexport const added = 2;\n", + ); + // Both units spell the namespace binding, whose object gained a key. + const applied = await workspace.build(); + expect(has(applied, "keys")).toBe(false); + expect(has(applied, "current")).toBe(false); + } finally { + await workspace.dispose(); + } +}, 300_000); + +test("a unit whose record holds another file's positions lowers again when that file changes", async () => { + const workspace = await Workspace.create({ + "lib.ts": + 'export function add(a: number, b: number): number {\n return a + b;\n}\nexport const label = "lib";\n', + "main.ts": [ + 'import * as lib from "./lib.js";', + "function show(): string {", + " const { add: plus, label: name } = lib;", + " return `${name} ${plus(1, 2)}`;", + "}", + "function other(): number {", + " return 42;", + "}", + "console.log(show(), other());", + "", + ].join("\n"), + }); + try { + await workspace.build(); + await workspace.edit("lib.ts", "export function add", "// A comment.\nexport function add"); + // The build compares its IR, positions included, with a clean build's. + const applied = await workspace.build(); + expect(has(applied, "other")).toBe(true); + } finally { + await workspace.dispose(); + } +}, 300_000); + +test("the first build into a cache writes the declaration tables and no unit records", async () => { + const workspace = await Workspace.create(MODULES); + try { + const cache = join(workspace.dir, "cache"); + process.env["SCRIPTC_CACHE_DIR"] = cache; + process.env["SCRIPTC_INCR_LOWERING"] = "1"; + const build = async (): Promise => { + const result = await compile(join(workspace.dir, "main.ts"), { + outDir: join(workspace.dir, "out"), + outPath: join(workspace.dir, "main.ir.json"), + outputKind: "ir", + }); + expect(result.ok).toBe(true); + }; + const entries = async (family: string): Promise => + readdir(join(cache, "units-v1", family)).catch(() => []); + await build(); + expect(await entries("program")).toHaveLength(1); + expect(await entries("files")).toEqual([]); + await build(); + expect((await entries("files")).length).toBeGreaterThan(0); + } finally { + await workspace.dispose(); + } +}, 300_000); diff --git a/scripts/generate-ir-codec.mjs b/scripts/generate-ir-codec.mjs new file mode 100644 index 000000000..4cd04f6be --- /dev/null +++ b/scripts/generate-ir-codec.mjs @@ -0,0 +1,440 @@ +import { createHash } from "node:crypto"; +import { readFileSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +// Generates packages/compiler/src/ir/codec.generated.ts from the types in +// ir.ts: for every object type an IrModule can reach, an encoder that +// writes its fields by name in declaration order and a decoder that reads +// them back into an object literal of that type. Unions that `kind` +// discriminates are written as the member's index; other unions as the +// index of the arm a runtime test picks. The byte format is in codec.ts. +// `--check` fails when the file is stale. +const root = fileURLToPath(new URL("..", import.meta.url)); +const require = createRequire(join(root, "package.json")); +const ts = require("typescript"); +const irPath = join(root, "packages/compiler/src/ir/ir.ts"); +const target = join(root, "packages/compiler/src/ir/codec.generated.ts"); + +const program = ts.createProgram([irPath], { strict: true, target: ts.ScriptTarget.ES2022 }); +const checker = program.getTypeChecker(); +const source = program.getSourceFile(irPath); + +/** Exported type names of ir.ts, by the type they declare. */ +const exported = new Map(); +ts.forEachChild(source, (node) => { + if ( + (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) && + node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) && + node.typeParameters === undefined + ) { + const type = checker.getTypeAtLocation(node.name); + if (!exported.has(type)) exported.set(type, node.name.text); + } +}); +/** Exported names by the member set of the union or the single type they + * declare: a field of type `IrExpr | undefined` codes its IrExpr part with + * IrExpr's coder. */ +const exportedBy = new Map(); +for (const [type, name] of exported) { + const parts = type.isUnion() ? type.types : [type]; + const key = parts + .map((p) => p.id) + .sort((a, b) => a - b) + .join("|"); + if (!exportedBy.has(key)) exportedBy.set(key, name); +} +const partsKey = (parts) => + parts + .map((p) => p.id) + .sort((a, b) => a - b) + .join("|"); +const used = new Set(["IrModule", "IrFunction"]); +const moduleType = [...exported].find(([, name]) => name === "IrModule")[0]; +const functionType = [...exported].find(([, name]) => name === "IrFunction")[0]; + +const F = ts.TypeFlags; +const NULLISH = F.Null | F.Undefined | F.Void; +const STRING = F.StringLike; +const NUMBER = F.NumberLike; +const BOOLEAN = F.BooleanLike; + +/** An intersection no value has: `IrType & { kind: "record" }` distributes + * over IrType's members, and all but one give `kind` two literal types. */ +function impossible(type) { + return type.isIntersection() && checker.typeToString(type) === "never"; +} + +/** The parts of `type` that are not null or undefined, and which of the two + * it allows. */ +function split(type) { + const parts = type.isUnion() ? type.types : [type]; + return { + present: parts.filter((part) => (part.flags & (NULLISH | F.Never)) === 0 && !impossible(part)), + nullable: parts.some((part) => (part.flags & F.Null) !== 0), + undefinable: parts.some((part) => (part.flags & (F.Undefined | F.Void)) !== 0), + }; +} + +/** The literal value of `kind` on an object type, or null. */ +function kindOf(type) { + const kind = checker.getPropertyOfType(type, "kind"); + if (kind === undefined) return null; + const kindType = checker.getTypeOfSymbol(kind); + return kindType.isStringLiteral() ? kindType.value : null; +} + +/** How a value of the non-nullish `parts` is coded. */ +function shapeOf(parts, where) { + for (const part of parts) + if ((part.flags & (F.Any | F.Unknown)) !== 0) throw new Error(`untyped value at ${where}`); + const objects = parts.filter((part) => (part.flags & (STRING | NUMBER | BOOLEAN)) === 0); + const strings = parts.filter((part) => (part.flags & STRING) !== 0); + const numbers = parts.filter((part) => (part.flags & NUMBER) !== 0); + const booleans = parts.filter((part) => (part.flags & BOOLEAN) !== 0); + if (objects.length === 0) { + const scalarKinds = [strings, numbers, booleans].filter((list) => list.length > 0).length; + if (scalarKinds === 1) + return strings.length > 0 + ? { code: "string" } + : numbers.length > 0 + ? { code: "number" } + : { code: "bool" }; + } + if (objects.length === 1 && strings.length + numbers.length + booleans.length === 0) + return checker.isArrayType(objects[0]) + ? { code: "array", item: objects[0] } + : { code: "object", type: objects[0] }; + if ( + objects.length > 1 && + strings.length + numbers.length + booleans.length === 0 && + objects.every((part) => kindOf(part) !== null && !checker.isArrayType(part)) + ) + return { code: "kinds", members: objects }; + // Arms told apart at run time: typeof for scalars, Array.isArray, a + // `kind` literal, or a property only one object arm declares. + const arms = []; + if (strings.length > 0) arms.push({ test: "string", parts: strings }); + if (numbers.length > 0) arms.push({ test: "number", parts: numbers }); + if (booleans.length > 0) arms.push({ test: "boolean", parts: booleans }); + const arrays = objects.filter((part) => checker.isArrayType(part)); + if (arrays.length > 1) throw new Error(`several array types at ${where}`); + if (arrays.length === 1) arms.push({ test: "array", parts: arrays }); + const records = objects.filter((part) => !checker.isArrayType(part)); + for (const record of records) { + const own = checker.getPropertiesOfType(record).map((p) => p.name); + const others = records.filter((other) => other !== record); + const required = checker + .getPropertiesOfType(record) + .filter((p) => (p.flags & ts.SymbolFlags.Optional) === 0) + .map((p) => p.name); + let key = required.find((name) => + others.every((other) => checker.getPropertyOfType(other, name) === undefined), + ); + // A property every arm has, whose object value has a key only this + // arm's does (IrFfiParam's callback.id and callback.release). + if (key === undefined) + for (const outer of required) { + const inner = (t) => { + const p = checker.getPropertyOfType(t, outer); + return p === undefined ? undefined : split(checker.getTypeOfSymbol(p)).present; + }; + const mine = inner(record); + if (mine?.length !== 1) continue; + const found = checker + .getPropertiesOfType(mine[0]) + .filter((p) => (p.flags & ts.SymbolFlags.Optional) === 0) + .map((p) => p.name) + .find((name) => + others.every((other) => { + const theirs = inner(other); + return ( + theirs === undefined || + theirs.every((t) => checker.getPropertyOfType(t, name) === undefined) + ); + }), + ); + if (found !== undefined) { + key = `${outer}.${found}`; + break; + } + } + if (records.length > 1 && key === undefined) + throw new Error( + `no property tells ${checker.typeToString(record)} apart at ${where}: ${own.join(",")}`, + ); + arms.push({ test: records.length === 1 ? "object" : `key:${key}`, parts: [record] }); + } + return { code: "arms", arms }; +} + +// One encoder and one decoder per object type, union and array the module +// reaches, named by a number in the order they are first reached. +const coders = new Map(); +const order = []; +function coderFor(parts, typeExpr, where) { + const key = partsKey(parts); + const known = coders.get(key); + if (known !== undefined) return known; + const shape = shapeOf(parts, where); + if (shape.code === "string" || shape.code === "number" || shape.code === "bool") return shape; + const exportedName = exportedBy.get(key); + if (exportedName !== undefined) used.add(exportedName); + const name = exportedName ?? `T${order.length}`; + const coder = { ...shape, name, typeExpr: exportedName ?? typeExpr, where }; + coders.set(key, coder); + order.push(coder); + return coder; +} + +/** The type expression of `parts`, `typeExpr` naming the field or element + * type they come from. */ +function presentExpr(typeExpr) { + return `NonNullable<${typeExpr}>`; +} + +/** Lines that write `value` of `type`, named `typeExpr`. A string field + * named `file` is written with IrWriter.file, which notes it as a source + * file the encoding names. */ +function encodeValue(value, type, typeExpr, where, optional, field = null) { + const { present, nullable, undefinable } = split(type); + const coder = coderFor(present, presentExpr(typeExpr), where); + const write = + coder.code === "string" + ? `w.${field === "file" ? "file" : "str"}(${value});` + : coder.code === "number" + ? `w.num(${value});` + : coder.code === "bool" + ? `w.bool(${value});` + : `enc${coder.name}(w, ${value});`; + if (!nullable && !undefinable && !optional) return [write]; + if (nullable && (undefinable || optional)) + return [ + `if (${value} === undefined) w.byte(0);`, + `else if (${value} === null) w.byte(1);`, + `else {`, + ` w.byte(2);`, + ` ${write}`, + `}`, + ]; + const missing = nullable ? "null" : "undefined"; + return [`if (${value} === ${missing}) w.byte(0);`, `else {`, ` w.byte(1);`, ` ${write}`, `}`]; +} + +/** An expression that reads a value of `type`; null-checks are statements + * through `local`. */ +function decodeValue(type, typeExpr, where, optional) { + const { present, nullable, undefinable } = split(type); + const coder = coderFor(present, presentExpr(typeExpr), where); + // A scalar read is cast to its literal type (`17`, `true`, a union of + // string literals), which the bytes cannot spell. + const scalar = (call, plain) => + present.every((part) => part.flags === plain) ? call : `(${call} as ${presentExpr(typeExpr)})`; + const read = + coder.code === "string" + ? scalar("r.str()", F.String) + : coder.code === "number" + ? scalar("r.num()", F.Number) + : coder.code === "bool" + ? present.length === 2 + ? "r.bool()" + : `(r.bool() as ${presentExpr(typeExpr)})` + : `dec${coder.name}(r)`; + if (!nullable && !undefinable && !optional) return read; + if (nullable && (undefinable || optional)) + return `r.tag(3, "${where}") === 0 ? undefined : r.lastTag === 1 ? null : ${read}`; + return `r.tag(2, "${where}") === 0 ? ${nullable ? "null" : "undefined"} : ${read}`; +} + +/** Field reads and writes of one object type. */ +function objectLines(type, typeExpr, where, kind) { + const enc = []; + const dec = []; + const fields = []; + let index = 0; + for (const property of checker.getPropertiesOfType(type)) { + if (property.name === "kind" && kind !== null) continue; + const fieldType = checker.getTypeOfSymbol(property); + const optional = (property.flags & ts.SymbolFlags.Optional) !== 0; + const fieldExpr = `${typeExpr}[${JSON.stringify(property.name)}]`; + const at = `${where}.${property.name}`; + if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(property.name)) + throw new Error(`field name ${property.name} at ${where} is not an identifier`); + const access = `v.${property.name}`; + enc.push(...encodeValue(access, fieldType, fieldExpr, at, optional, property.name)); + const local = `f${index++}`; + dec.push(`const ${local} = ${decodeValue(fieldType, fieldExpr, at, optional)};`); + fields.push({ name: property.name, local, optional }); + } + // The object is built with its fields in declaration order, an absent + // optional field left out where it would stand: an object whose keys + // follow that order (IrWriter.keys) reads back with the same key order. + const literal = [ + ...(kind === null ? [] : [`kind: ${JSON.stringify(kind)}`]), + ...fields.map((f) => + f.optional + ? `...(${f.local} !== undefined ? { ${f.name}: ${f.local} } : {})` + : `${f.name}: ${f.local}`, + ), + ]; + dec.push(`const o: ${typeExpr} = { ${literal.join(", ")} };`); + dec.push("return o;"); + const declared = `KEYS${keyLists.length}`; + keyLists.push( + `const ${declared}: readonly string[] = ${JSON.stringify([...(kind === null ? [] : ["kind"]), ...fields.map((f) => f.name)])};`, + ); + enc.unshift(`w.keys(Object.keys(v), ${declared});`); + return { enc, dec }; +} + +/** The declared field names of each object type, for IrWriter.keys. */ +const keyLists = []; + +const body = []; +function emit(coder) { + const { name, typeExpr } = coder; + if (coder.code === "object") { + const { enc, dec } = objectLines(coder.type, typeExpr, coder.where, null); + body.push( + `function enc${name}(w: IrWriter, v: ${typeExpr}): void {`, + ...enc.map((l) => ` ${l}`), + "}", + `function dec${name}(r: IrReader): ${typeExpr} {`, + ...dec.map((l) => ` ${l}`), + "}", + "", + ); + } else if (coder.code === "array") { + const item = checker.getTypeArguments(coder.item)[0]; + const itemExpr = `${typeExpr}[number]`; + const at = `${coder.where}[]`; + const enc = encodeValue("item", item, itemExpr, at, false); + const dec = decodeValue(item, itemExpr, at, false); + body.push( + `function enc${name}(w: IrWriter, v: ${typeExpr}): void {`, + " w.length(v.length);", + " for (const item of v) {", + ...enc.map((l) => ` ${l}`), + " }", + "}", + `function dec${name}(r: IrReader): ${typeExpr} {`, + " const length = r.length();", + ` const out: ${typeExpr}[number][] = [];`, + ` for (let i = 0; i < length; i++) out.push(${dec});`, + " return out;", + "}", + "", + ); + } else if (coder.code === "kinds") { + const members = [...coder.members].sort((a, b) => (kindOf(a) < kindOf(b) ? -1 : 1)); + const enc = [`function enc${name}(w: IrWriter, v: ${typeExpr}): void {`, " switch (v.kind) {"]; + const dec = [`function dec${name}(r: IrReader): ${typeExpr} {`, ` switch (r.length()) {`]; + members.forEach((member, index) => { + const kind = kindOf(member); + const memberExpr = `Extract<${typeExpr}, { kind: ${JSON.stringify(kind)} }>`; + const lines = objectLines(member, memberExpr, `${coder.where}<${kind}>`, kind); + enc.push( + ` case ${JSON.stringify(kind)}: {`, + ` w.length(${index});`, + ...lines.enc.map((l) => ` ${l}`), + " return;", + " }", + ); + dec.push(` case ${index}: {`, ...lines.dec.map((l) => ` ${l}`), " }"); + }); + // A kind the union does not declare writes nothing that decodes. + enc.push(" }", ` w.refuse("${name} kind " + String((v as { kind: unknown }).kind));`, "}"); + dec.push(" }", ` throw new Error("IR codec: invalid ${name} kind");`, "}", ""); + body.push(...enc, ...dec); + } else if (coder.code === "arms") { + const enc = [`function enc${name}(w: IrWriter, v: ${typeExpr}): void {`]; + const dec = [`function dec${name}(r: IrReader): ${typeExpr} {`, " switch (r.length()) {"]; + coder.arms.forEach((arm, index) => { + const armExpr = + arm.test === "string" || arm.test === "number" || arm.test === "boolean" + ? `Extract<${typeExpr}, ${arm.test}>` + : arm.test === "array" + ? `Extract<${typeExpr}, readonly unknown[]>` + : arm.test === "object" + ? `Exclude<${typeExpr}, string | number | boolean | readonly unknown[]>` + : arm.test.includes(".") + ? `Extract<${typeExpr}, { ${arm.test.slice(4).split(".")[0]}: { ${arm.test.slice(4).split(".")[1]}: unknown } }>` + : `Extract<${typeExpr}, { ${arm.test.slice(4)}: unknown }>`; + const test = + arm.test === "string" || arm.test === "number" || arm.test === "boolean" + ? `typeof v === ${JSON.stringify(arm.test)}` + : arm.test === "array" + ? "Array.isArray(v)" + : arm.test === "object" + ? "true" + : arm.test.includes(".") + ? `typeof v === "object" && ${JSON.stringify(arm.test.slice(4).split(".")[0])} in v && ${JSON.stringify(arm.test.slice(4).split(".")[1])} in (v as { ${arm.test.slice(4).split(".")[0]}: object }).${arm.test.slice(4).split(".")[0]}` + : `typeof v === "object" && ${JSON.stringify(arm.test.slice(4))} in v`; + const parts = arm.parts; + const armCoder = coderFor(parts, armExpr, `${coder.where}|${index}`); + const write = + armCoder.code === "string" + ? "w.str(v);" + : armCoder.code === "number" + ? "w.num(v);" + : armCoder.code === "bool" + ? "w.bool(v);" + : `enc${armCoder.name}(w, v as ${armExpr});`; + const read = + armCoder.code === "string" + ? "r.str()" + : armCoder.code === "number" + ? "r.num()" + : armCoder.code === "bool" + ? "r.bool()" + : `dec${armCoder.name}(r)`; + enc.push(` if (${test}) {`, ` w.length(${index});`, ` ${write}`, " return;", " }"); + dec.push(` case ${index}:`, ` return ${read} as ${typeExpr};`); + }); + enc.push(` throw new Error("IR codec: no arm of ${name} matches");`, "}"); + dec.push(" }", ` throw new Error("IR codec: invalid ${name} arm");`, "}", ""); + body.push(...enc, ...dec); + } +} + +coderFor([moduleType], "IrModule", "IrModule"); +coderFor([functionType], "IrFunction", "IrFunction"); +for (let index = 0; index < order.length; index++) emit(order[index]); + +const lines = [ + "// Generated from packages/compiler/src/ir/ir.ts by scripts/generate-ir-codec.mjs.", + "// Run `pnpm generate:ir-codec` after changing the IR's types.", + `import type { ${[...used].sort().join(", ")} } from "./ir.js";`, + 'import type { IrReader, IrWriter } from "./codec.js";', + "", + "/** A digest of the coders below: an encoding written with other coders is", + " * refused instead of misread, also after a change that keeps IR_VERSION. */", + `export const IR_CODEC_LAYOUT = ${JSON.stringify(createHash("sha256").update(body.join("\n")).digest("hex").slice(0, 16))};`, + "", + "/** Writes `mod` field by field (codec.ts describes the bytes). */", + "export function encodeIrModule(w: IrWriter, mod: IrModule): void {", + " encIrModule(w, mod);", + "}", + "/** Reads a module encodeIrModule wrote. */", + "export function decodeIrModule(r: IrReader): IrModule {", + " return decIrModule(r);", + "}", + "/** Writes one function. */", + "export function encodeIrFunction(w: IrWriter, fn: IrFunction): void {", + " encIrFunction(w, fn);", + "}", + "/** Reads a function encodeIrFunction wrote. */", + "export function decodeIrFunction(r: IrReader): IrFunction {", + " return decIrFunction(r);", + "}", + "", + ...keyLists, + "", + ...body, +]; +const text = lines.join("\n"); +if (process.argv.includes("--check")) { + if (readFileSync(target, "utf8") !== text) + throw new Error("codec.generated.ts is stale; run node scripts/generate-ir-codec.mjs"); +} else writeFileSync(target, text); From 6434bbf33b53e8fe59a96873c696ee60439b9a82 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 00:22:23 -0500 Subject: [PATCH 17/70] Pack jammed saturating loops one copy per lane and test their exit every fourth iteration The jammed loop of jamSaturatingLoops now tests whether every accumulator is full only when `j & 3` is 3, and keeps the inner loop's own bound. Past the iteration where every accumulator is full, the copies' ORs leave the accumulators full and every other value they compute is dead, so running up to three more iterations changes no result. The copies' statements now alternate in the jammed loop, and each local's copies are added next to each other. The vectorizer pairs neighboring values of the loop's header, which were a copy's real and imaginary parts in mandelbrot; it now packs the same value of two copies into one vector, without lane shuffles. --- .../src/ir/saturating-loop-jams.test.ts | 17 +++++++- .../compiler/src/ir/saturating-loop-jams.ts | 39 ++++++++++++++++--- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/ir/saturating-loop-jams.test.ts b/packages/compiler/src/ir/saturating-loop-jams.test.ts index e25b17e80..25320b7a6 100644 --- a/packages/compiler/src/ir/saturating-loop-jams.test.ts +++ b/packages/compiler/src/ir/saturating-loop-jams.test.ts @@ -162,9 +162,24 @@ test("jams a constant-count loop whose inner loop exits once its accumulator is expect(breaks).toBe(1); const last = inner.body[inner.body.length - 1]!; expect(last.kind).toBe("if"); - // Each copy keeps its own accumulator and lanes; `post` runs in order. + // It is tested every fourth iteration: `(j & 3) === 3 && ...`. + if (last.kind !== "if" || last.cond.kind !== "logical") throw new Error("expected an && exit"); + const fourth = last.cond.left; + expect(fourth.kind === "bin" && fourth.op === "===" && fourth.left.kind === "bin").toBe(true); + if (fourth.kind === "bin" && fourth.left.kind === "bin") { + expect(fourth.left.op).toBe("&"); + expect(fourth.left.left.kind === "varRef" && fourth.left.left.localId).toBe("j"); + } + // The copies take turns statement by statement. + const firstDecls = inner.body.slice(0, 4); + expect(firstDecls.every((s) => s.kind === "varDecl")).toBe(true); + expect(new Set(firstDecls.map((s) => (s.kind === "varDecl" ? s.localId : ""))).size).toBe(4); + // Each copy keeps its own accumulator and lanes; each local's copies sit + // next to each other; `post` runs in order. const added = out.functions[0]!.locals.filter((local) => local.id.startsWith("%jam.")); expect(added.filter((local) => local.name === "%jam.b").length).toBe(4); + for (let at = 0; at < added.length; at += 4) + expect(new Set(added.slice(at, at + 4).map((local) => local.name)).size).toBe(1); const block = body[1]!; expect(block.kind).toBe("block"); if (block.kind !== "block") return; diff --git a/packages/compiler/src/ir/saturating-loop-jams.ts b/packages/compiler/src/ir/saturating-loop-jams.ts index 96973a483..74c204fea 100644 --- a/packages/compiler/src/ir/saturating-loop-jams.ts +++ b/packages/compiler/src/ir/saturating-loop-jams.ts @@ -329,7 +329,13 @@ function matchJam( * elements that answer NaN out of range, and write their own locals. `pre` * and the inner loop read nothing `post` writes, so running later `pre`s * and inner loops first changes no value. Independent iterations then - * overlap their dependence chains. */ + * overlap their dependence chains. + * + * For the same reason the jammed loop may run past the iteration where every + * accumulator is full: it tests that every fourth iteration (`j & 3` is 3) + * and keeps the inner loop's own bound. The copies' statements alternate, + * and each local's copies are added next to each other, so the vectorizer + * packs one copy per lane. */ export function jamSaturatingLoops(mod: IrModule): IrModule { let changed = false; const functions = mod.functions.map((fn): IrFunction => { @@ -378,9 +384,13 @@ function emitJam( const own = declared([...pre, ...inner.body]); const j = (inner.init as IrStmt & { kind: "varDecl" }).localId; own.delete(j); + // Each local's copies are added next to each other: the vectorizer pairs + // neighboring values of the loop's header, so one copy per lane. + const renames = values.map(() => new Map()); + for (const id of own) + for (const [copy, rename] of renames.entries()) rename.set(id, fresh(locals.get(id)!, copy)); const copies = values.map((value, copy) => { - const rename = new Map(); - for (const id of own) rename.set(id, fresh(locals.get(id)!, copy)); + const rename = renames[copy]!; const rewrite = (list: readonly IrStmt[]): IrStmt[] => transformStmtList(list as IrStmt[], { expr: (e) => { @@ -428,11 +438,30 @@ function emitJam( ? full : { kind: "logical", op: "&&", left: allFull, right: full, type: BOOL, loc }; } + const three: IrExpr = { kind: "numLit", value: 3, type: F64, loc }; + const counter3: IrExpr = { + kind: "bin", + op: "&", + left: { kind: "varRef", localId: j, type: F64, loc }, + right: three, + type: F64, + loc, + }; + // `(j & 3) === 3` + const fourth: IrExpr = { kind: "bin", op: "===", left: counter3, right: three, type: BOOL, loc }; const jammed: IrStmt = { ...inner, body: [ - ...copies.flatMap((copy) => copy.body), - { kind: "if", cond: allFull!, then: [{ kind: "break", loc }], else_: null, loc }, + // The copies take turns statement by statement; the exit is tested + // every fourth iteration. + ...copies[0]!.body.flatMap((_, k) => copies.map((copy) => copy.body[k]!)), + { + kind: "if", + cond: { kind: "logical", op: "&&", left: fourth, right: allFull!, type: BOOL, loc }, + then: [{ kind: "break", loc }], + else_: null, + loc, + }, ], }; return { From 85fdf4ce742606ab42c3012c2bc7ce11c0ebf8d5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 00:40:53 -0500 Subject: [PATCH 18/70] Trim the per-chunk work of compiled object-mode stream callbacks - A typed write or transform callback whose encoding parameter is a pointer union with an undefined arm receives the null pointer directly when an object-mode write passed no encoding, instead of converting the encoding value through a dyn box (scr_stream_chunk_encoding_undefined). - The stream completion callbacks carry SCR_CLOSURE_KIND_RECEIVER_FREE: their thunks never read `this`, so compiled callers skip the receiver bracket. - The completion thunks borrow the stream from the closure's box instead of retaining and releasing it on every call; the closure, alive for the call, holds the box. - settle_next, entry_convert and clear_buffer_each test their early-out conditions in inline wrappers before calling the full functions. A corpus program covers the encoding argument in object mode, byte mode and transforms. --- .../src/backend/llvm/expr-stream-callbacks.ts | 42 +++++++++++++++---- .../8520-stream-object-encoding-arg.ts.json | 6 +++ packages/runtime/src/scr_closure.h | 3 +- packages/runtime/src/scr_stream.c | 33 +++++++++++---- packages/runtime/src/scr_stream.h | 3 ++ .../corpus/8520-stream-object-encoding-arg.ts | 42 +++++++++++++++++++ 6 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8520-stream-object-encoding-arg.ts.json create mode 100644 tests/corpus/8520-stream-object-encoding-arg.ts diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 3d9d8da76..a90764464 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -279,8 +279,6 @@ export function streamDoneFnFor( host.declare( `declare void @${entry}(ptr, ptr${twoSlot ? ", ptr, ptr" : valueData ? ", ptr" : ""})`, ); - host.declare(`declare ptr @scr_box_get_ref(ptr)`); - host.declare(`declare void @scr_stream_release_v(ptr)`); host.declare(`declare ptr @scr_error_retain_v(ptr)`); host.declare(`declare void @scr_union_release(ptr)`); const params = [ @@ -295,7 +293,10 @@ export function streamDoneFnFor( ...(twoSlot ? [` %bslot = alloca ptr`, ` %sslot = alloca ptr`] : []), ` %capsp = getelementptr inbounds %ScrClosure, ptr %self, i64 1`, ` %bx = load ptr, ptr %capsp`, - ` %s = call ptr @scr_box_get_ref(ptr %bx) ; the stream, +1`, + // the stream, borrowed: the box holds it, and the closure (alive for + // the call) holds the box + ` %sp = getelementptr inbounds %ScrBox, ptr %bx, i64 0, i32 5`, + ` %s = load ptr, ptr %sp`, ` store ptr null, ptr %eslot`, ...(twoSlot ? [` store ptr null, ptr %bslot`, ` store ptr null, ptr %sslot`] : []), ]; @@ -347,7 +348,6 @@ export function streamDoneFnFor( } else { d.push(` call void @${entry}(ptr %s, ptr %ev) ; moves err; borrows s`); } - d.push(` call void @scr_stream_release_v(ptr %s)`); if (errT !== undefined) d.push(` call void ${releaseSym(host.shapeHost, errT)}(ptr %e)`); if (dataT !== undefined && isRefCounted(dataT)) d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`); @@ -868,18 +868,46 @@ export function streamCbThunkFor( host.declare(`declare void @scr_dyn_release(ptr)`); host.declare(`declare zeroext i1 @scr_exc_pending()`); if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`); + // An object-mode write without an encoding argument passes + // undefined: a pointer union whose unit arm is undefined holds it as + // the null pointer, so that case skips the conversion. + const pointer = p.kind === "union" ? host.nullableUnions.get(p.unionId) : null; + const undefinedIsNull = + pointer !== null && + p.kind === "union" && + host.unionsById.get(p.unionId)?.arms[pointer.unitTag]?.kind === "undefinedT"; + if (undefinedIsNull) { + host.declare(`declare zeroext i1 @scr_stream_chunk_encoding_undefined(ptr)`); + d.push( + ` br label %etchk${i}`, + `etchk${i}:`, + ` %eu${i} = call zeroext i1 @scr_stream_chunk_encoding_undefined(ptr %s)`, + ` br i1 %eu${i}, label %etok${i}, label %etconv${i}`, + `etconv${i}:`, + ); + } d.push( ` %ev${i} = call ptr @scr_stream_chunk_encoding_value(ptr %s)`, ` %et${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %ev${i}, ptr null)`, ` call void @scr_dyn_release(ptr %ev${i})`, ` %etbad${i} = call zeroext i1 @scr_exc_pending()`, - ` br i1 %etbad${i}, label %etfail${i}, label %etok${i}`, + ` br i1 %etbad${i}, label %etfail${i}, label %${undefinedIsNull ? "etconvok" : "etok"}${i}`, `etfail${i}:`, ...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []), ` ret void`, - `etok${i}:`, ); - passed.push(`${ty} %et${i}`); + if (undefinedIsNull) { + d.push( + `etconvok${i}:`, + ` br label %etok${i}`, + `etok${i}:`, + ` %etv${i} = phi ptr [ null, %etchk${i} ], [ %et${i}, %etconvok${i} ]`, + ); + passed.push(`${ty} %etv${i}`); + } else { + d.push(`etok${i}:`); + passed.push(`${ty} %et${i}`); + } } else if (isErrPos) { // destroy's error argument: `Error | null` — wrap type-directedly. // The finished/pipeline callback ("e") may declare `Error | null | diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8520-stream-object-encoding-arg.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8520-stream-object-encoding-arg.ts.json new file mode 100644 index 000000000..5c024208a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8520-stream-object-encoding-arg.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8520-stream-object-encoding-arg.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_closure.h b/packages/runtime/src/scr_closure.h index 89f0378af..ccf3bc529 100644 --- a/packages/runtime/src/scr_closure.h +++ b/packages/runtime/src/scr_closure.h @@ -95,7 +95,8 @@ void scr_box_set_ref(ScrBox *b, void *v); /* releases old, owns new */ #define SCR_CLOSURE_KIND_CHECKED_ADAPTER 8u /* a checked callable adapter: the callable is in caps[0] */ /* The body never observes the ambient receiver (the `this` an unbound call * binds), so compiled callers skip the receiver bracket around an indirect - * call. Set only by the compiler; the kind proper is + * call. Set by the compiler, and by the runtime on the stream completion + * callbacks, whose thunks never read it; the kind proper is * function_kind & SCR_CLOSURE_KIND_MASK. */ #define SCR_CLOSURE_KIND_RECEIVER_FREE 16u #define SCR_CLOSURE_KIND_MASK 15u diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 872586976..1f07d167b 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -541,6 +541,7 @@ void scr_stream_release_v(void *s) { scr_stream_release(s); } * the stream. Returns +1. */ static ScrClosure *scr_stream_done_new(ScrStream *s, void *fn) { ScrClosure *c = scr_closure_new(fn, 1); + c->function_kind |= SCR_CLOSURE_KIND_RECEIVER_FREE; /* the completion thunks never read `this` */ ScrBox *bx = scr_box_new_obj(scr_stream_retain_v, scr_stream_release_v, scr_stream_trace); c->caps[0] = bx; scr_box_set_ref(bx, scr_stream_retain(s)); @@ -752,7 +753,12 @@ static void scr_stream_do_destroy(ScrStream *s, ScrError *err /*borrowed*/); static void scr_stream_notify_finished(ScrStream *s); static void *scr_stream_read_n(ScrStream *s, double size); static void *scr_stream_read_n_kind(ScrStream *s, double size, ScrEntryKind *kind); -static void scr_stream_settle_next(ScrStream *s); +static void scr_stream_settle_next_waiter(ScrStream *s); +/* A for-await iterator's parked next(): only a stream with a waiter that + * is not mid-read has anything to settle. */ +static inline void scr_stream_settle_next(ScrStream *s) { + if (s->st->r.next_waiter && !s->st->r.next_reading) scr_stream_settle_next_waiter(s); +} static ScrStream *scr_stream_alloc(const ScrVt *vt, const char *cls, bool has_r, bool has_w, double rhwm, double whwm, bool auto_destroy, bool emit_close, bool allow_half_open); @@ -989,7 +995,12 @@ static void scr_stream_call_read(ScrStream *s) { * multi-byte tail held pending) adds nothing but still clears the * reading flag. */ static int scr_stream_add_entry(ScrStream *s, void *entry /*moves*/, ScrEntryKind kind); -static void *scr_stream_entry_convert(ScrStreamState *st, void *entry, ScrEntryKind *kind); +static void *scr_stream_entry_convert_kind(ScrStreamState *st, void *entry, ScrEntryKind *kind); +/* An entry already of the buffer's kind is kept as-is. */ +static inline void *scr_stream_entry_convert(ScrStreamState *st, void *entry, ScrEntryKind *kind) { + if (*kind == scr_stream_rkind(st)) return entry; + return scr_stream_entry_convert_kind(st, entry, kind); +} static bool scr_stream_add_chunk(ScrStream *s, ScrBytes *chunk, bool front) { ScrStreamState *st = s->st; @@ -1395,8 +1406,6 @@ static void scr_stream_next_fulfill(ScrStreamState *st, ScrPromise *w /*moves*/, scr_queue_microtask(job); } -static void scr_stream_settle_next(ScrStream *s); - /* One pass of createAsyncIterator's loop for the parked waiter: * `stream.read()` (a _read below the hwm, then the buffered chunk; pushes * the _read makes join it), else the settled states (error, EOF), else @@ -1442,7 +1451,7 @@ static void scr_stream_next_wake_job(ScrClosure *job) { * error rejects, buffered content fulfills (one whole entry in from * mode, the whole buffer otherwise — Node's iterator read()), EOF * fulfills the sentinel. Called from push/eof/destroy transitions. */ -static void scr_stream_settle_next(ScrStream *s) { +static void scr_stream_settle_next_waiter(ScrStream *s) { ScrStreamState *st = s->st; if (!st->r.next_waiter || st->r.next_reading) return; if (st->errored && !st->error_emitted) { @@ -1964,7 +1973,7 @@ static void scr_stream_rbuf_to_dyn(ScrStreamState *st) { * A Buffer on a stream with a decoder decodes through it (Node's * decoder.write in readableAddChunkPushObjectMode); an entry of another * kind turns the buffer into dyn entries. */ -static void *scr_stream_entry_convert(ScrStreamState *st, void *entry, ScrEntryKind *kind) { +static void *scr_stream_entry_convert_kind(ScrStreamState *st, void *entry, ScrEntryKind *kind) { ScrEntryKind want = scr_stream_rkind(st); if (*kind == want) return entry; if (*kind == SCR_ENTRY_BYTES && want == SCR_ENTRY_STR && st->r.decoding) { @@ -2390,7 +2399,7 @@ static void scr_stream_writev_batch(ScrStream *s) { } /* The buffered writes, one at a time (Node's clearBuffer without _writev). */ -static void scr_stream_clear_buffer_each(ScrStream *s) { +static void scr_stream_clear_buffer_entries(ScrStream *s) { ScrStreamState *st = s->st; if (st->w.buffer_processing) return; st->w.buffer_processing = true; @@ -2412,6 +2421,12 @@ static void scr_stream_clear_buffer_each(ScrStream *s) { st->w.buffer_processing = false; } +/* An empty write queue has nothing to clear. */ +static inline void scr_stream_clear_buffer_each(ScrStream *s) { + ScrStreamState *st = s->st; + if (st->w.head < st->w.n) scr_stream_clear_buffer_entries(s); +} + /* The fs streams' clearBuffer: nothing while constructing, and _writev * for more than one buffered chunk. */ static SCR_COLD void scr_stream_clear_buffer_runtime(ScrStream *s) { @@ -6698,6 +6713,10 @@ ScrDyn *scr_stream_chunk_encoding_value(ScrStream *s) { return enc ? scr_dyn_retain(enc) : scr_dyn_undefined(); } +bool scr_stream_chunk_encoding_undefined(ScrStream *s) { + return scr_stream_writes_values(s) && s->st->w.cur_enc == NULL; +} + /* ── the readable surface ─────────────────────────────────────────────── */ bool scr_stream_push(ScrStream *s, ScrBytes *chunk) { diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 578712df2..2ae392258 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -556,6 +556,9 @@ ScrBytes *scr_stream_chunk_bytes(ScrStream *s, ScrBytes *chunk); struct ScrDyn *scr_stream_chunk_value(ScrStream *s, ScrBytes *chunk); ScrStr *scr_stream_chunk_encoding(ScrStream *s); ScrDyn *scr_stream_chunk_encoding_value(ScrStream *s); +/* True when the write in progress is an object-mode write that passed no + * encoding argument (its encoding value is undefined). */ +bool scr_stream_chunk_encoding_undefined(ScrStream *s); /* emit('data', value) with a checked-dynamic chunk (borrowed). */ bool scr_stream_emit_value(ScrStream *s, ScrStr *name, struct ScrDyn *v); /* Whether a dyn for-await step (readable.nextChunkDyn) answered the end. */ diff --git a/tests/corpus/8520-stream-object-encoding-arg.ts b/tests/corpus/8520-stream-object-encoding-arg.ts new file mode 100644 index 000000000..8ef57ad7d --- /dev/null +++ b/tests/corpus/8520-stream-object-encoding-arg.ts @@ -0,0 +1,42 @@ +// The encoding argument a typed write or transform callback receives: +// undefined for an object-mode write without one, the given string with +// one, and 'buffer' for byte-mode chunks. +import { Transform, Writable } from "node:stream"; + +const seen: string[] = []; +const objects = new Writable({ + objectMode: true, + write(chunk: { n: number }, encoding, cb) { + seen.push(`object ${chunk.n} ${typeof encoding} ${String(encoding)}`); + cb(); + }, +}); +objects.write({ n: 1 }); +objects.write({ n: 2 }, "utf8"); +objects.write({ n: 3 }, undefined); +objects.end(() => { + const bytes = new Writable({ + write(chunk: Buffer, encoding, cb) { + seen.push(`bytes ${chunk.length} ${typeof encoding} ${String(encoding)}`); + cb(); + }, + }); + bytes.write("abc"); + bytes.write(Buffer.from("de")); + bytes.end(() => { + const upper = new Transform({ + objectMode: true, + transform(row: { n: number }, encoding, cb) { + seen.push(`transform ${row.n} ${typeof encoding} ${String(encoding)}`); + cb(null, { n: row.n * 10 }); + }, + }); + upper.on("data", (row: { n: number }) => seen.push(`data ${row.n}`)); + upper.on("end", () => { + for (const line of seen) console.log(line); + }); + upper.write({ n: 4 }); + upper.write({ n: 5 }, "latin1"); + upper.end(); + }); +}); From 511bf04cb9b68ae301d23a29487240e489c6760a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 00:57:09 -0500 Subject: [PATCH 19/70] Refresh the runtime profile The string spare, the main thread's idle trim and the shared-bytes writes changed runtime functions that hold enough of the committed profile's counts to fail its staleness check (7.3% against a 5% limit). The profile is collected again on this change with scripts/refresh-runtime-profile.mjs (327 raw profiles), with the LLVM helper built from this tree's sources. --- .../profiles/linux-x64-gnu.profdata.gz | Bin 89083 -> 67691 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/runtime-pack-common/profiles/linux-x64-gnu.profdata.gz b/packages/runtime-pack-common/profiles/linux-x64-gnu.profdata.gz index 3bc3ecc28448262364cb5647b5e2019301ffb1ea..5d3fe0f897b5738fb02df60760f079bac6e14346 100644 GIT binary patch literal 67691 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

qr%-kBbkmp!@3mwas4-{s0 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 R>Sr?2{|^*M&Vl$@1OO7Mzv=)0 literal 89083 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%0A;tyJF_a}%exuJ`+@SFw4g(0 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

J8L7_-dO3In!%~hY};4g_oC5uCxS*DyYooDPX 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 From 2a39a976997f6670f41471bfc4b82f30f57fe1a5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 01:32:04 -0500 Subject: [PATCH 20/70] Test the direct index once in number-key lookups, and inline the probe in loops --- .../compiler/src/backend/llvm/emit-module.ts | 268 ++++++++++-------- .../compiler/src/backend/llvm/loop-state.ts | 21 ++ .../backend/llvm/map-number-lookup.test.ts | 57 +++- .../src/backend/llvm/map-number-lookup.ts | 10 +- .../map-number-lookups-in-loops.ts.json | 6 + tests/corpus/map-number-lookups-in-loops.ts | 158 +++++++++++ .../map-number-lookups-in-loops.ts.json | 1 + 7 files changed, 393 insertions(+), 128 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/map-number-lookups-in-loops.ts.json create mode 100644 tests/corpus/map-number-lookups-in-loops.ts create mode 100644 tests/harness/corpus-costs/map-number-lookups-in-loops.ts.json diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 6f261d4bb..092a87bf1 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -1377,25 +1377,139 @@ 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. */ +/** The `.loop` lookup helpers only read memory and always return, as + * declared: LLVM then merges two lookups of one key (a has() followed by + * get()) before it inlines them. */ +const INLINED_LOOKUP_ATTRS = "alwaysinline memory(read) nounwind willreturn"; + +/** The number-key entry lookups (sc_map_entry_f64 and sc_map_entry_f64_int) + * and their hashed probes. With `loop` they are the `.loop` flavor, which + * lookups inside a loop call (emitNumberMapEntry): each of its functions is + * inlined, so a lookup that finds its key in a bucket table makes no call, + * and the loop keeps no argument moves or spills around one. */ +function numberMapEntryHelpers(sz: string, loop: boolean): string[] { + const flavor = loop ? ".loop" : ""; + const attrs = loop ? `${INLINED_LOOKUP_ATTRS} ${FN_ATTRS}` : FN_ATTRS; + let mapTmp = 0; + const mapIndex = exactIndexLines("%k", "%nd", sz, () => `%ix${mapTmp++}`); + return [ + `define internal ptr @sc_map_entry_f64${flavor}(ptr %m, double %k) ${attrs} {`, + `entry:`, + ` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`, + ...(sz === "i64" + ? [` %nd32 = load i32, ptr %ndp`, ` %nd = zext i32 %nd32 to i64`] + : [` %nd = load i32, ptr %ndp`]), + // A non-integral key inside the table is as absent as one outside it. + ...mapIndex.lines.map((line) => ` ${line}`), + ` br i1 ${mapIndex.ok}, label %slot, label %slow`, + `slot:`, + sz === "i64" + ? ` %i = add i64 ${mapIndex.wide}, 0` + : ` %i = trunc i64 ${mapIndex.wide} 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`, + `slow:`, + ` %hashed = call ptr @sc_map_entry_f64_hashed${flavor}(ptr %m, double %k)`, + ` 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${flavor}(ptr %m, i64 %ki, double %k) ${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 %slow`, + `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`, + `slow:`, + ` %hashed = call ptr @sc_map_entry_f64_hashed_int${flavor}(ptr %m, i64 %ki)`, + ` ret ptr %hashed`, + `}`, + ``, + // 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${flavor}`, + loop, + "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${flavor}`, + loop, + "i64 %ki", + sz, + [` %kf = sitofp i64 %ki to double`, ` %kb = bitcast double %kf to i64`], + "%kf", + ), + ]; +} + +/** The hashed probe of a number-key map (scr_map_find_f64), out of line, + * or inlined with `loop`: `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, and neither does a map with the direct index, whose lookups reach + * the probe only for a key outside the index, which is absent. In a small + * map 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, + loop: boolean, param: string, sz: string, keyLines: string[], runtimeKey: string, ): string[] { return [ - `define internal ptr @${name}(ptr %m, ${param}) noinline ${FN_ATTRS} {`, + `define internal ptr @${name}(ptr %m, ${param}) ${loop ? INLINED_LOOKUP_ATTRS : "noinline"} ${FN_ATTRS} {`, `entry:`, ...keyLines, ` %x1s = lshr i64 %kb, 30`, @@ -1415,7 +1529,14 @@ function hashedNumberProbe( ` %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`, + ` br i1 %linear, label %unbucketed, label %buckets`, + // A map without a bucket table either indexes its keys directly, and + // then holds none outside the index, or scans its entries. + `unbucketed:`, + ` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`, + ` %nd = load i32, ptr %ndp`, + ` %indexed = icmp ne i32 %nd, 0`, + ` br i1 %indexed, label %miss, label %filter`, `filter:`, ` %f1s = lshr i64 %h, 44`, ` %f1a = and i64 %f1s, 63`, @@ -1582,117 +1703,22 @@ export function helperDefs(emitter: LlEmitter): string[] { } // Number-key Map/Set lookups probe the runtime's direct index inline: // while ndense is nonzero every live key is an integer below it, so an - // in-range integral key reads its slot (entry index + 1, 0 = absent) and - // any other key is absent. Maps without the index (ndense 0) hash the - // key in sc_map_entry_f64_hashed, which scans the entries of a small map - // or probes a compact bucket table as scr_map_find_f64 does, and calls - // the runtime lookup for a wide table. It stays out of line so that the - // direct-index path stays small at every call site. Both only read - // memory, so LLVM merges lookups of one key when nothing is stored in - // between (a has() followed by get(), for example). - if (emitter.decls.has(NUMBER_MAP_ENTRY_DECL)) { - const sz = emitter.sizeType; - let mapTmp = 0; - const mapIndex = exactIndexLines("%k", "%nd", sz, () => `%ix${mapTmp++}`); + // in-range integral key reads its slot (entry index + 1, 0 = absent). + // Any other key goes to sc_map_entry_f64_hashed, which probes a compact + // bucket table as scr_map_find_f64 does, calls the runtime lookup for a + // wide table, and in a map without a bucket table answers absent when the + // map has the index (a map with buckets keeps no index, scr_map.h) and + // scans the entries otherwise. A lookup in a hashed map so tests the + // index once. The probe stays out of line so that the direct-index path + // stays small at every call site, except in the `.loop` flavor + // (numberMapEntryHelpers). Both only read memory, so LLVM merges lookups + // of one key when nothing is stored in between (a has() followed by + // get(), for example). + if (emitter.decls.has(NUMBER_MAP_ENTRY_DECL)) defs.push( - `define internal ptr @sc_map_entry_f64(ptr %m, double %k) ${FN_ATTRS} {`, - `entry:`, - ` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`, - ...(sz === "i64" - ? [` %nd32 = load i32, ptr %ndp`, ` %nd = zext i32 %nd32 to i64`] - : [` %nd = load i32, ptr %ndp`]), - // A non-integral key inside the table is as absent as one outside it. - ...mapIndex.lines.map((line) => ` ${line}`), - ` br i1 ${mapIndex.ok}, label %slot, label %outside`, - `slot:`, - sz === "i64" - ? ` %i = add i64 ${mapIndex.wide}, 0` - : ` %i = trunc i64 ${mapIndex.wide} 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 ${sz} %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`, - `}`, - ``, - // 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_int(ptr %m, i64 %ki)`, - ` ret ptr %hashed`, - `}`, - ``, - // 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", - ), + ...numberMapEntryHelpers(emitter.sizeType, false), + ...numberMapEntryHelpers(emitter.sizeType, true), ); - } // Map/Set iteration steps read the entries array inline: an index is // live when it is below the entry count and its entry's cached hash is // nonzero (a tombstone's is zero), as scr_map_iter_live decides, or for diff --git a/packages/compiler/src/backend/llvm/loop-state.ts b/packages/compiler/src/backend/llvm/loop-state.ts index ce9189a45..be33ae517 100644 --- a/packages/compiler/src/backend/llvm/loop-state.ts +++ b/packages/compiler/src/backend/llvm/loop-state.ts @@ -3,11 +3,19 @@ * local arrays they rely on. */ import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js"; import type { IrFunction, IrStmt } from "../../ir/ir.js"; +import { everyStmtList } from "../../ir/traverse.js"; import { findInitializedByteLoopBindings } from "./byte-windows.js"; import type { DenseArrayLoop } from "./dense-array-loops.js"; import type { LlEmitter } from "./emitter.js"; import { defineModuleState } from "./emitter-state.js"; +/** The most IR nodes a function may have for its in-loop number-key + * lookups to inline the hashed probe (emitNumberMapEntry). Each inlined + * probe adds code to its function, and LLVM's passes over a large function + * take longer for it; a small function's loop of lookups, such as a k-mer + * count, gains the most from it. */ +const MAX_INLINED_LOOKUP_NODES = 200; + /** Sets the loop state for `fn` at the start of its emission; `numericFn` * is `fn` with its initializer bindings (emitFunction). */ export function beginFunctionLoops( @@ -32,6 +40,15 @@ export function beginFunctionLoops( loops.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes") ? findInitializedByteLoopBindings(fn) : new Map(); + loops.inlinedLookups = fewNodes(fn.body, MAX_INLINED_LOOKUP_NODES); +} + +/** Whether `body` has at most `max` statements and expressions. */ +function fewNodes(body: IrStmt[], max: number): boolean { + let count = 0; + const visit = (): boolean => ++count <= max; + everyStmtList(body, { expr: visit, stmt: visit }); + return count <= max; } /** The state LOOPS holds on each emitter. */ @@ -58,6 +75,9 @@ export interface LoopState { /** Counted loops whose integer-form safety test an enclosing version * guard already passed. */ readonly provenCountedGuards: Set; + /** Number-key lookups in this function's loops inline the hashed probe: + * the function has at most MAX_INLINED_LOOKUP_NODES nodes. */ + inlinedLookups: boolean; } export const LOOPS = defineModuleState(() => ({ @@ -73,4 +93,5 @@ export const LOOPS = defineModuleState(() => ({ bytesLoopEntries: null, bytesLoopFunction: null, provenCountedGuards: new Set(), + inlinedLookups: false, })); 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 f7bd87af1..5e1e006d9 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.test.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.test.ts @@ -117,8 +117,9 @@ test("string keys and reference values are retained inline, boolean slots compar expect(text).toContain("call ptr @scr_map_iter_val_ref("); }); -/** work(map, k): map.has(k) on a number-keyed map. */ -function lookupFixture(key: IrExpr = ref("k", F64)): IrModule { +/** work(map, k): map.has(k) on a number-keyed map, or with `loop`, the + * condition of `while (map.has(k)) {}`, after `filler` statements `k;`. */ +function lookupFixture(key: IrExpr = ref("k", F64), loop = false, filler = 0): IrModule { const map: IrType = { kind: "map", key: F64, value: F64 }; const params = [ { localId: "map", name: "map", type: map }, @@ -138,7 +139,17 @@ function lookupFixture(key: IrExpr = ref("k", F64)): IrModule { returnType: BOOL, loc, locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: true })), - body: [{ kind: "return", value: has, loc }], + body: loop + ? [ + ...Array.from({ length: filler }, (): IrStmt => ({ + kind: "exprStmt", + expr: ref("k", F64), + loc, + })), + { kind: "while", cond: has, body: [], loc }, + { kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc }, + ] + : [{ kind: "return", value: has, loc }], }; return { irVersion: 17, @@ -159,6 +170,10 @@ test("hashed number-key lookups probe inline and call the runtime only for wide module, )![0]; expect(helper).toContain("call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)"); + // A key outside the direct index goes to the probe, whose small-map + // branch tells a map with the index (field 16) from one without it. + expect(helper).toMatch(/br i1 %ix\d+, label %slot, label %slow\n/); + expect(helper).not.toContain("dense.any"); const hashed = /^define internal ptr @sc_map_entry_f64_hashed\(ptr %m, double %k\) noinline [^]*?^}/m.exec( module, @@ -171,7 +186,10 @@ test("hashed number-key lookups probe inline and call the runtime only for wide // 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("br i1 %linear, label %unbucketed, label %buckets"); + expect(hashed).toMatch( + /^unbucketed:\n[^]*i32 0, i32 16\n[^]*br i1 %indexed, label %miss, label %filter\n/m, + ); 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`); @@ -198,6 +216,7 @@ test("a key with an integer view tests the direct index on 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_int(ptr %m, i64 %ki)"); + expect(helper).toContain("br i1 %inside, label %slot, label %slow"); 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. @@ -220,6 +239,36 @@ test("a key with an integer view tests the direct index on the integer", () => { } }); +test("a lookup inside a loop calls the flavor that inlines the hashed probe", () => { + for (const key of [ref("k", F64), { kind: "numLit", value: 12, type: F64, loc } as IrExpr]) { + const { work, module } = emitted(lookupFixture(key, true), 64); + const name = key.kind === "numLit" ? "sc_map_entry_f64_int" : "sc_map_entry_f64"; + expect(work).toContain(`call ptr @${name}.loop(`); + expect(work).not.toContain(`call ptr @${name}(`); + // They only read memory and always return, so a has() and get() of one + // key merge before they are inlined. + const helper = new RegExp( + `^define internal ptr @${name}\\.loop\\([^)]*\\) alwaysinline memory\\(read\\) nounwind willreturn [^]*?^}`, + "m", + ).exec(module)![0]; + const hashed = + key.kind === "numLit" ? "sc_map_entry_f64_hashed_int" : "sc_map_entry_f64_hashed"; + expect(helper).toContain(`call ptr @${hashed}.loop(`); + expect(module).toMatch( + new RegExp( + `^define internal ptr @${hashed}\\.loop\\([^)]*\\) alwaysinline memory\\(read\\) nounwind willreturn `, + "m", + ), + ); + // A lookup outside a loop keeps the out-of-line probe, and so does a + // lookup in a loop of a function of more than 200 nodes. + expect(emitted(lookupFixture(key), 64).work).toContain(`call ptr @${name}(`); + const large = emitted(lookupFixture(key, true, 100), 64).work; + expect(large).toContain(`call ptr @${name}(`); + expect(large).not.toContain(".loop("); + } +}); + 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; diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.ts b/packages/compiler/src/backend/llvm/map-number-lookup.ts index 17a1563c3..da2e5981a 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.ts @@ -32,6 +32,7 @@ import type { IrExpr, IrType } from "../../ir/ir.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { exactInteger, widenInteger } from "./integer-values.js"; +import { LOOPS } from "./loop-state.js"; import { mapKeyAccess } from "./shapes.js"; export const NUMBER_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_f64(ptr, double)"; @@ -55,19 +56,22 @@ export function noteNumberMapKey(host: LlvmEmitterContext, key: LlValue, expr: I /** 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. */ + * that integer instead of converting the double back. A lookup inside a + * loop of a small function (LOOPS.inlinedLookups) calls the helpers' `.loop` + * flavor, which inlines the hashed probe. */ export function emitNumberMapEntry(host: LlvmEmitterContext, map: string, key: string): string { host.declare(NUMBER_MAP_ENTRY_DECL); const entry = host.B.tmp(); + const flavor = host.loopDepth > 0 && LOOPS.of(host).inlinedLookups ? ".loop" : ""; 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})`, + `${entry} = call ptr @sc_map_entry_f64_int${flavor}(ptr ${map}, i64 ${wide}, double ${key})`, ); return entry; } - host.B.line(`${entry} = call ptr @sc_map_entry_f64(ptr ${map}, double ${key})`); + host.B.line(`${entry} = call ptr @sc_map_entry_f64${flavor}(ptr ${map}, double ${key})`); return entry; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/map-number-lookups-in-loops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/map-number-lookups-in-loops.ts.json new file mode 100644 index 000000000..5c06e9db7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/map-number-lookups-in-loops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/map-number-lookups-in-loops.ts" + ], + "diags": [] +} diff --git a/tests/corpus/map-number-lookups-in-loops.ts b/tests/corpus/map-number-lookups-in-loops.ts new file mode 100644 index 000000000..afc45a539 --- /dev/null +++ b/tests/corpus/map-number-lookups-in-loops.ts @@ -0,0 +1,158 @@ +// Number-key Map and Set lookups inside loops while the collection changes +// between its direct index, a linear scan and a bucket table. + +// Dense keys, then a sparse key mid-loop moves the map to hashing; later +// lookups of the same keys still find them. +function denseToHashed(): string { + const m = new Map(); + for (let i = 0; i < 64; i++) m.set(i, i * 3); + let found = 0; + let sum = 0; + for (let i = 0; i < 200; i++) { + if (i === 50) m.set(1_000_000_007, -1); + const v = m.get(i % 80); + if (v !== undefined) { + found++; + sum += v; + } + if (m.has(1_000_000_007)) sum += 1; + } + return `${found} ${sum} ${m.size}`; +} + +// Counting into a map with many sparse keys: a bucket table from the start. +function hashedCounts(): string { + const counts = new Map(); + let key = 7; + for (let i = 0; i < 4000; i++) { + key = (key * 1103515245 + 12345) % 2147483648; + const k = key % 1009; + const c = counts.get(k * 1_000_003); + counts.set(k * 1_000_003, c === undefined ? 1 : c + 1); + } + let max = 0; + let total = 0; + for (const [k, c] of counts) { + total += c; + if (c > max) max = c; + if (k < 0) total = -1; + } + return `${counts.size} ${total} ${max}`; +} + +// Integer keys in a counter object map, as a k-mer count keeps them. +class Counter { + n: number; + constructor(n: number) { + this.n = n; + } +} + +function counterObjects(): string { + const freq = new Map(); + let key = 0; + let seed = 11; + for (let i = 0; i < 6000; i++) { + seed = (seed * 1103515245 + 12345) % 2147483648; + key = (key * 4 + (Math.floor(seed / 65536) % 4)) % 262144; + const cur = freq.get(key); + if (cur === undefined) freq.set(key, new Counter(1)); + else cur.n++; + } + let best = 0; + let bestKey = -1; + for (const [k, c] of freq) + if (c.n > best) { + best = c.n; + bestKey = k; + } + return `${freq.size} ${bestKey} ${best}`; +} + +// Fractional, negative, -0 and NaN keys looked up in a loop. +function doubleKeys(): string { + const m = new Map(); + const keys = [0.5, -3, -0, NaN, 2 ** 40, 1e-9, Infinity, -Infinity, 7]; + for (let i = 0; i < keys.length; i++) m.set(keys[i]!, `k${i}`); + const out: string[] = []; + const probes = [0.5, -3, 0, NaN, 2 ** 40, 1e-9, Infinity, -Infinity, 7, 8, 0.25, -1]; + for (let i = 0; i < probes.length; i++) out.push(`${m.get(probes[i]!) ?? "-"}`); + for (let i = 0; i < probes.length; i++) if (m.has(probes[i]!)) out.push("h"); + return out.join(","); +} + +// Deletes inside the loop, then the map refills with small keys. +function deleteAndRefill(): string { + const m = new Map(); + for (let i = 0; i < 1000; i++) m.set(i * 7919, i); + let hits = 0; + for (let i = 0; i < 1000; i++) { + if (m.has(i * 7919)) hits++; + m.delete(i * 7919); + } + for (let i = 0; i < 100; i++) m.set(i, i); + let sum = 0; + for (let i = 0; i < 150; i++) sum += m.get(i) ?? 0; + return `${hits} ${m.size} ${sum}`; +} + +// A Set's membership tests while it grows from a scan to a bucket table. +function setGrowth(): string { + const s = new Set(); + let present = 0; + for (let i = 0; i < 3000; i++) { + if (s.has((i * 37) % 1009)) present++; + s.add(((i * 37) % 1009) * 1000); + } + let again = 0; + for (let i = 0; i < 1009; i++) if (s.has(i * 1000)) again++; + return `${present} ${again} ${s.size}`; +} + +// Nested loops over two maps, the inner one keyed by the outer's values. +function nested(): string { + const outer = new Map(); + const inner = new Map(); + for (let i = 0; i < 300; i++) { + outer.set(i * 101, i); + inner.set(i, i * i); + } + let total = 0; + for (let i = 0; i < 300; i++) { + const v = outer.get(i * 101); + if (v === undefined) continue; + for (let j = 0; j < 3; j++) total += inner.get((v + j) % 300) ?? 0; + } + return `${total}`; +} + +// has() then get() of one key, with and without a write between them. +function hasThenGet(): string { + const m = new Map(); + for (let i = 0; i < 400; i++) m.set(i * 1_000_003, i); + let total = 0; + let misses = 0; + for (let i = 0; i < 800; i++) { + const k = (i % 500) * 1_000_003; + if (m.has(k)) total += m.get(k)!; + else misses++; + if (i % 3 === 0 && m.has(k)) { + m.set(k, m.get(k)! + 1000); + total += m.get(k)!; + } + if (i % 7 === 0 && !m.has(k + 1)) { + m.set(k + 1, i); + total += m.get(k + 1)! + (m.has(k + 1) ? 1 : 0); + } + } + return `${total} ${misses} ${m.size}`; +} + +console.log(denseToHashed()); +console.log(hashedCounts()); +console.log(counterObjects()); +console.log(doubleKeys()); +console.log(deleteAndRefill()); +console.log(setGrowth()); +console.log(nested()); +console.log(hasThenGet()); diff --git a/tests/harness/corpus-costs/map-number-lookups-in-loops.ts.json b/tests/harness/corpus-costs/map-number-lookups-in-loops.ts.json new file mode 100644 index 000000000..3ba66c4f0 --- /dev/null +++ b/tests/harness/corpus-costs/map-number-lookups-in-loops.ts.json @@ -0,0 +1 @@ +{ "plain": 8200, "san": 11500 } From 089a8e17996876f2f8194cb68d403482aaff054e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 01:47:31 -0500 Subject: [PATCH 21/70] fs.watch/fs.watchFile: fix listener removal, adapter ownership and argument checks - watcher.off/removeListener of a listener that is not the last one moves the later listeners down instead of up, so the removed closure leaves the list. - The watchFile adapters for zero- and one-parameter listeners release the Stats their listener has no parameter for, and the fs.watch adapter releases the event string once a dynamic value holds it. - A one-parameter fs.watch listener whose eventType is dynamic receives a boxed string through the emitted adapter. - fs.watch, fs.watchFile and fs.unwatchFile throw ERR_INVALID_ARG_VALUE for a path holding a null byte, and a new watchFile poller throws ERR_OUT_OF_RANGE when interval is not a uint32, as Node does. Both watchFile libCalls check for the pending exception. - fs.watchFile and fs.unwatchFile have surface-manifest entries, so a fence can deny them. - The self-hosted feature records list watchFile, and the node-types fence fixture uses fs.openAsBlob in place of the now-lowered fs.watchFile. Tests: tests/corpus/fs-watch-listener-shapes.ts (listener shapes, off of a middle listener, null-byte path) with its order baseline and cost file, the validation cases in fs-watch-file-polling.ts, and tests/harness/fs-watch-leaks.test.ts, which runs both programs under LeakSanitizer in the sanitized lane. --- .../src/backend/llvm/expr-stream-callbacks.ts | 108 ++++++++++-------- .../src/backend/llvm/lib-filesystem.ts | 47 ++++---- .../src/frontend/lowering/surfaces.ts | 16 +++ packages/compiler/surface-manifest.json | 14 +++ .../corpus/fs-watch-listener-shapes.ts.json | 6 + packages/runtime/src/scr_watch.c | 8 +- packages/runtime/src/scr_watch_file.c | 45 ++++++-- tests/corpus/fs-watch-file-polling.ts | 26 ++++- tests/corpus/fs-watch-listener-shapes.ts | 91 +++++++++++++++ tests/fixtures/node-types/fenced.ts | 4 +- .../__snapshots__/node-types-fenced.txt | 8 +- .../fs-watch-listener-shapes.ts.json | 1 + tests/harness/fs-watch-leaks.test.ts | 64 +++++++++++ .../self-hosting-native-toolchain.test.ts | 1 + .../harness/self-hosting-runtime-pack.test.ts | 1 + 15 files changed, 353 insertions(+), 87 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-watch-listener-shapes.ts.json create mode 100644 tests/corpus/fs-watch-listener-shapes.ts create mode 100644 tests/harness/corpus-costs/fs-watch-listener-shapes.ts.json create mode 100644 tests/harness/fs-watch-leaks.test.ts diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 00ee4d636..1f92182d8 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -966,10 +966,12 @@ registerReplay("streamDoneFnFor", (host, args) => registerReplay("fsRenameThunkFor", (host, args) => fsRenameThunkFor(host, args[0] as IrType & { kind: "func" }), ); -/** The (eventType, filename) fs.watch listener adapter: the runtime hands +/** The fs.watch listener adapter for an (eventType, filename) listener, or + * for an (eventType) listener whose parameter is dynamic: the runtime hands * the borrowed C strings, the thunk builds what the listener declared. The * listener owns its +1 arguments (the universal convention - * scr_watch_thunk_event follows). */ + * scr_watch_thunk_event follows), so a string the thunk boxes into a dynamic + * value is released once the box holds it. */ export function watchNameThunkFor( host: LlvmEmitterContext, cbT: IrType & { kind: "func" }, @@ -989,76 +991,84 @@ export function watchNameThunkFor( `entry:`, ` %evlen = call ${S} @strlen(ptr %event)`, ` %evstr = call ptr @scr_str_new(ptr %event, ${S} %evlen)`, - ` %slot = alloca ptr`, - ` %has = icmp ne ptr %name, null`, - ` br i1 %has, label %yes, label %no`, - `yes:`, ]; - const nameParam = cbT.params[1]!; - if (nameParam.kind === "dyn") { - host.declare(`declare ptr @scr_dyn_new_null()`); - host.declare(`declare ptr @scr_dyn_new_str(ptr)`); + const named = cbT.params.length === 2; + if (named) { d.push( - ` %nlen = call ${S} @strlen(ptr %name)`, - ` %nstr = call ptr @scr_str_new(ptr %name, ${S} %nlen)`, - ` %ndyn = call ptr @scr_dyn_new_str(ptr %nstr)`, - ` call void @scr_str_release(ptr %nstr)`, - ` store ptr %ndyn, ptr %slot`, - ` br label %go`, - `no:`, - ` %dn = call ptr @scr_dyn_new_null()`, - ` store ptr %dn, ptr %slot`, + ` %slot = alloca ptr`, + ` %has = icmp ne ptr %name, null`, + ` br i1 %has, label %yes, label %no`, + `yes:`, ); - } else { - if (nameParam.kind !== "union") - throw new InternalCompilerError("llvm emitter bug: fs.watch filename param is not a union"); - const def = host.unionsById.get(nameParam.unionId); - const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (strTag < 0 || nullTag < 0) - throw new InternalCompilerError("llvm emitter bug: fs.watch filename union lacks its arms"); - if (host.nullableUnions.has(nameParam.unionId)) { - // `string | null` erases: the owned payload IS the union's value. + const nameParam = cbT.params[1]!; + if (nameParam.kind === "dyn") { + host.declare(`declare ptr @scr_dyn_new_null()`); + host.declare(`declare ptr @scr_dyn_new_str(ptr)`); d.push( ` %nlen = call ${S} @strlen(ptr %name)`, ` %nstr = call ptr @scr_str_new(ptr %name, ${S} %nlen)`, - ` store ptr %nstr, ptr %slot`, + ` %ndyn = call ptr @scr_dyn_new_str(ptr %nstr)`, + ` call void @scr_str_release(ptr %nstr)`, + ` store ptr %ndyn, ptr %slot`, ` br label %go`, `no:`, - ` store ptr null, ptr %slot`, + ` %dn = call ptr @scr_dyn_new_null()`, + ` store ptr %dn, ptr %slot`, ); } else { - const rc = vAdapters(host.shapeHost, STRING); - host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`); - d.push( - ` %nlen = call ${S} @strlen(ptr %name)`, - ` %nstr = call ptr @scr_str_new(ptr %name, ${S} %nlen)`, - ` %nu = call ptr @scr_union_new_ref(i32 ${strTag}, ptr %nstr, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, STRING)})`, - ` store ptr %nu, ptr %slot`, - ` br label %go`, - `no:`, - ` store ptr ${host.unitInstanceRef(nameParam.unionId, nullTag)}, ptr %slot`, - ); + if (nameParam.kind !== "union") + throw new InternalCompilerError("llvm emitter bug: fs.watch filename param is not a union"); + const def = host.unionsById.get(nameParam.unionId); + const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; + const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; + if (strTag < 0 || nullTag < 0) + throw new InternalCompilerError("llvm emitter bug: fs.watch filename union lacks its arms"); + if (host.nullableUnions.has(nameParam.unionId)) { + // `string | null` erases: the owned payload IS the union's value. + d.push( + ` %nlen = call ${S} @strlen(ptr %name)`, + ` %nstr = call ptr @scr_str_new(ptr %name, ${S} %nlen)`, + ` store ptr %nstr, ptr %slot`, + ` br label %go`, + `no:`, + ` store ptr null, ptr %slot`, + ); + } else { + const rc = vAdapters(host.shapeHost, STRING); + host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`); + d.push( + ` %nlen = call ${S} @strlen(ptr %name)`, + ` %nstr = call ptr @scr_str_new(ptr %name, ${S} %nlen)`, + ` %nu = call ptr @scr_union_new_ref(i32 ${strTag}, ptr %nstr, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, STRING)})`, + ` store ptr %nu, ptr %slot`, + ` br label %go`, + `no:`, + ` store ptr ${host.unitInstanceRef(nameParam.unionId, nullTag)}, ptr %slot`, + ); + } } + d.push(` br label %go`, `go:`); } let ev: string; if (cbT.params[0]?.kind === "dyn") { + // The box retains the string; the thunk's own reference goes. host.declare(`declare ptr @scr_dyn_new_str(ptr)`); - d.push(` br label %go`, `go:`, ` %evd = call ptr @scr_dyn_new_str(ptr %evstr)`); + d.push( + ` %evd = call ptr @scr_dyn_new_str(ptr %evstr)`, + ` call void @scr_str_release(ptr %evstr)`, + ); ev = "%evd"; } else { - d.push(` br label %go`, `go:`); ev = "%evstr"; } d.push( - ` %arg = load ptr, ptr %slot`, ` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`, ` %fn = load ptr, ptr %fnp`, - ` call void %fn(ptr %cb, ptr ${ev}, ptr %arg)`, - ` ret void`, - `}`, - ``, ); + if (named) + d.push(` %arg = load ptr, ptr %slot`, ` call void %fn(ptr %cb, ptr ${ev}, ptr %arg)`); + else d.push(` call void %fn(ptr %cb, ptr ${ev})`); + d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); host.journal?.built(); return sym; diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index dc9c5c876..82685456e 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -1,7 +1,7 @@ /* Focused LLVM library-call emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; import { undefinedArmTag } from "../../ir/analysis.js"; -import { DYN, F64, STATS_T, STRING, type IrExpr } from "../../ir/ir.js"; +import { DYN, F64, STATS_T, STRING, type IrExpr, type IrType } from "../../ir/ir.js"; import { mangleRecordNew } from "../mangle.js"; import { arrNewCall, traceArg, vAdapters } from "./shapes.js"; import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js"; @@ -173,6 +173,22 @@ function emitObjEntryPairs(host: LlvmEmitterContext, e: LibCallExpr): LlValue { return out; } +/** The adapter of an fs.watch 'change' listener, by its declared shape: the + * runtime's for no parameters and for a string eventType, and one this + * module emits for a dynamic eventType or an (eventType, filename) listener. + * Only the runtime's adapters need a declaration. */ +function watchChangeAdapter(host: LlvmEmitterContext, cbT: IrType & { kind: "func" }): string { + const runtime = + cbT.params.length === 0 + ? "scr_watch_thunk0" + : cbT.params.length === 1 && cbT.params[0]!.kind !== "dyn" + ? "scr_watch_thunk_event" + : null; + if (runtime === null) return host.watchNameThunkFor(cbT); + host.declare(`declare void @${runtime}(ptr, ptr, ptr)`); + return runtime; +} + export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; const alwaysThrowSym = FS_ALWAYS_THROW_SYMS[e.fn]; @@ -468,15 +484,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const path = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); host.toHeap(cb); - const adapter = - cbT.params.length === 0 - ? "scr_watch_thunk0" - : cbT.params.length === 1 - ? "scr_watch_thunk_event" - : host.watchNameThunkFor(cbT); - // The named form is a definition this module emits; only the runtime's - // fixed adapters need a declaration. - if (cbT.params.length < 2) host.declare(`declare void @${adapter}(ptr, ptr, ptr)`); + const adapter = watchChangeAdapter(host, cbT); host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr, i1 zeroext)`); const t = B.tmp(); B.line( @@ -509,16 +517,8 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): let adapter = "null"; let wantsName = false; if (code === 0) { - adapter = - cbT.params.length === 0 - ? "scr_watch_thunk0" - : cbT.params.length === 1 - ? "scr_watch_thunk_event" - : host.watchNameThunkFor(cbT); + adapter = watchChangeAdapter(host, cbT); wantsName = cbT.params.length === 2; - // The named form is a definition this module emits; only the runtime's - // fixed adapters need a declaration. - if (cbT.params.length < 2) host.declare(`declare void @${adapter}(ptr, ptr, ptr)`); } else if (code === 1) { adapter = cbT.params.length === 0 ? "scr_watch_err_thunk0" : host.watchErrorThunkFor(cbT); if (cbT.params.length === 0) host.declare(`declare void @scr_watch_err_thunk0(ptr, ptr)`); @@ -553,7 +553,9 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): // (path, interval, persistent, listener) — the listener MOVES into // the poller's registry and the adapter is runtime-provided per // listener shape (zero, one or two Stats parameters). The poller - // re-arms a timer every interval, so this is a timer program. + // re-arms a timer every interval, so this is a timer program. May + // throw (a null byte in the path, or an interval that is not a + // uint32) — the standard pending check. host.usesTimers = true; const cbT = e.args[3]!.type; if (cbT.kind !== "func") @@ -569,18 +571,21 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): B.line( `call void @scr_fs_watch_file(ptr ${path.name}, double ${interval.name}, i1 ${persistent.name}, ptr ${cb.name}, ptr @${adapter})`, ); + host.emitPendingCheck(); return { name: "", type: e.type }; } if (e.fn === "fs.unwatchFile") { // Removal is by closure identity: the listener stays borrowed // (never heap-moved), exactly like watcher.off's; an absent one is - // the NULL that removes every listener on the path. + // the NULL that removes every listener on the path. A null byte in + // the path throws. const path = host.emitExpr(e.args[0]!); const cb = e.args[1] === undefined ? null : host.emitExpr(e.args[1]!); host.declare(`declare void @scr_fs_unwatch_file(ptr, ptr)`); B.line( `call void @scr_fs_unwatch_file(ptr ${path.name}, ptr ${cb === null ? "null" : cb.name})`, ); + host.emitPendingCheck(); return { name: "", type: e.type }; } if (e.fn === "fs.globTypesSync" || e.fn === "fsp.globTypes") { diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 6a06fbaad..0ef9029fd 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2416,6 +2416,22 @@ function buildAmbientSurfaceFns(): readonly AmbientSurfaceRow[] { fns: ["fs.callbackValue", "fs.callbackCall", "fs.callbackGlob"], note: "native filesystem callbacks, including stored platform adapter functions", }, + // fs.watchFile/fs.unwatchFile lower in lowerFsWatchFileCall, ahead of + // the module table. + { + id: "node-builtin.fs.watchFile", + kind: "node-builtin", + name: "fs.watchFile", + fns: ["fs.watchFile"], + note: "stat polling of a resolved path with Stats listeners, in statement position; bigint: true is refused", + }, + { + id: "node-builtin.fs.unwatchFile", + kind: "node-builtin", + name: "fs.unwatchFile", + fns: ["fs.unwatchFile"], + note: "removes one listener, or every listener, from a resolved path's poller", + }, // ── the Date slice (lowerDateCall/lowerNew): statics, construction, // stored read-only values, calendar getters, and ISO formatting. { diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index ea8295406..922c45e9e 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1279,6 +1279,13 @@ "name": "fs.unlinkSync", "status": "static" }, + { + "id": "node-builtin.fs.unwatchFile", + "kind": "node-builtin", + "name": "fs.unwatchFile", + "status": "static", + "note": "removes one listener, or every listener, from a resolved path's poller" + }, { "id": "node-builtin.fs.utimesSync", "kind": "node-builtin", @@ -1291,6 +1298,13 @@ "name": "fs.watch", "status": "static" }, + { + "id": "node-builtin.fs.watchFile", + "kind": "node-builtin", + "name": "fs.watchFile", + "status": "static", + "note": "stat polling of a resolved path with Stats listeners, in statement position; bigint: true is refused" + }, { "id": "node-builtin.fs.writeFileSync", "kind": "node-builtin", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-watch-listener-shapes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-watch-listener-shapes.ts.json new file mode 100644 index 000000000..15ac693c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-watch-listener-shapes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fs-watch-listener-shapes.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_watch.c b/packages/runtime/src/scr_watch.c index 0d5087961..d1730c75b 100644 --- a/packages/runtime/src/scr_watch.c +++ b/packages/runtime/src/scr_watch.c @@ -288,6 +288,12 @@ void scr_watcher_release_v(void *p) { scr_watcher_release((ScrWatcher *)p); } * filter. cb (nullable) moves in as the first listener. Result +1. */ ScrWatcher *scr_fs_watch(ScrStr *path, ScrClosure *cb /*moves, nullable*/, ScrWatchFn fn, bool wants_name) { + /* getValidatedPath: a null byte throws ERR_INVALID_ARG_VALUE before the + * path reaches the platform call. */ + if (!scr_fs_path_ok(path, "path")) { + if (cb != NULL) scr_closure_release(cb); + return NULL; /* exception pending */ + } #ifdef SCR_HAVE_INOTIFY /* The wd IS the validation: Node's Linux throw comes straight from * inotify_add_watch's errno, the same message shape as the open below. */ @@ -542,7 +548,7 @@ void scr_watcher_off(ScrWatcher *w, double event, ScrClosure *cb) { ScrWatchL *l = &w->ls[i - 1]; if (l->event != (uint8_t)event || l->cb != cb) continue; scr_closure_release(l->cb); - memmove(l + 1, l, (w->n - i) * sizeof *w->ls); + memmove(l, l + 1, (w->n - i) * sizeof *w->ls); w->n--; return; } diff --git a/packages/runtime/src/scr_watch_file.c b/packages/runtime/src/scr_watch_file.c index 6b40e5ed0..b1bee2826 100644 --- a/packages/runtime/src/scr_watch_file.c +++ b/packages/runtime/src/scr_watch_file.c @@ -32,6 +32,7 @@ #include "scr_runtime.h" #include +#include #include #include #include @@ -230,11 +231,31 @@ static void scr_watch_file_stop(ScrWatchFiler *p) { scr_watch_file_release(p); } +/* StatWatcher's validateUint32(interval, 'interval'): false after throwing + * Node's ERR_OUT_OF_RANGE RangeError. */ +static bool scr_watch_file_interval_ok(double interval) { + bool integer = isfinite(interval) && trunc(interval) == interval; + if (integer && interval >= 0 && interval <= 4294967295.0) return true; + char recv[48], msg[160]; + scr_num_received(interval, recv); + int len = snprintf(msg, sizeof msg, + "The value of \"interval\" is out of range. It must be %s. Received %s", + integer ? ">= 0 && <= 4294967295" : "an integer", recv); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); + return false; +} + /* fs.watchFile(path, interval, persistent, listener) — the listener MOVES - * in. A second call on the same resolved path adds the listener to the - * existing poller and ignores its options (Node's rule). */ + * in. A path holding a null byte throws Node's ERR_INVALID_ARG_VALUE. A + * second call on the same resolved path adds the listener to the existing + * poller and ignores its options (Node's rule); a new poller first checks + * `interval` as a uint32, and registers nothing when that throws. */ void scr_fs_watch_file(ScrStr *path, double interval, bool persistent, ScrClosure *cb, ScrWatchFileFn fn) { + if (!scr_fs_path_ok(path, "path")) { + scr_closure_release(cb); + return; /* exception pending */ + } ScrStr *resolved = scr_watch_file_resolve(path); ScrWatchFiler *existing = scr_watch_file_find(resolved); if (existing != NULL) { @@ -242,6 +263,11 @@ void scr_fs_watch_file(ScrStr *path, double interval, bool persistent, ScrClosur scr_watch_file_add(existing, cb, fn); return; } + if (!scr_watch_file_interval_ok(interval)) { + scr_str_release(resolved); + scr_closure_release(cb); + return; /* exception pending */ + } ScrWatchFiler *p = malloc(sizeof *p); if (!p) { scr_str_release(resolved); @@ -275,8 +301,10 @@ void scr_fs_watch_file(ScrStr *path, double interval, bool persistent, ScrClosur /* fs.unwatchFile(path[, listener]) — `cb` is borrowed (pointer identity); * NULL removes every listener on the resolved path. Removing the last one - * stops the poller. */ + * stops the poller. A path holding a null byte throws Node's + * ERR_INVALID_ARG_VALUE. */ void scr_fs_unwatch_file(ScrStr *path, ScrClosure *cb) { + if (!scr_fs_path_ok(path, "path")) return; /* exception pending */ ScrStr *resolved = scr_watch_file_resolve(path); ScrWatchFiler *p = scr_watch_file_find(resolved); scr_str_release(resolved); @@ -299,16 +327,17 @@ void scr_fs_unwatch_file(ScrStr *path, ScrClosure *cb) { } /* ── the runtime-provided listener adapters ─────────────────────────── - * The listener owns its +1 arguments (the universal convention - * scr_watch_thunk_event follows). */ + * The adapter receives +1 references and the listener owns those it takes + * (the universal convention scr_watch_thunk_event follows); the adapter + * releases the ones its listener has no parameter for. */ void scr_watch_file_thunk0(ScrClosure *cb, ScrStats *current, ScrStats *previous) { - (void)current; - (void)previous; + scr_stats_release(current); + scr_stats_release(previous); ((void (*)(ScrClosure *))cb->fn)(cb); } void scr_watch_file_thunk1(ScrClosure *cb, ScrStats *current, ScrStats *previous) { - (void)previous; + scr_stats_release(previous); ((void (*)(ScrClosure *, ScrStats *))cb->fn)(cb, current); } diff --git a/tests/corpus/fs-watch-file-polling.ts b/tests/corpus/fs-watch-file-polling.ts index 87cddf867..2282c7c6b 100644 --- a/tests/corpus/fs-watch-file-polling.ts +++ b/tests/corpus/fs-watch-file-polling.ts @@ -5,7 +5,9 @@ // sides zeroed, and the creation reports previous.size 0 and previous.mtimeMs 0. Two watchFile calls on one path share one poller; // unwatchFile(path, listener) removes one listener, unwatchFile(path) // removes the rest, and once no listener is left the poller stops and -// the process can exit. +// the process can exit. A new poller's interval must be a uint32 (a call on +// a watched path ignores its options), and a path holding a null byte +// throws for both calls. import * as fs from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; @@ -15,6 +17,26 @@ const existing = join(dir, "existing.txt"); const later = join(dir, "later.txt"); fs.writeFileSync(existing, "a"); +const invalid = join(dir, "invalid.txt"); +for (const interval of [-1, 1.5, Number.NaN, 2 ** 32]) { + try { + fs.watchFile(invalid, { interval }, () => {}); + console.log("interval", interval, "accepted"); + } catch (err) { + console.log("interval", interval, (err as NodeJS.ErrnoException).code, (err as Error).message); + } +} +try { + fs.watchFile("bad\0path", () => {}); +} catch (err) { + console.log("watchFile", (err as NodeJS.ErrnoException).code, (err as Error).message); +} +try { + fs.unwatchFile("bad\0path"); +} catch (err) { + console.log("unwatchFile", (err as NodeJS.ErrnoException).code, (err as Error).message); +} + let firstCalls = 0; const first = (current: fs.Stats, previous: fs.Stats): void => { firstCalls++; @@ -27,7 +49,7 @@ const second = (current: fs.Stats, previous: fs.Stats): void => { startMissingPhase(); }; fs.watchFile(existing, { interval: 20 }, first); -fs.watchFile(existing, { interval: 1000 }, second); +fs.watchFile(existing, { interval: -1 }, second); setTimeout(() => fs.writeFileSync(existing, "abcd"), 150); function startMissingPhase(): void { diff --git a/tests/corpus/fs-watch-listener-shapes.ts b/tests/corpus/fs-watch-listener-shapes.ts new file mode 100644 index 000000000..e6c399115 --- /dev/null +++ b/tests/corpus/fs-watch-listener-shapes.ts @@ -0,0 +1,91 @@ +// fs.watch and fs.watchFile listeners of each shape. watcher.off of a +// 'change' listener that is not the last one leaves the others, in order. +// A one-parameter listener typed any and a two-parameter listener whose +// eventType is any receive the event name as a string. fs.watchFile +// listeners with no parameter and with one parameter run once per change +// a poll sees, like the two-parameter form. Event counts and coalescing are +// deliberately unobserved (they differ between inotify, kqueue and +// FSEvents); each watchFile change waits for the previous one's event. +// A path holding a null byte throws before anything is watched. +import * as fs from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +const dir = fs.mkdtempSync(join(tmpdir(), "scr-watch-shapes-")); +const file = join(dir, "data.txt"); +fs.writeFileSync(file, "a"); + +try { + fs.watch("bad\0path", () => {}).close(); + console.log("watch accepted"); +} catch (err) { + console.log("watch", (err as NodeJS.ErrnoException).code, (err as Error).message); +} + +const order: string[] = []; +let anyEvent: any = null; +let dynEvent: any = null; +let named = 0; +const watcher = fs.watch(file, (eventType: any) => { + if (anyEvent === null) anyEvent = eventType; +}); +const first = () => { + order.push("first"); +}; +const second = () => { + order.push("second"); +}; +const third = () => { + order.push("third"); +}; +watcher.on("change", first); +watcher.on("change", second); +watcher.on("change", third); +watcher.off("change", second); +watcher.on("change", (eventType: any, filename: any) => { + if (dynEvent === null) dynEvent = eventType; + if (filename !== null) named++; +}); + +const bound = setTimeout(() => { + console.log("timed out"); + process.exit(1); +}, 10000); +const touch = setInterval(() => fs.appendFileSync(file, "x"), 40); +const poll = setInterval(() => { + if (anyEvent === null || dynEvent === null || order.length < 2) return; + clearInterval(poll); + clearInterval(touch); + watcher.close(); + console.log("after off", order.slice(0, 2).join(",")); + console.log("any eventType", typeof anyEvent, anyEvent === "change" || anyEvent === "rename"); + console.log("dynamic eventType", typeof dynEvent, dynEvent === "change" || dynEvent === "rename"); + startPolling(); +}, 10); + +function startPolling(): void { + let zeroCalls = 0; + let oneCalls = 0; + let size = 0; + const zero = (): void => { + zeroCalls++; + }; + const one = (current: fs.Stats): void => { + oneCalls++; + size = current.size; + if (oneCalls < 3) { + setTimeout(() => fs.writeFileSync(file, "x".repeat(oneCalls + 10)), 30); + return; + } + setTimeout(() => { + console.log("watchFile calls", zeroCalls === oneCalls, oneCalls, "size", size); + fs.unwatchFile(file); + clearTimeout(bound); + fs.rmSync(dir, { recursive: true, force: true }); + console.log("done"); + }, 60); + }; + fs.watchFile(file, { interval: 20 }, zero); + fs.watchFile(file, { interval: 20 }, one); + setTimeout(() => fs.writeFileSync(file, "x".repeat(10)), 100); +} diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 33cc56778..3fb41c490 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -32,10 +32,10 @@ ReadableStream.from(new Set([1, 2])); /* Members of SUPPORTED builtin modules beyond the lowered tables: they * typecheck under @types/node and fence with the module-qualified name — * calls and value reads alike. */ -import { watchFile } from "fs"; +import { openAsBlob } from "fs"; import { EventEmitter, on } from "node:events"; import { win32 } from "path"; -watchFile("x", () => {}); +openAsBlob("x"); on(new EventEmitter(), "tick"); console.log(win32.sep); /* URL getters and serialization lower under @types/node, but object-URL diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index ada6231d4..be6452270 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -43,18 +43,18 @@ fenced.ts:31:21 - error SC2020: 'ReadableStream.from() over Set iterable hint: arrays, Uint8Array, and strings are supported — spread other synchronous iterables into an array first: [...iterable] -fenced.ts:38:1 - error SC2020: 'fs.watchFile' is typed by @types/node but has no scriptc lowering yet +fenced.ts:38:1 - error SC2020: 'fs.openAsBlob' is typed by @types/node but has no scriptc lowering yet 37 | import { win32 } from "path"; - 38 | watchFile("x", () => {}); - | ^~~~~~~~~~~~~~~~~~~~~~~~ + 38 | openAsBlob("x"); + | ^~~~~~~~~~~~~~~ 39 | on(new EventEmitter(), "tick"); hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations) fenced.ts:39:1 - error SC2020: 'events.on' is typed by @types/node but has no scriptc lowering yet - 38 | watchFile("x", () => {}); + 38 | openAsBlob("x"); 39 | on(new EventEmitter(), "tick"); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 40 | console.log(win32.sep); diff --git a/tests/harness/corpus-costs/fs-watch-listener-shapes.ts.json b/tests/harness/corpus-costs/fs-watch-listener-shapes.ts.json new file mode 100644 index 000000000..c5606ebea --- /dev/null +++ b/tests/harness/corpus-costs/fs-watch-listener-shapes.ts.json @@ -0,0 +1 @@ +{ "plain": 3100, "san": 9200 } diff --git a/tests/harness/fs-watch-leaks.test.ts b/tests/harness/fs-watch-leaks.test.ts new file mode 100644 index 000000000..424d6ca56 --- /dev/null +++ b/tests/harness/fs-watch-leaks.test.ts @@ -0,0 +1,64 @@ +/* fs.watch and fs.watchFile listeners under LeakSanitizer. A listener owns + * the arguments its adapter hands it, and the adapter releases the ones the + * listener has no parameter for, including the event string it boxes for a + * dynamic parameter and the Stats a watchFile listener of zero or one + * parameter leaves out. The sanitized lanes otherwise run with + * detect_leaks=0, so this runs the watch programs with LeakSanitizer + * explicitly on. Linux only (Apple's ASan has no LeakSanitizer); runs in + * the SCRIPTC_SAN=1 lane. */ +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { describe, expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +const programs = [ + "tests/corpus/fs-watch-listener-shapes.ts", + "tests/corpus/fs-watch-file-polling.ts", +]; + +describe.skipIf(!sanitize || process.platform !== "linux")( + "fs watchers under LeakSanitizer", + () => { + test.each(programs)("%s reports no leaks", { timeout: 300_000 }, async (program) => { + const file = join(repoRoot, program); + const key = createHash("sha256") + .update(file) + .update(readFileSync(file)) + .update("san") + .digest("hex") + .slice(0, 16); + const outDir = join(cacheDir, `fs-watch-leaks-${key}`); + mkdirSync(outDir, { recursive: true }); + const result = await compile(file, { + outPath: join(outDir, "program"), + outDir, + sanitize: true, + backend: "llvm", + }); + if (!result.ok) { + throw new Error( + "program failed to compile:\n" + + result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"), + ); + } + const run = await execFileAsync(result.binaryPath, [], { + env: { ...process.env, ASAN_OPTIONS: "detect_leaks=1" }, + maxBuffer: 16 * 1024 * 1024, + }).catch((error: { code?: number; stdout?: string; stderr?: string }) => error); + expect(run.stderr).toBe(""); + expect("code" in run ? run.code : 0).toBe(0); + const expected = await execFileAsync(process.execPath, ["--no-warnings", file], { + maxBuffer: 16 * 1024 * 1024, + }); + expect(run.stdout).toBe(expected.stdout); + }); + }, +); diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index db1faab9b..642c60a5c 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -47,6 +47,7 @@ const features: NativeLinkFeatures = { dgramDyn: false, textCodecDyn: false, watch: false, + watchFile: false, foreignFfi: false, nodeTest: false, tls: false, diff --git a/tests/harness/self-hosting-runtime-pack.test.ts b/tests/harness/self-hosting-runtime-pack.test.ts index 859f820da..16f9e23fd 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -44,6 +44,7 @@ const base: NativeLinkFeatures = { dgramDyn: false, textCodecDyn: false, watch: false, + watchFile: false, foreignFfi: false, nodeTest: false, tls: false, From 8537c41171e982732c5a92bb18e70a84187af024 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 02:05:14 -0500 Subject: [PATCH 22/70] Keep the receiver bracket around stream completion calls The stream completion closures carried SCR_CLOSURE_KIND_RECEIVER_FREE, so compiled callers skipped the receiver bracket around cb(). A completion runs user code synchronously (the write(chunk, callback) callback, the next queued _write), and that code reads the ambient receiver, so the flag is removed and the completions bind `this` as any unbound call does. Corpus programs 8531 and 8532 call a saved completion from inside another object's method, from JavaScript and from a TypeScript class method that JavaScript calls, and check `this` in the callbacks it runs against Node. --- ...31-stream-saved-callback-receiver.mjs.json | 6 ++++ .../main.ts.json | 7 ++++ packages/runtime/src/scr_closure.h | 3 +- packages/runtime/src/scr_stream.c | 1 - .../8531-stream-saved-callback-receiver.mjs | 34 +++++++++++++++++++ .../label.js | 13 +++++++ .../main.ts | 32 +++++++++++++++++ 7 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8531-stream-saved-callback-receiver.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8532-stream-saved-callback-receiver/main.ts.json create mode 100644 tests/corpus/8531-stream-saved-callback-receiver.mjs create mode 100644 tests/corpus/8532-stream-saved-callback-receiver/label.js create mode 100644 tests/corpus/8532-stream-saved-callback-receiver/main.ts diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8531-stream-saved-callback-receiver.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8531-stream-saved-callback-receiver.mjs.json new file mode 100644 index 000000000..ab3125e3a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8531-stream-saved-callback-receiver.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8531-stream-saved-callback-receiver.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8532-stream-saved-callback-receiver/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8532-stream-saved-callback-receiver/main.ts.json new file mode 100644 index 000000000..73ad7e0e2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8532-stream-saved-callback-receiver/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/8532-stream-saved-callback-receiver/label.js", + "/tests/corpus/8532-stream-saved-callback-receiver/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_closure.h b/packages/runtime/src/scr_closure.h index ccf3bc529..89f0378af 100644 --- a/packages/runtime/src/scr_closure.h +++ b/packages/runtime/src/scr_closure.h @@ -95,8 +95,7 @@ void scr_box_set_ref(ScrBox *b, void *v); /* releases old, owns new */ #define SCR_CLOSURE_KIND_CHECKED_ADAPTER 8u /* a checked callable adapter: the callable is in caps[0] */ /* The body never observes the ambient receiver (the `this` an unbound call * binds), so compiled callers skip the receiver bracket around an indirect - * call. Set by the compiler, and by the runtime on the stream completion - * callbacks, whose thunks never read it; the kind proper is + * call. Set only by the compiler; the kind proper is * function_kind & SCR_CLOSURE_KIND_MASK. */ #define SCR_CLOSURE_KIND_RECEIVER_FREE 16u #define SCR_CLOSURE_KIND_MASK 15u diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 1f07d167b..590199468 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -541,7 +541,6 @@ void scr_stream_release_v(void *s) { scr_stream_release(s); } * the stream. Returns +1. */ static ScrClosure *scr_stream_done_new(ScrStream *s, void *fn) { ScrClosure *c = scr_closure_new(fn, 1); - c->function_kind |= SCR_CLOSURE_KIND_RECEIVER_FREE; /* the completion thunks never read `this` */ ScrBox *bx = scr_box_new_obj(scr_stream_retain_v, scr_stream_release_v, scr_stream_trace); c->caps[0] = bx; scr_box_set_ref(bx, scr_stream_retain(s)); diff --git a/tests/corpus/8531-stream-saved-callback-receiver.mjs b/tests/corpus/8531-stream-saved-callback-receiver.mjs new file mode 100644 index 000000000..886b4d29d --- /dev/null +++ b/tests/corpus/8531-stream-saved-callback-receiver.mjs @@ -0,0 +1,34 @@ +// A write's saved completion called later inside another object's method: +// the write(chunk, callback) callback it runs is called unbound, so its +// `this` is undefined (module code is strict), and the next queued write's +// _write sees the stream as `this`. +import { Writable } from "node:stream"; + +let saved = null; +const seen = []; +const label = (self) => (self === undefined ? "undefined" : self === w ? "stream" : self === holder ? "holder" : typeof self); +const w = new Writable({ + objectMode: true, + write(chunk, _enc, cb) { + seen.push(`write ${chunk.n} this=${label(this)}`); + if (chunk.n === 1) saved = cb; + else cb(); + }, +}); +const holder = { + run() { + saved(); + }, +}; +w.write({ n: 1 }, function () { + seen.push(`callback 1 this=${label(this)}`); +}); +w.write({ n: 2 }, function () { + seen.push(`callback 2 this=${label(this)}`); +}); +setTimeout(() => { + holder.run(); + w.end(() => { + for (const line of seen) console.log(line); + }); +}, 0); diff --git a/tests/corpus/8532-stream-saved-callback-receiver/label.js b/tests/corpus/8532-stream-saved-callback-receiver/label.js new file mode 100644 index 000000000..ab824f44d --- /dev/null +++ b/tests/corpus/8532-stream-saved-callback-receiver/label.js @@ -0,0 +1,13 @@ +// write(chunk, callback) callbacks that report their `this`; module code is +// strict, so an unbound call sees undefined. +export function makeCallback(seen, name, holder) { + return function () { + seen.push(`${name} this=${this === undefined ? "undefined" : this === holder ? "holder" : typeof this}`); + }; +} + +// Calls holder.run() as a method from JavaScript: run's own call of the +// saved completion happens while this call's receiver is the holder. +export function callRun(holder) { + holder.run(); +} diff --git a/tests/corpus/8532-stream-saved-callback-receiver/main.ts b/tests/corpus/8532-stream-saved-callback-receiver/main.ts new file mode 100644 index 000000000..dccb65c08 --- /dev/null +++ b/tests/corpus/8532-stream-saved-callback-receiver/main.ts @@ -0,0 +1,32 @@ +// A saved write completion called inside a class method, which JavaScript +// calls as holder.run(), runs the write(chunk, callback) callbacks unbound: +// a JavaScript callback sees `this` as undefined, not the method's +// receiver. +import { Writable } from "node:stream"; +import { callRun, makeCallback } from "./label.js"; + +let saved: ((error?: Error | null) => void) | null = null; +const seen: string[] = []; +const w = new Writable({ + objectMode: true, + write(chunk: { n: number }, _enc, cb) { + seen.push(`write ${chunk.n}`); + if (chunk.n === 1) saved = cb; + else cb(); + }, +}); +class Holder { + label = "holder"; + run(): void { + if (saved !== null) saved(); + } +} +const holder = new Holder(); +w.write({ n: 1 }, makeCallback(seen, "callback 1", holder)); +w.write({ n: 2 }, makeCallback(seen, "callback 2", holder)); +setTimeout(() => { + callRun(holder); + w.end(() => { + for (const line of seen) console.log(line); + }); +}, 0); From ad9fa7f510917f81f4f0e23ba59057a3e6928ebc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 02:07:17 -0500 Subject: [PATCH 23/70] Compile the functions a sparse profile never ran as cold code A profile merged with --sparse has no record of the functions its training never ran, so a build with it optimized them as without a profile, and on tsc-ts they are 72% of the program's IR. The block-count file a recording build writes now also names those functions, with an entry count of zero and no blocks. A --profile-use build that reads the file gives such a function an entry count of 0 and the cold attribute, as it does for a mismatched function the file records as never run, so LLVM optimizes it for size and places it with the other unlikely code. A function the file does not name, such as one added since the profile was collected, keeps no counts. --- docs/content/docs/cli.mdx | 4 +- native/llvm-codegen/src/emit.cpp | 24 +++- native/llvm-codegen/src/profile-blocks.cpp | 10 ++ native/llvm-codegen/src/profile-blocks.h | 2 + packages/compiler/test/native-profile.test.ts | 114 +++++++++++++++++- 5 files changed, 146 insertions(+), 8 deletions(-) diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index f439edf52..fc3ae2790 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -309,14 +309,14 @@ A profile that already holds context-sensitive counts cannot start a second phas ### Block counts -A profile matches a function only while the function's control flow hashes as it did when the profile was collected, so a change to the program or to scriptc leaves the changed functions without counts. A block-count file beside the profile, with its name and a `.blocks` extension in place of `.profdata` (`app.blocks` beside `app.profdata`), describes each function by the labels of its blocks instead. `--profile-use` reads it when it exists and annotates the functions whose control flow changed (the mismatched ones; a function the profile has no record of keeps no counts): blocks that keep their place in the function take their recorded counts, and the counts of the other blocks follow from the counts of the blocks and edges around them. The report then adds a line, and the warning counts only the functions that remain without counts: +A profile matches a function only while the function's control flow hashes as it did when the profile was collected, so a change to the program or to scriptc leaves the changed functions without counts. A block-count file beside the profile, with its name and a `.blocks` extension in place of `.profdata` (`app.blocks` beside `app.profdata`), describes each function by the labels of its blocks instead. `--profile-use` reads it when it exists and annotates the functions whose control flow changed (the mismatched ones): blocks that keep their place in the function take their recorded counts, and the counts of the other blocks follow from the counts of the blocks and edges around them. The file also names the functions that never ran while the profile was collected, which a profile merged with `--sparse` has no record of. Those functions are compiled as cold code (an entry count of 0, optimized for size and placed with the other unlikely code), which shortens the build. A function the file does not name, for example one added since the profile was collected, keeps no counts and is optimized as without a profile. The report then adds a line, and the warning counts only the functions that remain without counts: ``` scriptc: profile app-cs.profdata: 0 functions annotated, 1 mismatched, 0 without a record scriptc: profile app-cs.profdata: 1 function without profile counts annotated from app-cs.blocks ``` -A `--profile-use` build writes the block counts of the functions its profile annotates when the `SCRIPTC_RECORD_PROFILE_BLOCKS` environment variable names a path; such a build reuses no cached artifacts. For a context-sensitive profile, record them in the second phase, which the first-phase profile annotates: +A `--profile-use` build writes the block counts of the functions its profile annotates, and the names of the functions it has no record of, when the `SCRIPTC_RECORD_PROFILE_BLOCKS` environment variable names a path; such a build reuses no cached artifacts. For a context-sensitive profile, record them in the second phase, which the first-phase profile annotates: ```console $ SCRIPTC_RECORD_PROFILE_BLOCKS=app-cs.blocks scriptc build app.ts --optimization=speed --profile-use app.profdata --profile-generate profiles-cs/%p.profraw -o app-instrumented-cs diff --git a/native/llvm-codegen/src/emit.cpp b/native/llvm-codegen/src/emit.cpp index 17d69be72..3eeedd623 100644 --- a/native/llvm-codegen/src/emit.cpp +++ b/native/llvm-codegen/src/emit.cpp @@ -374,23 +374,34 @@ bool hasProfileCounts(const Function &F) { // Runs right after the profile annotation, before inlining and dead code // elimination change the functions it annotated: counts them and records -// their block counts, then annotates the functions whose control flow -// changed from FallbackCounts. A function the profile has no record of keeps -// no counts, as without block counts. Returns the analyses that remain valid. +// their block counts, then annotates from FallbackCounts the functions whose +// control flow changed, and the functions the profile has no record of that +// FallbackCounts records as never run. A sparse profile leaves out the +// functions its training never ran; the block counts name them, so they get +// an entry count of 0 and LLVM optimizes them as cold code, as a profile with +// zero counts would make it. A function neither names keeps no counts, as +// without block counts: it was added after the profile was collected. +// Returns the analyses that remain valid. PreservedAnalyses afterProfileAnnotation(Module &M, ProfileMatch &Match) { std::vector Mismatched; + std::vector Unrecorded; for (Function &F : M) { if (F.isDeclaration()) continue; if (!hasProfileCounts(F)) { if (hasHashMismatchTag(F)) Mismatched.push_back(&F); + else + Unrecorded.push_back(&F); continue; } ++Match.Annotated; if (Recorder) Recorder->add(F); } + if (Recorder) + for (Function *F : Unrecorded) + Recorder->addNeverRan(*F); if (!FallbackCounts) return PreservedAnalyses::all(); ProfileSummaryInfo Summary(M); @@ -403,6 +414,13 @@ PreservedAnalyses afterProfileAnnotation(Module &M, ProfileMatch &Match) { ++Match.Recovered; Preserved = PreservedAnalyses::none(); } + for (Function *F : Unrecorded) { + auto Record = FallbackCounts->find(F->getName()); + if (Record == FallbackCounts->end() || Record->second.EntryCount != 0) + continue; + annotateFromBlocks(*F, Record->second, Summary); + Preserved = PreservedAnalyses::none(); + } return Preserved; } diff --git a/native/llvm-codegen/src/profile-blocks.cpp b/native/llvm-codegen/src/profile-blocks.cpp index 4c94577a3..6504f585b 100644 --- a/native/llvm-codegen/src/profile-blocks.cpp +++ b/native/llvm-codegen/src/profile-blocks.cpp @@ -424,6 +424,16 @@ void annotateFromBlocks(Function &F, const FunctionBlocks &Record, markFrequency(F, EntryCount, MaxCount, Summary); } +void BlockRecorder::addNeverRan(const Function &F) { + std::string Text; + raw_string_ostream OS(Text); + OS << "f "; + writeEscaped(OS, F.getName()); + OS << " 0 0\n"; + std::lock_guard Guard(Lock); + Records.emplace(F.getName().str(), std::move(Text)); +} + void BlockRecorder::add(const Function &F) { std::optional Entry = F.getEntryCount(); if (!Entry || Entry->isSynthetic()) diff --git a/native/llvm-codegen/src/profile-blocks.h b/native/llvm-codegen/src/profile-blocks.h index 1db71b53e..9368bcbe2 100644 --- a/native/llvm-codegen/src/profile-blocks.h +++ b/native/llvm-codegen/src/profile-blocks.h @@ -71,6 +71,8 @@ class BlockRecorder { public: // Records F's block counts, which the profile annotation just gave it. void add(const llvm::Function &F); + // Records F, which the profile has no record of, as never run. + void addNeverRan(const llvm::Function &F); // Writes the file through a private sibling; returns an error message on // failure. std::optional write(llvm::StringRef Path); diff --git a/packages/compiler/test/native-profile.test.ts b/packages/compiler/test/native-profile.test.ts index e860d4010..6744a0973 100644 --- a/packages/compiler/test/native-profile.test.ts +++ b/packages/compiler/test/native-profile.test.ts @@ -130,6 +130,76 @@ function expectedExit(argc: number, edit = 0): number { return Number(total % 251n); } +/** A program whose `rare` runs only with more than ten arguments, which + * training never passes; `added` (with `extra`) is defined after the + * profile was collected. */ +function rareProgram(extra = false): string { + const lines = ["; profiled program with a function training never runs"]; + const helpers: [string, number][] = [ + ["hot", 5], + ["rare", 9], + ...(extra ? [["added", 11]] : []), + ] as [string, number][]; + for (const [name, factor] of helpers) + lines.push( + `define internal i64 @${name}(i64 %n) noinline {`, + "entry:", + ` %m = mul i64 %n, ${factor}`, + " %bit = and i64 %m, 1", + " %odd = icmp ne i64 %bit, 0", + " br i1 %odd, label %a, label %b", + "a:", + ` %x = add i64 %m, ${factor}`, + " br label %c", + "b:", + " %y = sub i64 %m, 1", + " br label %c", + "c:", + " %r = phi i64 [ %x, %a ], [ %y, %b ]", + " ret i64 %r", + "}", + ); + lines.push( + "define i32 @main(i32 %argc, ptr %argv) {", + "entry:", + " %w = sext i32 %argc to i64", + " %h = call i64 @hot(i64 %w)", + " %many = icmp ugt i32 %argc, 10", + " br i1 %many, label %rare.call, label %done", + "rare.call:", + " %r = call i64 @rare(i64 %h)", + ...(extra ? [" %e = call i64 @added(i64 %r)"] : []), + " br label %done", + "done:", + ` %v = phi i64 [ %h, %entry ], [ ${extra ? "%e" : "%r"}, %rare.call ]`, + " %low = and i64 %v, 127", + " %code = trunc i64 %low to i32", + " ret i32 %code", + "}", + ); + return lines.join("\n") + "\n"; +} + +function rareExit(argc: number): number { + const step = (n: number, factor: number) => { + const m = n * factor; + return m % 2 === 1 ? m + factor : m - 1; + }; + let v = step(argc, 5); + if (argc > 10) v = step(v, 9); + return v & 127; +} + +/** The section that holds `name` in an object file. */ +async function sectionOf(object: string, name: string): Promise { + const { stdout } = await execFileAsync("objdump", ["-t", object]); + return stdout + .split("\n") + .map((line) => line.trim().split(/\s+/)) + .find((fields) => fields.at(-1) === name) + ?.at(-3); +} + const INLINED = 6; /** A program whose helpers are too large for the inliner that runs before the @@ -263,8 +333,14 @@ async function run(executable: string, argc: number, env: NodeJS.ProcessEnv = {} } } -async function merge(output: string, inputs: string[]) { - return execFileAsync(helper!, ["merge-profiles", "--output", output, ...inputs]); +async function merge(output: string, inputs: string[], sparse = false) { + return execFileAsync(helper!, [ + "merge-profiles", + "--output", + output, + ...(sparse ? ["--sparse"] : []), + ...inputs, + ]); } /** An instrumented build of `source` run with 1 and 3 arguments, its raw @@ -275,6 +351,7 @@ async function collect( outputs: number, name: string, expected: (argc: number) => number = expectedExit, + sparse = false, ) { const raw = join(dir, `${name}-raw`); const objects = await emit(dir, source, { outputs, name, generate: join(raw, "%p.profraw") }); @@ -284,7 +361,7 @@ async function collect( const profiles = (await readdir(raw)).map((file) => join(raw, file)); expect(profiles).toHaveLength(2); const profile = join(dir, `${name}.profdata`); - await merge(profile, profiles); + await merge(profile, profiles, sparse); return { profile, raws: profiles }; } @@ -563,6 +640,37 @@ describe.runIf(helper !== null)("LLVM profile-guided optimization", () => { }); }); + test("block counts mark the functions a sparse profile never ran as cold", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-profile-blocks-never-")); + dirs.push(dir); + const { profile } = await collect(dir, rareProgram(), 1, "train", rareExit, true); + const blocks = join(dir, "train.blocks"); + await emit(dir, rareProgram(), { + outputs: 1, + name: "recorded", + use: profile, + record: blocks, + }); + const text = await readFile(blocks, "utf8"); + expect(text).toMatch(/^f hot [1-9]\d* [1-9]\d*$/m); + expect(text).toMatch(/^f rare 0 0$/m); + + // Without the block counts rare keeps no counts; with them it is cold + // code. added, which the block counts do not name, keeps no counts. + const [plain] = await emit(dir, rareProgram(), { outputs: 1, name: "plain", use: profile }); + const [cold] = await emit(dir, rareProgram(true), { + outputs: 1, + name: "cold", + use: profile, + blocks, + }); + expect(await sectionOf(plain!, "rare")).toBe(".text"); + expect(await sectionOf(cold!, "rare")).toMatch(/^\.text\.unlikely/); + expect(await sectionOf(cold!, "added")).toBe(".text"); + expect(await sectionOf(cold!, "hot")).toMatch(/^\.text\.hot/); + expect(await run(await link(dir, [cold!], "cold"), 2)).toBe(rareExit(2)); + }); + test("block counts need a profile and a file that parses", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-profile-blocks-error-")); dirs.push(dir); From ed46427ea7913b4f6244f776aa24c24ae678395e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 02:41:44 -0500 Subject: [PATCH 24/70] Read /proc/self/maps once per glibc thread heap A worker thread's large blocks come from glibc heaps, and the first large block of each heap made the allocator check /proc/self/maps before advising the heap huge pages. The check was skipped only for the heap the thread advised last, so threads that switched between heaps read the maps again at every switch: 9,000 to 11,000 reads in a type check of T3 web with four checkers. A process-wide table now records every heap decided, and each heap is read once. --- packages/runtime/src/scr_alloc.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index 13b83a396..4e179ed91 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -640,6 +640,30 @@ static _Atomic(uintptr_t) scr_sa_heap_page; /* the page size, read with the mode #define SCR_SA_THREAD_TRIM_THRESHOLD (8 << 20) static _Thread_local size_t scr_sa_thread_large_bytes; static _Thread_local uintptr_t scr_sa_thread_heap; /* the heap this thread advised last */ +/* The heaps decided already, process-wide: advised, or found not to be a + * whole glibc heap. Each heap is checked against /proc/self/maps once: the + * threads of a type check switch between heaps often, and reading the maps + * at each switch cost thousands of reads of a file that grows with the + * process's mappings. A full table decides again; a heap that glibc maps + * again at an address decided before keeps the pages it gets. */ +#define SCR_SA_HEAPS_SEEN 1024u +static _Atomic(uintptr_t) scr_sa_heaps_seen[SCR_SA_HEAPS_SEEN]; + +/* Whether `heap` was decided already; records it when it was not. */ +static bool scr_sa_heap_seen(uintptr_t heap) { + size_t h = (size_t)(((uint64_t)(heap / SCR_SA_GLIBC_HEAP) * 0x9E3779B97F4A7C15ull) >> 54) & (SCR_SA_HEAPS_SEEN - 1); + for (size_t k = 0; k < SCR_SA_HEAPS_SEEN; k++, h = (h + 1) & (SCR_SA_HEAPS_SEEN - 1)) { + uintptr_t cur = atomic_load_explicit(&scr_sa_heaps_seen[h], memory_order_relaxed); + if (cur == heap) return true; + if (cur == 0) { + if (atomic_compare_exchange_strong_explicit(&scr_sa_heaps_seen[h], &cur, heap, memory_order_relaxed, + memory_order_relaxed)) + return false; + if (cur == heap) return true; + } + } + return false; +} static _Atomic(int) scr_sa_thread_heaps; /* 0 undecided, 1 advised, 2 off */ extern void *__libc_malloc(size_t); @@ -716,6 +740,7 @@ static void scr_sa_thread_heap_note(void *p, size_t n) { uintptr_t heap = (uintptr_t)p & ~(SCR_SA_GLIBC_HEAP - 1); if (heap == scr_sa_thread_heap) return; scr_sa_thread_heap = heap; + if (scr_sa_heap_seen(heap)) return; if (scr_sa_glibc_heap_mapped(heap) && mprotect((void *)heap, SCR_SA_GLIBC_HEAP, PROT_READ | PROT_WRITE) == 0) (void)madvise((void *)heap, SCR_SA_GLIBC_HEAP, MADV_HUGEPAGE); } From 398e2e9e9cc92c18fc71393f5a140ce9a275242b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 02:42:47 -0500 Subject: [PATCH 25/70] Weigh program profile matches by the profile's counts The LLVM helper's emit reports the sums of the profile's counters on the functions it annotated and on the functions whose control flow changed since the profile was collected (annotated_counts and mismatched_counts), and the partition cache keeps them with each partition's match. The annotated ones are listed as the profile annotation leaves them, before block counts recover any. Builds of the native compiler print the share of the profile's counts that changed functions hold. --- native/llvm-codegen/src/emit.cpp | 143 +++++++++++++++--- .../compiler/src/backend/native-codegen.ts | 3 + .../src/backend/native-profile.test.ts | 14 ++ .../compiler/src/backend/native-profile.ts | 18 ++- packages/compiler/test/native-profile.test.ts | 29 +++- scripts/build-native-cli.mts | 9 ++ 6 files changed, 192 insertions(+), 24 deletions(-) diff --git a/native/llvm-codegen/src/emit.cpp b/native/llvm-codegen/src/emit.cpp index 17d69be72..ad02120fa 100644 --- a/native/llvm-codegen/src/emit.cpp +++ b/native/llvm-codegen/src/emit.cpp @@ -252,15 +252,20 @@ struct UnnameStructTypes : PassInfoMixin { // 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 since it was collected, or never run in training -// when the profile was merged with --sparse). A partition reused from the cache without its counts is counted -// in Unknown. Recovered counts the mismatched functions that --profile-blocks -// annotated, so it never exceeds Mismatched. +// when the profile was merged with --sparse). A partition reused from the cache +// without its counts is counted in Unknown. Recovered counts the mismatched +// functions that --profile-blocks annotated, so it never exceeds Mismatched. +// The two weights sum the profile's counters for the annotated and for the +// mismatched functions, so a changed hot function weighs more than a changed +// cold one. struct ProfileMatch { std::atomic Annotated{0}; std::atomic Mismatched{0}; std::atomic Missing{0}; std::atomic Recovered{0}; std::atomic Unknown{0}; + std::atomic AnnotatedCounts{0}; + std::atomic MismatchedCounts{0}; // Set once the annotation before inlining has run. The context-sensitive // annotation after inlining, which a single-module pipeline also runs, // reports its mismatched and missing functions again; they are not @@ -272,27 +277,32 @@ struct ProfileMatch { Mismatched += Other.Mismatched; Missing += Other.Missing; Recovered += Other.Recovered; + AnnotatedCounts += Other.AnnotatedCounts; + MismatchedCounts += Other.MismatchedCounts; } std::string serialize() const { - return formatv("{0} {1} {2} {3}", Annotated.load(), Mismatched.load(), - Missing.load(), Recovered.load()) + return formatv("{0} {1} {2} {3} {4} {5}", Annotated.load(), + Mismatched.load(), Missing.load(), Recovered.load(), + AnnotatedCounts.load(), MismatchedCounts.load()) .str(); } bool deserialize(StringRef Text) { - SmallVector Parts; + SmallVector Parts; Text.split(Parts, ' '); - uint64_t Values[4]; - if (Parts.size() != 4) + uint64_t Values[6]; + if (Parts.size() != 6) return false; - for (size_t I = 0; I < 4; ++I) + for (size_t I = 0; I < 6; ++I) if (Parts[I].getAsInteger(10, Values[I])) return false; Annotated += Values[0]; Mismatched += Values[1]; Missing += Values[2]; Recovered += Values[3]; + AnnotatedCounts += Values[4]; + MismatchedCounts += Values[5]; return true; } }; @@ -412,11 +422,77 @@ void reportProfileMatch(const ProfileMatch &Match) { {"mismatched", Match.Mismatched.load()}, {"missing", Match.Missing.load()}, {"recovered", Match.Recovered.load()}, - {"unknown_partitions", Match.Unknown.load()}}; + {"unknown_partitions", Match.Unknown.load()}, + {"annotated_counts", Match.AnnotatedCounts.load()}, + {"mismatched_counts", Match.MismatchedCounts.load()}}; json::Object Response{{"ok", true}, {"profile", std::move(Profile)}}; outs() << formatv("{0}\n", json::Value(std::move(Response))); } +// The sum of each function's counters in the indexed profile at `Path`, by +// profile name, read once per process. An unreadable profile has no counts +// (the annotation reports it). +const StringMap &profileCounts(const std::string &Path) { + static std::once_flag Once; + static StringMap Counts; + std::call_once(Once, [&Path] { + Expected> Reader = + IndexedInstrProfReader::create(Path, *vfs::getRealFileSystem()); + if (!Reader) { + consumeError(Reader.takeError()); + return; + } + for (const NamedInstrProfRecord &Record : **Reader) { + uint64_t Sum = 0; + for (uint64_t Count : Record.Counts) + Sum += Count; + Counts[Record.Name] += Sum; + } + consumeError((*Reader)->getError()); + }); + return Counts; +} + +// Weighs one module's profile match by the profile's counters: collects the +// annotated and the mismatched functions while the module is optimized, +// then adds their counts to the match. +class MatchWeigher { +public: + explicit MatchWeigher(const Module &M) { + for (const Function &F : M) + if (!F.isDeclaration()) + ProfileNames[F.getName()] = getIRPGOFuncName(F); + } + + void weigh(const std::string &ProfileUse, ProfileMatch &Match) const { + const StringMap &Counts = profileCounts(ProfileUse); + auto CountsOf = [&Counts](StringRef Name) -> uint64_t { + auto It = Counts.find(Name); + return It == Counts.end() ? 0 : It->second; + }; + // A context-sensitive profile annotates the module twice; each function + // counts once. + StringSet<> Seen; + for (const std::string &Name : Annotated) + if (Seen.insert(Name).second) + Match.AnnotatedCounts += CountsOf(Name); + // The mismatch warnings name functions by symbol. + for (const std::string &Name : Mismatched) { + auto It = ProfileNames.find(Name); + StringRef ProfileName = + It == ProfileNames.end() ? StringRef(Name) : StringRef(It->second); + if (Seen.insert(ProfileName).second) + Match.MismatchedCounts += CountsOf(ProfileName); + } + } + + std::vector Annotated; + std::vector Mismatched; + +private: + StringMap ProfileNames; +}; + // Reads the profile's header once, before any partition needs it: it must // hold IR-level counts. Context-sensitive counts lay out the code by them; // a profile that has them cannot take a second set. @@ -515,13 +591,15 @@ struct Pipeline { }); } - // Adds to Names the profile names of the functions the annotation gave - // counts to, as soon as that pass has run. + // Adds to Names the profile names of the functions the annotation before + // inlining gave counts to, as soon as that pass has run. void collectAnnotated(std::vector &Names) { Callbacks.registerAfterPassCallback( - [&Names](StringRef PassID, Any IR, const PreservedAnalyses &) { - if (PassID != "PGOInstrumentationUse") + [&Names, Done = false](StringRef PassID, Any IR, + const PreservedAnalyses &) mutable { + if (Done || PassID != "PGOInstrumentationUse") return; + Done = true; if (const auto *M = llvm::any_cast(&IR)) for (const Function &F : **M) { std::optional Entry = F.getEntryCount(); @@ -714,13 +792,21 @@ emitModule(Module &M, TargetMachine &Machine, OptimizationLevel Level, StringRef OutputPath, std::vector> &Temporaries, ProfileMatch &Match) { bool Annotating = Profile && Profile->Action == PGOOptions::IRUse; + std::string ProfileUse = Annotating ? Profile->ProfileFile : ""; + MatchWeigher Weigher(M); if (Annotating) M.getContext().setDiagnosticHandler( - std::make_unique(Match)); + std::make_unique(Match, &Weigher.Mismatched)); Pipeline P(Machine, Level, std::move(Profile)); - if (Annotating) + if (Annotating) { + // Registered first, so it lists the functions the profile itself + // annotated, before block counts recover any. + P.collectAnnotated(Weigher.Annotated); P.afterProfileAnnotation(Match); + } P.PB.buildPerModuleDefaultPipeline(Level).run(M, P.MAM); + if (Annotating) + Weigher.weigh(ProfileUse, Match); if (std::optional Failure = verify(M, "post_optimization_verification_failed")) return Failure; @@ -911,14 +997,16 @@ compileOptimizedPartitions(const EmitOptions &Options, return Failures.set({"target_machine_failed", LookupError}); LLVMContext Context; ProfileMatch PartitionMatch; + std::vector MismatchedNames; if (Annotating) - Context.setDiagnosticHandler( - std::make_unique(PartitionMatch)); + Context.setDiagnosticHandler(std::make_unique( + PartitionMatch, &MismatchedNames)); Expected> Loaded = loadPartition(Options, Plan.Modules[I], I, Context, *Machine); if (!Loaded) return Failures.set({"partition_failed", toString(Loaded.takeError())}); Module &Mod = **Loaded; + MatchWeigher Weigher(Mod); if (!Options.ImportBitcode.empty()) { // See emit(): the attribute is inert without a sanitizer pass and // would block inlining of the imported runtime bodies. @@ -937,8 +1025,12 @@ 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) + if (Annotating) { + // Registered first, so it lists the functions the profile itself + // annotated, before block counts recover any. + P.collectAnnotated(Weigher.Annotated); P.afterProfileAnnotation(PartitionMatch); + } ModulePassManager MPM = P.PB.buildThinLTOPreLinkDefaultPipeline(Level); MPM.addPass(UnnameStructTypes()); // The module hash is set at the thin link instead (see Names). @@ -948,6 +1040,8 @@ compileOptimizedPartitions(const EmitOptions &Options, MPM.run(Mod, P.MAM); } if (Annotating) { + Weigher.Mismatched = std::move(MismatchedNames); + Weigher.weigh(Options.ProfileUse, PartitionMatch); Match.add(PartitionMatch); // Stored before the summary, so a reused summary finds its counts. Cache.store(Key, "match", PartitionMatch.serialize()); @@ -1445,15 +1539,22 @@ std::optional emitSplitCodegen(const EmitOptions &Options, Module &M, TargetMachine &Machine, OptimizationLevel Level, ProfileMatch &Match) { bool Annotating = !Options.ProfileUse.empty(); + MatchWeigher Weigher(M); if (Annotating) M.getContext().setDiagnosticHandler( - std::make_unique(Match)); + std::make_unique(Match, &Weigher.Mismatched)); { Pipeline P(Machine, Level, profileOptions(Options)); - if (Annotating) + if (Annotating) { + // Registered first, so it lists the functions the profile itself + // annotated, before block counts recover any. + P.collectAnnotated(Weigher.Annotated); P.afterProfileAnnotation(Match); + } P.PB.buildPerModuleDefaultPipeline(Level).run(M, P.MAM); } + if (Annotating) + Weigher.weigh(Options.ProfileUse, Match); if (std::optional Failure = verify(M, "post_optimization_verification_failed")) return Failure; diff --git a/packages/compiler/src/backend/native-codegen.ts b/packages/compiler/src/backend/native-codegen.ts index c683ffaee..c7c18a8a7 100644 --- a/packages/compiler/src/backend/native-codegen.ts +++ b/packages/compiler/src/backend/native-codegen.ts @@ -563,6 +563,9 @@ function helperProfileMatch(match: NativeProfileMatch): Record { missing: match.missing, recovered: match.recovered, unknown_partitions: match.unknownPartitions, + ...(match.annotatedCounts === undefined || match.mismatchedCounts === undefined + ? {} + : { annotated_counts: match.annotatedCounts, mismatched_counts: match.mismatchedCounts }), }; } diff --git a/packages/compiler/src/backend/native-profile.test.ts b/packages/compiler/src/backend/native-profile.test.ts index 7d2119d4b..77207f834 100644 --- a/packages/compiler/src/backend/native-profile.test.ts +++ b/packages/compiler/src/backend/native-profile.test.ts @@ -145,6 +145,20 @@ test("the helper's profile match is read from its output", () => { '{"ok":true,"profile":{"annotated":1,"mismatched":0,"missing":0,"recovered":-1,"unknown_partitions":0}}\n', ), ).toBeNull(); + // With the profile's counters on the annotated and the mismatched functions. + expect( + parseNativeProfileMatch( + '{"ok":true,"profile":{"annotated":120,"mismatched":7,"missing":40,"recovered":0,"unknown_partitions":0,"annotated_counts":9000,"mismatched_counts":1000}}\n', + ), + ).toEqual({ + annotated: 120, + mismatched: 7, + missing: 40, + recovered: 0, + unknownPartitions: 0, + annotatedCounts: 9000, + mismatchedCounts: 1000, + }); // A helper that predates the report prints nothing. expect(parseNativeProfileMatch("")).toBeNull(); expect(parseNativeProfileMatch('{"ok":true}\n')).toBeNull(); diff --git a/packages/compiler/src/backend/native-profile.ts b/packages/compiler/src/backend/native-profile.ts index fa6fc134f..b581c73ed 100644 --- a/packages/compiler/src/backend/native-profile.ts +++ b/packages/compiler/src/backend/native-profile.ts @@ -111,13 +111,16 @@ export function nativeProfileHelperArgs(profile: NativeProfileOptions | undefine * profile has no record of (new, or never run while it was collected), * mismatched functions that the block-count file annotated (never more * than the mismatched ones), and partitions reused from the cache without - * their counts. */ + * their counts. Helpers that weigh the match also report the profile's + * counters on the annotated and on the mismatched functions. */ export interface NativeProfileMatch { annotated: number; mismatched: number; missing: number; recovered: number; unknownPartitions: number; + annotatedCounts?: number; + mismatchedCounts?: number; } /** The profile match in a successful helper emit's output, or null when the @@ -150,7 +153,18 @@ export function parseNativeProfileMatch(stdout: string): NativeProfileMatch | nu unknownPartitions === null ) continue; - return { annotated, mismatched, missing, recovered, unknownPartitions }; + const annotatedCounts = count("annotated_counts"); + const mismatchedCounts = count("mismatched_counts"); + return { + annotated, + mismatched, + missing, + recovered, + unknownPartitions, + ...(annotatedCounts === null || mismatchedCounts === null + ? {} + : { annotatedCounts, mismatchedCounts }), + }; } return null; } diff --git a/packages/compiler/test/native-profile.test.ts b/packages/compiler/test/native-profile.test.ts index e860d4010..8fbecb1ee 100644 --- a/packages/compiler/test/native-profile.test.ts +++ b/packages/compiler/test/native-profile.test.ts @@ -426,6 +426,27 @@ 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 weighs the functions that changed by their counts", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-profile-weights-")); + dirs.push(dir); + const { profile } = await collect(dir, program(), 1, "train"); + // f4 gained a branch, so the profile no longer describes it. + const edited = program(0, true); + const { outputs, stdout } = await emitReporting(dir, edited, { + outputs: 1, + name: "edited", + use: profile, + }); + expect(await run(await link(dir, outputs, "edited"), 2)).toBe(expectedExit(2)); + const { profile: match } = JSON.parse(stdout.trim().split("\n").at(-1)!) as { + profile: Record; + }; + expect(match["mismatched"]).toBe(1); + // main and the other f keep their counts; f4's are reported apart. + expect(match["mismatched_counts"]).toBeGreaterThan(0); + expect(match["annotated_counts"]).toBeGreaterThan(match["mismatched_counts"]!); + }); + test("the match report counts annotated functions that inlining removes", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-profile-match-")); dirs.push(dir); @@ -438,7 +459,11 @@ describe.runIf(helper !== null)("LLVM profile-guided optimization", () => { 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({ + const report = JSON.parse(stdout.trim().split("\n").at(-1)!) as { + ok: boolean; + profile: Record; + }; + expect(report).toMatchObject({ ok: true, profile: { annotated: INLINED + 1, @@ -446,8 +471,10 @@ describe.runIf(helper !== null)("LLVM profile-guided optimization", () => { missing: 0, recovered: 0, unknown_partitions: 0, + mismatched_counts: 0, }, }); + expect(report.profile["annotated_counts"]).toBeGreaterThan(0); const symbols = await execFileAsync("nm", [outputs[0]!]); expect(symbols.stdout).not.toMatch(/\bg\d\b/); }); diff --git a/scripts/build-native-cli.mts b/scripts/build-native-cli.mts index 6c17b829b..43eb2b27a 100644 --- a/scripts/build-native-cli.mts +++ b/scripts/build-native-cli.mts @@ -189,8 +189,17 @@ const match = useProfile && "artifact" in result ? result.artifact.profileMatch if (match !== undefined) { const profiled = match.annotated + match.mismatched; const share = profiled === 0 ? 0 : match.annotated / profiled; + // The same by the profile's counters: a changed hot function loses more + // of the profile's gain than a changed cold one. + const counts = + match.annotatedCounts === undefined || match.mismatchedCounts === undefined + ? 0 + : match.annotatedCounts + match.mismatchedCounts; console.log( `native compiler profile: ${match.annotated} of ${profiled} profiled functions match (${(100 * share).toFixed(1)}%); ${match.missing} functions have no profile` + + (counts > 0 + ? `; changed functions hold ${((100 * match.mismatchedCounts!) / counts).toFixed(1)}% of its counts` + : "") + (match.recovered > 0 ? `; ${match.recovered} functions annotated from the profile's block counts` : "") + From bfc32449bd012796454a7768702452ef79a74c02 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 02:49:31 -0500 Subject: [PATCH 26/70] Check section names only for ELF objects in the never-ran profile test Mach-O and COFF objects keep every function in one text section, so the test compares the objects built with and without the block counts on every target and checks the hot and unlikely sections on ELF targets. --- packages/compiler/test/native-profile.test.ts | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/compiler/test/native-profile.test.ts b/packages/compiler/test/native-profile.test.ts index 6744a0973..7af252657 100644 --- a/packages/compiler/test/native-profile.test.ts +++ b/packages/compiler/test/native-profile.test.ts @@ -190,6 +190,9 @@ function rareExit(argc: number): number { return v & 127; } +/** The target writes ELF objects. */ +const elfTarget = target !== null && !/apple|darwin|windows|mingw/.test(target.llvmTriple); + /** The section that holds `name` in an object file. */ async function sectionOf(object: string, name: string): Promise { const { stdout } = await execFileAsync("objdump", ["-t", object]); @@ -658,16 +661,27 @@ describe.runIf(helper !== null)("LLVM profile-guided optimization", () => { // Without the block counts rare keeps no counts; with them it is cold // code. added, which the block counts do not name, keeps no counts. const [plain] = await emit(dir, rareProgram(), { outputs: 1, name: "plain", use: profile }); + const [marked] = await emit(dir, rareProgram(), { + outputs: 1, + name: "marked", + use: profile, + blocks, + }); + expect(await readFile(marked!)).not.toEqual(await readFile(plain!)); const [cold] = await emit(dir, rareProgram(true), { outputs: 1, name: "cold", use: profile, blocks, }); - expect(await sectionOf(plain!, "rare")).toBe(".text"); - expect(await sectionOf(cold!, "rare")).toMatch(/^\.text\.unlikely/); - expect(await sectionOf(cold!, "added")).toBe(".text"); - expect(await sectionOf(cold!, "hot")).toMatch(/^\.text\.hot/); + // ELF objects name the sections of hot and unlikely code; Mach-O and + // COFF objects keep every function in one text section. + if (elfTarget) { + expect(await sectionOf(plain!, "rare")).toBe(".text"); + expect(await sectionOf(cold!, "rare")).toMatch(/^\.text\.unlikely/); + expect(await sectionOf(cold!, "added")).toBe(".text"); + expect(await sectionOf(cold!, "hot")).toMatch(/^\.text\.hot/); + } expect(await run(await link(dir, [cold!], "cold"), 2)).toBe(rareExit(2)); }); From b7bcbc68d8d8662cb9b118664d79955be238527a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 02:58:50 -0500 Subject: [PATCH 27/70] Pass a Buffer to a patched stdio write from an encoded data listener A byte write to a replaced process.stdout.write or process.stderr.write was boxed with the 'data' chunk helper, which answers a string while a listener of a socket or HTTP response with setEncoding("utf8") runs. A `process.stdout.write(buffer)` from such a listener reached the replacement as a string. Node passes the Buffer it was given. The stdio hook now boxes a byte write as a Buffer whatever listener is running. --- .../main.ts.json | 7 ++++ packages/runtime/src/scr_events.c | 5 ++- .../main.ts | 32 +++++++++++++++++++ .../patch.mjs | 8 +++++ .../main.ts.json | 1 + 5 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts.json create mode 100644 tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts create mode 100644 tests/corpus/stdout-patch-buffer-from-encoded-data/patch.mjs create mode 100644 tests/harness/corpus-costs/stdout-patch-buffer-from-encoded-data/main.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts.json new file mode 100644 index 000000000..c26adc078 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/stdout-patch-buffer-from-encoded-data/patch.mjs", + "/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index de1d12105..9b918061d 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -1534,9 +1534,12 @@ static bool scr_stdio_intercept(int fd, const void *data, size_t length, int fla } ScrDyn *chunk; if (flags & SCR_STDIO_HOOK_BYTES) { + /* Always a Buffer: a write from a 'data' listener of a stream with an + * encoding set is still the bytes it was given. */ ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)length); memcpy(b->data, data, length); - chunk = scr_dyn_new_chunk(b); + b->is_buffer = true; + chunk = scr_dyn_new_bytes(b); scr_bytes_release(b); } else { ScrStr *text = scr_str_new(data, length); diff --git a/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts b/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts new file mode 100644 index 000000000..95e626f36 --- /dev/null +++ b/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts @@ -0,0 +1,32 @@ +// A Buffer written to a replaced process.stdout.write from a 'data' listener +// of a socket or an HTTP response with setEncoding("utf8") arrives as a +// Buffer; the listener's own chunks are strings. patch.mjs installs the +// replacement and prints what it received at exit. +import * as http from "node:http"; +import * as net from "node:net"; +import "./patch.mjs"; + +const tcp = net.createServer((socket) => socket.end("tcp-data")); +tcp.listen(0, "127.0.0.1", () => { + const port = (tcp.address() as net.AddressInfo).port; + const client = net.connect(port, "127.0.0.1"); + client.setEncoding("utf8"); + client.on("data", (text: string) => { + process.stdout.write(Buffer.from(`socket ${typeof text} ${text}`)); + process.stdout.write(`socket text ${text}`); + }); + client.on("end", () => { + tcp.close(); + const server = http.createServer((req, res) => res.end("http-data")); + server.listen(0, "127.0.0.1", () => { + const httpPort = (server.address() as net.AddressInfo).port; + http.get(`http://127.0.0.1:${httpPort}/`, (res) => { + res.setEncoding("utf8"); + res.on("data", (text: string) => { + process.stdout.write(Buffer.from(`response ${typeof text} ${text}`)); + }); + res.on("end", () => server.close()); + }); + }); + }); +}); diff --git a/tests/corpus/stdout-patch-buffer-from-encoded-data/patch.mjs b/tests/corpus/stdout-patch-buffer-from-encoded-data/patch.mjs new file mode 100644 index 000000000..2432a176b --- /dev/null +++ b/tests/corpus/stdout-patch-buffer-from-encoded-data/patch.mjs @@ -0,0 +1,8 @@ +// Replaces process.stdout.write with a recorder; an exit handler prints what it saw. +const seen = []; +process.stdout.write = function (chunk) { + const kind = typeof chunk === "string" ? "string" : Buffer.isBuffer(chunk) ? "Buffer" : typeof chunk; + seen.push(`${kind}:${String(chunk)}`); + return true; +}; +process.on("exit", () => process.stderr.write(`${seen.join("\n")}\n`)); diff --git a/tests/harness/corpus-costs/stdout-patch-buffer-from-encoded-data/main.ts.json b/tests/harness/corpus-costs/stdout-patch-buffer-from-encoded-data/main.ts.json new file mode 100644 index 000000000..9e947229f --- /dev/null +++ b/tests/harness/corpus-costs/stdout-patch-buffer-from-encoded-data/main.ts.json @@ -0,0 +1 @@ +{ "plain": 5044, "san": 13845 } From b319186a684959a5b9d63906d9c5a11fe48fd1fe Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 03:01:39 -0500 Subject: [PATCH 28/70] Count the arguments a replaced stdio write receives A JavaScript function assigned to process.stdout.write or process.stderr.write takes its parameters from the call, not from Node's overloads, but the lowering returned before giving the function its arguments slot. `arguments.length` then folded to the declared parameter count (3 for `function (chunk, encoding, callback)` called with one argument, 0 for a function without parameters), and reading `arguments[i]` was refused at run time. The replacement now receives the call's arguments, so `arguments` holds what the caller passed, as in Node. --- .../src/frontend/lowering/call-signatures.ts | 3 ++ ...stdio-write-replacement-arguments.cjs.json | 6 +++ .../stdio-write-replacement-arguments.cjs | 43 +++++++++++++++++++ ...stdio-write-replacement-arguments.cjs.json | 1 + 4 files changed, 53 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdio-write-replacement-arguments.cjs.json create mode 100644 tests/corpus/stdio-write-replacement-arguments.cjs create mode 100644 tests/harness/corpus-costs/stdio-write-replacement-arguments.cjs.json diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 45329d077..783082288 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -1062,6 +1062,9 @@ export function lambdaSignature( type: DYN, mode: "required", })); + // `arguments` holds the call's own tuple: a console line passes one + // or two arguments whatever the replacement declares. + appendArgumentsParameter(lowerer, node, shapes, "arguments"); return { shapes, funcType: funcTypeFromParamShapes(shapes, DYN) }; } } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdio-write-replacement-arguments.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdio-write-replacement-arguments.cjs.json new file mode 100644 index 000000000..23315d78e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdio-write-replacement-arguments.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/stdio-write-replacement-arguments.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/stdio-write-replacement-arguments.cjs b/tests/corpus/stdio-write-replacement-arguments.cjs new file mode 100644 index 000000000..c33a78210 --- /dev/null +++ b/tests/corpus/stdio-write-replacement-arguments.cjs @@ -0,0 +1,43 @@ +// A replaced process.stdout.write or process.stderr.write sees the call's own +// arguments: arguments.length is the number the caller passed, whatever the +// replacement declares. +const seen = []; +const originalOut = process.stdout.write; +process.stdout.write = function (chunk, encodingOrCallback, callback) { + seen.push(`stdout declared3 ${arguments.length} ${typeof callback}`); + return true; +}; +process.stdout.write("one"); +process.stdout.write("two", "utf8"); +process.stdout.write("three", "utf8", () => {}); +process.stdout.write = function () { + seen.push(`stdout declared0 ${arguments.length} ${arguments[0]}`); + return true; +}; +process.stdout.write("zero"); +process.stdout.write = function (chunk) { + seen.push(`stdout declared1 ${arguments.length} ${arguments[1]}`); + return true; +}; +process.stdout.write("a", "latin1"); +process.stdout.write = originalOut; +const originalErr = process.stderr.write; +process.stderr.write = function (chunk, encodingOrCallback, callback) { + seen.push(`stderr declared3 ${arguments.length} ${typeof callback}`); + return true; +}; +process.stderr.write("one"); +process.stderr.write("two", "utf8"); +process.stderr.write("three", "utf8", () => {}); +process.stderr.write = function () { + seen.push(`stderr declared0 ${arguments.length} ${arguments[0]}`); + return true; +}; +process.stderr.write("zero"); +process.stderr.write = function (chunk) { + seen.push(`stderr declared1 ${arguments.length} ${arguments[1]}`); + return true; +}; +process.stderr.write("a", "latin1"); +process.stderr.write = originalErr; +console.log(seen.join("\n")); diff --git a/tests/harness/corpus-costs/stdio-write-replacement-arguments.cjs.json b/tests/harness/corpus-costs/stdio-write-replacement-arguments.cjs.json new file mode 100644 index 000000000..2b44f308c --- /dev/null +++ b/tests/harness/corpus-costs/stdio-write-replacement-arguments.cjs.json @@ -0,0 +1 @@ +{ "plain": 3757, "san": 5000 } From 7921e020c2808b090b149fe5f17f7af74be7b620 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 03:13:45 -0500 Subject: [PATCH 29/70] Deliver a line to a single line listener without the listener snapshot --- .../readline-line-listener-changes.ts.json | 6 ++++ packages/runtime/src/scr_readline.c | 11 +++++++ .../corpus/readline-line-listener-changes.ts | 32 +++++++++++++++++++ .../readline-line-listener-changes.ts.json | 1 + 4 files changed, 50 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readline-line-listener-changes.ts.json create mode 100644 tests/corpus/readline-line-listener-changes.ts create mode 100644 tests/harness/corpus-costs/readline-line-listener-changes.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readline-line-listener-changes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readline-line-listener-changes.ts.json new file mode 100644 index 000000000..f00b31e83 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readline-line-listener-changes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/readline-line-listener-changes.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_readline.c b/packages/runtime/src/scr_readline.c index 8d0108649..94becf5d7 100644 --- a/packages/runtime/src/scr_readline.c +++ b/packages/runtime/src/scr_readline.c @@ -105,6 +105,17 @@ void scr_rl_emit(ScrRl *rl, int ev, ScrStr *line, ScrError *err) { RlList *l = &rl->ev[ev]; size_t n = l->n; if (n == 0) return; + /* One persistent line listener (the usual 'line' handler): no snapshot + * and no once-listener pass. The retain keeps the closure alive if the + * listener removes itself. */ + if (n == 1 && ev == RL_EV_LINE && !l->v[0].once && !l->v[0].iter) { + RlEntry e = l->v[0]; + if (scr_exc_pending()) return; + scr_closure_retain(e.cb); + ((ScrRlLineFn)e.fn)(e.cb, scr_str_retain(line)); + scr_closure_release(e.cb); + return; + } /* Most events have a listener or two: their snapshot stays on the stack. */ RlEntry small[4]; RlEntry *snap = n <= 4 ? small : malloc(n * sizeof *snap); diff --git a/tests/corpus/readline-line-listener-changes.ts b/tests/corpus/readline-line-listener-changes.ts new file mode 100644 index 000000000..260a64774 --- /dev/null +++ b/tests/corpus/readline-line-listener-changes.ts @@ -0,0 +1,32 @@ +// 'line' listeners that change while a line is delivered: a listener added +// by the only line listener starts with the next line, after which both +// run in order; a once listener next to a persistent one runs for one line; +// and no listener sees a line twice. +import { createInterface } from "node:readline"; +import { Readable } from "node:stream"; + +const text = "a\nb\nc\nd\ne\nf\n"; + +const first = createInterface({ input: Readable.from([text]) }); +let seen = 0; +const counting = (line: string): void => { + seen++; + console.log("counting", line); + if (seen === 2) first.on("line", (later: string) => console.log("later", later)); +}; +first.on("line", counting); +first.on("close", () => { + console.log("first closed", seen); + + const second = createInterface({ input: Readable.from([text]) }); + second.once("line", (line: string) => console.log("once", line)); + second.on("line", (line: string) => console.log("every", line)); + second.on("close", () => { + console.log("second closed"); + + const third = createInterface({ input: Readable.from(["x\ny\n", "z\n"]) }); + const seenLines: string[] = []; + third.on("line", (line: string) => seenLines.push(line)); + third.on("close", () => console.log("third", seenLines.join(","))); + }); +}); diff --git a/tests/harness/corpus-costs/readline-line-listener-changes.ts.json b/tests/harness/corpus-costs/readline-line-listener-changes.ts.json new file mode 100644 index 000000000..c5cd06a1f --- /dev/null +++ b/tests/harness/corpus-costs/readline-line-listener-changes.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 7400 } From 37128308c39b19590ccd93f097b1258c0f4b0915 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 03:22:35 -0500 Subject: [PATCH 30/70] Match Node's emitWarning, kill, chdir and userInfo behavior process.emitWarning checks its arguments in Node's order: an options object replaces type with `options.type || "Warning"` and code with `options.code`, type and code are checked before the warning and for Error warnings too, an empty type names the warning "Warning", and a DeprecationWarning type returns at once under process.noDeprecation. Wrong kinds throw ERR_INVALID_ARG_TYPE with Node's "Received ..." tail. An instance of an Error subclass is an Error warning, and a typed record in the options position is read as the options object. The default report reads the warning's message, code and detail as property reads do, so an Error subclass's own code is reported. A deprecation thrown under process.throwDeprecation is thrown as the Error it encodes, so the uncaught report names it. process.kill throws ERR_UNKNOWN_SIGNAL for an unknown signal name, as subprocess.kill does, and a failed kill(2) is Node's errnoException with errno, code and syscall. A failed process.chdir names the working directory as path and the target as dest. os.userInfo() reads the passwd entry of the effective uid: uid and gid are the entry's pw_uid and pw_gid (-1 on Windows), not the process ids. os.homedir() answers $HOME whenever it is set, an empty value included. The minimal test/common for Node's test/parallel files gains isAIX, isIBMi and isSunOS. --- .../compiler/src/backend/llvm/lib-shared.ts | 2 + .../lowering/builtins/operating-system.ts | 9 +- .../src/frontend/lowering/surfaces.ts | 2 +- .../compiler/src/ir/builtin-signatures.ts | 15 +- .../corpus/os-userinfo-passwd-entry.ts.json | 16 ++ .../process-emit-warning-arguments.cjs.json | 6 + .../corpus/process-kill-chdir-errors.cjs.json | 6 + packages/runtime/src/scr_async_dyn.c | 191 +++++++++++------- packages/runtime/src/scr_child.c | 4 +- packages/runtime/src/scr_lib.c | 100 ++++++--- packages/runtime/src/scr_os.h | 8 +- tests/corpus/os-userinfo-passwd-entry.ts | 42 ++++ .../corpus/process-emit-warning-arguments.cjs | 103 ++++++++++ tests/corpus/process-kill-chdir-errors.cjs | 26 +++ .../os-userinfo-passwd-entry.ts.json | 1 + .../process-emit-warning-arguments.cjs.json | 1 + .../process-kill-chdir-errors.cjs.json | 1 + tests/node-parallel/common/index.cjs | 7 + 18 files changed, 429 insertions(+), 111 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/os-userinfo-passwd-entry.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-emit-warning-arguments.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-kill-chdir-errors.cjs.json create mode 100644 tests/corpus/os-userinfo-passwd-entry.ts create mode 100644 tests/corpus/process-emit-warning-arguments.cjs create mode 100644 tests/corpus/process-kill-chdir-errors.cjs create mode 100644 tests/harness/corpus-costs/os-userinfo-passwd-entry.ts.json create mode 100644 tests/harness/corpus-costs/process-emit-warning-arguments.cjs.json create mode 100644 tests/harness/corpus-costs/process-kill-chdir-errors.cjs.json diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 73fd0b799..35adf72f2 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -261,6 +261,8 @@ export const LIB_FN_SYMS: Record = { "os.userName": "scr_os_user_name", "os.userShell": "scr_os_user_shell", "os.userHomedir": "scr_os_user_homedir", + "os.userUid": "scr_os_user_uid", + "os.userGid": "scr_os_user_gid", "os.tmpdir": "scr_os_tmpdir", "process.argv": "scr_process_argv", "process.execArgv": "scr_process_exec_argv", diff --git a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts index ee189a1f3..3f3616f23 100644 --- a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts +++ b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts @@ -17,8 +17,9 @@ import { } from "../../../ir/ir.js"; /** `os.userInfo()` — the passwd-entry snapshot, Node's uv_os_get_passwd - * behind the call site's own mapped record shape: username = pw_name, - * uid/gid = getuid/getgid, shell = pw_shell (as the `string | null` + * for the effective uid behind the call site's own mapped record shape: + * username = pw_name, uid/gid = pw_uid/pw_gid (the user's primary group, + * not the process gid; -1 on Windows), shell = pw_shell (as the `string | null` * union @types/node declares — POSIX always answers the string arm; * null is Node's Windows answer), homedir = pw_dir (the PASSWD home, * NOT os.homedir's $HOME-first cascade — Node's own split). The record @@ -56,12 +57,12 @@ export function lowerOsUserInfoCall( } else if (f.name === "uid" && f.type.kind === "f64") { fields.push({ name: f.name, - value: { kind: "libCall", fn: "process.getuid", args: [], type: F64, loc }, + value: { kind: "libCall", fn: "os.userUid", args: [], type: F64, loc }, }); } else if (f.name === "gid" && f.type.kind === "f64") { fields.push({ name: f.name, - value: { kind: "libCall", fn: "process.getgid", args: [], type: F64, loc }, + value: { kind: "libCall", fn: "os.userGid", args: [], type: F64, loc }, }); } else if (f.name === "homedir" && f.type.kind === "string") { fields.push({ diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index fc2a5f389..5d79ea551 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2318,7 +2318,7 @@ export const BUILTIN_MODULE_FN_ALIASES: Record< }, os: { // lowerOsUserInfoCall assembles the record from scalar libCalls. - userInfo: ["os.userHomedir", "os.userShell"], + userInfo: ["os.userHomedir", "os.userShell", "os.userUid", "os.userGid"], // lowerOsCpusCall reads the cpusTake snapshot through its accessors. cpus: ["os.cpusModel", "os.cpusField"], }, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 3287ac54a..633391a0f 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -808,15 +808,18 @@ function buildLibFnSignatures() { "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). */ + /** The os.userInfo() fields (uv_os_get_passwd's slices of the passwd + * entry for the effective uid): pw_name, pw_shell, pw_dir — the PASSWD + * homedir, not os.homedir's $HOME-first cascade (Node's own split) — + * and pw_uid, pw_gid (-1 on Windows). The frontend assembles the + * UserInfo record from these. +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.userUid": { argTypes: [], result: F64 }, + "os.userGid": { argTypes: [], result: F64 }, "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). diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/os-userinfo-passwd-entry.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/os-userinfo-passwd-entry.ts.json new file mode 100644 index 000000000..8d287746c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/os-userinfo-passwd-entry.ts.json @@ -0,0 +1,16 @@ +{ + "order": [ + "/tests/corpus/os-userinfo-passwd-entry.ts" + ], + "diags": [ + { + "code": "SC0001", + "message": "Namespace 'NodeJS' has no exported member 'ProcessEnv'.", + "loc": { + "file": "/tests/corpus/os-userinfo-passwd-entry.ts", + "start": 1578, + "end": 1588 + } + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-emit-warning-arguments.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-emit-warning-arguments.cjs.json new file mode 100644 index 000000000..7fb8d4ad2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-emit-warning-arguments.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/process-emit-warning-arguments.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-kill-chdir-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-kill-chdir-errors.cjs.json new file mode 100644 index 000000000..6bb0c6cf1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-kill-chdir-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/process-kill-chdir-errors.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index 1d8febbc3..cd93defcc 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -932,7 +932,32 @@ bool scr_process_deprecation_flag_on(int which) { /* The warning's name as warning.toString() reads it: the own property, or * the Error class name an emitted Error instance inherits. +1 or NULL. */ +/* A warning's `key` as a property read sees it, including the fields of an + * Error subclass instance: +1, or NULL when there is none or the read threw + * (the exception is pending). */ +static ScrDyn *scr_warning_field(const ScrDyn *w, const char *key, size_t key_len) { + if (w->kind == SCR_DYN_TYPED_REF) { + /* The reference's own answer, else its materialized view's. */ + ScrStr *name = scr_str_new(key, key_len); + ScrDyn *value = scr_dyn_typed_ref_key_get(w, name); + scr_str_release(name); + if (value || scr_exc_pending()) return value; + ScrDyn *view = scr_dyn_typed_ref_materialize(w); + if (view && !scr_exc_pending()) value = scr_warning_field(view, key, key_len); + scr_dyn_release(view); + return value; + } + if (w->kind != SCR_DYN_OBJ) return NULL; + return scr_dyn_obj_read(w, key, key_len); +} + static ScrStr *scr_warning_name(const ScrDyn *w) { + if (w->kind == SCR_DYN_TYPED_REF) { + ScrDyn *en = scr_warning_field(w, "name", 4); + ScrStr *name = en && en->kind == SCR_DYN_STR ? scr_str_retain(en->v.str) : NULL; + scr_dyn_release(en); + if (name) return name; + } const ScrDyn *en = scr_dyn_obj_get(w, "name", 4); if (en) return en->kind == SCR_DYN_STR ? scr_str_retain(en->v.str) : NULL; ScrError *e = scr_errdyn_err_of(w); @@ -960,9 +985,17 @@ static void scr_warning_dispatch(ScrDyn *w) { bool deprecation = scr_warning_is_deprecation(w); if (deprecation && scr_process_deprecation_flag_on(0)) goto listeners; ScrStr *name = scr_warning_name(w); - const ScrDyn *em = scr_dyn_obj_get(w, "message", 7); - const ScrDyn *ec = scr_dyn_obj_get(w, "code", 4); - const ScrDyn *ed = scr_dyn_obj_get(w, "detail", 6); + ScrDyn *em = scr_warning_field(w, "message", 7); + ScrDyn *ec = scr_exc_pending() ? NULL : scr_warning_field(w, "code", 4); + ScrDyn *ed = scr_exc_pending() ? NULL : scr_warning_field(w, "detail", 6); + if (scr_exc_pending()) { + /* A read that threw reports nothing; the exception propagates. */ + scr_str_release(name); + scr_dyn_release(em); + scr_dyn_release(ec); + scr_dyn_release(ed); + return; + } ScrJsonBuf report; scr_jb_init(&report); char pid[32]; @@ -982,6 +1015,9 @@ static void scr_warning_dispatch(ScrDyn *w) { scr_jb_puts(&report, "\n"); scr_jb_put_str(&report, ed->v.str); } + scr_dyn_release(em); + scr_dyn_release(ec); + scr_dyn_release(ed); /* Node's one-time trace hint, after the first report; deprecations * name their own trace flag. */ if (!scr_warning_trace_hint_shown) { @@ -1086,8 +1122,9 @@ static void scr_warning_tick(ScrClosure *c) { } static void scr_warning_throw_tick(ScrClosure *c) { - ScrDyn *w = scr_box_get_ref(c->caps[0]); - scr_throw_ref(w, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + /* Thrown as the Error it encodes, so the uncaught report and catch + * bindings see an Error. */ + scr_dyn_throw(scr_box_get_ref(c->caps[0])); } static void scr_warning_defer(ScrDyn *w, void (*fire)(ScrClosure *)) { @@ -1199,18 +1236,76 @@ void scr_process_emit_warning_text(ScrStr *message) { scr_next_tick_raw(scr_warn_text_tick); } -static void scr_warn_bad_arg(const char *arg, const char *want) { - char buf[160]; - int n = snprintf(buf, sizeof buf, "The \"%s\" argument must be of type %s", arg, want); - scr_throw_error_msg_code(SCR_ERR_TYPE, buf, (size_t)n, "ERR_INVALID_ARG_TYPE"); +/* A dyn value of kind STR equal to `text`. */ +static bool scr_warn_str_is(const ScrDyn *value, const char *text) { + size_t n = strlen(text); + return value != NULL && value->kind == SCR_DYN_STR && value->v.str->len == n && + memcmp(value->v.str->data, text, n) == 0; +} + +/* process.emitWarning(warning, type, code, ctor) in the order Node's + * lib/internal/process/warning.js runs it: + * - a "DeprecationWarning" type string returns at once while + * process.noDeprecation is set; + * - an options object (any non-array object) replaces type with + * `options.type || "Warning"` and code with `options.code`, and keeps + * a string `options.detail`; a function in the type position is the + * ctor and clears code; + * - type, when not undefined, and code, when neither undefined nor a + * function, must be strings, for Error warnings too; + * - the warning must be a string or an Error. + * Wrong kinds throw ERR_INVALID_ARG_TYPE TypeErrors with Node's message. + * A string warning becomes an Error named `type || "Warning"`. The warning + * is emitted on the next tick. Borrows its arguments; NULL is an omitted + * argument. */ +static void scr_emit_warning_args(const ScrDyn *warning, const ScrDyn *type, const ScrDyn *code) { + const ScrDyn *detail = NULL; + if (scr_warn_str_is(type, "DeprecationWarning") && scr_process_deprecation_flag_on(0)) return; + if (type != NULL && type->kind == SCR_DYN_OBJ) { + const ScrDyn *t = scr_dyn_obj_get(type, "type", 4); + const ScrDyn *d = scr_dyn_obj_get(type, "detail", 6); + code = scr_dyn_obj_get(type, "code", 4); + if (d != NULL && d->kind == SCR_DYN_STR) detail = d; + type = t != NULL && scr_dyn_truthy(t) ? t : NULL; + } else if (type != NULL && type->kind == SCR_DYN_FUNC) { + type = NULL; + code = NULL; + } + if (type != NULL && type->kind == SCR_DYN_UNDEF) type = NULL; + if (type != NULL && type->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("type", "of type string", type); + return; + } + if (code != NULL && (code->kind == SCR_DYN_FUNC || code->kind == SCR_DYN_UNDEF)) code = NULL; + if (code != NULL && code->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("code", "of type string", code); + return; + } + /* An Error warning, an instance of an Error subclass included, keeps + * its own name and code (Node). */ + ScrError *error = scr_errdyn_err_of(warning); + if (error != NULL || (warning->kind == SCR_DYN_OBJ && scr_dyn_obj_get(warning, "%error", 6) != NULL) || + scr_dyn_ref_is_error(warning)) { + scr_error_release(error); + scr_warning_emit_deferred((ScrDyn *)warning); + return; + } + if (warning->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("warning", "of type string or an instance of Error", warning); + return; + } + ScrError *e = scr_error_new(SCR_ERR_ERROR, warning->v.str); + scr_str_release(e->name); + e->name = type != NULL && type->v.str->len > 0 ? scr_str_retain(type->v.str) : scr_str_new("Warning", 7); + e->name_present = true; + if (code != NULL) e->code = scr_str_retain(code->v.str); + if (detail != NULL) scr_error_set_prop(e, "detail", 6, scr_dyn_new_str(detail->v.str)); /* retains */ + ScrDyn *w = scr_dyn_from_error(e); + scr_error_release(e); + scr_warning_emit_deferred(w); + scr_dyn_release(w); } -/* process.emitWarning(...) — Node's full argument grammar over dyn - * values: (warning: string | Error), then for string warnings a type - * string / ctor function / options object ({type, code, detail}) second - * and a code string / ctor function third. Wrong kinds throw Node's - * ERR_INVALID_ARG_TYPE TypeErrors at the call; non-string details are - * ignored. The warning itself is emitted on the next tick. */ void scr_process_emit_warning(ScrDyn *args) { #ifdef SCR_WARNING_REPLACE if (scr_emit_warning_override != NULL) { @@ -1221,65 +1316,19 @@ void scr_process_emit_warning(ScrDyn *args) { #endif size_t argc = args->kind == SCR_DYN_ARR ? args->v.arr.len : 0; ScrDyn *const *items = args->kind == SCR_DYN_ARR ? args->v.arr.items : NULL; - ScrDyn *warning = argc >= 1 ? items[0] : NULL; - /* An Error-encoded warning: type/code arguments are ignored (Node). */ - if (warning != NULL && warning->kind == SCR_DYN_OBJ && - scr_dyn_obj_get(warning, "%error", 6) != NULL) { - scr_warning_emit_deferred(warning); - return; - } - if (warning == NULL || warning->kind != SCR_DYN_STR) { - scr_warn_bad_arg("warning", "string or an instance of Error"); - return; - } - const ScrStr *type = NULL; - const ScrStr *code = NULL; - const ScrStr *detail = NULL; - size_t i = 1; - if (i < argc && items[i]->kind != SCR_DYN_UNDEF) { - ScrDyn *a = items[i]; - if (a->kind == SCR_DYN_STR) { - type = a->v.str; - i++; - } else if (a->kind == SCR_DYN_FUNC) { - i = argc; /* ctor: consumed, stack-trace trimming only — ignored */ - } else if (a->kind == SCR_DYN_OBJ && scr_dyn_obj_get(a, "%error", 6) == NULL) { - const ScrDyn *t = scr_dyn_obj_get(a, "type", 4); - const ScrDyn *c = scr_dyn_obj_get(a, "code", 4); - const ScrDyn *d = scr_dyn_obj_get(a, "detail", 6); - if (t && t->kind == SCR_DYN_STR) type = t->v.str; - else if (t && t->kind != SCR_DYN_UNDEF) { - scr_warn_bad_arg("options.type", "string"); - return; - } - if (c && c->kind == SCR_DYN_STR) code = c->v.str; - if (d && d->kind == SCR_DYN_STR) detail = d->v.str; - i = argc; /* options form: no third argument (Node ignores it) */ - } else { - scr_warn_bad_arg("type", "string"); + const ScrDyn *type = argc >= 2 ? items[1] : NULL; + /* A typed record in the options position reads as the object it is. */ + ScrDyn *options = NULL; + if (type != NULL && type->kind == SCR_DYN_TYPED_REF && !scr_dyn_ref_is_array(type)) { + options = scr_dyn_typed_ref_materialize(type); + if (options == NULL || scr_exc_pending()) { + scr_dyn_release(options); return; } - } else if (i < argc) { - i++; /* explicit undefined type: default */ + type = options; } - if (i < argc && items[i]->kind != SCR_DYN_UNDEF) { - ScrDyn *a = items[i]; - if (a->kind == SCR_DYN_STR) code = a->v.str; - else if (a->kind != SCR_DYN_FUNC) { - scr_warn_bad_arg("code", "string"); - return; - } - } - ScrError *e = scr_error_new(SCR_ERR_ERROR, (ScrStr *)warning->v.str); - scr_str_release(e->name); - e->name = type ? scr_str_retain((ScrStr *)type) : scr_str_new("Warning", 7); - e->name_present = true; - if (code) e->code = scr_str_retain((ScrStr *)code); - if (detail) scr_error_set_prop(e, "detail", 6, scr_dyn_new_str((ScrStr *)detail)); /* retains */ - ScrDyn *w = scr_dyn_from_error(e); - scr_error_release(e); - scr_warning_emit_deferred(w); - scr_dyn_release(w); + scr_emit_warning_args(argc >= 1 ? items[0] : scr_dyn_undefined(), type, argc >= 3 ? items[2] : NULL); + scr_dyn_release(options); } diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index d4f9ca395..93169be80 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -2593,7 +2593,7 @@ bool scr_child_kill(ScrChild *c, const ScrStr *signal) { char *msg = malloc(cap); if (!msg) scr_child_oom(); int len = snprintf(msg, cap, "Unknown signal: %s", signal->data); - scr_throw_error_msg(SCR_ERR_TYPE, msg, (size_t)len); + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_UNKNOWN_SIGNAL"); free(msg); return false; } @@ -5462,7 +5462,7 @@ bool scr_child_kill(ScrChild *c, const ScrStr *signal) { char *msg = malloc(cap); if (!msg) scr_child_oom(); int len = snprintf(msg, cap, "Unknown signal: %s", signal->data); - scr_throw_error_msg(SCR_ERR_TYPE, msg, (size_t)len); + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_UNKNOWN_SIGNAL"); free(msg); return false; } diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 9ef71a223..175f2e980 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -1370,22 +1370,23 @@ static int scr_wasi_kill(int pid, int sig) { #define scr_sys_kill(pid, sig) kill((pid_t)(pid), (sig)) #endif +static const char *scr_errno_name(int e, char *fallback, size_t cap); +static const char *scr_errno_text(int e); +static ScrError *scr_fs_error_details(int e, const char *name, const char *message, size_t length, + const char *op, const ScrStr *path, const ScrStr *dest); + +/* Node's errnoException(err, "kill"): message "kill ", with own + * errno (the negative uv code), code and syscall properties. */ static bool scr_kill_send(int pid, int sig) { if (scr_sys_kill(pid, sig) == 0) return true; - const char *name = errno == ESRCH ? "ESRCH" - : errno == EPERM ? "EPERM" - : errno == EINVAL ? "EINVAL" - : NULL; + int e = errno; + char namebuf[16]; + const char *name = scr_errno_name(e, namebuf, sizeof namebuf); char msg[32]; - int len; - if (name) { - len = snprintf(msg, sizeof msg, "kill %s", name); - /* Node's errnoException carries code = the errno name. */ - scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, name); - } else { - len = snprintf(msg, sizeof msg, "kill E%d", errno); - scr_throw_error_msg(SCR_ERR_ERROR, msg, (size_t)len); - } + int len = snprintf(msg, sizeof msg, "kill %s", name); + ScrError *error = scr_fs_error_details(e, name, msg, (size_t)len, "kill", NULL, NULL); + error->errno_order = true; + scr_throw_obj(error, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); return false; } @@ -1405,7 +1406,7 @@ bool scr_process_kill_named(double pid, const ScrStr *signal) { scr_trap("scriptc: out of memory\n"); } int len = snprintf(msg, cap, "Unknown signal: %s", signal->data); - scr_throw_error_msg(SCR_ERR_TYPE, msg, (size_t)len); + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_UNKNOWN_SIGNAL"); free(msg); return false; } @@ -1489,6 +1490,10 @@ ScrStr *scr_os_user_homedir(void) { return scr_os_homedir(); /* uv_os_get_passwd reuses uv_os_homedir on win */ } +/* uv_os_get_passwd on Windows has no uid or gid: both are -1. */ +double scr_os_user_uid(void) { return -1; } +double scr_os_user_gid(void) { return -1; } + ScrStr *scr_os_release(void) { /* uv_os_uname on Windows: RtlGetVersion (the un-lied-to GetVersionEx), * rendered "major.minor.build" — Node answers e.g. "10.0.26100". */ @@ -1554,6 +1559,8 @@ ScrStr *scr_os_user_name(void) { } ScrStr *scr_os_user_shell(void) { return scr_str_new("", 0); } ScrStr *scr_os_user_homedir(void) { return scr_os_homedir(); } +double scr_os_user_uid(void) { return -1; } +double scr_os_user_gid(void) { return -1; } ScrStr *scr_os_release(void) { return scr_str_new("", 0); } ScrStr *scr_os_type(void) { return scr_str_new("WASI", 4); } double scr_os_totalmem(void) { return 0; } @@ -1565,26 +1572,27 @@ double scr_os_available_parallelism(void) { return 1; } ScrStr *scr_os_tmpdir(void) { return scr_str_new("/tmp", 4); } #else ScrStr *scr_os_homedir(void) { - /* uv_os_homedir: $HOME when set (even empty is "set" only if non-NULL; - * libuv requires non-empty), else the passwd entry. */ + /* uv_os_homedir: $HOME whenever it is set, an empty value included, + * else the passwd entry of the effective uid. */ const char *home = scr_getenv("HOME"); - if (home && home[0] != '\0') return scr_str_new(home, strlen(home)); + if (home) return scr_str_new(home, strlen(home)); struct passwd pw; struct passwd *result = NULL; char buf[8192]; - if (getpwuid_r(getuid(), &pw, buf, sizeof buf, &result) != 0 || !result || !result->pw_dir) { + if (getpwuid_r(geteuid(), &pw, buf, sizeof buf, &result) != 0 || !result || !result->pw_dir) { scr_trap("scriptc: os.homedir() failed\n"); } return scr_str_new(result->pw_dir, strlen(result->pw_dir)); } -/* The os.userInfo() field trio — uv_os_get_passwd's slices. One passwd - * lookup per call (three calls per userInfo record — cheap, no caching - * to invalidate). Failure aborts: Node throws a system error there, but - * no compiled program path reaches it for the running uid. */ +/* The os.userInfo() fields — uv_os_get_passwd's slices of the passwd + * entry for the effective uid. One passwd lookup per call (one call per + * userInfo field — cheap, no caching to invalidate). Failure aborts: Node + * throws a system error there, but no compiled program path reaches it + * for the running uid. */ static const struct passwd *scr_os_passwd(char *buf, size_t cap, struct passwd *pw) { struct passwd *result = NULL; - if (getpwuid_r(getuid(), pw, buf, cap, &result) != 0 || !result) { + if (getpwuid_r(geteuid(), pw, buf, cap, &result) != 0 || !result) { scr_trap("scriptc: os.userInfo() failed\n"); } return result; @@ -1614,6 +1622,20 @@ ScrStr *scr_os_user_homedir(void) { return scr_str_new(r->pw_dir, strlen(r->pw_dir)); } +/* The entry's pw_uid and pw_gid: the user's primary group, which can + * differ from the process gid. */ +double scr_os_user_uid(void) { + struct passwd pw; + char buf[8192]; + return (double)scr_os_passwd(buf, sizeof buf, &pw)->pw_uid; +} + +double scr_os_user_gid(void) { + struct passwd pw; + char buf[8192]; + return (double)scr_os_passwd(buf, sizeof buf, &pw)->pw_gid; +} + ScrStr *scr_os_release(void) { /* uname(2)'s release field — Node's uv_os_uname()-backed answer. */ struct utsname u; @@ -2983,6 +3005,33 @@ double scr_process_umask(double mask) { #endif } +/* Node's chdir failure: the uv exception carries the working directory + * as path and the target, as given, as dest ("ENOENT: no such file or + * directory, chdir '' -> ''"). */ +static void scr_process_chdir_throw(int e, const ScrStr *dir) { +#ifdef _WIN32 + ScrStr *cwd = scr_process_cwd(); +#else + char buf[4096]; + ScrStr *cwd = getcwd(buf, sizeof buf) ? scr_str_new(buf, strlen(buf)) : NULL; +#endif + if (cwd == NULL) { + scr_fs_throw(e, "chdir", dir); + return; + } + char namebuf[16]; + const char *name = scr_errno_name(e, namebuf, sizeof namebuf); + const char *text = scr_errno_text(e); + size_t cap = strlen(name) + strlen(text) + cwd->len + dir->len + 24; + char *msg = malloc(cap); + if (!msg) scr_trap("scriptc: out of memory\n"); + int len = snprintf(msg, cap, "%s: %s, chdir '%s' -> '%s'", name, text, cwd->data, dir->data); + ScrError *error = scr_fs_error_details(e, name, msg, (size_t)len, "chdir", cwd, dir); + free(msg); + scr_str_release(cwd); + scr_throw_obj(error, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + void scr_process_chdir(ScrStr *dir) { #ifdef SCR_WORKERS if (!scr_context_is_main()) { @@ -2996,9 +3045,9 @@ void scr_process_chdir(ScrStr *dir) { BOOL changed = wide && SetCurrentDirectoryW(wide); DWORD error = changed ? ERROR_SUCCESS : GetLastError(); free(wide); - if (!changed) scr_fs_throw(scr_fs_win_errno(error), "chdir", dir); + if (!changed) scr_process_chdir_throw(scr_fs_win_errno(error), dir); #else - if (chdir(dir->data) != 0) scr_fs_throw(errno, "chdir", dir); + if (chdir(dir->data) != 0) scr_process_chdir_throw(errno, dir); #endif } @@ -3065,6 +3114,7 @@ static const char *scr_errno_name(int e, char *fallback, size_t cap) { case ELOOP: return "ELOOP"; case ENOSYS: return "ENOSYS"; case ENOTSUP: return "ENOTSUP"; + case ESRCH: return "ESRCH"; default: snprintf(fallback, cap, "E%d", e); return fallback; diff --git a/packages/runtime/src/scr_os.h b/packages/runtime/src/scr_os.h index 41bee5a8f..eab3a017b 100644 --- a/packages/runtime/src/scr_os.h +++ b/packages/runtime/src/scr_os.h @@ -48,11 +48,15 @@ ScrDyn *scr_fs_constants_object(void); * and frozen (+1). */ ScrDyn *scr_node_constants(const ScrDyn *crypto_table); double scr_os_available_parallelism(void); /* uv_available_parallelism: CPUs this process may use, >= 1 */ -/* os.userInfo()'s field trio (pw_name / pw_shell / pw_dir — the passwd - * home, not the $HOME cascade). +1 fresh; abort on lookup failure. */ +/* os.userInfo()'s fields from the passwd entry of the effective uid + * (pw_name / pw_shell / pw_dir — the passwd home, not the $HOME cascade — + * and pw_uid / pw_gid, -1 on Windows). Strings +1 fresh; abort on lookup + * failure. */ ScrStr *scr_os_user_name(void); ScrStr *scr_os_user_shell(void); ScrStr *scr_os_user_homedir(void); +double scr_os_user_uid(void); +double scr_os_user_gid(void); /* fs.readdirSync(path, { withFileTypes: true }): one readdir pass * snapshotting name + entry kind (scr_lib.c) — the emitter walks it to diff --git a/tests/corpus/os-userinfo-passwd-entry.ts b/tests/corpus/os-userinfo-passwd-entry.ts new file mode 100644 index 000000000..71faebccb --- /dev/null +++ b/tests/corpus/os-userinfo-passwd-entry.ts @@ -0,0 +1,42 @@ +// os.userInfo() reads the passwd entry of the effective uid, so uid and +// gid are the entry's pw_uid and pw_gid (the user's primary group, which +// can differ from the process gid). os.homedir() answers $HOME whenever it +// is set, an empty value included. The program re-runs itself with HOME +// set to "" and with HOME removed, and prints each child's answer. +import { spawnSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import * as os from "node:os"; + +const mode = process.argv[2]; +if (mode === "home") { + console.log(JSON.stringify(os.homedir())); +} else { + const info = os.userInfo(); + console.log(Object.keys(info).join(",")); + // The /etc/passwd line for this user, when the file lists it. + let entry: string[] | undefined; + if (existsSync("/etc/passwd")) { + for (const line of readFileSync("/etc/passwd", "utf8").split("\n")) { + const fields = line.split(":"); + if (fields.length >= 7 && fields[0] === info.username) entry = fields; + } + } + if (entry) { + console.log("uid matches entry:", String(info.uid) === entry[2]); + console.log("gid matches entry:", String(info.gid) === entry[3]); + console.log("homedir matches entry:", info.homedir === entry[5]); + console.log("shell matches entry:", info.shell === entry[6]); + } else { + console.log("no passwd entry"); + } + const empty = spawnSync(process.execPath, [process.argv[1], "home"], { + encoding: "utf8", + env: { ...process.env, HOME: "" }, + }); + console.log("HOME empty:", empty.status, empty.stdout.trim()); + const env = { ...process.env }; + delete env.HOME; + const unset = spawnSync(process.execPath, [process.argv[1], "home"], { encoding: "utf8", env }); + const fromPasswd = entry ? JSON.stringify(entry[5]) : JSON.stringify(info.homedir); + console.log("HOME unset is passwd home:", unset.status, unset.stdout.trim() === fromPasswd); +} diff --git a/tests/corpus/process-emit-warning-arguments.cjs b/tests/corpus/process-emit-warning-arguments.cjs new file mode 100644 index 000000000..fefadd1af --- /dev/null +++ b/tests/corpus/process-emit-warning-arguments.cjs @@ -0,0 +1,103 @@ +// process.emitWarning's argument handling, in the order Node runs it: an +// options object replaces type with `options.type || "Warning"` and code +// with `options.code`; a function in the type position is the ctor; type +// and code are checked before the warning itself, for Error warnings too; +// an empty type names the warning "Warning". Wrong kinds throw +// ERR_INVALID_ARG_TYPE with Node's "Received ..." message. Under +// process.throwDeprecation the thrown deprecation is reported as the +// DeprecationWarning it is. The program re-runs itself and prints the +// child's output with process ids normalized. +"use strict"; +const { spawnSync } = require("child_process"); + +class CustomWarning extends Error { + constructor() { + super("custom"); + this.name = "CustomWarning"; + this.code = "CW1"; + } +} + +const mode = process.argv[2]; +if (mode === "child") { + process.on("warning", (w) => console.log("listener:", w.name, w.message, w.code, w.detail)); + // Untyped parameters: the arguments reach emitWarning as given. An + // argument left out is undefined, which emitWarning treats as omitted. + function emit(warning, type, code) { + process.emitWarning(warning, type, code); + } + const ctor = function marker() {}; + const forms = [ + ["plain"], + ["empty type", ""], + ["undefined type", undefined, "C3"], + ["typed", "TypedWarning", "C4"], + ["ctor type", ctor], + ["ctor code", "T5", ctor], + ["options", { type: "OptWarning", code: "C6", detail: "more" }], + ["options empty type", { type: "" }], + ["options only code", { code: "C7" }], + ["options zero type", { type: 0 }], + ["options detail ignored", { type: "D", detail: 1 }], + ["options third ignored", { type: "E" }, 5], + [new Error("error warning"), "Ignored", "IGNORED"], + ]; + for (const args of forms) emit(args[0], args[1], args[2]); + // An instance of an Error subclass keeps its own name and code. + process.emitWarning(new CustomWarning()); + emit(new CustomWarning(), "Ignored"); + + const invalid = [ + [1], + [{}], + [true], + [[]], + [null], + [], + ["", 1], + ["", null], + ["", ["a"]], + ["", true], + ["", "", 1], + ["", "", {}], + ["", "", true], + ["", "", []], + ["", { type: 5 }], + ["", { type: true }], + ["", { code: 5 }], + ["", { code: null }], + [new Error("e"), 5], + [new Error("e"), "T", 5], + [5, 5], + [undefined, "foo", "bar"], + [undefined], + ]; + for (const args of invalid) { + try { + emit(args[0], args[1], args[2]); + console.log("no throw:", args.length); + } catch (e) { + console.log(e.name, e.code, e.message); + } + } + + // The deprecation fast path returns before any argument is checked. + process.noDeprecation = true; + emit(5, "DeprecationWarning"); + process.emitWarning("dropped", "DeprecationWarning", "DEP_X"); + process.noDeprecation = false; + console.log("end of turn"); +} else if (mode === "throw") { + process.throwDeprecation = true; + process.emitWarning("thrown deprecation", "DeprecationWarning", "DEP_THROWN"); + console.log("before the tick"); +} else { + const child = spawnSync(process.execPath, [process.argv[1], "child"], { encoding: "utf8" }); + console.log(`status: ${child.status}`); + console.log(child.stdout); + console.log(child.stderr.replace(/\(node:\d+\)/g, "(node:)")); + const thrown = spawnSync(process.execPath, [process.argv[1], "throw"], { encoding: "utf8" }); + console.log(`throw status: ${thrown.status}`); + console.log(thrown.stdout); + console.log(thrown.stderr.includes("DeprecationWarning: thrown deprecation")); +} diff --git a/tests/corpus/process-kill-chdir-errors.cjs b/tests/corpus/process-kill-chdir-errors.cjs new file mode 100644 index 000000000..4f67a2382 --- /dev/null +++ b/tests/corpus/process-kill-chdir-errors.cjs @@ -0,0 +1,26 @@ +// process.kill and process.chdir failures carry Node's error properties: +// an unknown signal name throws ERR_UNKNOWN_SIGNAL; a failed kill(2) is +// Node's errnoException ("kill ESRCH" with errno, code and syscall); a +// failed chdir names the working directory as path and the target as dest +// in "chdir '' -> ''". +"use strict"; + +function report(label, run) { + try { + run(); + console.log(label, "no throw"); + } catch (err) { + const cwd = process.cwd(); + console.log(label, err.name, err.code, err.message.replace(cwd, "")); + console.log(" errno:", err.errno, "syscall:", err.syscall); + console.log(" path is cwd:", err.path === cwd, "dest:", err.dest); + console.log(" keys:", Object.keys(err).join(",")); + } +} + +report("unknown signal", () => process.kill(process.pid, "SIGNOPE")); +// A pid above the kernel's pid_max (2^22) names no process. +report("missing process", () => process.kill(4194304 + 12345, 0)); +report("chdir missing", () => process.chdir("/does/not/exist/kill-chdir")); +report("chdir file", () => process.chdir("/dev/null")); +console.log(process.kill(process.pid, 0)); diff --git a/tests/harness/corpus-costs/os-userinfo-passwd-entry.ts.json b/tests/harness/corpus-costs/os-userinfo-passwd-entry.ts.json new file mode 100644 index 000000000..1ede551d9 --- /dev/null +++ b/tests/harness/corpus-costs/os-userinfo-passwd-entry.ts.json @@ -0,0 +1 @@ +{ "plain": 3493, "san": 1546 } diff --git a/tests/harness/corpus-costs/process-emit-warning-arguments.cjs.json b/tests/harness/corpus-costs/process-emit-warning-arguments.cjs.json new file mode 100644 index 000000000..a9cb90039 --- /dev/null +++ b/tests/harness/corpus-costs/process-emit-warning-arguments.cjs.json @@ -0,0 +1 @@ +{ "plain": 5313, "san": 2297 } diff --git a/tests/harness/corpus-costs/process-kill-chdir-errors.cjs.json b/tests/harness/corpus-costs/process-kill-chdir-errors.cjs.json new file mode 100644 index 000000000..9b566ebf8 --- /dev/null +++ b/tests/harness/corpus-costs/process-kill-chdir-errors.cjs.json @@ -0,0 +1 @@ +{ "plain": 3123, "san": 1390 } diff --git a/tests/node-parallel/common/index.cjs b/tests/node-parallel/common/index.cjs index b52bba638..098351161 100644 --- a/tests/node-parallel/common/index.cjs +++ b/tests/node-parallel/common/index.cjs @@ -4,11 +4,15 @@ // test/common/index.js. Add a helper here, with Node's semantics, when a // lane's files need it. const assert = require("assert"); +const os = require("os"); const { inspect } = require("util"); const hasIntl = true; const isDebug = false; const isWindows = process.platform === "win32"; +const isAIX = os.type() === "AIX"; +const isIBMi = os.type() === "OS400"; +const isSunOS = process.platform === "sunos"; const mustCallChecks = []; function runCallChecks(exitCode) { @@ -119,7 +123,10 @@ function spawnPromisified(...args) { module.exports = { hasIntl, + isAIX, isDebug, + isIBMi, + isSunOS, isWindows, mustCall, mustCallAtLeast, From a7b673e4ffb311ab6e8d8858c927dc13fbaace64 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 03:32:36 -0500 Subject: [PATCH 31/70] Keep path module objects and bound path functions Node's values in untyped code A path family read by untyped code is a plain object holding Node's members in Node's order, built once per family and kept in the builtin-module registry. Object.keys, Object.entries, Object.assign, spreads, for-in and own-property checks over it see those members. path.posix.posix and the cross-family members refer to the registered objects; the registry releases their members at exit through the objects' own storage. Every builtin function value that becomes untyped boxes as its checked twin, under the builtin's name and length, after lowering: a module object's member, an assertion's argument, and a read of a binding whose only stores are that builtin (a const local, or a module-level binding). `ns.join === path.posix.join` holds for an untyped path namespace, and `const join = path.join` passed to an untyped function compares equal to path.join and validates its arguments with Node's errors. The lowerer journals the checked-twin table (each builtin's data, display name and location), so a unit replayed from the journal keeps its twins. --- .../limitations/builtin-function-values.mdx | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../frontend/lowering/lower-builtin-values.ts | 146 ++++++++++++++++++ .../compiler/src/frontend/lowering/lowerer.ts | 19 ++- .../src/frontend/lowering/surfaces.ts | 7 +- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 1 + .../path-module-values-untyped.cjs.json | 6 + packages/runtime/src/scr_blob.h | 1 + packages/runtime/src/scr_node_builtin.c | 53 ++++++- tests/corpus/path-module-values-untyped.cjs | 82 ++++++++++ .../path-module-values-untyped.cjs.json | 1 + 12 files changed, 311 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-module-values-untyped.cjs.json create mode 100644 tests/corpus/path-module-values-untyped.cjs create mode 100644 tests/harness/corpus-costs/path-module-values-untyped.cjs.json diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index d650eef3f..f6a14be60 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -2,6 +2,7 @@ Some builtins can be stored and passed as functions: - Lowered `node:path` functions across the bare, POSIX, and win32 modules, including optional `basename` and variadic `join`/`resolve`. Called through an untyped value, they validate their arguments with Node's `ERR_INVALID_ARG_TYPE`. + A path module held by untyped code is a plain object with Node's members in Node's order. - Supported zero-argument `node:os` functions and `querystring.escape`/`unescape`. - `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, `toUSVString`, `inherits`, `deprecate`, `callbackify`, `debuglog`, and `debug`. - `fs.existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, and `closeSync`, and `fs/promises.unlink`, `chmod`, and `rename`. diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 00b72b071..62a816331 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -304,6 +304,7 @@ export const LIB_FN_SYMS: Record = { "navigator.languages": "scr_navigator_languages", "navigator.platform": "scr_navigator_platform", "process.builtinModule": "scr_process_builtin_module", + "process.builtinObject": "scr_process_builtin_object", "process.builtinUnsupported": "scr_process_builtin_unsupported", "module.namespaceValue": "scr_module_namespace_value", "module.namespaceDyn": "scr_module_namespace_dyn_value", diff --git a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts index 2d9eb8964..1b037a28f 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts @@ -8,6 +8,7 @@ import { UNDEFINED_T, arrayOf, type IrExpr, + type IrFunction, type IrLibFn, type IrStmt, type IrType, @@ -16,6 +17,7 @@ import { typeKey, } from "../../ir/ir.js"; import { boolLit, dynUndefinedExpr, strLit, varRef } from "../../ir/build.js"; +import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; import { isJsSourceFile } from "../program.js"; import { funcTypeFromParamShapes, type ParamShape } from "./call-signatures.js"; import { keyedHelperName } from "./stable-names.js"; @@ -582,6 +584,27 @@ function moduleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr { type: { kind: "func", params: [STRING], ret: DYN }, loc, }; + // A path family is a plain object holding Node's members in Node's + // order, so reflection over it (Object.keys, Object.assign, spreads, + // own-property checks) sees them. + if (module.startsWith("path/")) { + return { + kind: "libCall", + fn: "process.builtinObject", + args: [ + strLit(module, loc), + box(getter), + { + kind: "arrayLit", + elems: PATH_MODULE_KEYS.map((key) => strLit(key, loc)), + type: arrayOf(STRING), + loc, + }, + ], + type: DYN, + loc, + }; + } return { kind: "libCall", fn: "process.builtinModule", @@ -591,6 +614,27 @@ function moduleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr { }; } +/** Object.keys of Node's path.posix and path.win32. */ +const PATH_MODULE_KEYS = [ + "resolve", + "normalize", + "isAbsolute", + "join", + "relative", + "toNamespacedPath", + "dirname", + "basename", + "extname", + "format", + "parse", + "matchesGlob", + "sep", + "delimiter", + "win32", + "posix", + "_makeLong", +]; + /** A first-class loader uses the same native functions as direct imports. * Module handles cache values and preserve aliases without a JS engine. */ export function lowerObjectAssignValue(lowerer: Lowerer, loc: SrcLoc): IrExpr { @@ -845,6 +889,108 @@ export function checkedNativeCallableValue( return { kind: "closure", fnName, captures: [], type: funcType, loc }; } +/** Every builtin function value that becomes untyped boxes as its checked + * twin, under the builtin's name and length, wherever the boxing happens: a + * module handle's member, an assertion's argument, or a read of a binding + * whose only stores are that builtin (`const join = path.join`): a const + * local, or a module-level binding, whose stores are all visible here. + * An untyped caller then holds the value that validates its arguments as + * Node does, and it compares equal to the same builtin read anywhere else. + * A binding read keeps its own evaluation, so its TDZ check still runs. */ +export function boxBuiltinValuesChecked(lowerer: Lowerer, functions: IrFunction[]): void { + const names = lowerer.builtinCallableValueNames; + if (names.size === 0) return; + const lengths = lowerer.builtinCallableValueLengths; + const builtin = (expr: IrExpr | null): expr is Extract => + expr !== null && + expr.kind === "closure" && + expr.captures.length === 0 && + (names.has(expr.fnName) || lowerer.builtinCheckedValues.has(expr.fnName)); + // Bindings and the one builtin closure they hold, or null when a binding + // stores anything else. Locals are keyed by their function. + const globals = new Set(lowerer.globalsList.map((g) => g.id)); + const bound = new Map | null>(); + const note = (key: string, value: IrExpr | null): void => { + const previous = bound.get(key); + const next = builtin(value) ? value : null; + bound.set( + key, + previous === undefined || (previous !== null && next?.fnName === previous.fnName) + ? next + : null, + ); + }; + const localKey = (fn: IrFunction, id: string): string => `${fn.name}\u0000${id}`; + for (const fn of functions) { + const constLocals = new Set(fn.locals.filter((l) => !l.mutable).map((l) => l.id)); + const keyOf = (id: string): string | null => + constLocals.has(id) ? localKey(fn, id) : globals.has(id) ? id : null; + // Any other node naming a binding (a for-of target, a catch binding, an + // expression-level write) may store something else in it. + const other = (node: object): void => { + for (const field of ["localId", "catchLocalId"]) { + const id = (node as Record)[field]; + const key = typeof id === "string" ? keyOf(id) : null; + if (key) note(key, null); + } + }; + everyStmtList(fn.body, { + expr: (expr) => { + if (expr.kind !== "varRef") other(expr); + return true; + }, + stmt: (stmt) => { + const key = stmt.kind === "varDecl" || stmt.kind === "assign" ? keyOf(stmt.localId) : null; + if (stmt.kind === "varDecl" && key) { + if (stmt.init) note(key, stmt.init); + } else if (stmt.kind === "assign" && key) note(key, stmt.value); + else other(stmt); + return true; + }, + }); + } + const lifted = lowerer.liftedFns.length; + for (const fn of functions) { + fn.body = transformStmtList(fn.body, { + stmt: (stmt) => stmt, + expr: (expr) => { + if (expr.kind !== "dynFrom") return expr; + const read = expr.value.kind === "varRef" ? expr.value : null; + const source = read + ? (bound.get(localKey(fn, read.localId)) ?? bound.get(read.localId) ?? null) + : builtin(expr.value) + ? expr.value + : null; + if (!source) return expr; + const value = lowerer.checkedBuiltinValue(source); + const fnName = + expr.fnName ?? + names.get(source.fnName) ?? + (value.kind === "closure" ? names.get(value.fnName) : undefined); + const fnLength = + expr.fnLength ?? + lengths.get(source.fnName) ?? + (value.kind === "closure" ? lengths.get(value.fnName) : undefined); + const boxed: IrExpr = { + ...expr, + value, + ...(fnName !== undefined ? { fnName } : {}), + ...(fnLength !== undefined ? { fnLength } : {}), + }; + if (!read) return boxed; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: read, loc: read.loc }], + result: boxed, + type: DYN, + loc: expr.loc, + }; + }, + }); + } + functions.push(...lowerer.liftedFns.slice(lifted)); +} + /** A JavaScript call of a builtin with named value parameters whose * arguments are not the native parameters' types or omit one * (`path.matchesGlob(123, glob)`, `path.toNamespacedPath()`): the call goes diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index c56f6187e..4f795dca9 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -179,6 +179,7 @@ import { } from "./coercions/unions.js"; import { assertedConversion } from "./coercions/asserted.js"; import { + boxBuiltinValuesChecked, lowerUtilTypeValue, checkedNativeCallableValue, pathModuleValue, @@ -3420,8 +3421,12 @@ export class Lowerer { /** Builtin function values whose `length` differs from their adapter's * parameter count (BuiltinModuleFn.valueLength), by adapter name. */ readonly builtinCallableValueLengths = new Map(); - /** Typed builtin adapters with a checked twin (checkedBuiltinValue). */ - readonly builtinCheckedValues = new Map IrExpr>(); + /** Typed builtin adapters with a checked twin (checkedBuiltinValue): the + * builtin, its display name and the location the twin is lowered at. */ + readonly builtinCheckedValues = new Map< + string, + { fn: BuiltinModuleFn; display: string; loc: SrcLoc } + >(); /** 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 @@ -4117,6 +4122,7 @@ export class Lowerer { mapTable("builtinCallableValueFns", this.builtinCallableValueFns), mapTable("builtinCallableValueNames", this.builtinCallableValueNames), countTable("builtinCallableValueLengths", this.builtinCallableValueLengths), + irDataTable("builtinCheckedValues", this.builtinCheckedValues), mapTable("regionRunHelpers", this.regionRunHelpers), mapTable("internTableHelpers", this.internTableHelpers), functionTable( @@ -9560,6 +9566,7 @@ export class Lowerer { /** Final retention, pruning, and module assembly shared by ordinary emit * and the retained reachability worklist. */ finishModule(functions: IrFunction[]): LowerResult { + boxBuiltinValuesChecked(this, functions); if (this.diags.length === 0) fuseArrayPipelines(this, functions); finalizeClassMethodValues(this, functions); finalizePrivateBrands(this, functions); @@ -17039,9 +17046,7 @@ export class Lowerer { }); } if (valueParams.some((param) => param.name !== undefined)) { - this.builtinCheckedValues.set(fnName, () => - checkedNativeCallableValue(this, fn, display, loc), - ); + this.builtinCheckedValues.set(fnName, { fn, display, loc }); } return { kind: "closure", fnName, captures: [], type: funcType, loc }; } @@ -17053,7 +17058,9 @@ export class Lowerer { checkedBuiltinValue(expr: IrExpr): IrExpr { const checked = expr.kind === "closure" ? this.builtinCheckedValues.get(expr.fnName) : undefined; - return checked ? checked() : expr; + return checked + ? checkedNativeCallableValue(this, checked.fn, checked.display, checked.loc) + : expr; } isBuiltinCallableValue(expr: IrExpr): boolean { diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index eef92c26b..515c48bf1 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2752,7 +2752,12 @@ function buildAmbientSurfaceFns(): readonly AmbientSurfaceRow[] { id: "node-builtin.process.getBuiltinModule", kind: "node-builtin", name: "process.getBuiltinModule", - fns: ["process.builtinId", "process.builtinModule", "process.builtinUnsupported"], + fns: [ + "process.builtinId", + "process.builtinModule", + "process.builtinObject", + "process.builtinUnsupported", + ], note: "native path and os export subsets plus current-thread worker_threads metadata; other modules and exports throw SC2020", }, { diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 6272b9e44..134556c3c 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -728,6 +728,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.keySetComputed", "process.builtinId", "process.builtinModule", + "process.builtinObject", "process.builtinUnsupported", "module.requireComputed", "module.resolveComputed", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index ea87f5bc8..210d61e4e 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3641,6 +3641,7 @@ function buildLibFnSignatures() { "process.versions": { argTypes: [], result: DYN }, "process.builtinId": { argTypes: [DYN, arrayOf(STRING)], result: STRING }, "process.builtinModule": { argTypes: [STRING, DYN], result: DYN }, + "process.builtinObject": { argTypes: [STRING, DYN, arrayOf(STRING)], 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 diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-module-values-untyped.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-module-values-untyped.cjs.json new file mode 100644 index 000000000..91c46efc0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/path-module-values-untyped.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/path-module-values-untyped.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_blob.h b/packages/runtime/src/scr_blob.h index 7c7893322..ace652f13 100644 --- a/packages/runtime/src/scr_blob.h +++ b/packages/runtime/src/scr_blob.h @@ -29,6 +29,7 @@ ScrDyn *scr_intl_segmenter_new(void); 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_object(ScrStr *id, ScrDyn *getter, ScrArr *keys); 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 diff --git a/packages/runtime/src/scr_node_builtin.c b/packages/runtime/src/scr_node_builtin.c index 37efa8822..71b523742 100644 --- a/packages/runtime/src/scr_node_builtin.c +++ b/packages/runtime/src/scr_node_builtin.c @@ -343,6 +343,8 @@ typedef struct ScrBuiltinModule { ScrDyn *getter; ScrDyn *exports; struct ScrBuiltinModule *next; + /* The exports object is the module value (scr_process_builtin_object). */ + bool object; } ScrBuiltinModule; static SCR_TL ScrBuiltinModule *scr_builtin_modules; @@ -364,6 +366,21 @@ static void scr_builtin_release(void *handle) { } static void scr_builtin_cleanup(void) { + /* An exports object can hold itself and the other path family + * (path.posix.posix, path.posix.win32): its member values are released + * first, so the objects are freed. The values are swapped out of the + * object's own storage, without reading its keys: the exit-time layout + * reset can run before this and free the layout the keys live in. */ + for (ScrBuiltinModule *module = scr_builtin_modules; module; module = module->next) { + if (!module->object) continue; + ScrDyn *exports = module->exports; + for (size_t i = 0; i < exports->v.obj.len; i++) { + ScrDyn **slot = exports->v.obj.layout ? &exports->v.obj.slots[i] : &exports->v.obj.entries[i].value; + ScrDyn *value = *slot; + *slot = scr_dyn_retain(scr_dyn_undefined()); + scr_dyn_release(value); + } + } while (scr_builtin_modules) { ScrBuiltinModule *module = scr_builtin_modules; scr_builtin_modules = module->next; @@ -439,11 +456,45 @@ ScrDyn *scr_process_builtin_module(ScrStr *id, ScrDyn *getter) { } ScrBuiltinModule *module = malloc(sizeof(*module)); if (!module) scr_trap("scriptc: out of memory\n"); - *module = (ScrBuiltinModule){ 1, scr_str_retain(id), scr_dyn_retain(getter), scr_dyn_new_obj(), scr_builtin_modules }; + *module = (ScrBuiltinModule){ 1, scr_str_retain(id), scr_dyn_retain(getter), scr_dyn_new_obj(), scr_builtin_modules, false }; scr_builtin_modules = module; return scr_dyn_new_handle(module, SCR_DYNH_BUILTIN_MODULE); } +/* A builtin module whose value is a plain object (the path families): every + * key in `keys`, in that order, holds the getter's answer, so Object.keys, + * Object.assign, spreads and own-property checks see Node's members. The + * object is built once per id and registered before its members are read, + * so a member naming the module itself or another family (path.posix.posix, + * path.posix.win32) answers the registered object. Borrows all three; +1. */ +ScrDyn *scr_process_builtin_object(ScrStr *id, ScrDyn *getter, ScrArr *keys) { + for (ScrBuiltinModule *module = scr_builtin_modules; module; module = module->next) { + if (module->id->len == id->len && memcmp(module->id->data, id->data, id->len) == 0) + return scr_dyn_retain(module->exports); + } + if (!scr_builtin_cleanup_registered) { + scr_atexit(scr_builtin_cleanup); + scr_builtin_cleanup_registered = true; + } + ScrBuiltinModule *module = malloc(sizeof(*module)); + if (!module) scr_trap("scriptc: out of memory\n"); + *module = (ScrBuiltinModule){ 1, scr_str_retain(id), scr_dyn_retain(getter), scr_dyn_new_obj(), scr_builtin_modules, true }; + scr_builtin_modules = module; + for (size_t i = 0; i < keys->len; i++) { + ScrStr *key = (ScrStr *)scr_arr_get_ref(keys, (double)i); + ScrDyn *arg = scr_dyn_new_str(key); + ScrDyn *value = scr_dyn_call(getter, &arg, 1, "builtin export"); + scr_dyn_release(arg); + if (!value) { + scr_str_release(key); + return NULL; + } + scr_dyn_obj_set(module->exports, key->data, key->len, value); + scr_str_release(key); + } + return scr_dyn_retain(module->exports); +} + ScrDyn *scr_process_builtin_unsupported(ScrStr *id, ScrStr *member) { ScrJsonBuf text; scr_jb_init(&text); diff --git a/tests/corpus/path-module-values-untyped.cjs b/tests/corpus/path-module-values-untyped.cjs new file mode 100644 index 000000000..41b8bab8f --- /dev/null +++ b/tests/corpus/path-module-values-untyped.cjs @@ -0,0 +1,82 @@ +// node:path module objects and function values held by untyped code. A +// path family passed through an untyped function is Node's plain object: +// Object.keys, Object.entries and Object.assign list its members in Node's +// order, and own-property checks see them. A function read through it is +// the same value as the typed read (`ns.join === path.posix.join`), and a +// const or module-level binding of a path function (`const join = +// path.join`) whose only store is that function, passed to untyped code, is +// that value too, so it compares equal and validates its arguments with +// Node's errors. +"use strict"; +const assert = require("assert"); +const path = require("path"); + +function pass(value) { + return value; +} + +// One value per function, whichever way it is read. +const ns = pass(path.posix); +assert.strictEqual(ns.join, path.posix.join); +assert.strictEqual(ns.join, path.join); +assert.strictEqual(pass(path).win32.basename, path.win32.basename); +assert.strictEqual(ns.posix, ns); +console.log(ns.join === path.posix.join, ns.join.name, ns.basename.name, ns.basename.length); + +// Reflection over the module objects. +function keys(o) { + return Object.keys(o); +} +function assign(o) { + return Object.assign({}, o); +} +console.log(keys(path.posix).join(",")); +console.log(keys(path.win32).join(",")); +console.log(keys(path).join(",") === keys(path.posix).join(",")); +const copy = assign(path.posix); +console.log(typeof copy.join, copy.join === path.posix.join, copy.sep, copy.delimiter, copy.win32 === path.win32); +const spread = pass({ ...pass(path.win32) }); +console.log(Object.keys(spread).length, spread.sep, spread.resolve === path.win32.resolve); +function strings(o) { + return Object.entries(o).filter(([, value]) => typeof value === "string"); +} +console.log(JSON.stringify(strings(path.posix)), JSON.stringify(strings(path.win32))); +function has(o, key) { + return Object.prototype.hasOwnProperty.call(o, key); +} +console.log(has(path.posix, "join"), has(path.win32, "_makeLong"), has(path.posix, "nope")); +let members = 0; +for (const key in pass(path.posix)) if (typeof path.posix[key] === "function") members++; +console.log(members); + +// Bindings of path functions passed to untyped code. +const join = path.join; +const basename = path.posix.basename; +let resolve = path.resolve; +function sameJoin(f) { + return f === path.join; +} +function sameBasename(f) { + return f === path.posix.basename; +} +function sameResolve(f) { + return f === path.resolve; +} +function call(f) { + return f("a", "b"); +} +console.log(sameJoin(join), sameBasename(basename), sameResolve(resolve), call(join)); +function local() { + const dirname = path.dirname; + return ((f) => f === path.dirname)(dirname); +} +console.log(local()); +function code(f) { + try { + f(5); + return "no throw"; + } catch (e) { + return `${e.name} ${e.code}`; + } +} +console.log(code(join), code(basename), code(resolve)); diff --git a/tests/harness/corpus-costs/path-module-values-untyped.cjs.json b/tests/harness/corpus-costs/path-module-values-untyped.cjs.json new file mode 100644 index 000000000..4345ae520 --- /dev/null +++ b/tests/harness/corpus-costs/path-module-values-untyped.cjs.json @@ -0,0 +1 @@ +{ "plain": 4655, "san": 2078 } From 6d170bf082c9b2c72c03cf42e7bd31871782f2e8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 03:36:13 -0500 Subject: [PATCH 32/70] Check every path namespace key now that format and parse have function values --- tests/corpus/path-namespace-spreads.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/corpus/path-namespace-spreads.js b/tests/corpus/path-namespace-spreads.js index c9931a451..e98d71b55 100644 --- a/tests/corpus/path-namespace-spreads.js +++ b/tests/corpus/path-namespace-spreads.js @@ -1,8 +1,7 @@ // Object spreads of node:path's namespaces copy the exports. The ES module // namespace has its keys sorted and also `default`, the module object; // posix, win32 and the default import are module objects, in Node's member -// order. The copies hold the module's own functions. The key lists leave out -// format and parse, which have no function values yet. +// order. The copies hold the module's own functions. import * as path from "node:path"; import defaultPath, { posix, win32 } from "node:path"; @@ -10,7 +9,7 @@ const fromNamespace = { ...path }; const fromDefault = { ...defaultPath }; const fromPosix = { ...posix }; const fromWin32 = { ...win32 }; -const keys = (copy) => JSON.stringify(Object.keys(copy).filter((key) => key !== "format" && key !== "parse")); +const keys = (copy) => JSON.stringify(Object.keys(copy)); console.log(keys(fromNamespace)); console.log(keys(fromPosix)); console.log(keys(fromDefault) === keys(fromPosix)); From 82fbe022b44fd472880e550f57c6db0dbe9a2ec5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 03:51:59 -0500 Subject: [PATCH 33/70] Deliver the lines a chunk split after a line listener closes the interface --- .../readline-close-in-line-listener.ts.json | 6 +++++ packages/runtime/src/scr_readline.c | 4 +++- .../corpus/readline-close-in-line-listener.ts | 23 +++++++++++++++++++ .../readline-close-in-line-listener.ts.json | 1 + 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readline-close-in-line-listener.ts.json create mode 100644 tests/corpus/readline-close-in-line-listener.ts create mode 100644 tests/harness/corpus-costs/readline-close-in-line-listener.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readline-close-in-line-listener.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readline-close-in-line-listener.ts.json new file mode 100644 index 000000000..0ea441125 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readline-close-in-line-listener.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/readline-close-in-line-listener.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_readline.c b/packages/runtime/src/scr_readline.c index 8d0108649..672753e2b 100644 --- a/packages/runtime/src/scr_readline.c +++ b/packages/runtime/src/scr_readline.c @@ -339,8 +339,10 @@ static void scr_rl_feed_text(ScrRl *rl, const char *p, size_t n) { } rl->line_len = 0; if (start < n) scr_rl_buf_append(rl, p + start, n - start); + /* Node delivers every line the chunk split, also after a line listener + * closes the interface; an exception a listener throws ends the chunk. */ for (size_t i = 0; i < n_lines; i++) { - if (rl->closed || scr_exc_pending()) { + if (scr_exc_pending()) { scr_str_release(lines[i]); continue; } diff --git a/tests/corpus/readline-close-in-line-listener.ts b/tests/corpus/readline-close-in-line-listener.ts new file mode 100644 index 000000000..c86242cab --- /dev/null +++ b/tests/corpus/readline-close-in-line-listener.ts @@ -0,0 +1,23 @@ +// A 'line' listener that closes the interface: the 'close' event runs at +// once, the rest of the lines already split from the same chunk are still +// delivered, as in Node, and later chunks are not read. +import { createInterface } from "node:readline"; +import { Readable } from "node:stream"; + +const first = createInterface({ input: Readable.from(["x\ny\nz\n", "after\n"]) }); +first.on("line", (line: string) => { + console.log("line", line); + if (line === "y") first.close(); +}); +first.on("close", () => console.log("closed")); + +// Two listeners; the second closes on the first line of a chunk of three. +const second = createInterface({ input: Readable.from(["a\nb\nc"]) }); +const order: string[] = []; +second.on("line", (line: string) => order.push(`one:${line}`)); +second.on("line", (line: string) => { + order.push(`two:${line}`); + if (line === "a") second.close(); +}); +second.on("close", () => order.push("close")); +setTimeout(() => console.log(order.join(" ")), 20); diff --git a/tests/harness/corpus-costs/readline-close-in-line-listener.ts.json b/tests/harness/corpus-costs/readline-close-in-line-listener.ts.json new file mode 100644 index 000000000..5ac0e468a --- /dev/null +++ b/tests/harness/corpus-costs/readline-close-in-line-listener.ts.json @@ -0,0 +1 @@ +{ "plain": 4600, "san": 6500 } From 7f33f37384f62a9fd078cbd369b7eb8b61766175 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 03:58:21 -0500 Subject: [PATCH 34/70] Run for-in over a receiver that may be null or undefined for-in over a value whose type adds null or undefined to one enumerable type (a record, an index-signature record, an array) was refused with SC1052. JavaScript runs such a loop zero times for null and undefined. The loop now evaluates the receiver once, skips it when it is null or undefined, and otherwise walks the value as its own type. Fixes #2403 Co-authored-by: MathurAditya724 <57684218+MathurAditya724@users.noreply.github.com> --- .../src/frontend/lowering/lower-stmts.ts | 54 +++++++++++++++++++ .../corpus/for-in-nullable-receiver.ts.json | 6 +++ tests/corpus/for-in-nullable-receiver.ts | 48 +++++++++++++++++ .../for-in-nullable-receiver.ts.json | 1 + 4 files changed, 109 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/for-in-nullable-receiver.ts.json create mode 100644 tests/corpus/for-in-nullable-receiver.ts create mode 100644 tests/harness/corpus-costs/for-in-nullable-receiver.ts.json diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 377b08917..9a02a6477 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -12983,7 +12983,61 @@ function lowerForIn(lowerer: Lowerer, stmt: ts.ForInStatement): IrStmt { receiver = lowerer.coerceToExpected(receiver, DYN); } } + return lowerForInReceiver(lowerer, stmt, labels, receiver); +} + +/** for-in over the lowered `receiver` of `stmt`. */ +function lowerForInReceiver( + lowerer: Lowerer, + stmt: ts.ForInStatement, + labels: string[] | undefined, + receiver: IrExpr, +): IrStmt { + const loc = locOf(stmt); const recvT = receiver.type; + // A receiver that may be null or undefined: for-in visits nothing for + // either, and walks a present value as its own type. The receiver is + // evaluated once. + if (recvT.kind === "union") { + const arms = lowerer.unions.get(recvT.unionId)?.arms ?? []; + const present = arms.filter((arm) => arm.kind !== "undefinedT" && arm.kind !== "nullT"); + if (present.length === 1 && present.length < arms.length) { + const arm = present[0]!; + const tag = lowerer.armTag(recvT.unionId, arm); + const recv = lowerer.declareHiddenLocal("%innullable", recvT); + const ref = varRef(recv.id, recvT, loc); + const narrowed: IrExpr = { + kind: "unionNarrow", + unionId: recvT.unionId, + tag, + value: ref, + type: arm, + loc, + }; + return { + kind: "block", + body: [ + { kind: "varDecl", localId: recv.id, init: receiver, loc }, + { + kind: "if", + cond: { + kind: "unionIsTag", + unionId: recvT.unionId, + tag, + negated: false, + value: ref, + type: BOOL, + loc, + }, + then: [lowerForInReceiver(lowerer, stmt, labels, narrowed)], + else_: null, + loc, + }, + ], + loc, + }; + } + } if (recvT.kind === "dyn") { const recv = lowerer.declareHiddenLocal("%indyn", DYN); const ref: IrExpr = { kind: "varRef", localId: recv.id, type: DYN, loc }; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/for-in-nullable-receiver.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/for-in-nullable-receiver.ts.json new file mode 100644 index 000000000..1f5d3afc6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/for-in-nullable-receiver.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/for-in-nullable-receiver.ts" + ], + "diags": [] +} diff --git a/tests/corpus/for-in-nullable-receiver.ts b/tests/corpus/for-in-nullable-receiver.ts new file mode 100644 index 000000000..d1e6c4d53 --- /dev/null +++ b/tests/corpus/for-in-nullable-receiver.ts @@ -0,0 +1,48 @@ +// for-in over a value that may be null or undefined runs zero times for +// either and walks a present record, array or index-signature record. The +// receiver is evaluated once. +function keys(value: { a: number } | null | undefined): string { + let result = ""; + for (const key in value) result += key; + return result; +} +console.log("[" + keys(null) + "] [" + keys(undefined) + "] [" + keys({ a: 1 }) + "]"); + +function maybeNull(value: { a: number; b: string } | null): string[] { + const out: string[] = []; + for (const key in value) out.push(key); + return out; +} +console.log(maybeNull(null), maybeNull({ a: 1, b: "x" })); + +function indexes(values: string[] | undefined): string { + let result = ""; + for (const index in values) result += index; + return result; +} +console.log(indexes(undefined), indexes(["p", "q", "r"])); + +function dictionary(map: Record | undefined): string { + const seen: string[] = []; + for (const key in map) seen.push(`${key}=${map![key]}`); + return seen.join(","); +} +console.log(dictionary(undefined), dictionary({ x: 1, y: 2 })); + +let reads = 0; +function source(present: boolean): { a: number; b: number } | undefined { + reads++; + return present ? { a: 1, b: 2 } : undefined; +} +const visited: string[] = []; +for (const key in source(true)) visited.push(key); +for (const key in source(false)) visited.push(key); +console.log(visited, reads); + +outer: for (const round of [1, 2]) { + for (const key in round === 1 ? { a: 1, b: 2, c: 3 } : null) { + if (key === "b") continue outer; + console.log(round, key); + } + console.log("after", round); +} diff --git a/tests/harness/corpus-costs/for-in-nullable-receiver.ts.json b/tests/harness/corpus-costs/for-in-nullable-receiver.ts.json new file mode 100644 index 000000000..1cfe56769 --- /dev/null +++ b/tests/harness/corpus-costs/for-in-nullable-receiver.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 3200 } From eff8ef743db680848b6490d63093a50b7654cffe Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 04:11:04 -0500 Subject: [PATCH 35/70] Read at the exact bigint position in fs.readSync Co-authored-by: MathurAditya724 <57684218+MathurAditya724@users.noreply.github.com> Fixes #2404 --- .../corpus/fs-read-bigint-position.cjs.json | 6 +++ .../fs-read-sync-bigint-position.cjs.json | 6 +++ packages/runtime/src/scr_bytes_io.c | 41 +++++++++++++++---- packages/runtime/src/scr_file_handle.h | 6 +++ packages/runtime/src/scr_lib.c | 41 ++++++++++++++----- tests/corpus/fs-read-bigint-position.cjs | 28 +++++++++++++ tests/corpus/fs-read-sync-bigint-position.cjs | 32 +++++++++++++++ .../fs-read-bigint-position.cjs.json | 1 + .../fs-read-sync-bigint-position.cjs.json | 1 + 9 files changed, 144 insertions(+), 18 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-read-bigint-position.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-read-sync-bigint-position.cjs.json create mode 100644 tests/corpus/fs-read-bigint-position.cjs create mode 100644 tests/corpus/fs-read-sync-bigint-position.cjs create mode 100644 tests/harness/corpus-costs/fs-read-bigint-position.cjs.json create mode 100644 tests/harness/corpus-costs/fs-read-sync-bigint-position.cjs.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-read-bigint-position.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-read-bigint-position.cjs.json new file mode 100644 index 000000000..24039a8ab --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-read-bigint-position.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fs-read-bigint-position.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-read-sync-bigint-position.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-read-sync-bigint-position.cjs.json new file mode 100644 index 000000000..4d4997e89 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-read-sync-bigint-position.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fs-read-sync-bigint-position.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 0c4bee4ee..035795619 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -166,6 +166,8 @@ static ScrDyn *scr_fs_cb_arg(ScrDyn *const *args, size_t argc, size_t index) { return index < argc ? args[index] : scr_dyn_undefined(); } +static bool scr_fs_bigint_position_chk(ScrBigInt *position, double length); + static ScrDyn *scr_fs_cb_option(const ScrDyn *options, const char *name) { if (scr_fs_dyn_absent(options)) return scr_dyn_retain(scr_dyn_undefined()); ScrDyn *object = options->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(options) : scr_dyn_retain((ScrDyn *)options); @@ -1129,8 +1131,30 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc if (ownedBuffer) length = scr_fs_cb_option_number(options, "length", 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)); + /* A bigint read position takes readSync's range check and reads at + * its exact value: a double would round it past 2^53. */ + ScrDyn *bigPosition = NULL; + if (readOp) { + ScrDyn *value = ownedBuffer ? scr_fs_cb_option(options, "position") + : scr_dyn_retain(count > 4 ? ARG(4) : scr_dyn_undefined()); + if (value && value->kind == SCR_DYN_BIGINT) bigPosition = value; + else scr_dyn_release(value); + } + double position = bigPosition ? -1 + : ownedBuffer ? scr_fs_cb_option_number(options, "position", -1) + : scr_fs_cb_number(ARG(4), "position", -1); + if (!scr_exc_pending()) { + if (bigPosition) { + if (scr_fs_read_offset_chk(offset) && + scr_fs_bigint_position_chk(bigPosition->v.bigint, length)) + result = scr_dyn_new_num(scr_fs_read_sync_at(fd, bytes, offset, length, + scr_bigint_to_i64(bigPosition->v.bigint))); + } else { + result = scr_dyn_new_num(readOp ? scr_fs_read_sync(fd, bytes, offset, length, position) + : scr_fs_write_sync(fd, bytes, offset, length, position)); + } + } + scr_dyn_release(bigPosition); } scr_bytes_release(bytes); /* The completion retains the original buffer, including options.buffer. */ @@ -2101,12 +2125,15 @@ double scr_fs_read_sync_dyn(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn double count = 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) - : position->v.num; + double off = scr_fs_dyn_absent(offset) ? 0 : offset->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); + /* A bigint position passed its range check above and reads at its exact + * value: a double would round it past 2^53. */ + double read = + !scr_fs_dyn_absent(position) && position->kind == SCR_DYN_BIGINT + ? scr_fs_read_sync_at(fd->v.num, bytes, off, count, scr_bigint_to_i64(position->v.bigint)) + : scr_fs_read_sync(fd->v.num, bytes, off, count, + scr_fs_dyn_absent(position) ? -1 : position->v.num); scr_bytes_release(bytes); return read; } diff --git a/packages/runtime/src/scr_file_handle.h b/packages/runtime/src/scr_file_handle.h index 5bdd15a65..4d038ffbe 100644 --- a/packages/runtime/src/scr_file_handle.h +++ b/packages/runtime/src/scr_file_handle.h @@ -60,6 +60,12 @@ ScrStats *scr_file_handle_stat(ScrFileHandle *h); * nonnegative positions leave that offset unchanged (pread/ReadFile seam). */ double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length, double position); +/* fs.readSync's offset check; false with ERR_OUT_OF_RANGE pending. */ +bool scr_fs_read_offset_chk(double offset); +/* scr_fs_read_sync after its offset and position checks, with an exact + * position (-1 for the current offset). */ +double scr_fs_read_sync_at(double fd, ScrBytes *buf, double offset, double length, + int64_t position); /* writeSync's classic Buffer window and utf8 string forms. position -1 (or * any other non-safe/nonnegative-integer value) writes at and advances the * current descriptor offset; safe nonnegative positions leave it unchanged. */ diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 9ef71a223..a676b09f1 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -3935,7 +3935,7 @@ void scr_fs_futimes(double fd, double atime, double mtime) { * supplies pread(2). Windows follows libuv's synchronous-handle recipe: * ReadFile with an OVERLAPPED byte offset, then restore the handle position * because synchronous ReadFile updates it even when OVERLAPPED is present. */ -static ssize_t scr_fs_pread(int fd, void *data, size_t length, double position) { +static ssize_t scr_fs_pread(int fd, void *data, size_t length, int64_t position) { #ifdef _WIN32 HANDLE handle = (HANDLE)_get_osfhandle(fd); if (handle == INVALID_HANDLE_VALUE) { @@ -4311,14 +4311,11 @@ void scr_fs_write_file_fd(double fd, ScrStr *data) { * throw the RangeError shape. Position -1 means the fd's current offset; * nonnegative positions do not advance it. Returns the byte count the OS * reports; errors carry the errno name like the other fd operations. */ -double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length, - double position) { - size_t bytelen = buf->len; /* u8 buffers: elem count == byte count */ +/* fs.readSync's offset check: an integer in [0, 2^53 - 1], Node's exact + * texts (validateOffset); false with ERR_OUT_OF_RANGE pending otherwise. */ +bool scr_fs_read_offset_chk(double offset) { char msg[160]; int mlen; - /* Node's exact texts: offset validates against MAX_SAFE_INTEGER's range - * form, length against the remaining window (validateOffset vs the - * buffer-bounds check in fs.readSync). */ char numbuf[40]; if (!(isfinite(offset) && trunc(offset) == offset)) { char recv[48]; @@ -4327,7 +4324,7 @@ double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length, "The value of \"offset\" is out of range. It must be an integer. Received %s", recv); scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE"); - return 0; + return false; } if (offset < 0 || offset > 9007199254740991.0) { numbuf[scr_f64_to_str(offset, numbuf)] = 0; @@ -4335,8 +4332,19 @@ double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length, "The value of \"offset\" is out of range. It must be >= 0 && <= 9007199254740991. Received %s", numbuf); scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE"); - return 0; + return false; } + return true; +} + +double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length, + double position) { + char msg[160]; + int mlen; + /* Node's exact texts: offset validates against MAX_SAFE_INTEGER's range + * form, length against the remaining window (validateOffset vs the + * buffer-bounds check in fs.readSync). */ + if (!scr_fs_read_offset_chk(offset)) return 0; if (!(isfinite(position) && trunc(position) == position)) { char recv[48]; scr_num_received(position, recv); @@ -4355,6 +4363,16 @@ 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; } + return scr_fs_read_sync_at(fd, buf, offset, length, (int64_t)position); +} + +/* fs.readSync after its offset and position checks: `position` is exact, + * so a bigint position past 2^53 reads where it says. */ +double scr_fs_read_sync_at(double fd, ScrBytes *buf, double offset, double length, + int64_t position) { + size_t bytelen = buf->len; /* u8 buffers: elem count == byte count */ + char msg[160]; + int mlen; /* `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). */ @@ -4417,7 +4435,7 @@ double scr_fs_read_zero(double fd, double position) { if (!scr_fs_write_fd_valid(fd)) return 0; uint8_t dummy; ssize_t n = position < 0 ? read((int)fd, &dummy, 0) - : scr_fs_pread((int)fd, &dummy, 0, position); + : scr_fs_pread((int)fd, &dummy, 0, (int64_t)position); if (n < 0) scr_fs_throw_nopath(errno, "read"); return 0; } @@ -4498,7 +4516,8 @@ int scr_fs_raw_open(const char *path, int flags, int mode) { } int64_t scr_fs_raw_read(int fd, void *data, size_t length, double position) { - ssize_t n = position >= 0 ? scr_fs_pread(fd, data, length, position) : read(fd, data, length); + ssize_t n = position >= 0 ? scr_fs_pread(fd, data, length, (int64_t)position) + : read(fd, data, length); return n < 0 ? -errno : n; } diff --git a/tests/corpus/fs-read-bigint-position.cjs b/tests/corpus/fs-read-bigint-position.cjs new file mode 100644 index 000000000..b54f331cf --- /dev/null +++ b/tests/corpus/fs-read-bigint-position.cjs @@ -0,0 +1,28 @@ +// fs.read's callback forms with bigint positions read at the exact +// position, including past 2^53 (beyond the file's end, so nothing is +// read), and -1n reads at the current offset. +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-read-cb-position-")); +const file = path.join(dir, "data"); +fs.writeFileSync(file, "abc"); +const fd = fs.openSync(file, "r"); +const big = 9007199254740993n; + +function step(label, run, next) { + run((error, bytesRead, buffer) => { + console.log(label, error ? error.code : null, bytesRead, buffer ? buffer.toString("latin1", 0, bytesRead) : ""); + next(); + }); +} + +step("positional", (cb) => fs.read(fd, Buffer.alloc(1), 0, 1, big, cb), () => + step("options", (cb) => fs.read(fd, { buffer: Buffer.alloc(1), position: big }, cb), () => + step("buffer and options", (cb) => fs.read(fd, Buffer.alloc(2), { position: big }, cb), () => + step("small", (cb) => fs.read(fd, Buffer.alloc(2), 0, 2, 1n, cb), () => + step("current", (cb) => fs.read(fd, Buffer.alloc(1), 0, 1, -1n, cb), () => { + fs.closeSync(fd); + fs.rmSync(dir, { recursive: true }); + }))))); diff --git a/tests/corpus/fs-read-sync-bigint-position.cjs b/tests/corpus/fs-read-sync-bigint-position.cjs new file mode 100644 index 000000000..4b3a36c22 --- /dev/null +++ b/tests/corpus/fs-read-sync-bigint-position.cjs @@ -0,0 +1,32 @@ +// fs.readSync with bigint positions reads at the exact position, including +// past 2^53 (beyond a one-byte file's end, so it reads nothing), leaves the +// descriptor's offset unchanged, and -1n reads at the current offset. +// Positions outside [-1n, 2n ** 63n - 1n - length] throw ERR_OUT_OF_RANGE. +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-read-position-")); +const file = path.join(dir, "data"); +fs.writeFileSync(file, "xyz"); +const fd = fs.openSync(file, "r"); +const buffer = Buffer.alloc(2); +try { + console.log(fs.readSync(fd, buffer, 0, 1, 0n), buffer.toString("latin1", 0, 1)); + console.log(fs.readSync(fd, buffer, 0, 2, 1n), buffer.toString("latin1")); + console.log(fs.readSync(fd, buffer, 0, 1, 9007199254740993n)); + console.log(fs.readSync(fd, buffer, 0, 1, 9223372036854775806n)); + // The positioned reads above left the current offset at 0. + console.log(fs.readSync(fd, buffer, 0, 1, -1n), buffer.toString("latin1", 0, 1)); + console.log(fs.readSync(fd, buffer, 0, 1, -1n), buffer.toString("latin1", 0, 1)); + for (const position of [-2n, 9223372036854775807n]) { + try { + fs.readSync(fd, buffer, 0, 1, position); + } catch (error) { + console.log(error.code, error.message); + } + } +} finally { + fs.closeSync(fd); + fs.rmSync(dir, { recursive: true }); +} diff --git a/tests/harness/corpus-costs/fs-read-bigint-position.cjs.json b/tests/harness/corpus-costs/fs-read-bigint-position.cjs.json new file mode 100644 index 000000000..9c76a3418 --- /dev/null +++ b/tests/harness/corpus-costs/fs-read-bigint-position.cjs.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 4500 } diff --git a/tests/harness/corpus-costs/fs-read-sync-bigint-position.cjs.json b/tests/harness/corpus-costs/fs-read-sync-bigint-position.cjs.json new file mode 100644 index 000000000..9c76a3418 --- /dev/null +++ b/tests/harness/corpus-costs/fs-read-sync-bigint-position.cjs.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 4500 } From 7e898a39ba740d61ca6e0299770c756365396629 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 04:11:38 -0500 Subject: [PATCH 36/70] Record the ts7 order baseline of os-userinfo-passwd-entry.ts without diagnostics The program has no preflight diagnostics; the recorded baseline listed an SC0001 for NodeJS.ProcessEnv that the 5.9.3 preflight does not report. --- .../tests/corpus/os-userinfo-passwd-entry.ts.json | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/os-userinfo-passwd-entry.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/os-userinfo-passwd-entry.ts.json index 8d287746c..5b8821ab2 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/os-userinfo-passwd-entry.ts.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/os-userinfo-passwd-entry.ts.json @@ -2,15 +2,5 @@ "order": [ "/tests/corpus/os-userinfo-passwd-entry.ts" ], - "diags": [ - { - "code": "SC0001", - "message": "Namespace 'NodeJS' has no exported member 'ProcessEnv'.", - "loc": { - "file": "/tests/corpus/os-userinfo-passwd-entry.ts", - "start": 1578, - "end": 1588 - } - } - ] + "diags": [] } From 2fee369fe99039e2b3fe3c37f053b52f3655f2ed Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 04:14:27 -0500 Subject: [PATCH 37/70] Split the Linux LLVM artifacts job and take its glibc packages from native_linux --- .github/workflows/ci.yml | 117 ++++++++++++++++++++++++++++++--------- 1 file changed, 92 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23936e46a..5c8c624b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -686,11 +686,17 @@ jobs: run: pnpm test:checker-identity --optimization dev llvm_artifacts_linux_x64: - needs: [validation_scope, lanes] + needs: [validation_scope, lanes, native_linux] if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }} name: test (Linux x64 LLVM artifacts, no clang) runs-on: ubuntu-24.04 - timeout-minutes: 30 + # The glibc helper and runtime pack come from native_linux, as the macOS + # artifact jobs take theirs from native_macos. The musl packages and the + # WASI smoke run in llvm_artifacts_linux_x64_musl and + # llvm_artifacts_linux_x64_wasi: together with a rebuild of the glibc + # packages, which a runtime change forces, the one job took 25 to 32 + # minutes. + timeout-minutes: 20 steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -700,32 +706,13 @@ jobs: with: node-version-file: .node-version cache: pnpm - - uses: ./.github/actions/llvm-linux - run: pnpm install --frozen-lockfile - uses: ./.github/actions/zig - - uses: ./.github/actions/native-packages + - uses: actions/download-artifact@v4 with: - target: linux-x64-gnu - - name: Build and verify Linux x64 musl release packages - run: | - pnpm --filter @scriptc/llvm-linux-x64-musl build:native - CC=zig AR=zig pnpm --filter @scriptc/runtime-linux-x64-musl build:native - helper=packages/llvm-linux-x64-musl/bin/scriptc-llvm-codegen - readelf -l "$helper" > "$RUNNER_TEMP/musl-helper-program-headers" - if grep -q INTERP "$RUNNER_TEMP/musl-helper-program-headers"; then - echo "musl helper must be a static executable" >&2 - exit 1 - fi - docker run --rm --volume "$GITHUB_WORKSPACE:/work:ro" alpine:3.22 \ - /work/$helper version --format=json | node -e ' - const value = JSON.parse(require("node:fs").readFileSync(0, "utf8")); - if (value.default_target !== "x86_64-unknown-linux-musl" || !value.supported_targets?.includes("wasm32-unknown-wasi")) process.exit(1); - ' - node packages/runtime-linux-x64-musl/scripts/verify.mjs - - uses: ./.github/actions/native-packages - with: - target: wasm32-wasi - helper: "false" + name: native-linux-x64-gnu + path: ${{ runner.temp }} + - run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz" - run: pnpm build - name: Linux helper object format and no-clang output contract run: | @@ -751,6 +738,82 @@ jobs: SCRIPTC_NO_CACHE=1 SCRIPTC_LEGACY_C_PIPELINE=0 node packages/cli/dist/main.js build tests/corpus/001-hello.ts -o /tmp/hello test "$(/tmp/hello)" = "hello world" ' + + llvm_artifacts_linux_x64_musl: + needs: [validation_scope, lanes, native_linux] + if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }} + name: test (Linux x64 LLVM artifacts, no clang, musl) + runs-on: ubuntu-24.04 + # The static musl helper and runtime pack, restored from the + # native-packages cache when their inputs are unchanged: about 9 minutes + # when both are built. The runtime build emits its units through the + # host's glibc helper (runtime-unit.mjs), which comes from native_linux. + timeout-minutes: 25 + 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 + - uses: ./.github/actions/llvm-linux + - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/zig + - 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" + - uses: ./.github/actions/native-packages + with: + target: linux-x64-musl + - name: Verify the Linux x64 musl release helper + run: | + helper=packages/llvm-linux-x64-musl/bin/scriptc-llvm-codegen + readelf -l "$helper" > "$RUNNER_TEMP/musl-helper-program-headers" + if grep -q INTERP "$RUNNER_TEMP/musl-helper-program-headers"; then + echo "musl helper must be a static executable" >&2 + exit 1 + fi + docker run --rm --volume "$GITHUB_WORKSPACE:/work:ro" alpine:3.22 \ + /work/$helper version --format=json | node -e ' + const value = JSON.parse(require("node:fs").readFileSync(0, "utf8")); + if (value.default_target !== "x86_64-unknown-linux-musl" || !value.supported_targets?.includes("wasm32-unknown-wasi")) process.exit(1); + ' + + llvm_artifacts_linux_x64_wasi: + needs: [validation_scope, lanes, native_linux] + if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }} + name: test (Linux x64 LLVM artifacts, no clang, wasm32) + runs-on: ubuntu-24.04 + # The wasm32-wasi runtime pack (restored from the native-packages cache + # when unchanged; its units go through native_linux's glibc helper) and + # the WASI smoke tests, which take 6 to 9 minutes. + timeout-minutes: 25 + 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 + - uses: ./.github/actions/llvm-linux + - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/zig + - 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" + - uses: ./.github/actions/native-packages + with: + target: wasm32-wasi + helper: "false" + - run: pnpm build - name: WASI helper object/runtime-pack smoke run: pnpm test packages/cli/test/wasi-runtime-pack.test.ts tests/harness/wasm-library.test.ts @@ -1120,6 +1183,8 @@ jobs: ts7_parity, checker_identity, llvm_artifacts_linux_x64, + llvm_artifacts_linux_x64_musl, + llvm_artifacts_linux_x64_wasi, llvm_artifacts_macos_arm64, llvm_artifacts_macos_arm64_contracts, llvm_artifacts_macos_x64, @@ -1154,6 +1219,8 @@ jobs: lane ts7_parity "${{ needs.ts7_parity.result }}" "${{ needs.lanes.outputs.linux }}" lane checker_identity "${{ needs.checker_identity.result }}" "${{ needs.lanes.outputs.linux }}" lane llvm_artifacts_linux_x64 "${{ needs.llvm_artifacts_linux_x64.result }}" "${{ needs.lanes.outputs.linux }}" + lane llvm_artifacts_linux_x64_musl "${{ needs.llvm_artifacts_linux_x64_musl.result }}" "${{ needs.lanes.outputs.linux }}" + lane llvm_artifacts_linux_x64_wasi "${{ needs.llvm_artifacts_linux_x64_wasi.result }}" "${{ needs.lanes.outputs.linux }}" lane llvm_artifacts_macos_arm64 "${{ needs.llvm_artifacts_macos_arm64.result }}" "${{ needs.lanes.outputs.macos }}" lane llvm_artifacts_macos_arm64_contracts "${{ needs.llvm_artifacts_macos_arm64_contracts.result }}" "${{ needs.lanes.outputs.macos }}" lane llvm_artifacts_macos_x64 "${{ needs.llvm_artifacts_macos_x64.result }}" "${{ needs.lanes.outputs.macos }}" From c533750aa3ab7bc1df9216264333cfec141c2f09 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 04:17:06 -0500 Subject: [PATCH 38/70] Test a jammed loop's exit every iteration when its counter may not advance The jammed loop tests its exit only when `(j & 3) === 3`. When the inner counter starts at NaN, an infinity, or a value that `j + 1` leaves unchanged, `j & 3` never becomes 3, and the loop ran past the point where every accumulator was full, without end when its bound never ends it. The every-fourth test is now used only when the inner loop starts at an int32 literal: the counter then takes consecutive integer values, so its low bits reach 3 within four iterations. From any other start, the jammed loop tests its exit every iteration. The unit test checks which starts get each test, and the corpus program adds loops starting at -Infinity and -(2 ** 54) that end once both pixels escape. --- .../src/ir/saturating-loop-jams.test.ts | 21 +++++++++++++++ .../compiler/src/ir/saturating-loop-jams.ts | 22 ++++++++++----- tests/corpus/saturating-loop-jams.ts | 27 +++++++++++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/ir/saturating-loop-jams.test.ts b/packages/compiler/src/ir/saturating-loop-jams.test.ts index 25320b7a6..7593af5a8 100644 --- a/packages/compiler/src/ir/saturating-loop-jams.test.ts +++ b/packages/compiler/src/ir/saturating-loop-jams.test.ts @@ -187,6 +187,27 @@ test("jams a constant-count loop whose inner loop exits once its accumulator is expect(posts.every((s) => s.kind === "assign" && s.localId === "res")).toBe(true); }); +test("tests the exit every iteration unless the inner loop starts at an int32 literal", () => { + const exitTest = (innerStart: IrExpr): IrExpr => { + const out = jamSaturatingLoops(moduleOf(nest({ innerStart }))); + const inner = loops(out.functions[0]!.body)[0]!; + const last = inner.body[inner.body.length - 1]!; + if (last.kind !== "if") throw new Error("expected an exit test"); + return last.cond; + }; + const everyFourth = (cond: IrExpr): boolean => { + if (cond.kind !== "logical" || cond.left.kind !== "bin") return false; + const low = cond.left.left; + return low.kind === "bin" && low.op === "&"; + }; + expect(everyFourth(exitTest(num(-5)))).toBe(true); + expect(everyFourth(exitTest(num(0x7fffffff)))).toBe(true); + // `j` may be NaN, an infinity or a value `j + 1` leaves unchanged. + expect(everyFourth(exitTest(ref("start")))).toBe(false); + expect(everyFourth(exitTest(num(2 ** 31)))).toBe(false); + expect(everyFourth(exitTest(num(-(2 ** 31) - 1)))).toBe(false); +}); + test("keeps loops whose exits or values do not allow running iterations together", () => { const reject = (body: IrStmt[]): void => { const mod = moduleOf(body); diff --git a/packages/compiler/src/ir/saturating-loop-jams.ts b/packages/compiler/src/ir/saturating-loop-jams.ts index 74c204fea..3773f713f 100644 --- a/packages/compiler/src/ir/saturating-loop-jams.ts +++ b/packages/compiler/src/ir/saturating-loop-jams.ts @@ -332,8 +332,9 @@ function matchJam( * overlap their dependence chains. * * For the same reason the jammed loop may run past the iteration where every - * accumulator is full: it tests that every fourth iteration (`j & 3` is 3) - * and keeps the inner loop's own bound. The copies' statements alternate, + * accumulator is full: when the inner loop starts at an int32 literal, it + * tests that every fourth iteration (`j & 3` is 3), and it keeps the inner + * loop's own bound. The copies' statements alternate, * and each local's copies are added next to each other, so the vectorizer * packs one copy per lane. */ export function jamSaturatingLoops(mod: IrModule): IrModule { @@ -382,7 +383,8 @@ function emitJam( const { inner, pre, post, values, counter } = jam; const loc = inner.loc; const own = declared([...pre, ...inner.body]); - const j = (inner.init as IrStmt & { kind: "varDecl" }).localId; + const init = inner.init as IrStmt & { kind: "varDecl" }; + const j = init.localId; own.delete(j); // Each local's copies are added next to each other: the vectorizer pairs // neighboring values of the loop's header, so one copy per lane. @@ -449,15 +451,23 @@ function emitJam( }; // `(j & 3) === 3` const fourth: IrExpr = { kind: "bin", op: "===", left: counter3, right: three, type: BOOL, loc }; + // `j` reaches a value whose low bits are 3 within four iterations when it + // starts at an int32 literal: it then takes consecutive integer values, + // which stay exact for more than 2^52 iterations. From another start (NaN, + // an infinity, a value where `j + 1` is `j`), the exit is tested every + // iteration. + const start = intLit(init.init); + const everyFourth = start !== null && start >= -0x80000000 && start <= 0x7fffffff; const jammed: IrStmt = { ...inner, body: [ - // The copies take turns statement by statement; the exit is tested - // every fourth iteration. + // The copies take turns statement by statement. ...copies[0]!.body.flatMap((_, k) => copies.map((copy) => copy.body[k]!)), { kind: "if", - cond: { kind: "logical", op: "&&", left: fourth, right: allFull!, type: BOOL, loc }, + cond: everyFourth + ? { kind: "logical", op: "&&", left: fourth, right: allFull!, type: BOOL, loc } + : allFull!, then: [{ kind: "break", loc }], else_: null, loc, diff --git a/tests/corpus/saturating-loop-jams.ts b/tests/corpus/saturating-loop-jams.ts index fb0fd5bb3..d4fd5e94d 100644 --- a/tests/corpus/saturating-loop-jams.ts +++ b/tests/corpus/saturating-loop-jams.ts @@ -142,6 +142,31 @@ function shifted(initial: Float64Array): number { return res; } +// The inner counter starts where `j++` leaves it unchanged or where its +// low bits never reach 3: the loop ends once both pixels escape. +function stuck(initial: Float64Array, start: number): number { + let res = 0; + for (let i = 0; i < 4; i += 2) { + let zr1 = initial[i]!; + let zr2 = initial[i + 1]!; + let b = 0; + for (let j = start; j < 40; j++) { + zr1 = zr1 * zr1 + 0.2; + if (zr1 > 2) { + b |= 2; + if (b === 3) break; + } + zr2 = zr2 * zr2 + 0.2; + if (zr2 > 2) { + b |= 1; + if (b === 3) break; + } + } + res = res * 4 + b; + } + return res; +} + const size = 64; const initial = new Float64Array(size); for (let x = 0; x < size; x++) initial[x] = (2 * x) / size - 1.5; @@ -162,3 +187,5 @@ console.log(row(initial, 0.5, (size >> 3) + 1, 50).slice(-2)); const r = new Float64Array([1.2, 0.5, 1.3, 0.9]); const s = new Float64Array([0.1, 0.1, 0.73, 0.74]); console.log(resets(q), resets(r), skips(q), skips(s), shifted(q), shifted(r)); +const e = new Float64Array([1.2, 1.5, 3, 0.9]); +console.log(stuck(e, -Infinity), stuck(e, -(2 ** 54)), stuck(e, 0.5), stuck(e, -7)); From 442c06fde3982b50df6c9b8127e8c465fe73cfd7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 04:20:01 -0500 Subject: [PATCH 39/70] Throw Node's TypeError when a nullable destructuring source is null In JavaScript files, a destructuring declaration whose source is one object or array type or null/undefined (`text.match(re)`, `items.find(...)`) threw a plain Error when the source was null or undefined. It now throws Node's TypeError with V8's message: "Cannot destructure property 'index' of 'text.match(...)' as it is null." for an object pattern, and "found is not iterable" or "object null is not iterable (cannot read property Symbol(Symbol.iterator))" for an array pattern. An object pattern whose source V8 spells in a form the compiler does not reproduce keeps the runtime fence. --- .../src/frontend/lowering/lower-stmts.ts | 87 +++++++++++++------ .../destructure-nullable-union-source.js.json | 6 ++ .../destructure-nullable-union-source.js | 63 ++++++++++++++ .../destructure-nullable-union-source.js.json | 1 + 4 files changed, 129 insertions(+), 28 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-nullable-union-source.js.json create mode 100644 tests/corpus/destructure-nullable-union-source.js create mode 100644 tests/harness/corpus-costs/destructure-nullable-union-source.js.json diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 377b08917..05a8cfa2d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -2549,13 +2549,26 @@ export function lowerDestructuringDecl( } } const out: IrStmt[] = []; + // V8's destructuring TypeError spells STATIC sources: an identifier + // source reads " is not iterable", an identifier-callee call + // " is not a function or its return value is not iterable"; + // everything else (property reads, method calls, parameters) gets the + // runtime kind wording — exactly V8's CallPrinter behavior. The DYN + // pattern arm and the unit arms of a narrowed union consume this. + const src = decl.initializer!; + const dynSpell = ts.isIdentifier(src) + ? `${src.text} is not iterable` + : ts.isCallExpression(src) && ts.isIdentifier(src.expression) + ? `${src.expression.text} is not a function or its return value is not iterable` + : undefined; // JS: a union source with exactly ONE destructurable arm beside unit // arms (`nameAndArgs.match(re)` — `RegExpMatchArray | null`, the // always-matches idiom) destructures through a VALIDATED narrow: the - // unit arms throw at runtime exactly where Node's TypeError would - // (the message is the fence's — a unit value here is the failure - // path either way). TypeScript keeps the fence: narrowing first is - // the annotated fix. + // unit arms throw Node's TypeError, through the checks a dyn source + // takes (RequireObjectCoercible for an object pattern, GetIterator for + // an array pattern). An object pattern whose source has no V8 + // spelling keeps the fence. TypeScript keeps the fence: narrowing + // first is the annotated fix. if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && init.type.kind === "union") { const def = lowerer.unions.get(init.type.unionId); const dataArms = (def?.arms ?? []).filter((a) => !isUnitType(a)); @@ -2571,6 +2584,26 @@ export function lowerDestructuringDecl( const utmp = lowerer.declareHiddenLocal("%destrU", init.type); out.push({ kind: "varDecl", localId: utmp.id, init, loc }); const uref: IrExpr = { kind: "varRef", localId: utmp.id, type: init.type, loc }; + const spelling = destrSpellingOf(src); + const check: IrExpr | null = ts.isArrayBindingPattern(decl.name) + ? { + kind: "dynIterN", + value: lowerer.coerceToExpected(uref, DYN), + count: decl.name.elements.nodes.length, + ...(dynSpell ? { notIterableMessage: dynSpell } : {}), + type: DYN, + loc, + } + : spelling !== null + ? { + kind: "dynDestrCheck", + value: lowerer.coerceToExpected(uref, DYN), + spelling, + ...firstPatternProp(lowerer, decl.name as ts.ObjectBindingPattern), + type: DYN, + loc, + } + : null; out.push({ kind: "if", cond: { @@ -2583,6 +2616,7 @@ export function lowerDestructuringDecl( loc, }, then: [ + ...(check !== null ? [{ kind: "exprStmt" as const, expr: check, loc }] : []), { kind: "runtimeFence", code: "SC1031", @@ -2635,42 +2669,19 @@ export function lowerDestructuringDecl( "destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call)", ); } - const first = (decl.name as ts.ObjectBindingPattern).elements.nodes[0]; - const firstKey = - first === undefined || first.dotDotDotToken !== undefined - ? undefined - : (first.propertyName ?? first.name); - // V8 names the first property when its key is an identifier, a string - // or a number; a computed first key takes the bare (source-only) form. - const firstProp = - firstKey === undefined || ts.isComputedPropertyName(firstKey) - ? undefined - : (patternKeyNameOf(lowerer, firstKey as ts.PropertyName) ?? undefined); out.push({ kind: "exprStmt", expr: { kind: "dynDestrCheck", value: { kind: "varRef", localId: tmp.id, type: init.type, loc }, spelling, - ...(firstProp !== undefined ? { firstProp } : {}), + ...firstPatternProp(lowerer, decl.name as ts.ObjectBindingPattern), type: init.type, loc, }, loc, }); } - // V8's destructuring TypeError spells STATIC sources: an identifier - // source reads " is not iterable", an identifier-callee call - // " is not a function or its return value is not iterable"; - // everything else (property reads, method calls, parameters) gets the - // runtime kind wording — exactly V8's CallPrinter behavior. Only the - // DYN pattern arm consumes this. - const src = decl.initializer!; - const dynSpell = ts.isIdentifier(src) - ? `${src.text} is not iterable` - : ts.isCallExpression(src) && ts.isIdentifier(src.expression) - ? `${src.expression.text} is not a function or its return value is not iterable` - : undefined; lowerer.lowerBindingPattern( decl.name as ts.ArrayBindingPattern | ts.ObjectBindingPattern, () => ({ kind: "varRef", localId: tmp.id, type: init.type, loc }), @@ -9589,6 +9600,26 @@ export function lowerDestructuringAssign( return { kind: "block", body: parts.stmts, loc }; } +/** The first property V8 names in an object pattern's destructuring + * TypeError ("Cannot destructure property 'a' of ..."): an identifier, + * string or numeric first key. A computed first key, a rest element or + * an empty pattern takes the bare (source-only) form. */ +function firstPatternProp( + lowerer: Lowerer, + pattern: ts.ObjectBindingPattern, +): { firstProp?: string } { + const first = pattern.elements.nodes[0]; + const firstKey = + first === undefined || first.dotDotDotToken !== undefined + ? undefined + : (first.propertyName ?? first.name); + const firstProp = + firstKey === undefined || ts.isComputedPropertyName(firstKey) + ? undefined + : (patternKeyNameOf(lowerer, firstKey as ts.PropertyName) ?? undefined); + return firstProp !== undefined ? { firstProp } : {}; +} + /** V8's spelling of a destructuring RHS in its TypeError message * ("Cannot destructure 'a' as it is undefined.") — plain identifiers * spell their name (parens unwrap), `this`, property chains and diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-nullable-union-source.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-nullable-union-source.js.json new file mode 100644 index 000000000..f0fff1758 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-nullable-union-source.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/destructure-nullable-union-source.js" + ], + "diags": [] +} diff --git a/tests/corpus/destructure-nullable-union-source.js b/tests/corpus/destructure-nullable-union-source.js new file mode 100644 index 000000000..84af858bf --- /dev/null +++ b/tests/corpus/destructure-nullable-union-source.js @@ -0,0 +1,63 @@ +// A destructuring declaration whose source is one object or array type or +// null/undefined (`s.match(re)`, `items.find(...)`) throws Node's TypeError +// when the source is null or undefined, with V8's message for the source +// spelling, and binds normally otherwise. + +function show(label, fn) { + try { + fn(); + } catch (error) { + console.log(label, error instanceof TypeError, error.message); + } +} + +const text = "abc"; +show("object-call", () => { + const { index } = text.match(/z/); + console.log("never", index); +}); +show("object-empty", () => { + const {} = text.match(/z/); + console.log("never"); +}); +show("object-identifier", () => { + const found = "abc".match(/z/); + const { index, input } = found; + console.log("never", index, input); +}); +show("object-string-key", () => { + const found = "abc".match(/z/); + const { "index": at } = found; + console.log("never", at); +}); +show("array-method-call", () => { + const [whole] = "abc".match(/z/); + console.log("never", whole); +}); +show("array-identifier", () => { + const found = "abc".match(/z/); + const [whole, group] = found; + console.log("never", whole, group); +}); + +const items = [ + { name: "a", size: 1 }, + { name: "b", size: 2 }, +]; +show("object-undefined", () => { + const { name } = items.find((item) => item.size > 5); + console.log("never", name); +}); +function firstMatch(text) { + return text.match(/q/); +} +show("array-function-call", () => { + const [first] = firstMatch("abc"); + console.log("never", first); +}); + +// Sources that hold a value bind as before. +const { index } = "abc".match(/b/); +const [whole] = "abc".match(/c/); +const { name, size } = items.find((item) => item.size > 1); +console.log("bound", index, whole, name, size); diff --git a/tests/harness/corpus-costs/destructure-nullable-union-source.js.json b/tests/harness/corpus-costs/destructure-nullable-union-source.js.json new file mode 100644 index 000000000..a3fd56122 --- /dev/null +++ b/tests/harness/corpus-costs/destructure-nullable-union-source.js.json @@ -0,0 +1 @@ +{ "plain": 2635, "san": 3894 } From 32745afc07324f42060f916a55e2a98af04bfa0d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 04:30:06 -0500 Subject: [PATCH 40/70] Bind generic arrow captures before parameter defaults, and count var redeclarations as writes - A generic arrow instance binds its trailing capture locals before it lowers the declared parameters, so a parameter default that reads an immutable enclosing binding resolves it instead of failing with SC1090. The captures stay trailing parameters. - A binding is written when one of its own declarations writes it: a `var` in a for-in or for-of head (every iteration), or a redeclaration that initializes (`var x = 1; ... var x = 2;`, a parameter or function declaration that a `var` redeclares). Such a binding is now shared with generic instances through the environment form, so a closure an instance returns reads the current value instead of a copy. --- .../src/frontend/lowering/binding-analysis.ts | 41 ++++++++++++++++++- .../frontend/lowering/generic-functions.ts | 15 ++++--- .../generic-arrow-enclosing-captures.ts | 11 +++++ .../corpus/generic-arrow-mutable-captures.ts | 22 ++++++++++ 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/binding-analysis.ts b/packages/compiler/src/frontend/lowering/binding-analysis.ts index c88c06c90..a37875f25 100644 --- a/packages/compiler/src/frontend/lowering/binding-analysis.ts +++ b/packages/compiler/src/frontend/lowering/binding-analysis.ts @@ -6,7 +6,8 @@ import { isGenericCallableMemberType } from "../type-mapper.js"; /** True when nothing in `sym`'s DECLARING FILE ever writes it after the * initializer: assignments (plain and compound, destructuring targets - * included), ++/--, and for-of/for-in expression targets all count. + * included), ++/--, for-of/for-in expression targets, a `var` in a + * for-of/for-in head, and redeclarations that initialize all count. * Sound file-locally for module-scope bindings because ESM import * bindings are read-only — no other file can write one. Cached per * symbol (the scan walks the whole file once). */ @@ -31,6 +32,7 @@ export function fileWritesBinding( file: ts.SourceFile, resolve: (ident: ts.Identifier) => ts.Symbol | null | undefined, ): boolean { + if (declarationsWriteBinding(sym)) return true; let written = false; // Text pre-check keeps the file walk cheap: only same-named // identifiers pay a symbol resolution. @@ -78,6 +80,43 @@ export function fileWritesBinding( return written; } +/** True when `sym`'s own declarations write it after it holds a value: + * a `var` in a for-in or for-of head is written on every iteration, and a + * binding declared more than once (`var x = 1; ... var x = 2;`, a + * parameter or function declaration that a `var` redeclares) is written by + * each initializing declaration that runs. Only value declarations count; + * a redeclaration without an initializer is counted too, which only + * over-approximates. */ +function declarationsWriteBinding(sym: ts.Symbol): boolean { + let values = 0; + let initializing = 0; + for (const d of sym.declarations) { + let root: ts.Node = d; + while ( + root.parent !== undefined && + (ts.isBindingElement(root) || + ts.isObjectBindingPattern(root) || + ts.isArrayBindingPattern(root)) + ) + root = root.parent; + if (ts.isVariableDeclaration(root)) { + const list = root.parent; + if ( + ts.isVariableDeclarationList(list) && + (list.flags & ts.NodeFlags.BlockScoped) === 0 && + (ts.isForInStatement(list.parent) || ts.isForOfStatement(list.parent)) + ) + return true; + values++; + if (root.initializer !== undefined) initializing++; + } else if (ts.isParameter(root) || ts.isFunctionDeclaration(d) || ts.isClassDeclaration(d)) { + values++; + initializing++; + } + } + return values > 1 && initializing > 0; +} + /** Strips the value-preserving wrappers off an expression: parens, * non-null assertions, `as`/`satisfies`/angle-bracket casts. What * remains is the expression that actually evaluates. */ diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 50f5930f4..4f49f98b0 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -1349,18 +1349,21 @@ export function lowerGenericInstance( fnCtx.captures!.push({ localId: entry.id, name: entry.name, type: entry.type }); } } - // Default-param initializers lower per instance, with the bindings - // threaded — a default mentioning T resolves like any body expression. - const declared = lowerer.declareParams(decl.parameters.nodes, inst.params); - params.push(...declared.params); // The enclosing-frame values travel as TRAILING parameters, after the // declared ones: a call lowers its arguments first (JavaScript's // order), then reads the captures. Binding each local to the captured - // symbol is what lets the body resolve the reference. + // symbol is what lets the body resolve the reference; the locals are + // bound before the declared parameters so that a default initializer + // reading a capture resolves it too. + const captureParams: IrParam[] = []; for (const capture of envForm ? [] : (info.captures ?? [])) { const local = lowerer.declareLocal(capture.nameNode, capture.name, capture.type, false); - params.push({ localId: local.id, name: capture.name, type: capture.type }); + captureParams.push({ localId: local.id, name: capture.name, type: capture.type }); } + // Default-param initializers lower per instance, with the bindings + // threaded — a default mentioning T resolves like any body expression. + const declared = lowerer.declareParams(decl.parameters.nodes, inst.params); + params.push(...declared.params, ...captureParams); const body = [...receiverInit, ...declared.prologue]; const bodyBlock = blockBodyOf(decl); if (bodyBlock) { diff --git a/tests/corpus/generic-arrow-enclosing-captures.ts b/tests/corpus/generic-arrow-enclosing-captures.ts index 3dac369da..c8c87cec1 100644 --- a/tests/corpus/generic-arrow-enclosing-captures.ts +++ b/tests/corpus/generic-arrow-enclosing-captures.ts @@ -72,3 +72,14 @@ function twice(offset: number, value: string): string { console.log(twice(10, "v")); console.log(twice(20, "w")); + +// Parameter defaults that read enclosing bindings: the captures are bound +// before the defaults lower. +function withDefault(base: number): string { + const unit = "px"; + const fmt = (value: T, suffix = unit, scale = base): string => + `${value}${suffix}x${scale}`; + return `${fmt(1)} ${fmt("2", "em")}`; +} + +console.log(withDefault(3)); diff --git a/tests/corpus/generic-arrow-mutable-captures.ts b/tests/corpus/generic-arrow-mutable-captures.ts index 50cb53e6e..64d77ee2b 100644 --- a/tests/corpus/generic-arrow-mutable-captures.ts +++ b/tests/corpus/generic-arrow-mutable-captures.ts @@ -77,10 +77,32 @@ function depth(): number { return walk(["a", "b", "c"], 0) * 10 + calls; } +// A `var` that a second declaration re-initializes is one binding the +// frame writes: a closure the generic arrow returned reads the new value. +function redeclared(): string { + var label = "first"; + const make = (value: T): (() => string) => () => `${label}:${String(value)}`; + const getter = make(1); + var label = "second"; + return `${getter()} ${make(true)()}`; +} + +// A `var` in a for-of head is one binding the loop writes on every +// iteration: closures returned in earlier iterations read the last value. +function loopVar(): string { + const readers: Array<() => string> = []; + for (var item of ["a", "b", "c"]) { + const make = (value: T): (() => string) => () => `${item}${String(value)}`; + readers.push(make(1)); + } + return readers.map((reader) => reader()).join(" "); +} + async function main(): Promise { console.log((await makeRunner("p").run(2)).join(" ")); console.log(accumulate(5)); console.log(snapshots()); console.log(depth()); + console.log(redeclared(), loopVar()); } main(); From 64f9481fb8a15bec461f2b5f3b94af2b398006d8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 04:41:05 -0500 Subject: [PATCH 41/70] Link the web stream classes for an element-access tee, pipeTo or pipeThrough The scan that decides whether a program uses a web stream operation the native streams lack only matched property accesses, so a TypeScript program whose one such operation was `body["tee"]()` kept the native streams and threw a TypeError at the call. Element accesses with a string literal key now count the same way, in the TypeScript scan and in the JavaScript one. Adds the corpus program web-streams-typed-element-tee.ts, with its corpus cost and order baseline. --- .../lowering/builtins/web-streams-dyn.ts | 39 +++++++++++++------ .../web-streams-typed-element-tee.ts.json | 6 +++ tests/corpus/web-streams-typed-element-tee.ts | 23 +++++++++++ .../web-streams-typed-element-tee.ts.json | 1 + 4 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-streams-typed-element-tee.ts.json create mode 100644 tests/corpus/web-streams-typed-element-tee.ts create mode 100644 tests/harness/corpus-costs/web-streams-typed-element-tee.ts.json diff --git a/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts b/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts index 67caec84a..809d5bb03 100644 --- a/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts +++ b/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts @@ -119,11 +119,8 @@ export function programUsesTypedWebStreams(lowerer: Lowerer): boolean { for (const file of lowerer.program.getSourceFiles()) { if (file.isDeclarationFile || isJsSourceFile(file)) continue; ts.walkPreorder(file, (node) => { - if ( - ts.isPropertyAccessExpression(node) && - WEB_VIEW_CALLS.has(node.name.text) && - isReadableStreamMember(lowerer, node) - ) + const member = staticMemberName(node); + if (member !== null && WEB_VIEW_CALLS.has(member) && isReadableStreamMember(lowerer, node)) found = true; else if (ts.isNewExpression(node)) { const name = stdlibGlobalNameOf(lowerer, unparenthesized(node.expression)); @@ -316,10 +313,29 @@ function isReadableStreamType(lowerer: Lowerer, type: ts.Type): boolean { ); } -/** Whether a member access resolves to a member of the stdlib - * ReadableStream interface. */ -function isReadableStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { - const member = lowerer.checker.getSymbolAtLocation(access.name); +type MemberAccess = ts.PropertyAccessExpression | ts.ElementAccessExpression; + +/** The member a property access names, or an element access with a string + * literal key (`body.tee` and `body["tee"]`); null for any other node. */ +function staticMemberName(node: ts.Node): string | null { + if (ts.isPropertyAccessExpression(node)) return node.name.text; + if (ts.isElementAccessExpression(node)) { + const key = node.argumentExpression; + if (ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) return key.text; + } + return null; +} + +/** Whether a member access (staticMemberName) resolves to a member of the + * stdlib ReadableStream interface. */ +function isReadableStreamMember(lowerer: Lowerer, access: ts.Node): boolean { + const key = ts.isPropertyAccessExpression(access) + ? access.name + : ts.isElementAccessExpression(access) + ? access.argumentExpression + : null; + if (key === null) return false; + const member = lowerer.checker.getSymbolAtLocation(key); if (member === undefined || !lowerer.isStdlibSymbol(member)) return false; let owner: ts.Node | undefined = member.declarations[0]?.parent; while (owner !== undefined && !ts.isInterfaceDeclaration(owner)) owner = owner.parent; @@ -366,9 +382,10 @@ function programUsesWebStreamOnlyMembers(lowerer: Lowerer, functions: IrFunction let found = false; ts.walkPreorder(file, (node) => { let receiver: ts.Type | null = null; - if (ts.isPropertyAccessExpression(node) && members.has(node.name.text)) { + const member = staticMemberName(node); + if (member !== null && members.has(member)) { if (isReadableStreamMember(lowerer, node)) found = true; - else receiver = lowerer.typeOf(node.expression); + else receiver = lowerer.typeOf((node as MemberAccess).expression); } else if (iterates && ts.isForOfStatement(node) && node.awaitModifier !== undefined) { receiver = lowerer.typeOf(node.expression); if (isReadableStreamType(lowerer, receiver)) found = true; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-streams-typed-element-tee.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-streams-typed-element-tee.ts.json new file mode 100644 index 000000000..61b037166 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-streams-typed-element-tee.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/web-streams-typed-element-tee.ts" + ], + "diags": [] +} diff --git a/tests/corpus/web-streams-typed-element-tee.ts b/tests/corpus/web-streams-typed-element-tee.ts new file mode 100644 index 000000000..744ff6bb3 --- /dev/null +++ b/tests/corpus/web-streams-typed-element-tee.ts @@ -0,0 +1,23 @@ +// ReadableStream tee() reached through an element access with a string key +// is the program's only web stream operation: the program still links the +// web stream classes, so a Response body splits into two branches. + +async function text(stream: ReadableStream): Promise { + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let out = ""; + for (;;) { + const result = await reader.read(); + if (result.done) break; + out += decoder.decode(result.value, { stream: true }); + } + return out + decoder.decode(); +} + +async function main(): Promise { + const body = new Response("split me").body!; + const [left, right] = body["tee"](); + console.log("element tee", await text(left), "|", await text(right), body.locked); +} + +void main(); diff --git a/tests/harness/corpus-costs/web-streams-typed-element-tee.ts.json b/tests/harness/corpus-costs/web-streams-typed-element-tee.ts.json new file mode 100644 index 000000000..5cce6d4a6 --- /dev/null +++ b/tests/harness/corpus-costs/web-streams-typed-element-tee.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 6000 } From de5c5c251729f74fba5edd9891aae74b387a3dc5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 04:54:25 -0500 Subject: [PATCH 42/70] Follow renamed and default imports and exports in reuse's changed names A changed name now also changes the names that hold it under another name: a renamed or default import, a renamed or default export, an `import x = A.b` and a renamed `require()` destructuring. A unit that spells only the local name of such an import lowers again when the imported declaration changes. Regenerate codec.generated.ts for the bigintStats IR type, and round-trip a BigIntStats program in the codec tests. --- .../src/frontend/lowering/unit-reuse.ts | 143 ++- packages/compiler/src/ir/codec.generated.ts | 922 +++++++++--------- packages/compiler/src/ir/codec.test.ts | 3 +- packages/compiler/test/unit-reuse.test.ts | 65 ++ 4 files changed, 651 insertions(+), 482 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/unit-reuse.ts b/packages/compiler/src/frontend/lowering/unit-reuse.ts index 5de2c7b0a..0a5289c77 100644 --- a/packages/compiler/src/frontend/lowering/unit-reuse.ts +++ b/packages/compiler/src/frontend/lowering/unit-reuse.ts @@ -31,7 +31,10 @@ * itself, since analyses read a binding's or property's writes anywhere; * - the names that refer to a module's namespace object, by a namespace * import or an `import()` or `require()` call, when the module's exports - * changed. + * changed; + * - the names that hold one of these under another name (Alias): a renamed + * or default import, a renamed or default export, a renamed `require()` + * destructuring, and the aliases of those. * * TypeScript resolves every dependency of a type through a name some text * spells, so a unit whose text spells none of these sees the same types, @@ -537,6 +540,71 @@ function namespaceSites( }); } +/** A name a binding of a module's text holds under another name: `from` + * is the imported or exported name, `to` the local or exported one + * (`import { from as to }`, `import to from`, whose `from` is `default`, + * `export { from as to }`, `export default from`). Text that spells `to` + * reaches the declaration of `from`. */ +type Alias = readonly [from: string, to: string]; + +/** The aliases `statement` makes (Alias). `names` are the names it + * declares. */ +function aliasesOf(statement: ts.Statement, names: readonly string[], out: Alias[]): void { + const add = (from: string, to: string): void => { + if (from !== to) out.push([from, to]); + }; + const specifiers = (elements: readonly (ts.ImportSpecifier | ts.ExportSpecifier)[]): void => { + for (const element of elements) + add((element.propertyName ?? element.name).text, element.name.text); + }; + if (ts.isImportDeclaration(statement)) { + const clause = statement.importClause; + if (clause?.name !== undefined) add("default", clause.name.text); + const bindings = clause?.namedBindings; + if (bindings !== undefined && ts.isNamedImports(bindings)) specifiers(bindings.elements.nodes); + return; + } + if (ts.isImportEqualsDeclaration(statement)) { + const reference = statement.moduleReference; + if (ts.isIdentifier(reference)) add(reference.text, statement.name.text); + else if (ts.isQualifiedName(reference)) add(reference.right.text, statement.name.text); + return; + } + if (ts.isExportDeclaration(statement)) { + const clause = statement.exportClause; + if (clause !== undefined && ts.isNamedExports(clause)) specifiers(clause.elements.nodes); + return; + } + const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined; + if ( + ts.isExportAssignment(statement) || + modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) === true + ) { + for (const name of names) add(name, "default"); + return; + } + // `const { from: to } = require("...")`. + if (ts.isVariableStatement(statement)) + for (const declaration of statement.declarationList.declarations.nodes) { + const initializer = declaration.initializer; + if ( + !ts.isObjectBindingPattern(declaration.name) || + initializer === undefined || + !ts.isCallExpression(initializer) || + !ts.isIdentifier(initializer.expression) || + initializer.expression.text !== "require" + ) + continue; + for (const element of declaration.name.elements.nodes) + if ( + element.propertyName !== undefined && + ts.isIdentifier(element.propertyName) && + ts.isIdentifier(element.name) + ) + add(element.propertyName.text, element.name.text); + } +} + /** A module's declaration table, its declarations' nodes, and the text of * its statements with global effect. A file that is not a module has global * effect as a whole. */ @@ -544,7 +612,7 @@ function fileTable( sf: ts.SourceFile, hooks: UnitReuseHooks, previous: FileTable | undefined, -): { table: FileTable; nodes: DeclarationNode[]; global: string } { +): { table: FileTable; nodes: DeclarationNode[]; aliases: Alias[]; global: string } { // A declaration with the text it had in the previous build writes what it // wrote then. const before = new Map((previous?.declarations ?? []).map((d) => [d.key, d])); @@ -556,6 +624,7 @@ function fileTable( const declarations: Declaration[] = []; const namespaces: NamespaceSite[] = []; const nodes: DeclarationNode[] = []; + const aliases: Alias[] = []; const keys = new Set(); let global = ""; const unique = (key: string): string => { @@ -568,6 +637,7 @@ function fileTable( for (const statement of sf.statements.nodes) { if (ts.isImportDeclaration(statement) || ts.isImportEqualsDeclaration(statement)) { namespaceSites(sf, statement, [], hooks, namespaces); + aliasesOf(statement, [], aliases); continue; } if (!module || hasGlobalEffect(statement)) { @@ -576,6 +646,7 @@ function fileTable( } const names = statementNames(statement, js, hooks); namespaceSites(sf, statement, names, hooks, namespaces); + aliasesOf(statement, names, aliases); const kind = String(statement.kind); if (ts.isClassDeclaration(statement) || ts.isInterfaceDeclaration(statement)) { // Each member is a declaration of its own; the rest of the class or @@ -647,6 +718,7 @@ function fileTable( namespaces, }, nodes, + aliases, global, }; } @@ -710,10 +782,12 @@ export class UnitReuse { private readonly tables: Map, previous: { program: string; tables: Map } | null, nodes: readonly DeclarationNode[], + aliases: readonly Alias[], ) { this.previous = previous !== null && previous.program === program ? previous.tables : null; this.records = previous !== null; - this.changed = this.previous === null ? new Set() : changedNames(this.previous, tables, nodes); + this.changed = + this.previous === null ? new Set() : changedNames(this.previous, tables, nodes, aliases); this.stats.changedNames = this.changed.size; } @@ -729,6 +803,7 @@ export class UnitReuse { const own = new Set(files); const tables = new Map(); const nodes: DeclarationNode[] = []; + const aliases: Alias[] = []; const previous = readProgram(store); const digest = createHash("sha256").update( `${PROGRAM_MAGIC}\n${IR_VERSION}\n${IR_CODEC_LAYOUT}\n`, @@ -737,6 +812,7 @@ export class UnitReuse { const file = fileTable(sf, hooks, previous?.tables.get(sf.fileName)); tables.set(sf.fileName, file.table); for (const node of file.nodes) nodes.push(node); + for (const alias of file.aliases) aliases.push(alias); digest.update(`${sf.fileName}\n${digestOf(file.global)}\n`); } const others = program @@ -745,7 +821,7 @@ export class UnitReuse { .map((sf) => `${sf.fileName}\n${digestOf(sf.text)}\n`) .sort(); for (const other of others) digest.update(other); - const reuse = new UnitReuse(store, digest.digest("hex"), tables, previous, nodes); + const reuse = new UnitReuse(store, digest.digest("hex"), tables, previous, nodes, aliases); reuse.stats.openMs = performance.now() - started; return reuse; } @@ -863,6 +939,7 @@ function changedNames( before: Map, now: Map, nodes: readonly DeclarationNode[], + aliases: readonly Alias[], ): Set { const surfaces = new Set(); const direct = new Set(); @@ -907,31 +984,47 @@ function changedNames( }; for (const [path, table] of now) compare(before.get(path), table); for (const [path, table] of before) if (!now.has(path)) compare(table, undefined); - if (surfaces.size > 0) { + // A name that holds a changed name under another name changes the same + // way (Alias). + const follow = (): boolean => { + let grew = false; + for (const [from, to] of aliases) { + if (surfaces.has(from) && !surfaces.has(to)) { + surfaces.add(to); + grew = true; + } + if (direct.has(from) && !direct.has(to)) { + direct.add(to); + grew = true; + } + } + return grew; + }; + let pending: { declaration: DeclarationNode; spells: Set }[] | null = null; + for (let grew = true; grew;) { + grew = follow(); + if (surfaces.size === 0) continue; // A declaration whose surface spells a changed name can have another - // type now (typeChanged); repeat until no surface adds a name. - const pending = nodes.map((declaration) => ({ + // type now (typeChanged); repeat until no surface or alias adds a name. + pending ??= nodes.map((declaration) => ({ declaration, spells: surfaceNames(declaration), })); - for (let grew = true; grew;) { - grew = false; - for (let index = 0; index < pending.length; index++) { - const entry = pending[index]!; - let hit = false; - for (const name of entry.spells) - if (surfaces.has(name)) { - hit = true; - break; - } - if (!hit) continue; - typeChanged(entry.declaration); - // Each declaration joins once. - pending[index] = pending[pending.length - 1]!; - pending.pop(); - index--; - grew = true; - } + for (let index = 0; index < pending.length; index++) { + const entry = pending[index]!; + let hit = false; + for (const name of entry.spells) + if (surfaces.has(name)) { + hit = true; + break; + } + if (!hit) continue; + typeChanged(entry.declaration); + // Each declaration joins once. + pending[index] = pending[pending.length - 1]!; + pending.pop(); + index--; + grew = true; } } for (const name of direct) surfaces.add(name); diff --git a/packages/compiler/src/ir/codec.generated.ts b/packages/compiler/src/ir/codec.generated.ts index 40d5fe3d2..064757b5e 100644 --- a/packages/compiler/src/ir/codec.generated.ts +++ b/packages/compiler/src/ir/codec.generated.ts @@ -5,7 +5,7 @@ import type { IrReader, IrWriter } from "./codec.js"; /** A digest of the coders below: an encoding written with other coders is * refused instead of misread, also after a change that keeps IR_VERSION. */ -export const IR_CODEC_LAYOUT = "978da33223a8447d"; +export const IR_CODEC_LAYOUT = "6493ba8347f973e2"; /** Writes `mod` field by field (codec.ts describes the bytes). */ export function encodeIrModule(w: IrWriter, mod: IrModule): void { @@ -32,13 +32,13 @@ const KEYS4: readonly string[] = ["int32Fields","int32Params","int32Returns","in const KEYS5: readonly string[] = ["kind","elem","optionalElements"]; const KEYS6: readonly string[] = ["kind"]; const KEYS7: readonly string[] = ["kind"]; -const KEYS8: readonly string[] = ["kind","elem"]; -const KEYS9: readonly string[] = ["kind"]; +const KEYS8: readonly string[] = ["kind"]; +const KEYS9: readonly string[] = ["kind","elem"]; const KEYS10: readonly string[] = ["kind"]; const KEYS11: readonly string[] = ["kind"]; const KEYS12: readonly string[] = ["kind"]; -const KEYS13: readonly string[] = ["kind","className"]; -const KEYS14: readonly string[] = ["kind"]; +const KEYS13: readonly string[] = ["kind"]; +const KEYS14: readonly string[] = ["kind","className"]; const KEYS15: readonly string[] = ["kind"]; const KEYS16: readonly string[] = ["kind"]; const KEYS17: readonly string[] = ["kind"]; @@ -47,199 +47,200 @@ const KEYS19: readonly string[] = ["kind"]; const KEYS20: readonly string[] = ["kind"]; const KEYS21: readonly string[] = ["kind"]; const KEYS22: readonly string[] = ["kind"]; -const KEYS23: readonly string[] = ["kind","params","ret","rest","restAbi","argumentsAll"]; -const KEYS24: readonly string[] = ["kind","async","yieldT","retT","nextT"]; -const KEYS25: readonly string[] = ["kind"]; +const KEYS23: readonly string[] = ["kind"]; +const KEYS24: readonly string[] = ["kind","params","ret","rest","restAbi","argumentsAll"]; +const KEYS25: readonly string[] = ["kind","async","yieldT","retT","nextT"]; const KEYS26: readonly string[] = ["kind"]; const KEYS27: readonly string[] = ["kind"]; const KEYS28: readonly string[] = ["kind"]; const KEYS29: readonly string[] = ["kind"]; const KEYS30: readonly string[] = ["kind"]; -const KEYS31: readonly string[] = ["kind","key","value"]; -const KEYS32: readonly string[] = ["kind","moduleId"]; -const KEYS33: readonly string[] = ["kind"]; +const KEYS31: readonly string[] = ["kind"]; +const KEYS32: readonly string[] = ["kind","key","value"]; +const KEYS33: readonly string[] = ["kind","moduleId"]; const KEYS34: readonly string[] = ["kind"]; const KEYS35: readonly string[] = ["kind"]; -const KEYS36: readonly string[] = ["kind","className"]; -const KEYS37: readonly string[] = ["kind"]; -const KEYS38: readonly string[] = ["kind","inner"]; -const KEYS39: readonly string[] = ["kind","shapeId"]; -const KEYS40: readonly string[] = ["kind"]; +const KEYS36: readonly string[] = ["kind"]; +const KEYS37: readonly string[] = ["kind","className"]; +const KEYS38: readonly string[] = ["kind"]; +const KEYS39: readonly string[] = ["kind","inner"]; +const KEYS40: readonly string[] = ["kind","shapeId"]; const KEYS41: readonly string[] = ["kind"]; const KEYS42: readonly string[] = ["kind"]; -const KEYS43: readonly string[] = ["kind","elem"]; -const KEYS44: readonly string[] = ["kind"]; +const KEYS43: readonly string[] = ["kind"]; +const KEYS44: readonly string[] = ["kind","elem"]; const KEYS45: readonly string[] = ["kind"]; const KEYS46: readonly string[] = ["kind"]; const KEYS47: readonly string[] = ["kind"]; const KEYS48: readonly string[] = ["kind"]; const KEYS49: readonly string[] = ["kind"]; -const KEYS50: readonly string[] = ["kind","unionId"]; -const KEYS51: readonly string[] = ["kind"]; +const KEYS50: readonly string[] = ["kind"]; +const KEYS51: readonly string[] = ["kind","unionId"]; const KEYS52: readonly string[] = ["kind"]; -const KEYS53: readonly string[] = ["file","start","end"]; -const KEYS54: readonly string[] = ["yieldT","nextT","resultType","closesSource","tag","inheritsTag"]; -const KEYS55: readonly string[] = ["name","localCaptures","localBaseCapture","baseValueGlobal","jsName","jsLength","ownStaticFields","prototypeDataHelper","instancePrototypeHelper","stringTagHelper","tracksOwnFields","errorNameAfter","errorBagOrder","symbolFields","runtime","arrayElem","arrayFields","arraySpecies","base","fields","methods","prototypeNames","abstract","abstractMethods","genericOf","loc"]; -const KEYS56: readonly string[] = ["id","name","type","mutable","tdz","initFlag","source"]; -const KEYS57: readonly string[] = ["id","fields","tuple","indexValue","freezable","declaredOrder","accessorOrder","orderVariants"]; -const KEYS58: readonly string[] = ["id","arms","discriminant"]; -const KEYS59: readonly string[] = ["file","pkg"]; -const KEYS60: readonly string[] = ["root","files","links","location"]; -const KEYS61: readonly string[] = ["name","symbol","library","callbackOperation","callbackTarget","params","returns"]; -const KEYS62: readonly string[] = ["buildIdSymbol","abiVersionSymbol","buildId","abiVersion"]; -const KEYS63: readonly string[] = ["sha256","bytes"]; -const KEYS64: readonly string[] = ["sha256","size","reason"]; -const KEYS65: readonly string[] = ["localId","name","type","owned"]; -const KEYS66: readonly string[] = ["id","name","type","mutable","source","boxed","tdz"]; -const KEYS67: readonly string[] = ["localId","name","type","owned","slot"]; -const KEYS68: readonly string[] = ["kind","arr","index","loc"]; -const KEYS69: readonly string[] = ["kind","arr","index","value","loc"]; -const KEYS70: readonly string[] = ["kind","arr","length","loc","pop","reserve"]; -const KEYS71: readonly string[] = ["kind","arr","index","loc"]; -const KEYS72: readonly string[] = ["kind","localId","value","initializes","loc"]; -const KEYS73: readonly string[] = ["kind","labels","body","loc"]; -const KEYS74: readonly string[] = ["kind","label","loc"]; -const KEYS75: readonly string[] = ["kind","arr","index","value","loc"]; -const KEYS76: readonly string[] = ["kind","label","loc"]; -const KEYS77: readonly string[] = ["kind","body","cond","labels","loc"]; -const KEYS78: readonly string[] = ["kind","expr","loc"]; -const KEYS79: readonly string[] = ["kind","obj","className","field","value","sloppy","define","attributesChecked","loc"]; -const KEYS80: readonly string[] = ["kind","init","cond","update","body","labels","loc"]; -const KEYS81: readonly string[] = ["kind","localId","iterable","body","labels","loc"]; -const KEYS82: readonly string[] = ["kind","cond","then","else_","loc"]; -const KEYS83: readonly string[] = ["kind","obj","shapeId","key","loc"]; -const KEYS84: readonly string[] = ["kind","obj","shapeId","key","value","overflowOnly","loc"]; -const KEYS85: readonly string[] = ["kind","obj","shapeId","field","value","loc"]; -const KEYS86: readonly string[] = ["kind","localId","loc"]; -const KEYS87: readonly string[] = ["kind","value","awaited","loc"]; -const KEYS88: readonly string[] = ["kind","code","message","loc"]; -const KEYS89: readonly string[] = ["kind","disc","cases","labels","loc"]; -const KEYS90: readonly string[] = ["kind","value","loc"]; -const KEYS91: readonly string[] = ["kind","tryBody","catchBody","catchLocalId","dropFinallyErrors","finallyBody","suppressFinallyErrors","loc"]; -const KEYS92: readonly string[] = ["kind","localId","init","loc"]; -const KEYS93: readonly string[] = ["kind","cond","body","labels","loc"]; -const KEYS94: readonly string[] = ["kind","shapeId"]; -const KEYS95: readonly string[] = ["loc","scope"]; -const KEYS96: readonly string[] = ["field","cases"]; -const KEYS97: readonly string[] = ["name","slot","params","returns","unregisteredTrap"]; -const KEYS98: readonly string[] = ["symbol","fnName","params","returns","inboundBytesTrap","inboundIntTrap","inboundCstringTrap"]; -const KEYS99: readonly string[] = ["code","teaching","remediation"]; -const KEYS100: readonly string[] = ["kind","method","receiver","args","type","loc"]; -const KEYS101: readonly string[] = ["kind","arr","index","type","loc"]; +const KEYS53: readonly string[] = ["kind"]; +const KEYS54: readonly string[] = ["file","start","end"]; +const KEYS55: readonly string[] = ["yieldT","nextT","resultType","closesSource","tag","inheritsTag"]; +const KEYS56: readonly string[] = ["name","localCaptures","localBaseCapture","baseValueGlobal","jsName","jsLength","ownStaticFields","prototypeDataHelper","instancePrototypeHelper","stringTagHelper","tracksOwnFields","errorNameAfter","errorBagOrder","symbolFields","runtime","arrayElem","arrayFields","arraySpecies","base","fields","methods","prototypeNames","abstract","abstractMethods","genericOf","loc"]; +const KEYS57: readonly string[] = ["id","name","type","mutable","tdz","initFlag","source"]; +const KEYS58: readonly string[] = ["id","fields","tuple","indexValue","freezable","declaredOrder","accessorOrder","orderVariants"]; +const KEYS59: readonly string[] = ["id","arms","discriminant"]; +const KEYS60: readonly string[] = ["file","pkg"]; +const KEYS61: readonly string[] = ["root","files","links","location"]; +const KEYS62: readonly string[] = ["name","symbol","library","callbackOperation","callbackTarget","params","returns"]; +const KEYS63: readonly string[] = ["buildIdSymbol","abiVersionSymbol","buildId","abiVersion"]; +const KEYS64: readonly string[] = ["sha256","bytes"]; +const KEYS65: readonly string[] = ["sha256","size","reason"]; +const KEYS66: readonly string[] = ["localId","name","type","owned"]; +const KEYS67: readonly string[] = ["id","name","type","mutable","source","boxed","tdz"]; +const KEYS68: readonly string[] = ["localId","name","type","owned","slot"]; +const KEYS69: readonly string[] = ["kind","arr","index","loc"]; +const KEYS70: readonly string[] = ["kind","arr","index","value","loc"]; +const KEYS71: readonly string[] = ["kind","arr","length","loc","pop","reserve"]; +const KEYS72: readonly string[] = ["kind","arr","index","loc"]; +const KEYS73: readonly string[] = ["kind","localId","value","initializes","loc"]; +const KEYS74: readonly string[] = ["kind","labels","body","loc"]; +const KEYS75: readonly string[] = ["kind","label","loc"]; +const KEYS76: readonly string[] = ["kind","arr","index","value","loc"]; +const KEYS77: readonly string[] = ["kind","label","loc"]; +const KEYS78: readonly string[] = ["kind","body","cond","labels","loc"]; +const KEYS79: readonly string[] = ["kind","expr","loc"]; +const KEYS80: readonly string[] = ["kind","obj","className","field","value","sloppy","define","attributesChecked","loc"]; +const KEYS81: readonly string[] = ["kind","init","cond","update","body","labels","loc"]; +const KEYS82: readonly string[] = ["kind","localId","iterable","body","labels","loc"]; +const KEYS83: readonly string[] = ["kind","cond","then","else_","loc"]; +const KEYS84: readonly string[] = ["kind","obj","shapeId","key","loc"]; +const KEYS85: readonly string[] = ["kind","obj","shapeId","key","value","overflowOnly","loc"]; +const KEYS86: readonly string[] = ["kind","obj","shapeId","field","value","loc"]; +const KEYS87: readonly string[] = ["kind","localId","loc"]; +const KEYS88: readonly string[] = ["kind","value","awaited","loc"]; +const KEYS89: readonly string[] = ["kind","code","message","loc"]; +const KEYS90: readonly string[] = ["kind","disc","cases","labels","loc"]; +const KEYS91: readonly string[] = ["kind","value","loc"]; +const KEYS92: readonly string[] = ["kind","tryBody","catchBody","catchLocalId","dropFinallyErrors","finallyBody","suppressFinallyErrors","loc"]; +const KEYS93: readonly string[] = ["kind","localId","init","loc"]; +const KEYS94: readonly string[] = ["kind","cond","body","labels","loc"]; +const KEYS95: readonly string[] = ["kind","shapeId"]; +const KEYS96: readonly string[] = ["loc","scope"]; +const KEYS97: readonly string[] = ["field","cases"]; +const KEYS98: readonly string[] = ["name","slot","params","returns","unregisteredTrap"]; +const KEYS99: readonly string[] = ["symbol","fnName","params","returns","inboundBytesTrap","inboundIntTrap","inboundCstringTrap"]; +const KEYS100: readonly string[] = ["code","teaching","remediation"]; +const KEYS101: readonly string[] = ["kind","method","receiver","args","type","loc"]; const KEYS102: readonly string[] = ["kind","arr","index","type","loc"]; -const KEYS103: readonly string[] = ["kind","elems","spreads","capacity","type","loc"]; -const KEYS104: readonly string[] = ["kind","length","type","loc"]; -const KEYS105: readonly string[] = ["kind","arr","index","type","loc"]; -const KEYS106: readonly string[] = ["kind","localId","value","type","loc"]; -const KEYS107: readonly string[] = ["kind","value","type","adopt","loc"]; -const KEYS108: readonly string[] = ["kind","value","promiseTag","type","loc"]; -const KEYS109: readonly string[] = ["kind","op","left","right","type","loc"]; -const KEYS110: readonly string[] = ["kind","value","type","loc"]; -const KEYS111: readonly string[] = ["kind","method","receiver","args","type","loc","invalidNaN"]; -const KEYS112: readonly string[] = ["kind","source","from","type","loc"]; -const KEYS113: readonly string[] = ["kind","callee","args","type","loc"]; -const KEYS114: readonly string[] = ["kind","callee","receiver","args","type","loc"]; -const KEYS115: readonly string[] = ["kind","value","className","type","loc"]; -const KEYS116: readonly string[] = ["kind","value","type","loc"]; -const KEYS117: readonly string[] = ["kind","value","test","className","negated","type","loc"]; -const KEYS118: readonly string[] = ["kind","value","type","loc"]; -const KEYS119: readonly string[] = ["kind","id","type","loc"]; -const KEYS120: readonly string[] = ["kind","className","type","loc","captures"]; -const KEYS121: readonly string[] = ["kind","fnName","captures","adapts","type","loc"]; -const KEYS122: readonly string[] = ["kind","value","type","loc"]; -const KEYS123: readonly string[] = ["kind","elems","type","loc"]; -const KEYS124: readonly string[] = ["kind","callee","receiver","calleeName","calleeNameValue","args","spreads","type","loc"]; -const KEYS125: readonly string[] = ["kind","value","preserveRefs","adopt","exact","inferredSite","origin","type","loc"]; -const KEYS126: readonly string[] = ["kind","value","spelling","firstProp","type","loc"]; -const KEYS127: readonly string[] = ["kind","value","type","loc","liveRef","fnName","fnLength"]; -const KEYS128: readonly string[] = ["kind","key","negated","value","type","loc"]; -const KEYS129: readonly string[] = ["kind","recv","method","calleeName","calleeNameValue","args","type","loc"]; -const KEYS130: readonly string[] = ["kind","value","count","notIterableMessage","type","loc"]; -const KEYS131: readonly string[] = ["kind","key","optional","value","type","loc"]; -const KEYS132: readonly string[] = ["kind","fields","type","loc"]; -const KEYS133: readonly string[] = ["kind","left","right","negated","type","loc"]; -const KEYS134: readonly string[] = ["kind","test","bytesElem","negated","value","type","loc"]; -const KEYS135: readonly string[] = ["kind","import","args","type","loc"]; -const KEYS136: readonly string[] = ["kind","unionId","type","loc"]; -const KEYS137: readonly string[] = ["kind","obj","className","field","type","loc"]; -const KEYS138: readonly string[] = ["kind","op","prefix","obj","className","field","fieldDyn","type","loc"]; -const KEYS139: readonly string[] = ["kind","mode","gen","arg","type","loc"]; -const KEYS140: readonly string[] = ["kind","op","prefix","localId","type","loc"]; -const KEYS141: readonly string[] = ["kind","value","className","type","loc"]; -const KEYS142: readonly string[] = ["kind","value","classValue","type","loc"]; -const KEYS143: readonly string[] = ["kind","name","args","type","loc"]; -const KEYS144: readonly string[] = ["kind","value","indent","circular","type","loc"]; -const KEYS145: readonly string[] = ["kind","fn","args","type","loc","prototypeIdentityOnly","realTimer","assertOptions"]; -const KEYS146: readonly string[] = ["kind","op","left","right","type","loc"]; -const KEYS147: readonly string[] = ["kind","method","receiver","args","type","loc"]; -const KEYS148: readonly string[] = ["kind","seed","type","loc"]; -const KEYS149: readonly string[] = ["kind","moduleId","type","loc"]; -const KEYS150: readonly string[] = ["kind","className","args","type","loc"]; -const KEYS151: readonly string[] = ["kind","executor","type","loc"]; -const KEYS152: readonly string[] = ["kind","callee","args","type","loc"]; -const KEYS153: readonly string[] = ["kind","left","right","type","loc"]; -const KEYS154: readonly string[] = ["kind","value","spelling","type","loc"]; -const KEYS155: readonly string[] = ["kind","id","receiver","body","type","loc"]; -const KEYS156: readonly string[] = ["kind","left","right","retag","type","loc"]; -const KEYS157: readonly string[] = ["kind","value","type","loc"]; -const KEYS158: readonly string[] = ["kind","type","loc"]; -const KEYS159: readonly string[] = ["kind","source","overrides","type","loc"]; -const KEYS160: readonly string[] = ["kind","obj","shapeId","field","type","loc"]; +const KEYS103: readonly string[] = ["kind","arr","index","type","loc"]; +const KEYS104: readonly string[] = ["kind","elems","spreads","capacity","type","loc"]; +const KEYS105: readonly string[] = ["kind","length","type","loc"]; +const KEYS106: readonly string[] = ["kind","arr","index","type","loc"]; +const KEYS107: readonly string[] = ["kind","localId","value","type","loc"]; +const KEYS108: readonly string[] = ["kind","value","type","adopt","loc"]; +const KEYS109: readonly string[] = ["kind","value","promiseTag","type","loc"]; +const KEYS110: readonly string[] = ["kind","op","left","right","type","loc"]; +const KEYS111: readonly string[] = ["kind","value","type","loc"]; +const KEYS112: readonly string[] = ["kind","method","receiver","args","type","loc","invalidNaN"]; +const KEYS113: readonly string[] = ["kind","source","from","type","loc"]; +const KEYS114: readonly string[] = ["kind","callee","args","type","loc"]; +const KEYS115: readonly string[] = ["kind","callee","receiver","args","type","loc"]; +const KEYS116: readonly string[] = ["kind","value","className","type","loc"]; +const KEYS117: readonly string[] = ["kind","value","type","loc"]; +const KEYS118: readonly string[] = ["kind","value","test","className","negated","type","loc"]; +const KEYS119: readonly string[] = ["kind","value","type","loc"]; +const KEYS120: readonly string[] = ["kind","id","type","loc"]; +const KEYS121: readonly string[] = ["kind","className","type","loc","captures"]; +const KEYS122: readonly string[] = ["kind","fnName","captures","adapts","type","loc"]; +const KEYS123: readonly string[] = ["kind","value","type","loc"]; +const KEYS124: readonly string[] = ["kind","elems","type","loc"]; +const KEYS125: readonly string[] = ["kind","callee","receiver","calleeName","calleeNameValue","args","spreads","type","loc"]; +const KEYS126: readonly string[] = ["kind","value","preserveRefs","adopt","exact","inferredSite","origin","type","loc"]; +const KEYS127: readonly string[] = ["kind","value","spelling","firstProp","type","loc"]; +const KEYS128: readonly string[] = ["kind","value","type","loc","liveRef","fnName","fnLength"]; +const KEYS129: readonly string[] = ["kind","key","negated","value","type","loc"]; +const KEYS130: readonly string[] = ["kind","recv","method","calleeName","calleeNameValue","args","type","loc"]; +const KEYS131: readonly string[] = ["kind","value","count","notIterableMessage","type","loc"]; +const KEYS132: readonly string[] = ["kind","key","optional","value","type","loc"]; +const KEYS133: readonly string[] = ["kind","fields","type","loc"]; +const KEYS134: readonly string[] = ["kind","left","right","negated","type","loc"]; +const KEYS135: readonly string[] = ["kind","test","bytesElem","negated","value","type","loc"]; +const KEYS136: readonly string[] = ["kind","import","args","type","loc"]; +const KEYS137: readonly string[] = ["kind","unionId","type","loc"]; +const KEYS138: readonly string[] = ["kind","obj","className","field","type","loc"]; +const KEYS139: readonly string[] = ["kind","op","prefix","obj","className","field","fieldDyn","type","loc"]; +const KEYS140: readonly string[] = ["kind","mode","gen","arg","type","loc"]; +const KEYS141: readonly string[] = ["kind","op","prefix","localId","type","loc"]; +const KEYS142: readonly string[] = ["kind","value","className","type","loc"]; +const KEYS143: readonly string[] = ["kind","value","classValue","type","loc"]; +const KEYS144: readonly string[] = ["kind","name","args","type","loc"]; +const KEYS145: readonly string[] = ["kind","value","indent","circular","type","loc"]; +const KEYS146: readonly string[] = ["kind","fn","args","type","loc","prototypeIdentityOnly","realTimer","assertOptions"]; +const KEYS147: readonly string[] = ["kind","op","left","right","type","loc"]; +const KEYS148: readonly string[] = ["kind","method","receiver","args","type","loc"]; +const KEYS149: readonly string[] = ["kind","seed","type","loc"]; +const KEYS150: readonly string[] = ["kind","moduleId","type","loc"]; +const KEYS151: readonly string[] = ["kind","className","args","type","loc"]; +const KEYS152: readonly string[] = ["kind","executor","type","loc"]; +const KEYS153: readonly string[] = ["kind","callee","args","type","loc"]; +const KEYS154: readonly string[] = ["kind","left","right","type","loc"]; +const KEYS155: readonly string[] = ["kind","value","spelling","type","loc"]; +const KEYS156: readonly string[] = ["kind","id","receiver","body","type","loc"]; +const KEYS157: readonly string[] = ["kind","left","right","retag","type","loc"]; +const KEYS158: readonly string[] = ["kind","value","type","loc"]; +const KEYS159: readonly string[] = ["kind","type","loc"]; +const KEYS160: readonly string[] = ["kind","source","overrides","type","loc"]; const KEYS161: readonly string[] = ["kind","obj","shapeId","field","type","loc"]; -const KEYS162: readonly string[] = ["kind","obj","shapeId","key","overflowOnly","missNaN","type","loc"]; -const KEYS163: readonly string[] = ["kind","fields","nullProto","type","loc"]; -const KEYS164: readonly string[] = ["kind","obj","shapeId","type","loc"]; -const KEYS165: readonly string[] = ["kind","obj","shapeId","key","type","loc"]; -const KEYS166: readonly string[] = ["kind","obj","shapeId","type","loc"]; -const KEYS167: readonly string[] = ["kind","method","receiver","args","type","loc"]; -const KEYS168: readonly string[] = ["kind","pattern","flags","type","loc"]; -const KEYS169: readonly string[] = ["kind","type","loc"]; -const KEYS170: readonly string[] = ["kind","stmts","result","generatorDelegate","diverges","type","loc"]; -const KEYS171: readonly string[] = ["kind","method","receiver","args","type","loc"]; -const KEYS172: readonly string[] = ["kind","seed","type","loc"]; -const KEYS173: readonly string[] = ["kind","op","left","right","type","loc"]; -const KEYS174: readonly string[] = ["kind","left","right","type","loc"]; -const KEYS175: readonly string[] = ["kind","negated","left","right","type","loc"]; -const KEYS176: readonly string[] = ["kind","method","receiver","args","type","loc"]; -const KEYS177: readonly string[] = ["kind","value","type","loc"]; -const KEYS178: readonly string[] = ["kind","key","cooked","type","loc"]; -const KEYS179: readonly string[] = ["kind","cond","then","else_","type","loc"]; -const KEYS180: readonly string[] = ["kind","operand","type","loc"]; +const KEYS162: readonly string[] = ["kind","obj","shapeId","field","type","loc"]; +const KEYS163: readonly string[] = ["kind","obj","shapeId","key","overflowOnly","missNaN","type","loc"]; +const KEYS164: readonly string[] = ["kind","fields","nullProto","type","loc"]; +const KEYS165: readonly string[] = ["kind","obj","shapeId","type","loc"]; +const KEYS166: readonly string[] = ["kind","obj","shapeId","key","type","loc"]; +const KEYS167: readonly string[] = ["kind","obj","shapeId","type","loc"]; +const KEYS168: readonly string[] = ["kind","method","receiver","args","type","loc"]; +const KEYS169: readonly string[] = ["kind","pattern","flags","type","loc"]; +const KEYS170: readonly string[] = ["kind","type","loc"]; +const KEYS171: readonly string[] = ["kind","stmts","result","generatorDelegate","diverges","type","loc"]; +const KEYS172: readonly string[] = ["kind","method","receiver","args","type","loc"]; +const KEYS173: readonly string[] = ["kind","seed","type","loc"]; +const KEYS174: readonly string[] = ["kind","op","left","right","type","loc"]; +const KEYS175: readonly string[] = ["kind","left","right","type","loc"]; +const KEYS176: readonly string[] = ["kind","negated","left","right","type","loc"]; +const KEYS177: readonly string[] = ["kind","method","receiver","args","type","loc"]; +const KEYS178: readonly string[] = ["kind","value","type","loc"]; +const KEYS179: readonly string[] = ["kind","key","cooked","type","loc"]; +const KEYS180: readonly string[] = ["kind","cond","then","else_","type","loc"]; const KEYS181: readonly string[] = ["kind","operand","type","loc"]; -const KEYS182: readonly string[] = ["kind","op","operand","type","loc"]; -const KEYS183: readonly string[] = ["kind","unionId","field","value","type","loc"]; -const KEYS184: readonly string[] = ["kind","unionId","negated","sameValue","left","right","type","loc"]; -const KEYS185: readonly string[] = ["kind","unionId","tag","union","func","negated","type","loc"]; -const KEYS186: readonly string[] = ["kind","unionId","tag","negated","value","type","loc"]; -const KEYS187: readonly string[] = ["kind","unionId","key","value","type","loc"]; -const KEYS188: readonly string[] = ["kind","unionId","tag","value","type","loc"]; -const KEYS189: readonly string[] = ["kind","fromId","unionId","tags","value","type","loc"]; -const KEYS190: readonly string[] = ["kind","unionId","tag","value","type","loc"]; -const KEYS191: readonly string[] = ["kind","unit","type","loc"]; -const KEYS192: readonly string[] = ["kind","value","type","loc"]; -const KEYS193: readonly string[] = ["kind","localId","type","loc"]; -const KEYS194: readonly string[] = ["kind","className","method","args","type","loc","targets"]; -const KEYS195: readonly string[] = ["kind","value","awaited","delegated","captureCompletion","type","loc"]; -const KEYS196: readonly string[] = ["name","type"]; +const KEYS182: readonly string[] = ["kind","operand","type","loc"]; +const KEYS183: readonly string[] = ["kind","op","operand","type","loc"]; +const KEYS184: readonly string[] = ["kind","unionId","field","value","type","loc"]; +const KEYS185: readonly string[] = ["kind","unionId","negated","sameValue","left","right","type","loc"]; +const KEYS186: readonly string[] = ["kind","unionId","tag","union","func","negated","type","loc"]; +const KEYS187: readonly string[] = ["kind","unionId","tag","negated","value","type","loc"]; +const KEYS188: readonly string[] = ["kind","unionId","key","value","type","loc"]; +const KEYS189: readonly string[] = ["kind","unionId","tag","value","type","loc"]; +const KEYS190: readonly string[] = ["kind","fromId","unionId","tags","value","type","loc"]; +const KEYS191: readonly string[] = ["kind","unionId","tag","value","type","loc"]; +const KEYS192: readonly string[] = ["kind","unit","type","loc"]; +const KEYS193: readonly string[] = ["kind","value","type","loc"]; +const KEYS194: readonly string[] = ["kind","localId","type","loc"]; +const KEYS195: readonly string[] = ["kind","className","method","args","type","loc","targets"]; +const KEYS196: readonly string[] = ["kind","value","awaited","delegated","captureCompletion","type","loc"]; const KEYS197: readonly string[] = ["name","type"]; -const KEYS198: readonly string[] = ["path","size","sha256","placeholder"]; -const KEYS199: readonly string[] = ["path","target"]; -const KEYS200: readonly string[] = ["diff","skipPrototype"]; -const KEYS201: readonly string[] = ["returnType"]; -const KEYS202: readonly string[] = ["test","body"]; -const KEYS203: readonly string[] = ["field","globalId"]; -const KEYS204: readonly string[] = ["field","wellKnown"]; -const KEYS205: readonly string[] = ["tag","values"]; -const KEYS206: readonly string[] = ["context"]; -const KEYS207: readonly string[] = ["callback"]; +const KEYS198: readonly string[] = ["name","type"]; +const KEYS199: readonly string[] = ["path","size","sha256","placeholder"]; +const KEYS200: readonly string[] = ["path","target"]; +const KEYS201: readonly string[] = ["diff","skipPrototype"]; +const KEYS202: readonly string[] = ["returnType"]; +const KEYS203: readonly string[] = ["test","body"]; +const KEYS204: readonly string[] = ["field","globalId"]; +const KEYS205: readonly string[] = ["field","wellKnown"]; +const KEYS206: readonly string[] = ["tag","values"]; +const KEYS207: readonly string[] = ["context"]; const KEYS208: readonly string[] = ["callback"]; -const KEYS209: readonly string[] = ["arg","what"]; -const KEYS210: readonly string[] = ["key","value"]; +const KEYS209: readonly string[] = ["callback"]; +const KEYS210: readonly string[] = ["arg","what"]; const KEYS211: readonly string[] = ["key","value"]; -const KEYS212: readonly string[] = ["name","value"]; -const KEYS213: readonly string[] = ["name","value","overflow"]; -const KEYS214: readonly string[] = ["id","params","returns","lifetime","invoke"]; -const KEYS215: readonly string[] = ["release","params","returns"]; +const KEYS212: readonly string[] = ["key","value"]; +const KEYS213: readonly string[] = ["name","value"]; +const KEYS214: readonly string[] = ["name","value","overflow"]; +const KEYS215: readonly string[] = ["id","params","returns","lifetime","invoke"]; +const KEYS216: readonly string[] = ["release","params","returns"]; function encIrModule(w: IrWriter, v: IrModule): void { w.keys(Object.keys(v), KEYS0); @@ -780,91 +781,96 @@ function encIrType(w: IrWriter, v: IrType): void { w.keys(Object.keys(v), KEYS6); return; } - case "bool": { + case "bigintStats": { w.length(2); w.keys(Object.keys(v), KEYS7); return; } - case "bytes": { + case "bool": { w.length(3); w.keys(Object.keys(v), KEYS8); + return; + } + case "bytes": { + w.length(4); + w.keys(Object.keys(v), KEYS9); w.str(v.elem); return; } case "caught": { - w.length(4); - w.keys(Object.keys(v), KEYS9); - return; - } - case "child": { w.length(5); w.keys(Object.keys(v), KEYS10); return; } - case "childStream": { + case "child": { w.length(6); w.keys(Object.keys(v), KEYS11); return; } - case "childWriter": { + case "childStream": { w.length(7); w.keys(Object.keys(v), KEYS12); return; } - case "classval": { + case "childWriter": { w.length(8); w.keys(Object.keys(v), KEYS13); + return; + } + case "classval": { + w.length(9); + w.keys(Object.keys(v), KEYS14); w.str(v.className); return; } case "cryptoHash": { - w.length(9); - w.keys(Object.keys(v), KEYS14); - return; - } - case "cryptoHmac": { w.length(10); w.keys(Object.keys(v), KEYS15); return; } - case "date": { + case "cryptoHmac": { w.length(11); w.keys(Object.keys(v), KEYS16); return; } - case "dgramSocket": { + case "date": { w.length(12); w.keys(Object.keys(v), KEYS17); return; } - case "dir": { + case "dgramSocket": { w.length(13); w.keys(Object.keys(v), KEYS18); return; } - case "dyn": { + case "dir": { w.length(14); w.keys(Object.keys(v), KEYS19); return; } - case "f64": { + case "dyn": { w.length(15); w.keys(Object.keys(v), KEYS20); return; } - case "fileHandle": { + case "f64": { w.length(16); w.keys(Object.keys(v), KEYS21); return; } - case "fsWatcher": { + case "fileHandle": { w.length(17); w.keys(Object.keys(v), KEYS22); return; } - case "func": { + case "fsWatcher": { w.length(18); w.keys(Object.keys(v), KEYS23); + return; + } + case "func": { + w.length(19); + w.keys(Object.keys(v), KEYS24); encT36(w, v.params); encIrType(w, v.ret); if (v.rest === undefined) w.byte(0); @@ -885,8 +891,8 @@ function encIrType(w: IrWriter, v: IrType): void { return; } case "generator": { - w.length(19); - w.keys(Object.keys(v), KEYS24); + w.length(20); + w.keys(Object.keys(v), KEYS25); if (v.async === undefined) w.byte(0); else { w.byte(1); @@ -898,151 +904,151 @@ function encIrType(w: IrWriter, v: IrType): void { return; } case "http2Session": { - w.length(20); - w.keys(Object.keys(v), KEYS25); - return; - } - case "http2Stream": { w.length(21); w.keys(Object.keys(v), KEYS26); return; } - case "httpClientReq": { + case "http2Stream": { w.length(22); w.keys(Object.keys(v), KEYS27); return; } - case "httpReq": { + case "httpClientReq": { w.length(23); w.keys(Object.keys(v), KEYS28); return; } - case "httpRes": { + case "httpReq": { w.length(24); w.keys(Object.keys(v), KEYS29); return; } - case "jsval": { + case "httpRes": { w.length(25); w.keys(Object.keys(v), KEYS30); return; } - case "map": { + case "jsval": { w.length(26); w.keys(Object.keys(v), KEYS31); + return; + } + case "map": { + w.length(27); + w.keys(Object.keys(v), KEYS32); encIrType(w, v.key); encIrType(w, v.value); return; } case "moduleNs": { - w.length(27); - w.keys(Object.keys(v), KEYS32); + w.length(28); + w.keys(Object.keys(v), KEYS33); w.str(v.moduleId); return; } case "netServer": { - w.length(28); - w.keys(Object.keys(v), KEYS33); - return; - } - case "netSocket": { w.length(29); w.keys(Object.keys(v), KEYS34); return; } - case "nullT": { + case "netSocket": { w.length(30); w.keys(Object.keys(v), KEYS35); return; } - case "object": { + case "nullT": { w.length(31); w.keys(Object.keys(v), KEYS36); + return; + } + case "object": { + w.length(32); + w.keys(Object.keys(v), KEYS37); w.str(v.className); return; } case "procStream": { - w.length(32); - w.keys(Object.keys(v), KEYS37); + w.length(33); + w.keys(Object.keys(v), KEYS38); return; } case "promise": { - w.length(33); - w.keys(Object.keys(v), KEYS38); + w.length(34); + w.keys(Object.keys(v), KEYS39); encIrType(w, v.inner); return; } case "record": { - w.length(34); - w.keys(Object.keys(v), KEYS39); + w.length(35); + w.keys(Object.keys(v), KEYS40); w.str(v.shapeId); return; } case "regex": { - w.length(35); - w.keys(Object.keys(v), KEYS40); - return; - } - case "searchParams": { w.length(36); w.keys(Object.keys(v), KEYS41); return; } - case "secureCtx": { + case "searchParams": { w.length(37); w.keys(Object.keys(v), KEYS42); return; } - case "set": { + case "secureCtx": { w.length(38); w.keys(Object.keys(v), KEYS43); + return; + } + case "set": { + w.length(39); + w.keys(Object.keys(v), KEYS44); encIrType(w, v.elem); return; } case "spawnRes": { - w.length(39); - w.keys(Object.keys(v), KEYS44); - return; - } - case "stats": { w.length(40); w.keys(Object.keys(v), KEYS45); return; } - case "string": { + case "stats": { w.length(41); w.keys(Object.keys(v), KEYS46); return; } - case "symbol": { + case "string": { w.length(42); w.keys(Object.keys(v), KEYS47); return; } - case "testCtx": { + case "symbol": { w.length(43); w.keys(Object.keys(v), KEYS48); return; } - case "undefinedT": { + case "testCtx": { w.length(44); w.keys(Object.keys(v), KEYS49); return; } - case "union": { + case "undefinedT": { w.length(45); w.keys(Object.keys(v), KEYS50); + return; + } + case "union": { + w.length(46); + w.keys(Object.keys(v), KEYS51); w.str(v.unionId); return; } case "url": { - w.length(46); - w.keys(Object.keys(v), KEYS51); + w.length(47); + w.keys(Object.keys(v), KEYS52); return; } case "void": { - w.length(47); - w.keys(Object.keys(v), KEYS52); + w.length(48); + w.keys(Object.keys(v), KEYS53); return; } } @@ -1061,72 +1067,76 @@ function decIrType(r: IrReader): IrType { return o; } case 2: { - const o: Extract = { kind: "bool" }; + const o: Extract = { kind: "bigintStats" }; return o; } case 3: { + const o: Extract = { kind: "bool" }; + return o; + } + case 4: { const f0 = (r.str() as NonNullable["elem"]>); const o: Extract = { kind: "bytes", elem: f0 }; return o; } - case 4: { + case 5: { const o: Extract = { kind: "caught" }; return o; } - case 5: { + case 6: { const o: Extract = { kind: "child" }; return o; } - case 6: { + case 7: { const o: Extract = { kind: "childStream" }; return o; } - case 7: { + case 8: { const o: Extract = { kind: "childWriter" }; return o; } - case 8: { + case 9: { const f0 = r.str(); const o: Extract = { kind: "classval", className: f0 }; return o; } - case 9: { + case 10: { const o: Extract = { kind: "cryptoHash" }; return o; } - case 10: { + case 11: { const o: Extract = { kind: "cryptoHmac" }; return o; } - case 11: { + case 12: { const o: Extract = { kind: "date" }; return o; } - case 12: { + case 13: { const o: Extract = { kind: "dgramSocket" }; return o; } - case 13: { + case 14: { const o: Extract = { kind: "dir" }; return o; } - case 14: { + case 15: { const o: Extract = { kind: "dyn" }; return o; } - case 15: { + case 16: { const o: Extract = { kind: "f64" }; return o; } - case 16: { + case 17: { const o: Extract = { kind: "fileHandle" }; return o; } - case 17: { + case 18: { const o: Extract = { kind: "fsWatcher" }; return o; } - case 18: { + case 19: { const f0 = decT36(r); const f1 = decIrType(r); const f2 = r.tag(2, "IrFunction.returnType.rest") === 0 ? undefined : (r.bool() as NonNullable["rest"]>); @@ -1135,7 +1145,7 @@ function decIrType(r: IrReader): IrType { const o: Extract = { kind: "func", params: f0, ret: f1, ...(f2 !== undefined ? { rest: f2 } : {}), ...(f3 !== undefined ? { restAbi: f3 } : {}), ...(f4 !== undefined ? { argumentsAll: f4 } : {}) }; return o; } - case 19: { + case 20: { const f0 = r.tag(2, "IrFunction.returnType.async") === 0 ? undefined : (r.bool() as NonNullable["async"]>); const f1 = decIrType(r); const f2 = decIrType(r); @@ -1143,123 +1153,123 @@ function decIrType(r: IrReader): IrType { const o: Extract = { kind: "generator", ...(f0 !== undefined ? { async: f0 } : {}), yieldT: f1, retT: f2, nextT: f3 }; return o; } - case 20: { + case 21: { const o: Extract = { kind: "http2Session" }; return o; } - case 21: { + case 22: { const o: Extract = { kind: "http2Stream" }; return o; } - case 22: { + case 23: { const o: Extract = { kind: "httpClientReq" }; return o; } - case 23: { + case 24: { const o: Extract = { kind: "httpReq" }; return o; } - case 24: { + case 25: { const o: Extract = { kind: "httpRes" }; return o; } - case 25: { + case 26: { const o: Extract = { kind: "jsval" }; return o; } - case 26: { + case 27: { const f0 = decIrType(r); const f1 = decIrType(r); const o: Extract = { kind: "map", key: f0, value: f1 }; return o; } - case 27: { + case 28: { const f0 = r.str(); const o: Extract = { kind: "moduleNs", moduleId: f0 }; return o; } - case 28: { + case 29: { const o: Extract = { kind: "netServer" }; return o; } - case 29: { + case 30: { const o: Extract = { kind: "netSocket" }; return o; } - case 30: { + case 31: { const o: Extract = { kind: "nullT" }; return o; } - case 31: { + case 32: { const f0 = r.str(); const o: Extract = { kind: "object", className: f0 }; return o; } - case 32: { + case 33: { const o: Extract = { kind: "procStream" }; return o; } - case 33: { + case 34: { const f0 = decIrType(r); const o: Extract = { kind: "promise", inner: f0 }; return o; } - case 34: { + case 35: { const f0 = r.str(); const o: Extract = { kind: "record", shapeId: f0 }; return o; } - case 35: { + case 36: { const o: Extract = { kind: "regex" }; return o; } - case 36: { + case 37: { const o: Extract = { kind: "searchParams" }; return o; } - case 37: { + case 38: { const o: Extract = { kind: "secureCtx" }; return o; } - case 38: { + case 39: { const f0 = decIrType(r); const o: Extract = { kind: "set", elem: f0 }; return o; } - case 39: { + case 40: { const o: Extract = { kind: "spawnRes" }; return o; } - case 40: { + case 41: { const o: Extract = { kind: "stats" }; return o; } - case 41: { + case 42: { const o: Extract = { kind: "string" }; return o; } - case 42: { + case 43: { const o: Extract = { kind: "symbol" }; return o; } - case 43: { + case 44: { const o: Extract = { kind: "testCtx" }; return o; } - case 44: { + case 45: { const o: Extract = { kind: "undefinedT" }; return o; } - case 45: { + case 46: { const f0 = r.str(); const o: Extract = { kind: "union", unionId: f0 }; return o; } - case 46: { + case 47: { const o: Extract = { kind: "url" }; return o; } - case 47: { + case 48: { const o: Extract = { kind: "void" }; return o; } @@ -1307,7 +1317,7 @@ function decT19(r: IrReader): NonNullable { } function encSrcLoc(w: IrWriter, v: SrcLoc): void { - w.keys(Object.keys(v), KEYS53); + w.keys(Object.keys(v), KEYS54); w.file(v.file); w.num(v.start); w.num(v.end); @@ -1321,7 +1331,7 @@ function decSrcLoc(r: IrReader): SrcLoc { } function encT21(w: IrWriter, v: NonNullable): void { - w.keys(Object.keys(v), KEYS54); + w.keys(Object.keys(v), KEYS55); encIrType(w, v.yieldT); encIrType(w, v.nextT); encT40(w, v.resultType); @@ -1353,7 +1363,7 @@ function decT21(r: IrReader): NonNullable { } function encIrClassDef(w: IrWriter, v: IrClassDef): void { - w.keys(Object.keys(v), KEYS55); + w.keys(Object.keys(v), KEYS56); w.str(v.name); if (v.localCaptures === undefined) w.byte(0); else { @@ -1506,7 +1516,7 @@ function decIrClassDef(r: IrReader): IrClassDef { } function encIrGlobal(w: IrWriter, v: IrGlobal): void { - w.keys(Object.keys(v), KEYS56); + w.keys(Object.keys(v), KEYS57); w.str(v.id); w.str(v.name); encIrType(w, v.type); @@ -1540,7 +1550,7 @@ function decIrGlobal(r: IrReader): IrGlobal { } function encIrRecordShape(w: IrWriter, v: IrRecordShape): void { - w.keys(Object.keys(v), KEYS57); + w.keys(Object.keys(v), KEYS58); w.str(v.id); encT44(w, v.fields); if (v.tuple === undefined) w.byte(0); @@ -1588,7 +1598,7 @@ function decIrRecordShape(r: IrReader): IrRecordShape { } function encIrUnionDef(w: IrWriter, v: IrUnionDef): void { - w.keys(Object.keys(v), KEYS58); + w.keys(Object.keys(v), KEYS59); w.str(v.id); encT36(w, v.arms); if (v.discriminant === undefined) w.byte(0); @@ -1606,7 +1616,7 @@ function decIrUnionDef(r: IrReader): IrUnionDef { } function encT26(w: IrWriter, v: NonNullable[number]>): void { - w.keys(Object.keys(v), KEYS59); + w.keys(Object.keys(v), KEYS60); w.file(v.file); w.str(v.pkg); } @@ -1618,7 +1628,7 @@ function decT26(r: IrReader): NonNullable { } function encIrLibIdentity(w: IrWriter, v: IrLibIdentity): void { - w.keys(Object.keys(v), KEYS62); + w.keys(Object.keys(v), KEYS63); w.str(v.buildIdSymbol); w.str(v.abiVersionSymbol); w.str(v.buildId); @@ -1727,7 +1737,7 @@ function decIrLibIdentity(r: IrReader): IrLibIdentity { } function encIrWasmModule(w: IrWriter, v: IrWasmModule): void { - w.keys(Object.keys(v), KEYS63); + w.keys(Object.keys(v), KEYS64); w.str(v.sha256); w.str(v.bytes); } @@ -1739,7 +1749,7 @@ function decIrWasmModule(r: IrReader): IrWasmModule { } function encIrWasmUntranslated(w: IrWriter, v: IrWasmUntranslated): void { - w.keys(Object.keys(v), KEYS64); + w.keys(Object.keys(v), KEYS65); w.str(v.sha256); w.num(v.size); w.str(v.reason); @@ -1753,7 +1763,7 @@ function decIrWasmUntranslated(r: IrReader): IrWasmUntranslated { } function encIrParam(w: IrWriter, v: IrParam): void { - w.keys(Object.keys(v), KEYS65); + w.keys(Object.keys(v), KEYS66); w.str(v.localId); w.str(v.name); encIrType(w, v.type); @@ -1786,7 +1796,7 @@ function decT36(r: IrReader): NonNullable["par } function encIrLocal(w: IrWriter, v: IrLocal): void { - w.keys(Object.keys(v), KEYS66); + w.keys(Object.keys(v), KEYS67); w.str(v.id); w.str(v.name); encIrType(w, v.type); @@ -1820,7 +1830,7 @@ function decIrLocal(r: IrReader): IrLocal { } function encT38(w: IrWriter, v: NonNullable[number]>): void { - w.keys(Object.keys(v), KEYS67); + w.keys(Object.keys(v), KEYS68); w.str(v.localId); w.str(v.name); encIrType(w, v.type); @@ -1845,7 +1855,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { switch (v.kind) { case "arrayDelete": { w.length(0); - w.keys(Object.keys(v), KEYS68); + w.keys(Object.keys(v), KEYS69); encIrExpr(w, v.arr); encIrExpr(w, v.index); encSrcLoc(w, v.loc); @@ -1853,7 +1863,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "arraySet": { w.length(1); - w.keys(Object.keys(v), KEYS69); + w.keys(Object.keys(v), KEYS70); encIrExpr(w, v.arr); encIrExpr(w, v.index); encIrExpr(w, v.value); @@ -1862,7 +1872,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "arraySetLength": { w.length(2); - w.keys(Object.keys(v), KEYS70); + w.keys(Object.keys(v), KEYS71); encIrExpr(w, v.arr); encIrExpr(w, v.length); encSrcLoc(w, v.loc); @@ -1880,7 +1890,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "arraySetUndefined": { w.length(3); - w.keys(Object.keys(v), KEYS71); + w.keys(Object.keys(v), KEYS72); encIrExpr(w, v.arr); encIrExpr(w, v.index); encSrcLoc(w, v.loc); @@ -1888,7 +1898,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "assign": { w.length(4); - w.keys(Object.keys(v), KEYS72); + w.keys(Object.keys(v), KEYS73); w.str(v.localId); encIrExpr(w, v.value); if (v.initializes === undefined) w.byte(0); @@ -1901,7 +1911,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "block": { w.length(5); - w.keys(Object.keys(v), KEYS73); + w.keys(Object.keys(v), KEYS74); if (v.labels === undefined) w.byte(0); else { w.byte(1); @@ -1913,7 +1923,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "break": { w.length(6); - w.keys(Object.keys(v), KEYS74); + w.keys(Object.keys(v), KEYS75); if (v.label === undefined) w.byte(0); else { w.byte(1); @@ -1924,7 +1934,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "bytesSet": { w.length(7); - w.keys(Object.keys(v), KEYS75); + w.keys(Object.keys(v), KEYS76); encIrExpr(w, v.arr); encIrExpr(w, v.index); encIrExpr(w, v.value); @@ -1933,7 +1943,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "continue": { w.length(8); - w.keys(Object.keys(v), KEYS76); + w.keys(Object.keys(v), KEYS77); if (v.label === undefined) w.byte(0); else { w.byte(1); @@ -1944,7 +1954,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "doWhile": { w.length(9); - w.keys(Object.keys(v), KEYS77); + w.keys(Object.keys(v), KEYS78); encT19(w, v.body); encIrExpr(w, v.cond); if (v.labels === undefined) w.byte(0); @@ -1957,14 +1967,14 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "exprStmt": { w.length(10); - w.keys(Object.keys(v), KEYS78); + w.keys(Object.keys(v), KEYS79); encIrExpr(w, v.expr); encSrcLoc(w, v.loc); return; } case "fieldSet": { w.length(11); - w.keys(Object.keys(v), KEYS79); + w.keys(Object.keys(v), KEYS80); encIrExpr(w, v.obj); w.str(v.className); w.str(v.field); @@ -1989,7 +1999,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "for": { w.length(12); - w.keys(Object.keys(v), KEYS80); + w.keys(Object.keys(v), KEYS81); if (v.init === null) w.byte(0); else { w.byte(1); @@ -2016,7 +2026,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "forOf": { w.length(13); - w.keys(Object.keys(v), KEYS81); + w.keys(Object.keys(v), KEYS82); w.str(v.localId); encIrExpr(w, v.iterable); encT19(w, v.body); @@ -2030,7 +2040,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "if": { w.length(14); - w.keys(Object.keys(v), KEYS82); + w.keys(Object.keys(v), KEYS83); encIrExpr(w, v.cond); encT19(w, v.then); if (v.else_ === null) w.byte(0); @@ -2043,7 +2053,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "recordKeyDelete": { w.length(15); - w.keys(Object.keys(v), KEYS83); + w.keys(Object.keys(v), KEYS84); encIrExpr(w, v.obj); w.str(v.shapeId); encIrExpr(w, v.key); @@ -2052,7 +2062,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "recordKeySet": { w.length(16); - w.keys(Object.keys(v), KEYS84); + w.keys(Object.keys(v), KEYS85); encIrExpr(w, v.obj); w.str(v.shapeId); encIrExpr(w, v.key); @@ -2067,7 +2077,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "recordSet": { w.length(17); - w.keys(Object.keys(v), KEYS85); + w.keys(Object.keys(v), KEYS86); encIrExpr(w, v.obj); w.str(v.shapeId); w.str(v.field); @@ -2077,14 +2087,14 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "rethrow": { w.length(18); - w.keys(Object.keys(v), KEYS86); + w.keys(Object.keys(v), KEYS87); w.str(v.localId); encSrcLoc(w, v.loc); return; } case "return": { w.length(19); - w.keys(Object.keys(v), KEYS87); + w.keys(Object.keys(v), KEYS88); if (v.value === null) w.byte(0); else { w.byte(1); @@ -2100,7 +2110,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "runtimeFence": { w.length(20); - w.keys(Object.keys(v), KEYS88); + w.keys(Object.keys(v), KEYS89); w.str(v.code); w.str(v.message); encSrcLoc(w, v.loc); @@ -2108,7 +2118,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "switch": { w.length(21); - w.keys(Object.keys(v), KEYS89); + w.keys(Object.keys(v), KEYS90); encIrExpr(w, v.disc); encT54(w, v.cases); if (v.labels === undefined) w.byte(0); @@ -2121,14 +2131,14 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "throw": { w.length(22); - w.keys(Object.keys(v), KEYS90); + w.keys(Object.keys(v), KEYS91); encIrExpr(w, v.value); encSrcLoc(w, v.loc); return; } case "tryCatch": { w.length(23); - w.keys(Object.keys(v), KEYS91); + w.keys(Object.keys(v), KEYS92); encT19(w, v.tryBody); if (v.catchBody === null) w.byte(0); else { @@ -2160,7 +2170,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "varDecl": { w.length(24); - w.keys(Object.keys(v), KEYS92); + w.keys(Object.keys(v), KEYS93); w.str(v.localId); if (v.init === null) w.byte(0); else { @@ -2172,7 +2182,7 @@ function encIrStmt(w: IrWriter, v: IrStmt): void { } case "while": { w.length(25); - w.keys(Object.keys(v), KEYS93); + w.keys(Object.keys(v), KEYS94); encIrExpr(w, v.cond); encT19(w, v.body); if (v.labels === undefined) w.byte(0); @@ -2399,7 +2409,7 @@ function decIrStmt(r: IrReader): IrStmt { } function encT40(w: IrWriter, v: NonNullable["resultType"]>): void { - w.keys(Object.keys(v), KEYS94); + w.keys(Object.keys(v), KEYS95); w.str(v.kind); w.str(v.shapeId); } @@ -2437,7 +2447,7 @@ function decT42(r: IrReader): NonNullable { } function encIrBindingSource(w: IrWriter, v: IrBindingSource): void { - w.keys(Object.keys(v), KEYS95); + w.keys(Object.keys(v), KEYS96); encSrcLoc(w, v.loc); encSrcLoc(w, v.scope); } @@ -2475,7 +2485,7 @@ function decT45(r: IrReader): NonNullable { } function encIrUnionDiscriminant(w: IrWriter, v: IrUnionDiscriminant): void { - w.keys(Object.keys(v), KEYS96); + w.keys(Object.keys(v), KEYS97); w.str(v.field); encT58(w, v.cases); } @@ -2526,7 +2536,7 @@ function decT49(r: IrReader): NonNullable { } function encIrLibCallback(w: IrWriter, v: IrLibCallback): void { - w.keys(Object.keys(v), KEYS97); + w.keys(Object.keys(v), KEYS98); w.str(v.name); w.num(v.slot); encT62(w, v.params); @@ -2544,7 +2554,7 @@ function decIrLibCallback(r: IrReader): IrLibCallback { } function encIrLibExport(w: IrWriter, v: IrLibExport): void { - w.keys(Object.keys(v), KEYS98); + w.keys(Object.keys(v), KEYS99); w.str(v.symbol); w.str(v.fnName); encT63(w, v.params); @@ -2578,7 +2588,7 @@ function decIrLibExport(r: IrReader): IrLibExport { } function encIrLibTrapOverlay(w: IrWriter, v: IrLibTrapOverlay): void { - w.keys(Object.keys(v), KEYS99); + w.keys(Object.keys(v), KEYS100); w.str(v.code); if (v.teaching === undefined) w.byte(0); else { @@ -2603,7 +2613,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { switch (v.kind) { case "arrIntrinsic": { w.length(0); - w.keys(Object.keys(v), KEYS100); + w.keys(Object.keys(v), KEYS101); w.str(v.method); encIrExpr(w, v.receiver); encT64(w, v.args); @@ -2613,7 +2623,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "arrayGet": { w.length(1); - w.keys(Object.keys(v), KEYS101); + w.keys(Object.keys(v), KEYS102); encIrExpr(w, v.arr); encIrExpr(w, v.index); encIrType(w, v.type); @@ -2622,7 +2632,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "arrayHas": { w.length(2); - w.keys(Object.keys(v), KEYS102); + w.keys(Object.keys(v), KEYS103); encIrExpr(w, v.arr); encIrExpr(w, v.index); encIrType(w, v.type); @@ -2631,7 +2641,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "arrayLit": { w.length(3); - w.keys(Object.keys(v), KEYS103); + w.keys(Object.keys(v), KEYS104); encT64(w, v.elems); if (v.spreads === undefined) w.byte(0); else { @@ -2649,7 +2659,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "arrayNewLen": { w.length(4); - w.keys(Object.keys(v), KEYS104); + w.keys(Object.keys(v), KEYS105); encIrExpr(w, v.length); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -2657,7 +2667,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "arrayState": { w.length(5); - w.keys(Object.keys(v), KEYS105); + w.keys(Object.keys(v), KEYS106); encIrExpr(w, v.arr); encIrExpr(w, v.index); encIrType(w, v.type); @@ -2666,7 +2676,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "assignExpr": { w.length(6); - w.keys(Object.keys(v), KEYS106); + w.keys(Object.keys(v), KEYS107); w.str(v.localId); encIrExpr(w, v.value); encIrType(w, v.type); @@ -2675,7 +2685,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "awaitExpr": { w.length(7); - w.keys(Object.keys(v), KEYS107); + w.keys(Object.keys(v), KEYS108); encIrExpr(w, v.value); encIrType(w, v.type); if (v.adopt === undefined) w.byte(0); @@ -2688,7 +2698,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "awaitUnionExpr": { w.length(8); - w.keys(Object.keys(v), KEYS108); + w.keys(Object.keys(v), KEYS109); encIrExpr(w, v.value); w.num(v.promiseTag); encIrType(w, v.type); @@ -2697,7 +2707,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "bin": { w.length(9); - w.keys(Object.keys(v), KEYS109); + w.keys(Object.keys(v), KEYS110); w.str(v.op); encIrExpr(w, v.left); encIrExpr(w, v.right); @@ -2707,7 +2717,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "boolLit": { w.length(10); - w.keys(Object.keys(v), KEYS110); + w.keys(Object.keys(v), KEYS111); w.bool(v.value); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -2715,7 +2725,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "bytesIntrinsic": { w.length(11); - w.keys(Object.keys(v), KEYS111); + w.keys(Object.keys(v), KEYS112); w.str(v.method); encIrExpr(w, v.receiver); encT64(w, v.args); @@ -2730,7 +2740,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "bytesNew": { w.length(12); - w.keys(Object.keys(v), KEYS112); + w.keys(Object.keys(v), KEYS113); if (v.source === null) w.byte(0); else { w.byte(1); @@ -2747,7 +2757,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "call": { w.length(13); - w.keys(Object.keys(v), KEYS113); + w.keys(Object.keys(v), KEYS114); w.str(v.callee); encT64(w, v.args); encIrType(w, v.type); @@ -2756,7 +2766,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "callValue": { w.length(14); - w.keys(Object.keys(v), KEYS114); + w.keys(Object.keys(v), KEYS115); encIrExpr(w, v.callee); if (v.receiver === undefined) w.byte(0); else { @@ -2770,7 +2780,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "caughtCheck": { w.length(15); - w.keys(Object.keys(v), KEYS115); + w.keys(Object.keys(v), KEYS116); encIrExpr(w, v.value); w.str(v.className); encIrType(w, v.type); @@ -2779,7 +2789,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "caughtNarrow": { w.length(16); - w.keys(Object.keys(v), KEYS116); + w.keys(Object.keys(v), KEYS117); encIrExpr(w, v.value); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -2787,7 +2797,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "caughtTest": { w.length(17); - w.keys(Object.keys(v), KEYS117); + w.keys(Object.keys(v), KEYS118); encIrExpr(w, v.value); w.str(v.test); if (v.className === undefined) w.byte(0); @@ -2806,7 +2816,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "caughtToDyn": { w.length(18); - w.keys(Object.keys(v), KEYS118); + w.keys(Object.keys(v), KEYS119); encIrExpr(w, v.value); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -2814,7 +2824,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "chainRecv": { w.length(19); - w.keys(Object.keys(v), KEYS119); + w.keys(Object.keys(v), KEYS120); w.str(v.id); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -2822,7 +2832,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "classRef": { w.length(20); - w.keys(Object.keys(v), KEYS120); + w.keys(Object.keys(v), KEYS121); w.str(v.className); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -2835,7 +2845,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "closure": { w.length(21); - w.keys(Object.keys(v), KEYS121); + w.keys(Object.keys(v), KEYS122); w.str(v.fnName); encT8(w, v.captures); if (v.adapts === undefined) w.byte(0); @@ -2849,7 +2859,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "downcast": { w.length(22); - w.keys(Object.keys(v), KEYS122); + w.keys(Object.keys(v), KEYS123); encIrExpr(w, v.value); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -2857,7 +2867,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "dynArrLit": { w.length(23); - w.keys(Object.keys(v), KEYS123); + w.keys(Object.keys(v), KEYS124); encT64(w, v.elems); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -2865,7 +2875,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "dynCall": { w.length(24); - w.keys(Object.keys(v), KEYS124); + w.keys(Object.keys(v), KEYS125); encIrExpr(w, v.callee); if (v.receiver === undefined) w.byte(0); else { @@ -2890,7 +2900,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "dynCheck": { w.length(25); - w.keys(Object.keys(v), KEYS125); + w.keys(Object.keys(v), KEYS126); encIrExpr(w, v.value); if (v.preserveRefs === undefined) w.byte(0); else { @@ -2923,7 +2933,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "dynDestrCheck": { w.length(26); - w.keys(Object.keys(v), KEYS126); + w.keys(Object.keys(v), KEYS127); encIrExpr(w, v.value); w.str(v.spelling); if (v.firstProp === undefined) w.byte(0); @@ -2937,7 +2947,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "dynFrom": { w.length(27); - w.keys(Object.keys(v), KEYS127); + w.keys(Object.keys(v), KEYS128); encIrExpr(w, v.value); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -2960,7 +2970,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "dynHasKey": { w.length(28); - w.keys(Object.keys(v), KEYS128); + w.keys(Object.keys(v), KEYS129); w.str(v.key); if (v.negated === undefined) w.byte(0); else { @@ -2974,7 +2984,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "dynInvoke": { w.length(29); - w.keys(Object.keys(v), KEYS129); + w.keys(Object.keys(v), KEYS130); encIrExpr(w, v.recv); w.str(v.method); w.str(v.calleeName); @@ -2990,7 +3000,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "dynIterN": { w.length(30); - w.keys(Object.keys(v), KEYS130); + w.keys(Object.keys(v), KEYS131); encIrExpr(w, v.value); w.num(v.count); if (v.notIterableMessage === undefined) w.byte(0); @@ -3004,7 +3014,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "dynKeyGet": { w.length(31); - w.keys(Object.keys(v), KEYS131); + w.keys(Object.keys(v), KEYS132); encIrExpr(w, v.key); if (v.optional === undefined) w.byte(0); else { @@ -3018,7 +3028,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "dynObjLit": { w.length(32); - w.keys(Object.keys(v), KEYS132); + w.keys(Object.keys(v), KEYS133); if (v.fields === undefined) w.byte(0); else { w.byte(1); @@ -3030,7 +3040,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "dynScalarEq": { w.length(33); - w.keys(Object.keys(v), KEYS133); + w.keys(Object.keys(v), KEYS134); encIrExpr(w, v.left); encIrExpr(w, v.right); if (v.negated === undefined) w.byte(0); @@ -3044,7 +3054,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "dynTest": { w.length(34); - w.keys(Object.keys(v), KEYS134); + w.keys(Object.keys(v), KEYS135); w.str(v.test); if (v.bytesElem === undefined) w.byte(0); else { @@ -3063,7 +3073,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "ffiCall": { w.length(35); - w.keys(Object.keys(v), KEYS135); + w.keys(Object.keys(v), KEYS136); w.str(v.import); encT64(w, v.args); encIrType(w, v.type); @@ -3072,7 +3082,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "fieldAbsent": { w.length(36); - w.keys(Object.keys(v), KEYS136); + w.keys(Object.keys(v), KEYS137); w.str(v.unionId); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -3080,7 +3090,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "fieldGet": { w.length(37); - w.keys(Object.keys(v), KEYS137); + w.keys(Object.keys(v), KEYS138); encIrExpr(w, v.obj); w.str(v.className); w.str(v.field); @@ -3090,7 +3100,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "fieldIncDec": { w.length(38); - w.keys(Object.keys(v), KEYS138); + w.keys(Object.keys(v), KEYS139); w.str(v.op); w.bool(v.prefix); encIrExpr(w, v.obj); @@ -3103,7 +3113,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "genResume": { w.length(39); - w.keys(Object.keys(v), KEYS139); + w.keys(Object.keys(v), KEYS140); w.str(v.mode); encIrExpr(w, v.gen); if (v.arg === null) w.byte(0); @@ -3117,7 +3127,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "incDec": { w.length(40); - w.keys(Object.keys(v), KEYS140); + w.keys(Object.keys(v), KEYS141); w.str(v.op); w.bool(v.prefix); w.str(v.localId); @@ -3127,7 +3137,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "instanceOf": { w.length(41); - w.keys(Object.keys(v), KEYS141); + w.keys(Object.keys(v), KEYS142); encIrExpr(w, v.value); w.str(v.className); encIrType(w, v.type); @@ -3136,7 +3146,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "instanceOfValue": { w.length(42); - w.keys(Object.keys(v), KEYS142); + w.keys(Object.keys(v), KEYS143); encIrExpr(w, v.value); encIrExpr(w, v.classValue); encIrType(w, v.type); @@ -3145,7 +3155,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "intrinsic": { w.length(43); - w.keys(Object.keys(v), KEYS143); + w.keys(Object.keys(v), KEYS144); w.str(v.name); encT64(w, v.args); encIrType(w, v.type); @@ -3154,7 +3164,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "jsonStringify": { w.length(44); - w.keys(Object.keys(v), KEYS144); + w.keys(Object.keys(v), KEYS145); encIrExpr(w, v.value); if (v.indent === undefined) w.byte(0); else { @@ -3172,7 +3182,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "libCall": { w.length(45); - w.keys(Object.keys(v), KEYS145); + w.keys(Object.keys(v), KEYS146); w.str(v.fn); encT64(w, v.args); encIrType(w, v.type); @@ -3196,7 +3206,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "logical": { w.length(46); - w.keys(Object.keys(v), KEYS146); + w.keys(Object.keys(v), KEYS147); w.str(v.op); encIrExpr(w, v.left); encIrExpr(w, v.right); @@ -3206,7 +3216,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "mapIntrinsic": { w.length(47); - w.keys(Object.keys(v), KEYS147); + w.keys(Object.keys(v), KEYS148); w.str(v.method); encIrExpr(w, v.receiver); encT64(w, v.args); @@ -3216,7 +3226,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "mapNew": { w.length(48); - w.keys(Object.keys(v), KEYS148); + w.keys(Object.keys(v), KEYS149); if (v.seed === undefined) w.byte(0); else { w.byte(1); @@ -3228,7 +3238,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "moduleNsRef": { w.length(49); - w.keys(Object.keys(v), KEYS149); + w.keys(Object.keys(v), KEYS150); w.str(v.moduleId); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -3236,7 +3246,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "new": { w.length(50); - w.keys(Object.keys(v), KEYS150); + w.keys(Object.keys(v), KEYS151); w.str(v.className); encT64(w, v.args); encIrType(w, v.type); @@ -3245,7 +3255,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "newPromise": { w.length(51); - w.keys(Object.keys(v), KEYS151); + w.keys(Object.keys(v), KEYS152); encIrExpr(w, v.executor); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -3253,7 +3263,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "newValue": { w.length(52); - w.keys(Object.keys(v), KEYS152); + w.keys(Object.keys(v), KEYS153); encIrExpr(w, v.callee); encT64(w, v.args); encIrType(w, v.type); @@ -3262,7 +3272,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "nullish": { w.length(53); - w.keys(Object.keys(v), KEYS153); + w.keys(Object.keys(v), KEYS154); encIrExpr(w, v.left); encIrExpr(w, v.right); encIrType(w, v.type); @@ -3271,7 +3281,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "numLit": { w.length(54); - w.keys(Object.keys(v), KEYS154); + w.keys(Object.keys(v), KEYS155); w.num(v.value); if (v.spelling === undefined) w.byte(0); else { @@ -3284,7 +3294,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "optChain": { w.length(55); - w.keys(Object.keys(v), KEYS155); + w.keys(Object.keys(v), KEYS156); w.str(v.id); encIrExpr(w, v.receiver); encIrExpr(w, v.body); @@ -3294,7 +3304,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "orDefault": { w.length(56); - w.keys(Object.keys(v), KEYS156); + w.keys(Object.keys(v), KEYS157); encIrExpr(w, v.left); encIrExpr(w, v.right); if (v.retag === undefined) w.byte(0); @@ -3308,7 +3318,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "promiseVoidWiden": { w.length(57); - w.keys(Object.keys(v), KEYS157); + w.keys(Object.keys(v), KEYS158); encIrExpr(w, v.value); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -3316,14 +3326,14 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "promiseWithResolvers": { w.length(58); - w.keys(Object.keys(v), KEYS158); + w.keys(Object.keys(v), KEYS159); encIrType(w, v.type); encSrcLoc(w, v.loc); return; } case "recordClone": { w.length(59); - w.keys(Object.keys(v), KEYS159); + w.keys(Object.keys(v), KEYS160); encIrExpr(w, v.source); encT70(w, v.overrides); encIrType(w, v.type); @@ -3332,7 +3342,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "recordGet": { w.length(60); - w.keys(Object.keys(v), KEYS160); + w.keys(Object.keys(v), KEYS161); encIrExpr(w, v.obj); w.str(v.shapeId); w.str(v.field); @@ -3342,7 +3352,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "recordHas": { w.length(61); - w.keys(Object.keys(v), KEYS161); + w.keys(Object.keys(v), KEYS162); encIrExpr(w, v.obj); w.str(v.shapeId); w.str(v.field); @@ -3352,7 +3362,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "recordKeyGet": { w.length(62); - w.keys(Object.keys(v), KEYS162); + w.keys(Object.keys(v), KEYS163); encIrExpr(w, v.obj); w.str(v.shapeId); encIrExpr(w, v.key); @@ -3372,7 +3382,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "recordLit": { w.length(63); - w.keys(Object.keys(v), KEYS163); + w.keys(Object.keys(v), KEYS164); encT71(w, v.fields); if (v.nullProto === undefined) w.byte(0); else { @@ -3385,7 +3395,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "recordNullProto": { w.length(64); - w.keys(Object.keys(v), KEYS164); + w.keys(Object.keys(v), KEYS165); encIrExpr(w, v.obj); w.str(v.shapeId); encIrType(w, v.type); @@ -3394,7 +3404,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "recordOvfHas": { w.length(65); - w.keys(Object.keys(v), KEYS165); + w.keys(Object.keys(v), KEYS166); encIrExpr(w, v.obj); w.str(v.shapeId); encIrExpr(w, v.key); @@ -3404,7 +3414,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "recordOvfKeys": { w.length(66); - w.keys(Object.keys(v), KEYS166); + w.keys(Object.keys(v), KEYS167); encIrExpr(w, v.obj); w.str(v.shapeId); encIrType(w, v.type); @@ -3413,7 +3423,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "regexIntrinsic": { w.length(67); - w.keys(Object.keys(v), KEYS167); + w.keys(Object.keys(v), KEYS168); w.str(v.method); encIrExpr(w, v.receiver); encT64(w, v.args); @@ -3423,7 +3433,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "regexLit": { w.length(68); - w.keys(Object.keys(v), KEYS168); + w.keys(Object.keys(v), KEYS169); w.str(v.pattern); w.str(v.flags); encIrType(w, v.type); @@ -3432,14 +3442,14 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "selfRef": { w.length(69); - w.keys(Object.keys(v), KEYS169); + w.keys(Object.keys(v), KEYS170); encIrType(w, v.type); encSrcLoc(w, v.loc); return; } case "seqExpr": { w.length(70); - w.keys(Object.keys(v), KEYS170); + w.keys(Object.keys(v), KEYS171); encT19(w, v.stmts); encIrExpr(w, v.result); if (v.generatorDelegate === undefined) w.byte(0); @@ -3458,7 +3468,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "setIntrinsic": { w.length(71); - w.keys(Object.keys(v), KEYS171); + w.keys(Object.keys(v), KEYS172); w.str(v.method); encIrExpr(w, v.receiver); encT64(w, v.args); @@ -3468,7 +3478,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "setNew": { w.length(72); - w.keys(Object.keys(v), KEYS172); + w.keys(Object.keys(v), KEYS173); if (v.seed === undefined) w.byte(0); else { w.byte(1); @@ -3480,7 +3490,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "strCmp": { w.length(73); - w.keys(Object.keys(v), KEYS173); + w.keys(Object.keys(v), KEYS174); w.str(v.op); encIrExpr(w, v.left); encIrExpr(w, v.right); @@ -3490,7 +3500,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "strConcat": { w.length(74); - w.keys(Object.keys(v), KEYS174); + w.keys(Object.keys(v), KEYS175); encIrExpr(w, v.left); encIrExpr(w, v.right); encIrType(w, v.type); @@ -3499,7 +3509,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "strEq": { w.length(75); - w.keys(Object.keys(v), KEYS175); + w.keys(Object.keys(v), KEYS176); w.bool(v.negated); encIrExpr(w, v.left); encIrExpr(w, v.right); @@ -3509,7 +3519,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "strIntrinsic": { w.length(76); - w.keys(Object.keys(v), KEYS176); + w.keys(Object.keys(v), KEYS177); w.str(v.method); encIrExpr(w, v.receiver); encT64(w, v.args); @@ -3519,7 +3529,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "strLit": { w.length(77); - w.keys(Object.keys(v), KEYS177); + w.keys(Object.keys(v), KEYS178); w.str(v.value); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -3527,7 +3537,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "templateStrings": { w.length(78); - w.keys(Object.keys(v), KEYS178); + w.keys(Object.keys(v), KEYS179); w.str(v.key); encT8(w, v.cooked); encIrType(w, v.type); @@ -3536,7 +3546,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "ternary": { w.length(79); - w.keys(Object.keys(v), KEYS179); + w.keys(Object.keys(v), KEYS180); encIrExpr(w, v.cond); encIrExpr(w, v.then); encIrExpr(w, v.else_); @@ -3546,7 +3556,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "toBool": { w.length(80); - w.keys(Object.keys(v), KEYS180); + w.keys(Object.keys(v), KEYS181); encIrExpr(w, v.operand); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -3554,7 +3564,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "toString": { w.length(81); - w.keys(Object.keys(v), KEYS181); + w.keys(Object.keys(v), KEYS182); encIrExpr(w, v.operand); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -3562,7 +3572,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "unary": { w.length(82); - w.keys(Object.keys(v), KEYS182); + w.keys(Object.keys(v), KEYS183); w.str(v.op); encIrExpr(w, v.operand); encIrType(w, v.type); @@ -3571,7 +3581,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "unionDisc": { w.length(83); - w.keys(Object.keys(v), KEYS183); + w.keys(Object.keys(v), KEYS184); w.str(v.unionId); w.str(v.field); encIrExpr(w, v.value); @@ -3581,7 +3591,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "unionEq": { w.length(84); - w.keys(Object.keys(v), KEYS184); + w.keys(Object.keys(v), KEYS185); w.str(v.unionId); w.bool(v.negated); w.bool(v.sameValue); @@ -3593,7 +3603,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "unionFuncEq": { w.length(85); - w.keys(Object.keys(v), KEYS185); + w.keys(Object.keys(v), KEYS186); w.str(v.unionId); w.num(v.tag); encIrExpr(w, v.union); @@ -3605,7 +3615,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "unionIsTag": { w.length(86); - w.keys(Object.keys(v), KEYS186); + w.keys(Object.keys(v), KEYS187); w.str(v.unionId); w.num(v.tag); w.bool(v.negated); @@ -3616,7 +3626,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "unionKeyGet": { w.length(87); - w.keys(Object.keys(v), KEYS187); + w.keys(Object.keys(v), KEYS188); w.str(v.unionId); encIrExpr(w, v.key); encIrExpr(w, v.value); @@ -3626,7 +3636,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "unionNarrow": { w.length(88); - w.keys(Object.keys(v), KEYS188); + w.keys(Object.keys(v), KEYS189); w.str(v.unionId); w.num(v.tag); encIrExpr(w, v.value); @@ -3636,7 +3646,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "unionRetype": { w.length(89); - w.keys(Object.keys(v), KEYS189); + w.keys(Object.keys(v), KEYS190); w.str(v.fromId); w.str(v.unionId); encT65(w, v.tags); @@ -3647,7 +3657,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "unionWrap": { w.length(90); - w.keys(Object.keys(v), KEYS190); + w.keys(Object.keys(v), KEYS191); w.str(v.unionId); w.num(v.tag); encIrExpr(w, v.value); @@ -3657,7 +3667,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "unitLit": { w.length(91); - w.keys(Object.keys(v), KEYS191); + w.keys(Object.keys(v), KEYS192); w.str(v.unit); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -3665,7 +3675,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "upcast": { w.length(92); - w.keys(Object.keys(v), KEYS192); + w.keys(Object.keys(v), KEYS193); encIrExpr(w, v.value); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -3673,7 +3683,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "varRef": { w.length(93); - w.keys(Object.keys(v), KEYS193); + w.keys(Object.keys(v), KEYS194); w.str(v.localId); encIrType(w, v.type); encSrcLoc(w, v.loc); @@ -3681,7 +3691,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "virtualCall": { w.length(94); - w.keys(Object.keys(v), KEYS194); + w.keys(Object.keys(v), KEYS195); w.str(v.className); w.str(v.method); encT64(w, v.args); @@ -3696,7 +3706,7 @@ function encIrExpr(w: IrWriter, v: IrExpr): void { } case "yieldExpr": { w.length(95); - w.keys(Object.keys(v), KEYS195); + w.keys(Object.keys(v), KEYS196); if (v.value === null) w.byte(0); else { w.byte(1); @@ -4587,7 +4597,7 @@ function decT55(r: IrReader): NonNullable[number]>): void { - w.keys(Object.keys(v), KEYS196); + w.keys(Object.keys(v), KEYS197); w.str(v.name); encIrType(w, v.type); } @@ -4599,7 +4609,7 @@ function decT56(r: IrReader): NonNullable[numb } function encT57(w: IrWriter, v: NonNullable[number]>): void { - w.keys(Object.keys(v), KEYS197); + w.keys(Object.keys(v), KEYS198); w.str(v.name); encIrType(w, v.type); } @@ -4624,7 +4634,7 @@ function decT58(r: IrReader): NonNullable { } function encT59(w: IrWriter, v: NonNullable[number]>): void { - w.keys(Object.keys(v), KEYS198); + w.keys(Object.keys(v), KEYS199); w.str(v.path); w.num(v.size); w.str(v.sha256); @@ -4644,7 +4654,7 @@ function decT59(r: IrReader): NonNullable[number]>): void { - w.keys(Object.keys(v), KEYS199); + w.keys(Object.keys(v), KEYS200); w.str(v.path); w.str(v.target); } @@ -4771,7 +4781,7 @@ function decT67(r: IrReader): NonNullable } function encT68(w: IrWriter, v: NonNullable["assertOptions"]>): void { - w.keys(Object.keys(v), KEYS200); + w.keys(Object.keys(v), KEYS201); if (v.diff === undefined) w.byte(0); else { w.byte(1); @@ -4830,7 +4840,7 @@ function decT71(r: IrReader): NonNullable } function encT72(w: IrWriter, v: NonNullable["captureCompletion"]>): void { - w.keys(Object.keys(v), KEYS201); + w.keys(Object.keys(v), KEYS202); encIrType(w, v.returnType); } function decT72(r: IrReader): NonNullable["captureCompletion"]> { @@ -4840,7 +4850,7 @@ function decT72(r: IrReader): NonNullable } function encT73(w: IrWriter, v: NonNullable["cases"]>[number]>): void { - w.keys(Object.keys(v), KEYS202); + w.keys(Object.keys(v), KEYS203); if (v.test === null) w.byte(0); else { w.byte(1); @@ -4856,7 +4866,7 @@ function decT73(r: IrReader): NonNullable[number]>, { globalId: unknown }>): void { - w.keys(Object.keys(v), KEYS203); + w.keys(Object.keys(v), KEYS204); w.str(v.field); w.str(v.globalId); } @@ -4868,7 +4878,7 @@ function decT74(r: IrReader): Extract[number]>, { wellKnown: unknown }>): void { - w.keys(Object.keys(v), KEYS204); + w.keys(Object.keys(v), KEYS205); w.str(v.field); w.str(v.wellKnown); } @@ -4880,7 +4890,7 @@ function decT75(r: IrReader): Extract[number]>): void { - w.keys(Object.keys(v), KEYS205); + w.keys(Object.keys(v), KEYS206); w.num(v.tag); encT85(w, v.values); } @@ -4892,7 +4902,7 @@ function decT76(r: IrReader): NonNullable["spreads"]>[number]>): void { - w.keys(Object.keys(v), KEYS209); + w.keys(Object.keys(v), KEYS210); w.num(v.arg); w.str(v.what); } @@ -4934,7 +4944,7 @@ function decT80(r: IrReader): NonNullable["fields"]>[number]>): void { - w.keys(Object.keys(v), KEYS210); + w.keys(Object.keys(v), KEYS211); encIrExpr(w, v.key); encIrExpr(w, v.value); } @@ -4946,7 +4956,7 @@ function decT81(r: IrReader): NonNullable["seed"]>[number]>): void { - w.keys(Object.keys(v), KEYS211); + w.keys(Object.keys(v), KEYS212); encIrExpr(w, v.key); encIrExpr(w, v.value); } @@ -4958,7 +4968,7 @@ function decT82(r: IrReader): NonNullable["overrides"]>[number]>): void { - w.keys(Object.keys(v), KEYS212); + w.keys(Object.keys(v), KEYS213); w.str(v.name); encIrExpr(w, v.value); } @@ -4970,7 +4980,7 @@ function decT83(r: IrReader): NonNullable["fields"]>[number]>): void { - w.keys(Object.keys(v), KEYS213); + w.keys(Object.keys(v), KEYS214); w.str(v.name); encIrExpr(w, v.value); if (v.overflow === undefined) w.byte(0); @@ -5001,7 +5011,7 @@ function decT85(r: IrReader): NonNullable): void { - w.keys(Object.keys(v), KEYS214); + w.keys(Object.keys(v), KEYS215); w.str(v.id); encT89(w, v.params); w.str(v.returns); @@ -5019,7 +5029,7 @@ function decT86(r: IrReader): NonNullable { } function encT87(w: IrWriter, v: NonNullable): void { - w.keys(Object.keys(v), KEYS215); + w.keys(Object.keys(v), KEYS216); w.str(v.release); encT89(w, v.params); w.str(v.returns); diff --git a/packages/compiler/src/ir/codec.test.ts b/packages/compiler/src/ir/codec.test.ts index 538bd85de..ce443af52 100644 --- a/packages/compiler/src/ir/codec.test.ts +++ b/packages/compiler/src/ir/codec.test.ts @@ -17,7 +17,7 @@ import { F64, STRING, VOID, type IrFunction, type IrModule } from "./ir.js"; import { deserializeModuleBytes } from "./serialize.js"; // Programs with many node kinds: classes, closures, async, generators, -// dynamic values, streams, records, regexes and lone surrogates. +// dynamic values, streams, records, regexes, lone surrogates and BigIntStats. const PROGRAMS = [ "1003-json-parse-unions.ts", "114-live-record-delete.ts", @@ -26,6 +26,7 @@ const PROGRAMS = [ "4900-console-instances.ts", "4991-lone-surrogate-halves.ts", "5612-readable-from-iterables.cjs", + "7018-bigint-stats.ts", "7110-als-completion-callbacks.ts", "7321-speculative-loop-bail-sites.ts", ]; diff --git a/packages/compiler/test/unit-reuse.test.ts b/packages/compiler/test/unit-reuse.test.ts index 80393cf0f..da79ced88 100644 --- a/packages/compiler/test/unit-reuse.test.ts +++ b/packages/compiler/test/unit-reuse.test.ts @@ -313,3 +313,68 @@ test("the first build into a cache writes the declaration tables and no unit rec await workspace.dispose(); } }, 300_000); + +const ALIASES = { + "values.ts": [ + "export const limit: number = 3;", + "export const unit = 2;", + "export default function scale(value: number): number {", + " return value * unit;", + "}", + "export { unit as step };", + "", + ].join("\n"), + "main.ts": [ + 'import { limit as max, step } from "./values.js";', + 'import grow from "./values.js";', + "function cap(value: number): string {", + " return `${value} of ${max}`;", + "}", + "function stepped(value: number): number {", + " return value + step;", + "}", + "function grown(value: number): number {", + " return grow(value);", + "}", + "console.log(cap(5), stepped(1), grown(4));", + "", + ].join("\n"), +}; + +test("a renamed import lowers again when its declaration's surface changes", async () => { + const workspace = await Workspace.create(ALIASES); + try { + await workspace.build(); + await workspace.edit( + "values.ts", + "export const limit: number = 3;", + 'export const limit = "3";', + ); + const applied = await workspace.build(); + expect(has(applied, "cap")).toBe(false); + expect(has(applied, "grown")).toBe(true); + } finally { + await workspace.dispose(); + } +}, 300_000); + +test("an import of a renamed export or of a default export lowers again when its declaration changes", async () => { + const workspace = await Workspace.create(ALIASES); + try { + await workspace.build(); + await workspace.edit("values.ts", "export const unit = 2;", "export const unit = 5;"); + let applied = await workspace.build(); + expect(has(applied, "stepped")).toBe(false); + expect(has(applied, "cap")).toBe(true); + await workspace.edit( + "values.ts", + "export default function scale(value: number): number {", + "export default function scale(value: number, by = 1): number {", + ); + applied = await workspace.build(); + expect(has(applied, "grown")).toBe(false); + expect(has(applied, "cap")).toBe(true); + } finally { + await workspace.dispose(); + } +}, 300_000); From 7d1f9580eeebf3b73350575bd4f9cfe8deb51967 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 04:55:52 -0500 Subject: [PATCH 43/70] Release path module members through the object core's accessors The exit-time cleanup of the path module objects swapped each member value out of the object's storage by reading v.obj.slots and v.obj.entries directly. It now goes through scr_dyn_obj_count and a new scr_dyn.h accessor, scr_dyn_obj_exchange_value_at, which stores a value at an index without reading the keys, so the cleanup still runs after the exit-time layout reset. --- packages/runtime/src/scr_dyn.h | 11 +++++++++++ packages/runtime/src/scr_node_builtin.c | 12 ++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 47f9046b6..839e654f0 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -770,6 +770,17 @@ static inline __attribute__((always_inline)) ScrDynProp scr_dyn_obj_prop_at(cons e->accessor, e->writable, e->enumerable, e->configurable}; } +/* Stores `value` (moved) as the value of data property i and returns the + * previous value (owned by the caller). Reads no keys and does not follow + * the layout pointer, so exit-time teardown can use it after the layouts + * are freed. */ +static inline ScrDyn *scr_dyn_obj_exchange_value_at(ScrDyn *o, size_t i, ScrDyn *value) { + ScrDyn **slot = o->v.obj.layout ? &o->v.obj.slots[i] : &o->v.obj.entries[i].value; + ScrDyn *previous = *slot; + *slot = value; + return previous; +} + /* The index of own property `key`, or SIZE_MAX. A linear scan in * dictionary mode: for code that looks up options and headers, not for hot * property reads. */ diff --git a/packages/runtime/src/scr_node_builtin.c b/packages/runtime/src/scr_node_builtin.c index 71b523742..d312a6e80 100644 --- a/packages/runtime/src/scr_node_builtin.c +++ b/packages/runtime/src/scr_node_builtin.c @@ -368,18 +368,14 @@ static void scr_builtin_release(void *handle) { static void scr_builtin_cleanup(void) { /* An exports object can hold itself and the other path family * (path.posix.posix, path.posix.win32): its member values are released - * first, so the objects are freed. The values are swapped out of the - * object's own storage, without reading its keys: the exit-time layout + * first, so the objects are freed. The values are swapped out without + * reading the keys (scr_dyn_obj_exchange_value_at): the exit-time layout * reset can run before this and free the layout the keys live in. */ for (ScrBuiltinModule *module = scr_builtin_modules; module; module = module->next) { if (!module->object) continue; ScrDyn *exports = module->exports; - for (size_t i = 0; i < exports->v.obj.len; i++) { - ScrDyn **slot = exports->v.obj.layout ? &exports->v.obj.slots[i] : &exports->v.obj.entries[i].value; - ScrDyn *value = *slot; - *slot = scr_dyn_retain(scr_dyn_undefined()); - scr_dyn_release(value); - } + for (size_t i = 0; i < scr_dyn_obj_count(exports); i++) + scr_dyn_release(scr_dyn_obj_exchange_value_at(exports, i, scr_dyn_retain(scr_dyn_undefined()))); } while (scr_builtin_modules) { ScrBuiltinModule *module = scr_builtin_modules; From 96739536d2d30a9ce450ddb593efc8456e639ec6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 06:22:18 -0500 Subject: [PATCH 44/70] Read fs.read's position option once and check a bigint against length |= 0 fs.read's options forms read the position option a second time when it was not a bigint, so a getter ran twice and a stateful one could move the read. The option is now read once. A bigint position was checked against the length before `length |= 0`. A fractional or infinite length made the range check build its bound from a null BigInt and crash. The check and the read now take the converted length, so such a read completes with zero or the truncated count of bytes, as in Node. --- packages/runtime/src/scr_bytes_io.c | 45 ++++++++++++------------ tests/corpus/fs-read-bigint-position.cjs | 37 +++++++++++++++++-- 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 035795619..0d3156c26 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -167,6 +167,7 @@ static ScrDyn *scr_fs_cb_arg(ScrDyn *const *args, size_t argc, size_t index) { } static bool scr_fs_bigint_position_chk(ScrBigInt *position, double length); +static double scr_fs_int32(double n); static ScrDyn *scr_fs_cb_option(const ScrDyn *options, const char *name) { if (scr_fs_dyn_absent(options)) return scr_dyn_retain(scr_dyn_undefined()); @@ -1131,30 +1132,28 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc if (ownedBuffer) length = scr_fs_cb_option_number(options, "length", length); else if (readOp && count >= 4) length = readLength; else length = scr_fs_cb_number(ARG(3), "length", length); - /* A bigint read position takes readSync's range check and reads at - * its exact value: a double would round it past 2^53. */ - ScrDyn *bigPosition = NULL; - if (readOp) { - ScrDyn *value = ownedBuffer ? scr_fs_cb_option(options, "position") - : scr_dyn_retain(count > 4 ? ARG(4) : scr_dyn_undefined()); - if (value && value->kind == SCR_DYN_BIGINT) bigPosition = value; - else scr_dyn_release(value); - } - double position = bigPosition ? -1 - : ownedBuffer ? scr_fs_cb_option_number(options, "position", -1) - : scr_fs_cb_number(ARG(4), "position", -1); + /* The position option is read once, so a getter runs once. A bigint + * read position takes readSync's range check against `length |= 0` + * and reads at its exact value: a double would round it past 2^53. */ + ScrDyn *positionValue = ownedBuffer ? scr_fs_cb_option(options, "position") + : scr_dyn_retain(ARG(4)); + bool bigPosition = readOp && positionValue && positionValue->kind == SCR_DYN_BIGINT; + double position = bigPosition || !positionValue + ? -1 + : scr_fs_cb_number(positionValue, "position", -1); if (!scr_exc_pending()) { if (bigPosition) { + double count32 = scr_fs_int32(length); if (scr_fs_read_offset_chk(offset) && - scr_fs_bigint_position_chk(bigPosition->v.bigint, length)) - result = scr_dyn_new_num(scr_fs_read_sync_at(fd, bytes, offset, length, - scr_bigint_to_i64(bigPosition->v.bigint))); + scr_fs_bigint_position_chk(positionValue->v.bigint, count32)) + result = scr_dyn_new_num(scr_fs_read_sync_at(fd, bytes, offset, count32, + scr_bigint_to_i64(positionValue->v.bigint))); } else { result = scr_dyn_new_num(readOp ? scr_fs_read_sync(fd, bytes, offset, length, position) : scr_fs_write_sync(fd, bytes, offset, length, position)); } } - scr_dyn_release(bigPosition); + scr_dyn_release(positionValue); } scr_bytes_release(bytes); /* The completion retains the original buffer, including options.buffer. */ @@ -1938,14 +1937,16 @@ static bool scr_fs_length_int32(const ScrDyn *length, double *out) { } else if (!scr_dyn_number_coerce_js(length, &n)) { return false; } - if (!isfinite(n)) { - *out = 0; - return true; - } + *out = scr_fs_int32(n); + return true; +} + +/* ToInt32 of a number: 0 for NaN and the infinities. */ +static double scr_fs_int32(double n) { + if (!isfinite(n)) return 0; double m = fmod(trunc(n), 4294967296.0); if (m < 0) m += 4294967296.0; - *out = m >= 2147483648.0 ? m - 4294967296.0 : m; - return true; + return m >= 2147483648.0 ? m - 4294967296.0 : m; } /* A bigint read position in [-1n, 2n ** 63n - 1n - BigInt(length)] diff --git a/tests/corpus/fs-read-bigint-position.cjs b/tests/corpus/fs-read-bigint-position.cjs index b54f331cf..424506491 100644 --- a/tests/corpus/fs-read-bigint-position.cjs +++ b/tests/corpus/fs-read-bigint-position.cjs @@ -1,6 +1,8 @@ // fs.read's callback forms with bigint positions read at the exact // position, including past 2^53 (beyond the file's end, so nothing is -// read), and -1n reads at the current offset. +// read), and -1n reads at the current offset. A fractional or infinite +// length takes `length |= 0` before the position check, and a position +// getter in either options form runs once. const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); @@ -23,6 +25,35 @@ step("positional", (cb) => fs.read(fd, Buffer.alloc(1), 0, 1, big, cb), () => step("buffer and options", (cb) => fs.read(fd, Buffer.alloc(2), { position: big }, cb), () => step("small", (cb) => fs.read(fd, Buffer.alloc(2), 0, 2, 1n, cb), () => step("current", (cb) => fs.read(fd, Buffer.alloc(1), 0, 1, -1n, cb), () => { - fs.closeSync(fd); - fs.rmSync(dir, { recursive: true }); + fractional(); }))))); + +function counted(values) { + const options = { calls: 0 }; + Object.defineProperty(options, "position", { + get() { + return values[options.calls++]; + }, + }); + return options; +} + +function fractional() { + step("fractional", (cb) => fs.read(fd, { buffer: Buffer.alloc(2), length: 0.5, position: 1n }, cb), () => + step("infinite", (cb) => fs.read(fd, Buffer.alloc(2), { length: Infinity, position: 1n }, cb), () => + step("truncated", (cb) => fs.read(fd, { buffer: Buffer.alloc(3), length: 2.7, position: 1n }, cb), () => { + const first = counted([0, 2]); + first.buffer = Buffer.alloc(1); + first.length = 1; + step("getter options", (cb) => fs.read(fd, first, cb), () => { + console.log("getter options calls", first.calls); + const second = counted([2, 0]); + second.length = 1; + step("getter buffer and options", (cb) => fs.read(fd, Buffer.alloc(1), second, cb), () => { + console.log("getter buffer and options calls", second.calls); + fs.closeSync(fd); + fs.rmSync(dir, { recursive: true }); + }); + }); + }))); +} From 1acdda7328bff137cb61fb86203e2a5ec02ee1e6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 06:33:43 -0500 Subject: [PATCH 45/70] Read and write cached regular files on the loop thread in fs streams On Linux, an fs stream's read or write of a regular file first tries preadv2/pwritev2 with RWF_NOWAIT on the loop thread. When the page cache holds the data, the step completes there and its completion waits for the next loop turn (scr_work_submit_done), as a pool job's completion does, so timers and I/O still run between steps. EAGAIN, a kernel or file system without RWF_NOWAIT, and other files take the pool as before. - A read the cache holds only part of finishes on the pool, so each chunk is the one a single read() returns. - A partial write completes with the bytes written; the stream submits the rest, as after any partial write. - Writes stay on the pool when RLIMIT_FSIZE is finite, so SIGXFSZ cannot reach the loop thread. scr_work_submit_done queues a completed job on a list the loop moves to the done queue at its next sleep step, which is skipped while jobs wait. Corpus 8530 covers a positioned read with a small highWaterMark, a write stream with a start position into an existing file, and an append stream over cached files, against Node. --- .../8530-fs-stream-cached-positions.ts.json | 6 ++ packages/runtime/src/scr_async.c | 89 ++++++++++++++++++- packages/runtime/src/scr_fs_stream.c | 75 +++++++++++++++- packages/runtime/src/scr_lib.h | 4 + .../corpus/8530-fs-stream-cached-positions.ts | 36 ++++++++ 5 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8530-fs-stream-cached-positions.ts.json create mode 100644 tests/corpus/8530-fs-stream-cached-positions.ts diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8530-fs-stream-cached-positions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8530-fs-stream-cached-positions.ts.json new file mode 100644 index 000000000..9fd2b0d20 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8530-fs-stream-cached-positions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8530-fs-stream-cached-positions.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 8f540d3d6..872026b97 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -3540,15 +3540,25 @@ struct ScrFsRenameOwner { ScrFsRenameOp **tail; size_t pending; ScrFsRenameWake wake; + ScrFsRenameOp *next_turn; + ScrFsRenameOp **next_turn_tail; }; static SCR_TL ScrFsRenameOwner *scr_fs_rename_owner; #define scr_fs_rename_done (scr_fs_rename_owner->done) #define scr_fs_rename_done_tail (scr_fs_rename_owner->tail) #define scr_fs_rename_pending_count (scr_fs_rename_owner->pending) +#define scr_fs_rename_next_turn (scr_fs_rename_owner->next_turn) +#define scr_fs_rename_next_turn_tail (scr_fs_rename_owner->next_turn_tail) #else static ScrFsRenameOp *scr_fs_rename_done = NULL; static ScrFsRenameOp **scr_fs_rename_done_tail = &scr_fs_rename_done; static size_t scr_fs_rename_pending_count = 0; +/* Jobs the main thread completed itself (scr_work_submit_done): they join + * the done queue at the loop's next sleep step (scr_fs_renames_promote), so + * their completions wait one loop turn as a pool job's do. Main thread + * only, so no lock. */ +static ScrFsRenameOp *scr_fs_rename_next_turn = NULL; +static ScrFsRenameOp **scr_fs_rename_next_turn_tail = &scr_fs_rename_next_turn; static ScrFsRenameWake scr_fs_rename_wake_global = SCR_FS_RENAME_WAKE_NONE; #endif static size_t scr_fs_rename_worker_count = 0; @@ -3659,6 +3669,13 @@ static void scr_fs_rename_op_release(ScrFsRenameOp *op) { #ifdef SCR_WORKERS static void scr_fs_rename_context_cleanup(void) { ScrFsRenameOwner *owner = scr_fs_rename_owner; + /* Jobs completed on this thread need no wait. */ + while (owner->next_turn) { + ScrFsRenameOp *op = owner->next_turn; + owner->next_turn = op->next; + owner->pending--; + scr_fs_rename_op_release(op); + } /* Outstanding native work may still borrow payload storage. Wait for its * publication before reclaiming it, without invoking script callbacks * during context teardown. Other script contexts keep using the pool. */ @@ -3682,6 +3699,7 @@ static ScrFsRenameOwner *scr_fs_rename_context(void) { ScrFsRenameOwner *owner = calloc(1, sizeof(*owner)); if (!owner) scr_oom(); owner->tail = &owner->done; + owner->next_turn_tail = &owner->next_turn; owner->wake = (ScrFsRenameWake)SCR_FS_RENAME_WAKE_NONE; if (scr_atexit(scr_fs_rename_context_cleanup) != 0) { free(owner); @@ -3800,9 +3818,33 @@ static void scr_fs_renames_shutdown(void) { scr_fs_rename_op_release(done); done = next; } + while (scr_fs_rename_next_turn != NULL) { + ScrFsRenameOp *next = scr_fs_rename_next_turn->next; + scr_fs_rename_op_release(scr_fs_rename_next_turn); + scr_fs_rename_next_turn = next; + } + scr_fs_rename_next_turn_tail = &scr_fs_rename_next_turn; #endif } +/* At the loop's sleep step: the jobs this thread completed during the turn + * join the done queue, so the next turn delivers them, one per checkpoint + * as pool completions are. Answers whether any joined (the sleep is then + * skipped). */ +static bool scr_fs_renames_promote(void) { +#ifdef SCR_WORKERS + if (!scr_fs_rename_owner) return false; +#endif + if (scr_fs_rename_next_turn == NULL) return false; + scr_fs_rename_lock_enter(); + *scr_fs_rename_done_tail = scr_fs_rename_next_turn; + scr_fs_rename_done_tail = scr_fs_rename_next_turn_tail; + scr_fs_rename_lock_leave(); + scr_fs_rename_next_turn = NULL; + scr_fs_rename_next_turn_tail = &scr_fs_rename_next_turn; + return true; +} + static bool scr_fs_renames_pending(void) { #ifdef SCR_WORKERS if (!scr_fs_rename_owner) return false; @@ -3966,6 +4008,7 @@ static size_t scr_fs_rename_pending_count = 0; static bool scr_fs_renames_pending(void) { return scr_fs_rename_pending_count != 0; } static int scr_fs_renames_pollfd(void) { return -1; } static bool scr_fs_renames_can_wait(void) { return false; } +static bool scr_fs_renames_promote(void) { return false; } static bool scr_fs_renames_dispatch(void) { ScrFsRenameOp *op = scr_fs_rename_done; @@ -4005,6 +4048,7 @@ static bool scr_fs_renames_pending(void) { return false; } static bool scr_fs_renames_dispatch(void) { return false; } static int scr_fs_renames_pollfd(void) { return -1; } static bool scr_fs_renames_can_wait(void) { return false; } +static bool scr_fs_renames_promote(void) { return false; } #endif void scr_fs_rename_async(ScrStr *oldpath, ScrStr *newpath, @@ -4069,6 +4113,44 @@ void scr_work_submit(void *payload, ScrWorkFn work, ScrWorkFn after, #endif } +#if defined(__wasi__) +static void scr_work_already_done(void *payload) { (void)payload; } +#endif + +void scr_work_submit_done(void *payload, ScrWorkFn after, ScrWorkFn destroy) { +#if defined(SCR_LIB) + (void)after; + destroy(payload); + scr_trap("scriptc: native worker jobs are not supported by this target\n"); +#else + ScrFsRenameOp *op = calloc(1, sizeof *op); + if (!op) scr_trap("scriptc: out of memory\n"); + op->generic = true; + op->payload = payload; + op->after = after; + op->destroy = destroy; +#if defined(__wasi__) + op->work = &scr_work_already_done; /* the dispatch runs work, then after */ + *scr_fs_rename_done_tail = op; + scr_fs_rename_done_tail = &op->next; +#else + op->ran = true; +#ifdef SCR_WORKERS + op->owner = scr_fs_rename_context(); +#endif + if (!scr_fs_rename_shutdown_registered) { + if (atexit(scr_fs_renames_shutdown) != 0) { + scr_trap("scriptc: could not register worker cleanup\n"); + } + scr_fs_rename_shutdown_registered = true; + } + *scr_fs_rename_next_turn_tail = op; + scr_fs_rename_next_turn_tail = &op->next; +#endif + scr_fs_rename_pending_count++; +#endif +} + void scr_fs_rename_thunk0(ScrClosure *cb, ScrError *err) { (void)err; ((void (*)(ScrClosure *))cb->fn)(cb); @@ -5238,11 +5320,13 @@ void scr_loop_hosted_io_state(ScrNodeIoState *st) { * end of the turn that queued them and run in the next one, as the loop * pass delivers them; Node's own fs requests take a turn or more too. */ if (st->phase == SCR_NODE_IO_CHECK) scr_loop_deferred_ready = scr_loop_deferred_count; + /* jobs this thread completed during the turn: delivered next turn */ + bool promoted = st->phase == SCR_NODE_IO_CHECK && scr_fs_renames_promote(); st->alive = kids_reffed || kids_failed || events || net || dgram || watch || ffi || stream || renames || crypto || deferred || workers; /* Queued deferred callbacks keep the poll from sleeping (their work is * done; only the delivery waits for the next turn). */ - st->ready = stream || crypto || deferred || kids_failed || + st->ready = stream || crypto || deferred || promoted || kids_failed || (scr_loop_children != NULL && scr_loop_children->ready()); #ifdef SCR_WORKERS if (scr_workers_ready_fn && scr_workers_ready_fn()) st->ready = 1; @@ -5699,6 +5783,9 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { due = now; scr_loop_deferred_ready = scr_loop_deferred_count; } + /* Jobs this thread completed during the turn (scr_work_submit_done) + * join the done queue: the next turn delivers them, no sleep. */ + if (scr_fs_renames_promote()) due = now; /* Child polling can drain a pipe into the IPC queue after its dispatch * station has run. Completed sends and disconnects can also become ready * during that turn. No fd will wake us for this userspace work: return to diff --git a/packages/runtime/src/scr_fs_stream.c b/packages/runtime/src/scr_fs_stream.c index 940068a7b..421438b94 100644 --- a/packages/runtime/src/scr_fs_stream.c +++ b/packages/runtime/src/scr_fs_stream.c @@ -19,6 +19,11 @@ #include #include #include +#if defined(__linux__) && !defined(__ANDROID__) +#include +#include +#include +#endif #define SCR_FSS_MAX_SAFE 9007199254740991.0 @@ -39,6 +44,10 @@ typedef struct ScrFsStream { ScrError *destroy_err; /* owned: the waiting _destroy's error */ void *owner; /* a FileHandle the stream reads or writes, or NULL */ const ScrFsStreamOwnerOps *owner_ops; + /* the descriptor scr_fss_try_inline last checked (0 before the first + * check) and whether its reads and writes may run on the loop thread */ + int inline_fd; + bool inline_ok; } ScrFsStream; static void scr_fss_free(void *p) { @@ -77,6 +86,7 @@ typedef struct ScrFssOp { size_t off, len; double pos; /* < 0: the current offset */ int64_t result; /* answer or -errno */ + size_t got; /* READ: bytes the loop thread already read into buf */ int retries; /* WRITE: zero-byte retries */ ScrError *carry; /* FSYNC/CLOSE: the error to pass on (+1) */ /* The AsyncLocalStorage context the step was started in (+1): its @@ -88,7 +98,13 @@ static void scr_fss_work(void *p) { ScrFssOp *op = p; switch (op->kind) { case SCR_FSS_OPEN: op->result = scr_fs_raw_open(op->path, op->oflags, op->mode); break; - case SCR_FSS_READ: op->result = scr_fs_raw_read(op->fd, op->buf, op->len, op->pos); break; + case SCR_FSS_READ: { + /* the rest of a read the loop thread began (scr_fss_try_inline) */ + int64_t r = scr_fs_raw_read(op->fd, op->buf + op->got, op->len - op->got, + op->pos >= 0 ? op->pos + (double)op->got : -1); + op->result = op->got == 0 ? r : r < 0 ? (int64_t)op->got : (int64_t)op->got + r; + break; + } case SCR_FSS_WRITE: op->result = scr_fs_raw_write(op->fd, op->chunk->data + op->off, op->len, op->pos); break; @@ -127,7 +143,64 @@ static void scr_fss_after_in_context(void *p) { scr_als_dispatch_leave(prev); } +#if defined(__linux__) && !defined(__ANDROID__) && defined(RWF_NOWAIT) +/* Without a file-size limit a regular file's write cannot raise SIGXFSZ on + * the loop thread (scr_fs_write_once's guard is then not needed). */ +static bool scr_fss_size_unlimited(void) { + static int unlimited = -1; + if (unlimited < 0) { + struct rlimit lim; + unlimited = getrlimit(RLIMIT_FSIZE, &lim) == 0 && lim.rlim_cur == RLIM_INFINITY; + } + return unlimited == 1; +} + +/* A read or write of a regular file whose data the page cache holds runs on + * the loop thread: RWF_NOWAIT answers EAGAIN instead of blocking. The + * completion still waits for the next loop turn (scr_work_submit_done), as + * a pool job's does. A read the cache holds only part of finishes on the + * pool, so the chunk is the one a single read() returns; a partial write + * completes with the bytes written, and the rest is submitted again, as + * after any partial write. Answers whether the step completed here. */ +static bool scr_fss_try_inline(ScrFssOp *op) { + if (op->kind != SCR_FSS_READ && op->kind != SCR_FSS_WRITE) return false; + ScrFsStream *f = scr_fss_of(op->s); + if (f->inline_fd != op->fd + 1) { + struct stat st; + f->inline_fd = op->fd + 1; + f->inline_ok = fstat(op->fd, &st) == 0 && S_ISREG(st.st_mode); + } + if (!f->inline_ok || op->len == 0) return false; + if (op->kind == SCR_FSS_WRITE && !scr_fss_size_unlimited()) return false; + off_t at = op->pos >= 0 ? (off_t)op->pos : -1; + ssize_t n; + if (op->kind == SCR_FSS_READ) { + struct iovec iov = {op->buf, op->len}; + n = preadv2(op->fd, &iov, 1, at, RWF_NOWAIT); + } else { + struct iovec iov = {op->chunk->data + op->off, op->len}; + n = pwritev2(op->fd, &iov, 1, at, RWF_NOWAIT); + } + if (n < 0) { + /* a kernel or file system without RWF_NOWAIT: the pool from now on */ + if (errno == EOPNOTSUPP || errno == EINVAL || errno == ENOSYS) f->inline_ok = false; + return false; + } + if (n == 0 && op->kind == SCR_FSS_WRITE) return false; + if (op->kind == SCR_FSS_READ && n > 0 && (size_t)n < op->len) { + op->got = (size_t)n; + return false; + } + op->result = n; + scr_work_submit_done(op, &scr_fss_after_in_context, &scr_fss_op_destroy); + return true; +} +#endif + static void scr_fss_submit(ScrFssOp *op) { +#if defined(__linux__) && !defined(__ANDROID__) && defined(RWF_NOWAIT) + if (scr_fss_try_inline(op)) return; +#endif scr_work_submit(op, &scr_fss_work, &scr_fss_after_in_context, &scr_fss_op_destroy); } diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index bc0184573..daaf9aae0 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -575,6 +575,10 @@ int scr_fs_raw_close(int fd); typedef void (*ScrWorkFn)(void *payload); void scr_work_submit(void *payload, ScrWorkFn work, ScrWorkFn after, ScrWorkFn destroy); +/* A job whose work already ran on the main runtime thread: `after` runs on + * the next loop turn, as a pool job's completion does, and `destroy` + * follows it. */ +void scr_work_submit_done(void *payload, ScrWorkFn after, ScrWorkFn destroy); /* fs.rename: the syscall is submitted to a native worker immediately and * its error-first callback fires on a later event-loop turn through an diff --git a/tests/corpus/8530-fs-stream-cached-positions.ts b/tests/corpus/8530-fs-stream-cached-positions.ts new file mode 100644 index 000000000..1361085d3 --- /dev/null +++ b/tests/corpus/8530-fs-stream-cached-positions.ts @@ -0,0 +1,36 @@ +// fs streams over a cached regular file: chunk sizes of a positioned read +// with a small highWaterMark, a write stream with a start position into an +// existing file, and an append stream, checked against the file contents. +import { createReadStream, createWriteStream, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const dir = mkdtempSync(join(tmpdir(), "scr-fs-cached-")); +const file = join(dir, "data.bin"); +const bytes = Buffer.alloc(100000); +for (let i = 0; i < bytes.length; i++) bytes[i] = (i * 7) & 0xff; +writeFileSync(file, bytes); + +const sizes: number[] = []; +let sum = 0; +createReadStream(file, { start: 1000, end: 50999, highWaterMark: 4096 }) + .on("data", (c: Buffer) => { + sizes.push(c.length); + for (const b of c) sum = (sum + b) % 65521; + }) + .on("end", () => { + console.log(`read chunks=${sizes.length} first=${sizes[0]} last=${sizes[sizes.length - 1]} sum=${sum}`); + const out = createWriteStream(file, { flags: "r+", start: 10 }); + for (let i = 0; i < 50; i++) out.write(Buffer.alloc(1000, i)); + out.end(() => { + const after = readFileSync(file); + console.log(`positioned len=${after.length} at10=${after[10]} at1009=${after[1009]} at1010=${after[1010]} at50009=${after[50009]} at50010=${after[50010]}`); + const app = createWriteStream(file, { flags: "a" }); + app.write("tail-1;"); + app.end("tail-2", () => { + const final = readFileSync(file); + console.log(`append len=${final.length} end=${final.subarray(final.length - 13).toString()}`); + rmSync(dir, { recursive: true }); + }); + }); + }); From ea0cdd04186a89fde6c547c68efe425762a24323 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 06:36:29 -0500 Subject: [PATCH 46/70] Match macOS objects and paths in the native install, loop poll and profile tests The nightly macOS lanes failed these tests on Linux-specific expectations: - native-install: installNativeCli resolves the installation directory, and on macOS /tmp is a link to /private/tmp. The fixture root is now resolved too. - counted-loop-emission: two tests counted polls of @scr_exc_flag, the thread's pending word that ELF executables read. Other targets poll the process-wide alert word. The tests now emit for x86_64 Linux, as the file's other ELF tests do. - native-partitions: Mach-O objects define an ltmp label at the start of each section, which the function lists now leave out. - native-profile: profile temperature appears in ELF section names (.text.hot.); Mach-O sections carry no such prefix. The section name check, and the check that recovered block counts change the objects (they move f4 into the hot section), now run on ELF objects only. --- packages/cli/test/native-install.test.ts | 7 +++++-- packages/compiler/test/counted-loop-emission.test.ts | 11 +++++++++-- packages/compiler/test/native-partitions.test.ts | 12 ++++++++++-- packages/compiler/test/native-profile.test.ts | 11 +++++++++-- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/cli/test/native-install.test.ts b/packages/cli/test/native-install.test.ts index 7a6d3ef16..369bc392b 100644 --- a/packages/cli/test/native-install.test.ts +++ b/packages/cli/test/native-install.test.ts @@ -4,6 +4,7 @@ import { mkdtempSync, mkdirSync, readFileSync, + realpathSync, renameSync, rmSync, statSync, @@ -22,8 +23,10 @@ afterEach(() => { }); function fixture() { - const root = mkdtempSync( - join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-install-"), + // installNativeCli resolves the installation directory, and on macOS /tmp + // is a link to /private/tmp, so the fixture's paths start resolved. + const root = realpathSync( + mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-install-")), ); roots.push(root); const directory = join(root, "installation"); diff --git a/packages/compiler/test/counted-loop-emission.test.ts b/packages/compiler/test/counted-loop-emission.test.ts index 41f96e006..30a139a81 100644 --- a/packages/compiler/test/counted-loop-emission.test.ts +++ b/packages/compiler/test/counted-loop-emission.test.ts @@ -85,7 +85,11 @@ test("worker programs poll counted loops over typed arrays between blocks of ite 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"))); + // ELF executables poll the thread's pending word (@scr_exc_flag); + // other targets read the process-wide alert word. + return emitLlvmModule(deserializeModule(await readFile(outPath, "utf8")), { + targetTriple: "x86_64-unknown-linux-gnu", + }); }; // Every emitted version of a function (the number and int32 entries). const bodies = (ll: string, name: string): string => @@ -254,7 +258,10 @@ test("worker programs poll no loop whose proven ranges bound its runs inside ano 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"))); + // The polls counted below read the thread's pending word of ELF executables. + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8")), { + targetTriple: "x86_64-unknown-linux-gnu", + }); const body = (name: string): string => [ ...ll.matchAll( diff --git a/packages/compiler/test/native-partitions.test.ts b/packages/compiler/test/native-partitions.test.ts index ea906eea7..52c6b42f1 100644 --- a/packages/compiler/test/native-partitions.test.ts +++ b/packages/compiler/test/native-partitions.test.ts @@ -8,6 +8,9 @@ import { afterEach, describe, expect, test } from "vitest"; import { nativeCodegenTarget, nativeHelperForTarget } from "../src/backend/targets.js"; const execFileAsync = promisify(execFile); +// Mach-O objects define a local label (ltmp0, ltmp1, ...) at the start of +// each section; it names no function. +const machoSectionLabel = /^ltmp\d+$/; const target = nativeCodegenTarget(); const helperSpec = target === null ? null : nativeHelperForTarget(target); const helper = (() => { @@ -264,7 +267,7 @@ describe.runIf(helper !== null)("LLVM program partitions", () => { return stdout .split("\n") .map((line) => /^[0-9a-fA-F]+ [Tt] _?(\w+)$/.exec(line.trim())?.[1]) - .filter((name): name is string => name !== undefined); + .filter((name): name is string => name !== undefined && !machoSectionLabel.test(name)); }; const whole = await defined( (await emit(dir, program(), { outputs: 1, optLevel: "2", name: "whole" }))[0]!, @@ -390,7 +393,12 @@ describe.runIf(helper !== null)("LLVM program partitions", () => { expect(stdout).toMatch(/ t _?heavy$/m); for (const part of parts.slice(1)) { const listed = await execFileAsync("nm", ["--defined-only", part]); - expect(listed.stdout.trim()).toBe(""); + const symbols = listed.stdout + .split("\n") + .filter( + (line) => line.trim() !== "" && !machoSectionLabel.test(line.trim().split(" ").at(-1)!), + ); + expect(symbols).toEqual([]); } // The first output is the object that a single output produces. const first = await readFile(parts[0]!); diff --git a/packages/compiler/test/native-profile.test.ts b/packages/compiler/test/native-profile.test.ts index e860d4010..7ef9034cb 100644 --- a/packages/compiler/test/native-profile.test.ts +++ b/packages/compiler/test/native-profile.test.ts @@ -8,6 +8,7 @@ import { afterEach, describe, expect, test } from "vitest"; import { nativeCodegenTarget, nativeHelperForTarget } from "../src/backend/targets.js"; const execFileAsync = promisify(execFile); +const isElf = (object: Buffer) => object.subarray(0, 4).equals(Buffer.from("\x7fELF", "latin1")); const target = nativeCodegenTarget(); const helperSpec = target === null ? null : nativeHelperForTarget(target); const helper = (() => { @@ -327,7 +328,9 @@ describe.runIf(helper !== null)("LLVM profile-guided optimization", () => { use: merged, }); const bytes = Buffer.concat(await Promise.all(objects.map((object) => readFile(object)))); - expect(bytes.includes(".text.hot.main")).toBe(true); + // ELF objects name each function's section by its profile temperature; + // Mach-O sections carry no such prefix. + if (isElf(bytes)) expect(bytes.includes(".text.hot.main")).toBe(true); const optimized = await link(dir, objects, "laidout"); expect(await run(optimized, 1)).toBe(inlinedExit(1)); expect(await run(optimized, 3)).toBe(inlinedExit(3)); @@ -497,8 +500,12 @@ describe.runIf(helper !== null)("LLVM profile-guided optimization", () => { recovered: 1, }); expect(await run(await link(dir, recovered.outputs, "recovered"), 2)).toBe(expectedExit(2)); + // The recovered counts move f4 into the hot section. ELF objects name + // it (.text.hot.); Mach-O sections carry no such prefix, so there the + // two builds are the same. const bytes = (paths: string[]) => Promise.all(paths.map((path) => readFile(path))); - expect(await bytes(recovered.outputs)).not.toEqual(await bytes(stale.outputs)); + const recoveredBytes = await bytes(recovered.outputs); + if (isElf(recoveredBytes[0]!)) expect(recoveredBytes).not.toEqual(await bytes(stale.outputs)); // The block counts key the partition cache. const before = await cacheEntries(cache); From 593673f87a041449b0fdd04a18beb0f21278646d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 06:38:22 -0500 Subject: [PATCH 47/70] Provide the libnode pack to the shape overlay npm lanes The scheduled shape overlay lanes for npm and npm-static failed: on Linux x64 CI the npm suites' Node island cases require SCRIPTC_LIBNODE (tests/harness/libnode.ts), and this job did not run .github/actions/libnode. The job now runs it for those two suites, as the other Linux x64 and macOS test lanes do. --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23936e46a..99c9702fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -464,6 +464,10 @@ jobs: name: native-linux-x64-gnu-shape-overlay path: ${{ runner.temp }} - run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz" + # The npm suites' Node island cases load Node from a libnode pack, and + # fail rather than skip in this lane when none is configured. + - uses: ./.github/actions/libnode + if: matrix.suite == 'npm' || matrix.suite == 'npm-static' - name: Run the suite with the overlay on run: | case "${{ matrix.suite }}" in From d478dc9ec5b295f163de67962574e758aa700511 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 07:05:55 -0500 Subject: [PATCH 48/70] Send a zero from an inline RWF_NOWAIT read to the pool Linux 5.9 and 5.10 answer 0 instead of EAGAIN for an RWF_NOWAIT read of data that is not cached. The inline read took that 0 as end of file and ended the ReadStream early. A zero from the inline read now goes to the pool, whose read returns the data or confirms end of file; this costs one pool round trip at the end of each file. fs-stream-nowait.test.ts preloads a preadv2 shim that answers 0 for every RWF_NOWAIT read and checks that a ReadStream still reads the whole file, as Node does. --- packages/runtime/src/scr_fs_stream.c | 5 ++- tests/fixtures/fs-stream-nowait-read.ts | 23 +++++++++++++ tests/fixtures/fs-stream-nowait-zero.c | 13 ++++++++ tests/harness/fs-stream-nowait.test.ts | 44 +++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/fs-stream-nowait-read.ts create mode 100644 tests/fixtures/fs-stream-nowait-zero.c create mode 100644 tests/harness/fs-stream-nowait.test.ts diff --git a/packages/runtime/src/scr_fs_stream.c b/packages/runtime/src/scr_fs_stream.c index 421438b94..ae53f5d4e 100644 --- a/packages/runtime/src/scr_fs_stream.c +++ b/packages/runtime/src/scr_fs_stream.c @@ -186,7 +186,10 @@ static bool scr_fss_try_inline(ScrFssOp *op) { if (errno == EOPNOTSUPP || errno == EINVAL || errno == ENOSYS) f->inline_ok = false; return false; } - if (n == 0 && op->kind == SCR_FSS_WRITE) return false; + /* A zero is not taken as EOF here: some kernels (Linux 5.9 and 5.10) + * answer 0 instead of EAGAIN for an uncached RWF_NOWAIT read, so the + * pool's read decides, as it does for a zero-byte write. */ + if (n == 0) return false; if (op->kind == SCR_FSS_READ && n > 0 && (size_t)n < op->len) { op->got = (size_t)n; return false; diff --git a/tests/fixtures/fs-stream-nowait-read.ts b/tests/fixtures/fs-stream-nowait-read.ts new file mode 100644 index 000000000..5e9fd3739 --- /dev/null +++ b/tests/fixtures/fs-stream-nowait-read.ts @@ -0,0 +1,23 @@ +// Reads a file it writes first through a ReadStream and prints the byte +// count and a checksum (fs-stream-nowait.test.ts runs it with a preadv2 +// shim). +import { createReadStream, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const dir = mkdtempSync(join(tmpdir(), "scr-nowait-")); +const file = join(dir, "data.bin"); +const data = Buffer.alloc(300000); +for (let i = 0; i < data.length; i++) data[i] = (i * 13) & 0xff; +writeFileSync(file, data); +let bytes = 0; +let sum = 0; +createReadStream(file, { highWaterMark: 16384 }) + .on("data", (c: Buffer) => { + bytes += c.length; + for (const b of c) sum = (sum + b) % 65521; + }) + .on("end", () => { + console.log(`bytes=${bytes} sum=${sum}`); + rmSync(dir, { recursive: true }); + }); diff --git a/tests/fixtures/fs-stream-nowait-zero.c b/tests/fixtures/fs-stream-nowait-zero.c new file mode 100644 index 000000000..69626997f --- /dev/null +++ b/tests/fixtures/fs-stream-nowait-zero.c @@ -0,0 +1,13 @@ +/* LD_PRELOAD shim for fs-stream-nowait.test.ts: preadv2 with RWF_NOWAIT + * answers 0, as Linux 5.9 and 5.10 do for an uncached RWF_NOWAIT read; + * other calls go to the real preadv2. */ +#define _GNU_SOURCE +#include +#include + +ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags) { + if (flags & RWF_NOWAIT) return 0; + ssize_t (*real)(int, const struct iovec *, int, off_t, int) = + (ssize_t (*)(int, const struct iovec *, int, off_t, int))dlsym(RTLD_NEXT, "preadv2"); + return real(fd, iov, iovcnt, offset, flags); +} diff --git a/tests/harness/fs-stream-nowait.test.ts b/tests/harness/fs-stream-nowait.test.ts new file mode 100644 index 000000000..a6ba9e074 --- /dev/null +++ b/tests/harness/fs-stream-nowait.test.ts @@ -0,0 +1,44 @@ +/* fs streams read a regular file with RWF_NOWAIT on the loop thread first + * (scr_fs_stream.c). Linux 5.9 and 5.10 answer 0 instead of EAGAIN for + * an uncached RWF_NOWAIT read; a preloaded preadv2 shim reproduces that + * answer, and the stream must still read the whole file, as Node does. */ +import { execFileSync, spawnSync } from "node:child_process"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +describe.skipIf(process.platform !== "linux" || sanitize)("fs stream RWF_NOWAIT reads", () => { + test("a zero from an RWF_NOWAIT read is not taken as EOF", async () => { + const outDir = join(cacheDir, "fs-stream-nowait"); + const result = await compile(join(repoRoot, "tests/fixtures/fs-stream-nowait-read.ts"), { + outPath: join(outDir, "read"), + outDir, + backend: "llvm", + }); + if (!result.ok) { + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + } + const shim = join(outDir, "nowait-zero.so"); + execFileSync("clang", [ + "-shared", + "-fPIC", + join(repoRoot, "tests/fixtures/fs-stream-nowait-zero.c"), + "-o", + shim, + "-ldl", + ]); + const expected = "bytes=300000 sum=50681\n"; + const plain = spawnSync(result.binaryPath, [], { encoding: "utf8" }); + expect(plain.stdout).toBe(expected); + const shimmed = spawnSync(result.binaryPath, [], { + encoding: "utf8", + env: { ...process.env, LD_PRELOAD: shim }, + }); + expect(shimmed.status).toBe(0); + expect(shimmed.stdout).toBe(expected); + }); +}); From b421cf6a3804aca0825d4601da238b90c130fd65 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 07:12:59 -0500 Subject: [PATCH 49/70] Pass every argument to emitter listeners that read arguments, and deliver the leak warning at exit --- .../compiler/src/backend/llvm/lib-dispatch.ts | 3 ++ .../compiler/src/backend/llvm/lib-process.ts | 6 +++ .../frontend/lowering/lower-event-emitter.ts | 27 +++++++++++- .../8640-emitter-arguments-listeners.cjs.json | 6 +++ .../8641-emitter-leak-warning-exit.cjs.json | 6 +++ .../8640-emitter-arguments-listeners.cjs | 44 +++++++++++++++++++ .../corpus/8641-emitter-leak-warning-exit.cjs | 18 ++++++++ .../8640-emitter-arguments-listeners.cjs.json | 1 + .../8641-emitter-leak-warning-exit.cjs.json | 1 + 9 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8640-emitter-arguments-listeners.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8641-emitter-leak-warning-exit.cjs.json create mode 100644 tests/corpus/8640-emitter-arguments-listeners.cjs create mode 100644 tests/corpus/8641-emitter-leak-warning-exit.cjs create mode 100644 tests/harness/corpus-costs/8640-emitter-arguments-listeners.cjs.json create mode 100644 tests/harness/corpus-costs/8641-emitter-leak-warning-exit.cjs.json diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 8378fabea..1f4c02f7b 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -214,6 +214,9 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; + // The generic emitter registrations queue the maxListeners leak warning + // for the next tick like emitter.on (lib-process.ts). + if (e.fn === "emitter.onFlex" || e.fn === "emitter.onAny") host.usesTimers = true; if (host.coroAwait && e.fn === "async.hop") { host.emitCoroAwait(null); return { name: "", type: e.type }; diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 01efd3354..d8b94fac0 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -867,6 +867,10 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr const cbT = e.args[2]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: emitter.on listener not a func"); + // A registration past the emitter's maxListeners queues the leak + // warning for the next tick (scr_events_emitter.c), which the loop + // delivers before the process exits. + host.usesTimers = true; const args = e.args.map((a) => host.emitExpr(a)); const { fn: adapterFn, shim } = host.emitterFixedAdapter(cbT); // The wrapper's capture box owns its OWN +1 of the listener; the @@ -897,6 +901,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr const adT = e.args[3]!.type; if (adT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: emitter.onDyn adapter not a func"); + host.usesTimers = true; // the leak warning's tick (emitter.on) const args = e.args.map((a) => host.emitExpr(a)); const { fn: adapterFn, shim } = host.emitterFixedAdapter(adT); host.toHeap(args[3]!); // the frame's +1 moves into the wrapper's box @@ -1046,6 +1051,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr // emitter.on/onDyn, but the DATA adapter (the three-slot payload ABI // — scr_stream_emit_data) behind the arity-3 fixed shim. const isDyn = e.fn === "emitter.onDataDyn"; + host.usesTimers = true; // the leak warning's tick (emitter.on) const cbT = e.args[isDyn ? 3 : 2]!.type; if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} listener not a func`); diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index f44a3d6d8..be9de0e5a 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -49,6 +49,7 @@ import type { Lowerer } from "./lowerer.js"; import { dynFallbackType, newFnCtx } from "./lowerer.js"; import type { ClassInfo } from "./lower-classes.js"; import { isJsSourceFile, locOf } from "../program.js"; +import { bodyReadsArguments } from "../arguments-usage.js"; import { arrayOf, BOOL, @@ -339,11 +340,13 @@ function emitterEvents(lowerer: Lowerer): Map { const cbCt = lowerer.typeOf(node.arguments.nodes[1]); const cbT = lowerer.mapTypeOf(cbCt) ?? dynFallbackType(lowerer, node.arguments.nodes[1], cbCt); + const pack = readsArgumentsPack(lowerer, node.arguments.nodes[1]); // A TypeScript listener with no parameters is typed too: it // registers without the adapter, so an untyped emit reaches it // only through the event's fixed ABI. A JavaScript function // without parameters can still read `arguments`. if ( + !pack && cbT?.kind !== "dyn" && (cbT?.kind !== "func" || (cbT.params.length === 0 && !isJsSourceFile(sf)) || @@ -358,7 +361,8 @@ function emitterEvents(lowerer: Lowerer): Map { // undefined (exactly JS's extra-parameter semantics). if ( cbT?.kind === "dyn" || - (cbT?.kind === "func" && cbT.params.some((p) => p.kind === "dyn")) + (cbT?.kind === "func" && cbT.params.some((p) => p.kind === "dyn")) || + pack ) { const sig = sigOf(name); sig.hasListener = true; @@ -714,6 +718,26 @@ function tupleOf( return sig.tuple; } +/** True when the listener is a function whose body reads `arguments`: it + * lowers with an arguments-pack ABI and takes every emitted argument, + * which the typed path's call with a prefix of the event's tuple would not + * pass, so it registers as a checked-dynamic listener. */ +function readsArgumentsPack(lowerer: Lowerer, node: ts.Expression): boolean { + let declaration: ts.Node | undefined; + try { + declaration = lowerer.checker.getCallSignatures(lowerer.typeOf(node))[0]?.declaration; + } catch { + return false; + } + return ( + declaration !== undefined && + ts.isFunctionLike(declaration) && + !ts.isArrowFunction(declaration) && + isJsSourceFile(ts.getSourceFileOfNode(declaration)) && + bodyReadsArguments(declaration) + ); +} + /** How a listener argument is dyn-flavored: "dyn" for a checked-dynamic * VALUE (a mustCall wrapper, a listener that rode an untyped binding), * "func" for a function whose parameters include dyn (the JS lane's @@ -734,6 +758,7 @@ function dynListenerFlavor(lowerer: Lowerer, node: ts.Expression): "dyn" | "func if (t?.kind === "dyn") return "dyn"; if (t?.kind === "func") { if (t.params.some((p) => p.kind === "dyn")) return "func"; + if (readsArgumentsPack(lowerer, node)) return "func"; // The inferred signature of a wrapper can omit its runtime arguments // pack. Register the checked callable produced by the factory itself. return tryLowerExpression(lowerer, node)?.type.kind === "dyn" ? "dyn" : null; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8640-emitter-arguments-listeners.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8640-emitter-arguments-listeners.cjs.json new file mode 100644 index 000000000..8a0867122 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8640-emitter-arguments-listeners.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8640-emitter-arguments-listeners.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8641-emitter-leak-warning-exit.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8641-emitter-leak-warning-exit.cjs.json new file mode 100644 index 000000000..5c4ad7909 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8641-emitter-leak-warning-exit.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8641-emitter-leak-warning-exit.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/8640-emitter-arguments-listeners.cjs b/tests/corpus/8640-emitter-arguments-listeners.cjs new file mode 100644 index 000000000..db0d5b6d6 --- /dev/null +++ b/tests/corpus/8640-emitter-arguments-listeners.cjs @@ -0,0 +1,44 @@ +// Listeners that read `arguments` receive every emitted argument, whether +// the event's other listeners and emits are typed or untyped: a function +// whose body reads `arguments` takes the whole argument list, not a +// prefix of the event's tuple. +const { EventEmitter } = require("events"); + +const e = new EventEmitter(); +const counts = []; +e.on("numArgs", function () { + counts.push(arguments.length); +}); +e.on("foo", function () { + counts.push(arguments.length); +}); +e.on("foo", function () { + const n = arguments.length; + counts.push(n); +}); +e.emit("numArgs"); +e.emit("numArgs", null); +e.emit("numArgs", null, null); +e.emit("numArgs", null, null, null, null, null); +e.emit("foo", null, null, null, null); +console.log(counts.join(",")); + +// A listener reading its arguments by index, next to one with parameters. +const f = new EventEmitter(); +f.on("pair", (a, b) => console.log("params", a, b)); +f.on("pair", function () { + console.log("arguments", arguments.length, arguments[0], arguments[1], arguments[2]); +}); +f.emit("pair", 1, "two"); +f.emit("pair", 3, "four", true); + +// A once listener reading arguments, and a listener added during an emit. +const g = new EventEmitter(); +g.once("tick", function () { + console.log("once", Array.prototype.slice.call(arguments).join("|")); + g.on("tick", function () { + console.log("later", arguments.length); + }); +}); +g.emit("tick", "a", "b", "c"); +g.emit("tick", "d"); diff --git a/tests/corpus/8641-emitter-leak-warning-exit.cjs b/tests/corpus/8641-emitter-leak-warning-exit.cjs new file mode 100644 index 000000000..1c484c481 --- /dev/null +++ b/tests/corpus/8641-emitter-leak-warning-exit.cjs @@ -0,0 +1,18 @@ +// @exit: 1 +// The MaxListenersExceededWarning of an emitter that passes its limit is +// delivered before the process exits, also in a program with no other +// asynchronous work, once per event name. The 'warning' listener prints it +// (the stderr report carries the pid, so the @exit directive keeps stderr +// out of the comparison). +process.on("warning", (w) => console.log("warning:", w.name, "|", w.message)); +const { EventEmitter } = require("events"); + +const e = new EventEmitter(); +e.setMaxListeners(1); +e.on("event-type", () => {}); +e.on("event-type", () => {}); +e.on("event-type", () => {}); +e.on("other", () => {}); +e.on("other", () => {}); +console.log("added", e.listenerCount("event-type"), e.listenerCount("other")); +process.exitCode = 1; diff --git a/tests/harness/corpus-costs/8640-emitter-arguments-listeners.cjs.json b/tests/harness/corpus-costs/8640-emitter-arguments-listeners.cjs.json new file mode 100644 index 000000000..18b8d7955 --- /dev/null +++ b/tests/harness/corpus-costs/8640-emitter-arguments-listeners.cjs.json @@ -0,0 +1 @@ +{ "plain": 8000, "san": 8100 } diff --git a/tests/harness/corpus-costs/8641-emitter-leak-warning-exit.cjs.json b/tests/harness/corpus-costs/8641-emitter-leak-warning-exit.cjs.json new file mode 100644 index 000000000..d4cd0dbbb --- /dev/null +++ b/tests/harness/corpus-costs/8641-emitter-leak-warning-exit.cjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 5000 } From 2473207af24e4f2b0e78d2b470590df8758436f9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 07:18:44 -0500 Subject: [PATCH 50/70] Name the emitter corpus programs instead of numbering them Rename 8640-emitter-arguments-listeners.cjs and 8641-emitter-leak-warning-exit.cjs to emitter-arguments-listeners.cjs and emitter-leak-warning-exit.cjs, with their cost files and ts7 baselines, as AGENTS.md asks for new corpus programs. --- .../tests/corpus/8640-emitter-arguments-listeners.cjs.json | 6 ------ .../tests/corpus/8641-emitter-leak-warning-exit.cjs.json | 6 ------ .../tests/corpus/emitter-arguments-listeners.cjs.json | 6 ++++++ .../tests/corpus/emitter-leak-warning-exit.cjs.json | 6 ++++++ ...uments-listeners.cjs => emitter-arguments-listeners.cjs} | 0 ...-leak-warning-exit.cjs => emitter-leak-warning-exit.cjs} | 0 ...teners.cjs.json => emitter-arguments-listeners.cjs.json} | 0 ...ing-exit.cjs.json => emitter-leak-warning-exit.cjs.json} | 0 8 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8640-emitter-arguments-listeners.cjs.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8641-emitter-leak-warning-exit.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-arguments-listeners.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-leak-warning-exit.cjs.json rename tests/corpus/{8640-emitter-arguments-listeners.cjs => emitter-arguments-listeners.cjs} (100%) rename tests/corpus/{8641-emitter-leak-warning-exit.cjs => emitter-leak-warning-exit.cjs} (100%) rename tests/harness/corpus-costs/{8640-emitter-arguments-listeners.cjs.json => emitter-arguments-listeners.cjs.json} (100%) rename tests/harness/corpus-costs/{8641-emitter-leak-warning-exit.cjs.json => emitter-leak-warning-exit.cjs.json} (100%) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8640-emitter-arguments-listeners.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8640-emitter-arguments-listeners.cjs.json deleted file mode 100644 index 8a0867122..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8640-emitter-arguments-listeners.cjs.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/8640-emitter-arguments-listeners.cjs" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8641-emitter-leak-warning-exit.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8641-emitter-leak-warning-exit.cjs.json deleted file mode 100644 index 5c4ad7909..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8641-emitter-leak-warning-exit.cjs.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/8641-emitter-leak-warning-exit.cjs" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-arguments-listeners.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-arguments-listeners.cjs.json new file mode 100644 index 000000000..3fdaa5121 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-arguments-listeners.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/emitter-arguments-listeners.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-leak-warning-exit.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-leak-warning-exit.cjs.json new file mode 100644 index 000000000..feb0e0908 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-leak-warning-exit.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/emitter-leak-warning-exit.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/8640-emitter-arguments-listeners.cjs b/tests/corpus/emitter-arguments-listeners.cjs similarity index 100% rename from tests/corpus/8640-emitter-arguments-listeners.cjs rename to tests/corpus/emitter-arguments-listeners.cjs diff --git a/tests/corpus/8641-emitter-leak-warning-exit.cjs b/tests/corpus/emitter-leak-warning-exit.cjs similarity index 100% rename from tests/corpus/8641-emitter-leak-warning-exit.cjs rename to tests/corpus/emitter-leak-warning-exit.cjs diff --git a/tests/harness/corpus-costs/8640-emitter-arguments-listeners.cjs.json b/tests/harness/corpus-costs/emitter-arguments-listeners.cjs.json similarity index 100% rename from tests/harness/corpus-costs/8640-emitter-arguments-listeners.cjs.json rename to tests/harness/corpus-costs/emitter-arguments-listeners.cjs.json diff --git a/tests/harness/corpus-costs/8641-emitter-leak-warning-exit.cjs.json b/tests/harness/corpus-costs/emitter-leak-warning-exit.cjs.json similarity index 100% rename from tests/harness/corpus-costs/8641-emitter-leak-warning-exit.cjs.json rename to tests/harness/corpus-costs/emitter-leak-warning-exit.cjs.json From f96217e289391d40440d20406292c8e4540646e4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 07:24:45 -0500 Subject: [PATCH 51/70] Count loop-body var initializers as writes, and skip overload signatures - A `var` with an initializer inside a loop body of its own function runs once per iteration, so the binding counts as written, and generic instances share it instead of copying an earlier iteration's value. - Only a function declaration with a body initializes its binding: overload signatures no longer make an overloaded function count as redeclared, so it is still a function that nothing reassigns (for example as a pipeline() function stage). Adds the corpus program pipeline-overloaded-stage.ts, with its corpus cost and order baseline, and a loop-body case to generic-arrow-mutable-captures.ts. --- .../src/frontend/lowering/binding-analysis.ts | 50 ++++++++++++++++--- .../corpus/pipeline-overloaded-stage.ts.json | 6 +++ .../corpus/generic-arrow-mutable-captures.ts | 15 +++++- tests/corpus/pipeline-overloaded-stage.ts | 29 +++++++++++ .../pipeline-overloaded-stage.ts.json | 1 + 5 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/pipeline-overloaded-stage.ts.json create mode 100644 tests/corpus/pipeline-overloaded-stage.ts create mode 100644 tests/harness/corpus-costs/pipeline-overloaded-stage.ts.json diff --git a/packages/compiler/src/frontend/lowering/binding-analysis.ts b/packages/compiler/src/frontend/lowering/binding-analysis.ts index a37875f25..09a3bd3c5 100644 --- a/packages/compiler/src/frontend/lowering/binding-analysis.ts +++ b/packages/compiler/src/frontend/lowering/binding-analysis.ts @@ -81,12 +81,14 @@ export function fileWritesBinding( } /** True when `sym`'s own declarations write it after it holds a value: - * a `var` in a for-in or for-of head is written on every iteration, and a - * binding declared more than once (`var x = 1; ... var x = 2;`, a - * parameter or function declaration that a `var` redeclares) is written by - * each initializing declaration that runs. Only value declarations count; - * a redeclaration without an initializer is counted too, which only - * over-approximates. */ + * a `var` in a for-in or for-of head is written on every iteration, a + * `var` with an initializer inside a loop body is written each time the + * body runs, and a binding declared more than once (`var x = 1; ... var x + * = 2;`, a parameter or function declaration that a `var` redeclares) is + * written by each initializing declaration that runs. Only value + * declarations count, and of functions only implementations: overload + * signatures have no body. A redeclaration without an initializer is + * counted too, which only over-approximates. */ function declarationsWriteBinding(sym: ts.Symbol): boolean { let values = 0; let initializing = 0; @@ -108,8 +110,15 @@ function declarationsWriteBinding(sym: ts.Symbol): boolean { ) return true; values++; - if (root.initializer !== undefined) initializing++; - } else if (ts.isParameter(root) || ts.isFunctionDeclaration(d) || ts.isClassDeclaration(d)) { + if (root.initializer !== undefined) { + if (insideLoopBody(root)) return true; + initializing++; + } + } else if ( + ts.isParameter(root) || + (ts.isFunctionDeclaration(d) && d.body !== undefined) || + ts.isClassDeclaration(d) + ) { values++; initializing++; } @@ -117,6 +126,31 @@ function declarationsWriteBinding(sym: ts.Symbol): boolean { return values > 1 && initializing > 0; } +/** Whether a declaration sits in the body of a loop of its own function, + * where it runs once per iteration. */ +function insideLoopBody(node: ts.Node): boolean { + let child = node; + for (let parent = node.parent; parent !== undefined; parent = parent.parent) { + if ( + ts.isFunctionLike(parent) || + ts.isClassStaticBlockDeclaration(parent) || + ts.isSourceFile(parent) + ) + return false; + if ( + (ts.isForStatement(parent) || + ts.isForInStatement(parent) || + ts.isForOfStatement(parent) || + ts.isWhileStatement(parent) || + ts.isDoStatement(parent)) && + parent.statement === child + ) + return true; + child = parent; + } + return false; +} + /** Strips the value-preserving wrappers off an expression: parens, * non-null assertions, `as`/`satisfies`/angle-bracket casts. What * remains is the expression that actually evaluates. */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/pipeline-overloaded-stage.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/pipeline-overloaded-stage.ts.json new file mode 100644 index 000000000..08016e9bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/pipeline-overloaded-stage.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/pipeline-overloaded-stage.ts" + ], + "diags": [] +} diff --git a/tests/corpus/generic-arrow-mutable-captures.ts b/tests/corpus/generic-arrow-mutable-captures.ts index 64d77ee2b..58e104b42 100644 --- a/tests/corpus/generic-arrow-mutable-captures.ts +++ b/tests/corpus/generic-arrow-mutable-captures.ts @@ -98,11 +98,24 @@ function loopVar(): string { return readers.map((reader) => reader()).join(" "); } +// A `var` with an initializer in a loop body is one binding the body writes +// on every iteration: closures returned in earlier iterations read the last +// value. (The binding is declared with the type its hoisted storage has.) +function loopInitializer(): string { + const readers: Array<() => string> = []; + for (let i = 0; i < 3; i++) { + var step: string | undefined = `s${i}`; + const make = (value: T): (() => string) => () => `${step}${String(value)}`; + readers.push(make(i)); + } + return readers.map((reader) => reader()).join(" "); +} + async function main(): Promise { console.log((await makeRunner("p").run(2)).join(" ")); console.log(accumulate(5)); console.log(snapshots()); console.log(depth()); - console.log(redeclared(), loopVar()); + console.log(redeclared(), loopVar(), loopInitializer()); } main(); diff --git a/tests/corpus/pipeline-overloaded-stage.ts b/tests/corpus/pipeline-overloaded-stage.ts new file mode 100644 index 000000000..43502c2cc --- /dev/null +++ b/tests/corpus/pipeline-overloaded-stage.ts @@ -0,0 +1,29 @@ +// pipeline() with a function stage that has overload signatures: the +// signatures have no body, so the stage is still one function declaration +// that nothing reassigns, and the pipeline lowers it like any other stage. +import { pipeline } from "node:stream/promises"; +import { Readable, Writable } from "node:stream"; + +function shout(source: AsyncIterable): AsyncGenerator; +function shout(source: AsyncIterable): AsyncGenerator; +async function* shout(source: AsyncIterable): AsyncGenerator { + for await (const chunk of source) yield `${String(chunk).toUpperCase()}!`; +} + +async function main(): Promise { + const seen: string[] = []; + await pipeline( + Readable.from(["a", "b", "c"]), + shout, + new Writable({ + objectMode: true, + write(chunk: unknown, _encoding, callback) { + seen.push(String(chunk)); + callback(); + }, + }), + ); + console.log(seen.join(" ")); +} + +void main(); diff --git a/tests/harness/corpus-costs/pipeline-overloaded-stage.ts.json b/tests/harness/corpus-costs/pipeline-overloaded-stage.ts.json new file mode 100644 index 000000000..5cce6d4a6 --- /dev/null +++ b/tests/harness/corpus-costs/pipeline-overloaded-stage.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 6000 } From a4c19ae4b4ba4b38749713635b6b498d62759f28 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 13:04:21 -0700 Subject: [PATCH 52/70] Host the bare compareArray from the upstream assert.js harness harness/assert.js defines `compareArray(a, b)` beside `assert`: a plain function that RETURNS a boolean, whose throwing counterpart is `assert.compareArray` (already hosted). The adapter now injects the bare name into every prepared test and implements the upstream comparison: read both `.length`s, so a nullish operand throws the same TypeError, return false on a length mismatch, otherwise compare elements by SameValue. That also admits the compareArray.js include when the only use is a direct call. A value reference or property read stays excluded, because it would need the upstream function object and its `format` expando. Co-Authored-By: Claude Opus 5.5 --- tests/harness/test262-runner.test.ts | 12 ++++++++++-- tests/harness/test262.test.ts | 20 ++++++++++++++++++++ tests/test262/harness.ts | 22 ++++++++++++++++++++++ tests/test262/support.mjs | 8 +++++++- 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/tests/harness/test262-runner.test.ts b/tests/harness/test262-runner.test.ts index 3ddf3ca84..3873d1c34 100644 --- a/tests/harness/test262-runner.test.ts +++ b/tests/harness/test262-runner.test.ts @@ -250,14 +250,22 @@ test("built-in error assertions are admitted without permitting constructor alia expect(exclusion(alias, metadata(alias), "strict")).toBe("harness:assert-surface"); }); -test("the compareArray include only admits the implemented assertion form", () => { +test("the compareArray include admits the assertion and the bare call form only", () => { const accepted = source( "includes: [compareArray.js]", "assert.compareArray([1, NaN], [1, NaN]);", ); expect(exclusion(accepted, metadata(accepted), "strict")).toBeUndefined(); const globalHelper = source("includes: [compareArray.js]", "compareArray([1], [1]);"); - expect(exclusion(globalHelper, metadata(globalHelper), "strict")).toBe( + expect(exclusion(globalHelper, metadata(globalHelper), "strict")).toBeUndefined(); + // assert.js supplies compareArray, so a test needs no include to call it. + const withoutInclude = source("description: intl", "assert(!compareArray([1], [2]));"); + expect(exclusion(withoutInclude, metadata(withoutInclude), "strict")).toBeUndefined(); + const valueReference = source( + "includes: [compareArray.js]", + "const compare = compareArray; assert(compare([1], [1]));", + ); + expect(exclusion(valueReference, metadata(valueReference), "strict")).toBe( "harness:compareArray-surface", ); }); diff --git a/tests/harness/test262.test.ts b/tests/harness/test262.test.ts index 678685103..948c016bb 100644 --- a/tests/harness/test262.test.ts +++ b/tests/harness/test262.test.ts @@ -189,6 +189,26 @@ assert.notSameValue(1, "1"); source: "assert.compareArray([1, 2], [1, 3]);", }, { name: "array signed zero mismatch", status: "fail", source: "assert.compareArray([0], [-0]);" }, + { + name: "bare compareArray returns true for matching contents", + status: "pass", + source: "assert(compareArray([1, NaN, -0], [1, NaN, -0]));", + }, + { + name: "bare compareArray returns false for a length or element mismatch", + status: "pass", + source: "assert(!compareArray([1], [1, 2])); assert(!compareArray([1, 2], [1, 3]));", + }, + { + name: "bare compareArray compares operands without a length as equal", + status: "pass", + source: "assert(compareArray(1, 2)); assert(compareArray({}, {}));", + }, + { + name: "bare compareArray does not throw on a mismatch", + status: "fail", + source: "assert(compareArray([1], [2]));", + }, { name: "assert requires true, not truthiness", status: "fail", source: "assert(1);" }, { name: "exact built-in exception", diff --git a/tests/test262/harness.ts b/tests/test262/harness.ts index dce1be99c..8a0f7bd1a 100644 --- a/tests/test262/harness.ts +++ b/tests/test262/harness.ts @@ -60,6 +60,28 @@ export function assert(value: unknown, message?: unknown): void { if (value !== true) throw new Test262Error(text(message, "Expected true")); } +function lengthOf(value: unknown): unknown { + return (value as { length?: unknown }).length; +} + +function elementAt(value: unknown, index: number): unknown { + return (value as Record)[index]; +} + +/** The bare `compareArray(a, b)` the upstream `harness/assert.js` defines + * alongside `assert`. It RETURNS a boolean (the throwing assertion is + * `assert.compareArray`), and it has no shape checks of its own: it reads + * `.length` off both operands, so a null operand throws TypeError there and + * here, and two operands without a length of their own compare equal. */ +export function compareArray(actual: unknown, expected: unknown): boolean { + const length = lengthOf(actual); + if (lengthOf(expected) !== length) return false; + for (let i = 0; typeof length === "number" && i < length; i++) { + if (!same(elementAt(expected, i), elementAt(actual, i))) return false; + } + return true; +} + export namespace assert { export function sameValue(actual: unknown, expected: unknown, message?: unknown): void { if (!same(actual, expected)) throw new Test262Error(text(message, "Expected SameValue")); diff --git a/tests/test262/support.mjs b/tests/test262/support.mjs index 52739f91a..ecb5d45c4 100644 --- a/tests/test262/support.mjs +++ b/tests/test262/support.mjs @@ -169,7 +169,11 @@ export function exclusion(source, meta, variant) { // Supported assertion calls; aliases, mutations, and reflection stay out. } else reason = "harness:assert-surface"; } else if (meta.includes.includes("compareArray.js") && ts.isIdentifier(node) && node.text === "compareArray" && + !(ts.isCallExpression(node.parent) && node.parent.expression === node) && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node && ts.isIdentifier(node.parent.expression) && node.parent.expression.text === "assert")) { + // assert.js defines the bare compareArray; only its direct call form is + // hosted (a value reference or property read would need the upstream + // function object and its format expando). reason = "harness:compareArray-surface"; } else if (ts.isIdentifier(node) && node.text === "Test262Error") { if (!ts.isNewExpression(node.parent) || node.parent.expression !== node) reason = "harness:Test262Error-surface"; @@ -192,7 +196,9 @@ export function exclusion(source, meta, variant) { } export function prepare(source, asyncTest = false, marker = completion, variant = "strict", includes = []) { - const names = asyncTest ? "assert, Test262Error, $DONE" : "assert, Test262Error"; + const names = asyncTest + ? "assert, compareArray, Test262Error, $DONE" + : "assert, compareArray, Test262Error"; const end = asyncTest ? "" : `;console.log(${JSON.stringify(marker)});\n`; const helpers = (includes.includes("propertyHelper.js") ? `${propertyHelperSource}\n` : "") + From c6db09c8e4f1c3d6bb4e15f372e2758f52af13b8 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 13:07:02 -0700 Subject: [PATCH 53/70] Relax the async-iterator protocol demand in untyped JavaScript TS2504 demands a `[Symbol.asyncIterator]()` method that returns an async iterator. In untyped JavaScript, whether a value is async-iterable is a runtime fact of the value rather than a static one, exactly like the sync demand (2488) relaxed just above. `for await` over a non-async-iterable and `yield*` in an async generator both lower through the runtime protocol openers, which read the method, call it, and throw the same TypeError Node throws when it is missing, uncallable, or returns a non-object. Only diagnostics change, so only variants that previously held a 2504 refusal can move: the relaxed set is consulted only when JavaScript strictness is suppressed, and 2504 is the sole code this adds. The sync sibling 2505 stays enforced on the typed lanes, and TypeScript sources keep 2504 because the relaxation applies to `.js`/`.mjs`/`.cjs`/`.jsx` only. Corpus 8423-async-iterator-protocol-shapes.js pins the seven protocol shapes Node rejects and the two it accepts. Co-Authored-By: Claude Opus 5.5 --- .../frontend/inferred-js-diagnostics.test.ts | 20 ++++ packages/compiler/src/frontend/tsc-codes.ts | 10 ++ .../8423-async-iterator-protocol-shapes.js | 96 +++++++++++++++++++ ...423-async-iterator-protocol-shapes.js.json | 1 + 4 files changed, 127 insertions(+) create mode 100644 tests/corpus/8423-async-iterator-protocol-shapes.js create mode 100644 tests/harness/corpus-costs/8423-async-iterator-protocol-shapes.js.json diff --git a/packages/compiler/src/frontend/inferred-js-diagnostics.test.ts b/packages/compiler/src/frontend/inferred-js-diagnostics.test.ts index 96d285f03..49eab16b5 100644 --- a/packages/compiler/src/frontend/inferred-js-diagnostics.test.ts +++ b/packages/compiler/src/frontend/inferred-js-diagnostics.test.ts @@ -94,3 +94,23 @@ test.for([ invalid, ); }); + +test("JavaScript async generators may delegate over a runtime async-iterator shape", () => { + expect( + diagnostics( + `export async function* delegated() { yield* { [Symbol.asyncIterator]: 42 }; } +export async function* looped() { for await (const value of { [Symbol.asyncIterator]() { return "text"; } }) console.dir(value); }`, + 'import { delegated, looped } from "./library.js"; delegated(); looped();', + ), + ).toEqual([]); +}); + +test("TypeScript keeps the async-iterator demand", () => { + expect( + diagnostics( + "export async function* delegated() { yield* { [Symbol.asyncIterator]: 42 }; }", + 'import { delegated } from "./library.js"; delegated();', + "ts", + ).some((diagnostic) => diagnostic.message.includes("Symbol.asyncIterator")), + ).toBe(true); +}); diff --git a/packages/compiler/src/frontend/tsc-codes.ts b/packages/compiler/src/frontend/tsc-codes.ts index a30945be7..1f9ce923e 100644 --- a/packages/compiler/src/frontend/tsc-codes.ts +++ b/packages/compiler/src/frontend/tsc-codes.ts @@ -109,6 +109,16 @@ export const JS_RELAXED_TSC_CODES: ReadonlySet = new Set([ 2488, 2721, 2722, + // the async-iterator protocol demand (2504: "must have a + // '[Symbol.asyncIterator]()' method that returns an async iterator"). + // Whether a value is async-iterable is a runtime fact of untyped JS, exactly + // like the sync demand (2488) relaxed above: `for await` over a non-iterable + // value and `yield*` in an async generator both lower through the runtime + // protocol openers, which read the method, call it, and throw TypeError at + // the site when it is missing, uncallable, or returns a non-object — the + // same protocol failure Node reports. 2505 is the sync sibling on the typed + // lowering lanes and stays enforced. + 2504, // an UNUSED @ts-expect-error: the directive annotated an error visible // under the project's OWN tsconfig world — scriptc's world deliberately // differs (adopted options, hidden declaration twins), and an unused diff --git a/tests/corpus/8423-async-iterator-protocol-shapes.js b/tests/corpus/8423-async-iterator-protocol-shapes.js new file mode 100644 index 000000000..f27d3acb6 --- /dev/null +++ b/tests/corpus/8423-async-iterator-protocol-shapes.js @@ -0,0 +1,96 @@ +// Async iteration over values whose [Symbol.asyncIterator] shape only exists +// at run time. JavaScript puts no static demand on the receiver: the protocol +// reads the method, calls it, and throws TypeError when the method is missing, +// is not callable, or returns a non-object. The checker must not refuse the +// source for the shapes inference cannot prove. +function report(label, fn) { + return Promise.resolve() + .then(fn) + .then( + (value) => console.log(label, "ok", value), + (error) => console.log(label, error instanceof TypeError, error.name), + ); +} + +async function* delegate(value) { + yield* value; +} + +async function drain(value) { + const values = []; + for await (const item of value) values.push(item); + return values.join(","); +} + +async function main() { + // A well-formed async iterator written inline still delegates. + await report("inline", () => + drain({ + async *[Symbol.asyncIterator]() { + yield "a"; + yield "b"; + }, + }), + ); + + // The method is a data property, not a function. + await report("method-not-callable", () => drain({ [Symbol.asyncIterator]: 42 })); + + // The method returns a number instead of an iterator object. + await report("result-not-object", () => drain({ [Symbol.asyncIterator]() { return 42; } })); + + // The returned iterator's next is not callable. + await report("next-not-callable", () => + drain({ + [Symbol.asyncIterator]() { + return { next: "nope" }; + }, + }), + ); + + // No async iterator at all and no sync fallback either. + await report("no-protocol", () => drain({ value: 1 })); + + // yield* delegates through the same protocol, so it throws the same way. + await report("yield-star-not-callable", async () => { + let seen = ""; + for await (const value of delegate({ [Symbol.asyncIterator]: true })) seen += value; + return seen; + }); + + await report("yield-star-result-not-object", async () => { + let seen = ""; + for await (const value of delegate({ [Symbol.asyncIterator]() { return "text"; } })) seen += value; + return seen; + }); + + // A non-callable async iterator wins over a working sync iterator: the + // protocol only falls back when the method is undefined. + await report("async-shadows-sync", () => + drain({ + [Symbol.asyncIterator]: 0, + *[Symbol.iterator]() { + yield "never"; + }, + }), + ); + + // A value with only a sync iterable iterates through the sync fallback. + await report("sync-only", () => + drain({ + *[Symbol.iterator]() { + yield "s1"; + yield "s2"; + }, + }), + ); + + // yield* over arrays and strings keeps working. + await report("yield-star-array", async () => { + let seen = ""; + for await (const value of delegate([1, 2])) seen += value; + return seen; + }); +} + +main(); diff --git a/tests/harness/corpus-costs/8423-async-iterator-protocol-shapes.js.json b/tests/harness/corpus-costs/8423-async-iterator-protocol-shapes.js.json new file mode 100644 index 000000000..35e276558 --- /dev/null +++ b/tests/harness/corpus-costs/8423-async-iterator-protocol-shapes.js.json @@ -0,0 +1 @@ +{ "plain": 8004 } From b234e06f2055a7daeeb2b26336c94614652ff006 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 15:10:30 -0700 Subject: [PATCH 54/70] Enforce the ECMAScript early errors the checker does not model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Static semantics reject a program before it runs. Until now the compiler borrowed that rejection from whatever tsc happened to report, and Test262's negative-parse suites show where the borrowing stops: programs the spec makes SyntaxErrors compile and run. This adds the two largest families the grab-bag misses (plan §7.7 D-4, option c), each under its own preflight code, so a checker relaxation can never remove the only rejection a parse-negative program had: SC0005 a statement list declares one lexical name twice (`{ class f {} async function f() {} }`) SC0006 a lexical name is `var`-declared in the same statement list (`switch (1) { case 1: let x; case 2: var x; }`) SC0007 a declaration where the grammar admits only a Statement (`while (false) function f() {}`, `l: class c {}`, `if (x) async function f() {}`) frontend/early-errors.ts is a preflight walk over JavaScript sources, so a rejected program fails where a tsc parse error fails, before any lowering. A case block is one lexical scope across its clauses; a list's VarDeclaredNames reach into nested blocks but never into a function or a class body; and Annex B's allowances are honored exactly: two plain function declarations may repeat in a sloppy block (B.3.3.1), and a plain function may be an if/else body or a labelled statement in sloppy code (B.3.2) — an async function, a generator or a class never gets either allowance, and neither does any function as a loop or 'with' body. Strictness comes from Node's own module classification (isNodeEsmFile7), not from the checker's external-module bit: moduleDetection=force gives every file module scope, CommonJS included, and taking that bit as strictness cost the Annex B exemption on the annexB function-redeclaration tests. Typed sources are untouched — their checker diagnostics (TS2451 and friends) remain the gate, and re-reporting a collision under a second code would only duplicate them. tests/test262/support.mjs counts one of these codes at a location inside the test's own source as the parse-negative match, the same standard the helper already applies to tsc's early errors. The diagnostics fixtures pin both families, including the strict half of SC0007 where Node refuses the two Annex B shapes as well. The pass only reads the AST and only refuses programs, so no typed program's IR changes. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/coverage/surface-manifest.ts | 2 +- packages/compiler/src/diagnostics/codes.ts | 3 + .../compiler/src/diagnostics/diagnostic.ts | 8 +- .../compiler/src/frontend/early-errors.ts | 364 ++++++++++++++++++ packages/compiler/src/frontend/program.ts | 6 + packages/compiler/src/frontend/tsc-codes.ts | 15 + packages/compiler/surface-manifest.json | 2 +- tests/diagnostics/js-block-redeclaration.js | 18 + .../js-single-statement-declaration.js | 10 + .../diagnostics/js-single-statement-strict.js | 8 + .../js-block-redeclaration.js.txt | 41 ++ .../js-single-statement-declaration.js.txt | 48 +++ .../js-single-statement-strict.js.txt | 20 + tests/test262/support.mjs | 21 + 14 files changed, 562 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/src/frontend/early-errors.ts create mode 100644 tests/diagnostics/js-block-redeclaration.js create mode 100644 tests/diagnostics/js-single-statement-declaration.js create mode 100644 tests/diagnostics/js-single-statement-strict.js create mode 100644 tests/harness/__snapshots__/js-block-redeclaration.js.txt create mode 100644 tests/harness/__snapshots__/js-single-statement-declaration.js.txt create mode 100644 tests/harness/__snapshots__/js-single-statement-strict.js.txt diff --git a/packages/compiler/src/coverage/surface-manifest.ts b/packages/compiler/src/coverage/surface-manifest.ts index b87a8065d..d0cff1eb2 100644 --- a/packages/compiler/src/coverage/surface-manifest.ts +++ b/packages/compiler/src/coverage/surface-manifest.ts @@ -98,7 +98,7 @@ function coverageNotes(): string[] { "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.", + "Process-level diagnostic codes are not surface entries: SC0001-SC0007 are preflight gates (SC0005-SC0007 are the ECMAScript early errors), 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.", ]; diff --git a/packages/compiler/src/diagnostics/codes.ts b/packages/compiler/src/diagnostics/codes.ts index 1e35903cd..bd597cb01 100644 --- a/packages/compiler/src/diagnostics/codes.ts +++ b/packages/compiler/src/diagnostics/codes.ts @@ -19,6 +19,9 @@ export const DIAGNOSTIC_CODES = { SC0002: "project configuration incompatible with scriptc", SC0003: "malformed imported JSON module", SC0004: "TypeScript checker panic", + SC0005: "duplicate lexical declaration in a block or switch statement list", + SC0006: "lexical declaration colliding with a var in the same block", + SC0007: "declaration in a single-statement position", SC1010: "package imports outside the supported set", SC1012: "default exports/imports (residual edges)", SC1013: "namespace imports (residual edges)", diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 0cabb6ddb..de377441f 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -7,8 +7,12 @@ * Code bands: * SC0xxx preflight gates: TypeScript errors passed through (SC0001), * project-config incompatibilities (SC0002), malformed imported - * JSON modules (SC0003), and upstream tsgo checker panics - * surfaced as source-anchored diagnostics (SC0004) + * JSON modules (SC0003), upstream tsgo checker panics surfaced + * as source-anchored diagnostics (SC0004), and the ECMAScript + * early errors the checker does not model: a duplicated or + * `var`-colliding lexical declaration in a block or switch + * statement list (SC0005/SC0006) and a declaration where the + * grammar admits only a Statement (SC0007) * SC1xxx supported-TypeScript-not-yet: valid TS outside the current subset * SC2xxx scriptc type rules (types we cannot compile yet) * SC3xxx backend/target coverage: the program is valid, but the selected diff --git a/packages/compiler/src/frontend/early-errors.ts b/packages/compiler/src/frontend/early-errors.ts new file mode 100644 index 000000000..37dd71b48 --- /dev/null +++ b/packages/compiler/src/frontend/early-errors.ts @@ -0,0 +1,364 @@ +/* ECMAScript early errors the TypeScript checker does not model. + * + * Static semantics reject a program before it runs, and until this pass + * existed scriptc borrowed that rejection from whatever tsc happened to + * report (`Let x` declarations at an odd position, a redeclared block name + * TypeScript refuses). Test262's negative-parse suites show where the + * borrowing stops: 1,198 programs the spec makes SyntaxErrors compile — the + * constructs below are the ones the corpus covers, in descending variant + * count (plan §7.7 D-4 option (c)). + * + * One code per construct, so C1's checker relaxation stays safe: relaxing a + * tsc code that doubles as an early error removes the ONLY rejection some + * parse-negative test had, which is why these codes live beside + * JS_RELAXED_TSC_CODES in tsc-codes.ts and never depend on it. + * + * Scope: JavaScript sources only. A TypeScript source is type-checked, and + * its checker diagnostics (TS2451 and friends) are the gate; re-reporting + * the same collision under a second code would only duplicate them. + * + * The pass runs at preflight, before any lowering, and reports through + * SC0xxx — the preflight band — so a rejected program fails the same way a + * tsc error does. */ + +import * as ts from "./ts7/adapter.js"; +import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; +import type { SrcLoc } from "../ir/ir.js"; +import { isJsSourceFileName } from "./tsc-codes.js"; + +/** The early-error codes. SC0005 and SC0006 are the Block/CaseBlock static + * semantics (a statement list may not declare one lexical name twice, and a + * lexical name may not be `var`-declared in the same list); SC0007 is a + * declaration where the grammar admits only a Statement. */ +export const EARLY_ERROR_CODES = ["SC0005", "SC0006", "SC0007"] as const; + +const NO_DUPLICATE_LEXICAL = "SC0005"; +const LEXICAL_VAR_COLLISION = "SC0006"; +const NOT_A_STATEMENT = "SC0007"; + +/** Whether the code around a node is strict — the one input this pass needs + * from outside: Node's own module classification (program.ts's + * isNodeEsmFile7: fixed extensions, package type, then syntax markers). The + * checker's external-module bit cannot answer it, because + * moduleDetection=force gives every file module scope, CommonJS included. */ +export type IsStrictCode = (sf: ts.SourceFile, node: ts.Node) => boolean; + +export function earlyErrorDiagnostics7( + files: readonly ts.SourceFile[], + isModuleCode: (sf: ts.SourceFile) => boolean, +): ScrDiagnostic[] { + const diags: ScrDiagnostic[] = []; + const isStrict: IsStrictCode = (sf, node) => isStrictCode(sf, node, isModuleCode); + for (const sf of files) { + if (!isJsSourceFileName(sf.fileName)) continue; + // Iterative walk: a deeply nested program must not overflow the stack + // before the lowering's own nesting fence runs. + const stack: ts.Node[] = [sf]; + while (stack.length > 0) { + const node = stack.pop()!; + checkStatementListScope(sf, node, diags, isStrict); + checkStatementSlot(node, diags, isStrict); + ts.forEachChild(node, (child) => { + stack.push(child); + return undefined; + }); + } + } + return diags; +} + +function locOf(node: ts.Node): SrcLoc { + const sf = ts.getSourceFileOfNode(node); + return { file: sf.fileName, start: node.getStart(sf), end: node.end }; +} + +function report(diags: ScrDiagnostic[], code: EarlyErrorCode, node: ts.Node, message: string) { + diags.push({ code, message, loc: locOf(node) }); +} + +type EarlyErrorCode = (typeof EARLY_ERROR_CODES)[number]; + +/* ── SC0005 / SC0006: Block and CaseBlock static semantics ─────────────── + * + * Block : { StatementList } + * CaseBlock : { CaseClauses_opt DefaultClause CaseClauses_opt } + * + * * It is a Syntax Error if the LexicallyDeclaredNames of StatementList + * contains any duplicate entries. + * * It is a Syntax Error if any element of the LexicallyDeclaredNames of + * StatementList also occurs in the VarDeclaredNames of StatementList. + * + * A case block is ONE lexical scope across its clauses, and VarDeclaredNames + * reach into nested blocks (a `var` anywhere below the block belongs to it) + * but never into a function or class body. The duplicate rule's one carve-out + * is Annex B's B.3.3.1: two plain function declarations of the same name in + * the same block are legal in non-strict code. */ + +function checkStatementListScope( + sf: ts.SourceFile, + node: ts.Node, + diags: ScrDiagnostic[], + isStrict: IsStrictCode, +): void { + let statements: readonly ts.Statement[] | null = null; + if (ts.isBlock(node)) statements = node.statements.nodes; + else if (ts.isCaseBlock(node)) { + statements = node.clauses.nodes.flatMap((clause) => clause.statements.nodes); + } + if (statements === null) return; + if (statements.length < 2) return; + + const lexical = new Map(); + for (const stmt of statements) { + for (const [name, decl] of lexicalDeclarationsOf(stmt)) { + const list = lexical.get(name); + if (list === undefined) lexical.set(name, [decl]); + else list.push(decl); + } + } + if (lexical.size === 0) return; + + const varNames = new Set(); + for (const stmt of statements) collectVarDeclaredNames(stmt, varNames); + + for (const [name, decls] of lexical) { + if (varNames.has(name)) { + report( + diags, + LEXICAL_VAR_COLLISION, + decls[0]!, + `the lexical declaration '${name}' collides with a 'var' declaration of the same name in this block`, + ); + continue; + } + if (decls.length < 2) continue; + // Annex B B.3.3.1: duplicate plain function declarations are the web's + // legacy, and only non-strict code keeps them. + if (decls.every((d) => ts.isFunctionDeclaration(d) && !isEnhancedFunction(d))) { + if (!isStrict(sf, decls[0]!)) continue; + } + report( + diags, + NO_DUPLICATE_LEXICAL, + decls[1]!, + `the declaration '${name}' is already declared in this block's statement list`, + ); + } +} + +/** The names a DIRECT statement of a statement list declares lexically + * (let/const/using, class, and every function declaration form), paired with + * the node to point the diagnostic at. */ +function lexicalDeclarationsOf(stmt: ts.Statement): Array<[string, ts.Node]> { + const names: Array<[string, ts.Node]> = []; + if (ts.isVariableStatement(stmt)) { + if ((stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0) return names; + for (const decl of stmt.declarationList.declarations.nodes) + collectBindingNames(decl.name, names); + return names; + } + if (ts.isClassDeclaration(stmt) || ts.isFunctionDeclaration(stmt)) { + const name = stmt.name; + if (name !== undefined && ts.isIdentifier(name)) names.push([name.text, stmt]); + } + return names; +} + +function collectBindingNames(name: ts.BindingName, into: Array<[string, ts.Node]>): void { + if (ts.isIdentifier(name)) { + into.push([name.text, name]); + return; + } + for (const element of name.elements.nodes) { + if (ts.isOmittedExpression(element)) continue; + if (ts.isBindingElement(element) && element.name !== undefined) + collectBindingNames(element.name, into); + } +} + +/** VarDeclaredNames of a statement: every `var`-declared name below it, + * stopping at function and class boundaries (their declarations are not + * var-scoped here). */ +function collectVarDeclaredNames(root: ts.Node, into: Set): void { + const stack: ts.Node[] = [root]; + while (stack.length > 0) { + const node = stack.pop()!; + if (ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node)) + continue; + if (ts.isVariableDeclarationList(node) && (node.flags & ts.NodeFlags.BlockScoped) === 0) { + const names: Array<[string, ts.Node]> = []; + for (const decl of node.declarations.nodes) collectBindingNames(decl.name, names); + for (const [name] of names) into.add(name); + } + ts.forEachChild(node, (child) => { + stack.push(child); + return undefined; + }); + } +} + +/** `async function` and `function*` are Annex B's allowance never — only the + * plain form is. */ +function isEnhancedFunction(decl: ts.FunctionDeclaration): boolean { + if (decl.asteriskToken !== undefined) return true; + return (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Async) !== 0; +} + +/* ── SC0007: a declaration where the grammar admits only a Statement ───── + * + * Every `Statement` slot below admits Statement productions only, and only + * Annex B widens three of them to a plain FunctionDeclaration in non-strict + * code: the body of an `if`/`else` and a labelled statement. Async functions, + * generators, classes and lexical declarations are never admitted, and a + * LABELLED function is never admitted as an if/else body (`IsLabelledFunction` + * is an explicit early error there, so `if (x) l: function f(){}` fails even + * in sloppy code). */ +type StatementSlot = "if" | "loop" | "with" | "label"; + +function checkStatementSlot(node: ts.Node, diags: ScrDiagnostic[], isStrict: IsStrictCode): void { + if (ts.isIfStatement(node)) { + checkStatementPosition(node.thenStatement, "if", diags, isStrict); + if (node.elseStatement !== undefined) + checkStatementPosition(node.elseStatement, "if", diags, isStrict); + return; + } + if ( + ts.isWhileStatement(node) || + ts.isDoStatement(node) || + ts.isForStatement(node) || + ts.isForInStatement(node) || + ts.isForOfStatement(node) + ) { + checkStatementPosition(node.statement, "loop", diags, isStrict); + return; + } + if (ts.isWithStatement(node)) { + checkStatementPosition(node.statement, "with", diags, isStrict); + return; + } + if (ts.isLabeledStatement(node)) checkStatementPosition(node.statement, "label", diags, isStrict); +} + +function checkStatementPosition( + stmt: ts.Statement, + slot: StatementSlot, + diags: ScrDiagnostic[], + isStrict: IsStrictCode, +): void { + const sf = ts.getSourceFileOfNode(stmt); + // A labelled function is an early error as an if-clause, loop, or 'with' + // body in BOTH modes (Node agrees); only a plain labelled STATEMENT is + // Annex B's, and that is the `slot === "label"` case, whose terminal + // function the function branches below handle. + const labelTerminal = + slot === "label" || !ts.isLabeledStatement(stmt) ? null : labelledFunctionOf(stmt); + if (labelTerminal !== null) { + report( + diags, + NOT_A_STATEMENT, + labelTerminal, + "a labelled function declaration cannot be the body of a loop, a 'with' statement, or an 'if' clause", + ); + return; + } + if (ts.isVariableStatement(stmt)) { + if ((stmt.declarationList.flags & ts.NodeFlags.BlockScoped) !== 0) + report( + diags, + NOT_A_STATEMENT, + stmt, + "a lexical declaration cannot appear in a single-statement position", + ); + return; + } + if (ts.isClassDeclaration(stmt)) { + report( + diags, + NOT_A_STATEMENT, + stmt, + "a class declaration cannot appear in a single-statement position", + ); + return; + } + if (!ts.isFunctionDeclaration(stmt)) return; + if (isEnhancedFunction(stmt)) { + report( + diags, + NOT_A_STATEMENT, + stmt, + "an async function or generator declaration can only appear at the top level or inside a block", + ); + return; + } + // A plain function declaration: Annex B admits it as an if/else body and as + // a labelled statement, in non-strict code only, and never as a loop's or a + // 'with' statement's body. + if (slot === "loop" || slot === "with") { + report( + diags, + NOT_A_STATEMENT, + stmt, + "a function declaration can only appear at the top level, inside a block, or as the body of an if statement", + ); + return; + } + if (isStrict(sf, stmt)) + report( + diags, + NOT_A_STATEMENT, + stmt, + "strict mode code may not declare a function in a single-statement position", + ); +} + +/** The function declaration at the end of a `label:` chain, when `stmt` is a + * labelled statement around one. */ +function labelledFunctionOf(stmt: ts.Statement): ts.FunctionDeclaration | null { + let cur: ts.Statement = stmt; + while (ts.isLabeledStatement(cur)) cur = cur.statement; + return ts.isFunctionDeclaration(cur) ? cur : null; +} + +/* ── strictness of the code around a node ─────────────────────────────── + * + * A directive prologue's "use strict" applies to the function (or script) it + * opens, arrows cannot carry one, and a class body is strict by + * construction. Module sources are strict whether they say so or not, and + * Node's classification — not the checker's forced external-module scope — + * decides which sources are modules. */ +function isStrictCode( + sf: ts.SourceFile, + node: ts.Node, + isModuleCode: (sf: ts.SourceFile) => boolean, +): boolean { + if (isModuleCode(sf)) return true; + for (let n: ts.Node | undefined = node; n !== undefined; n = n.parent) { + if (ts.isSourceFile(n)) return hasUseStrictPrologue(n.statements.nodes); + if ( + ts.isClassStaticBlockDeclaration(n) || + ts.isMethodDeclaration(n) || + ts.isConstructorDeclaration(n) || + ts.isGetAccessorDeclaration(n) || + ts.isSetAccessorDeclaration(n) || + ts.isPropertyDeclaration(n) + ) + return true; + if (ts.isArrowFunction(n)) continue; + if (ts.isFunctionLike(n)) { + const body = n.body; + if (body !== undefined && ts.isBlock(body) && hasUseStrictPrologue(body.statements.nodes)) + return true; + } + } + return false; +} + +function hasUseStrictPrologue(statements: readonly ts.Statement[]): boolean { + for (const stmt of statements) { + if (!ts.isExpressionStatement(stmt)) return false; + const expr = stmt.expression; + if (!ts.isStringLiteral(expr)) return false; + if (expr.text === "use strict") return true; + } + return false; +} diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 348ca658e..0d3269b3c 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -136,6 +136,7 @@ import { JS_RELAXED_TSC_CODES, } from "./tsc-codes.js"; import { trackedFileExists, trackedReadFile, trackedRealpath } from "./input-tracker.js"; +import { earlyErrorDiagnostics7 } from "./early-errors.js"; import { forkTargetPaths } from "./fork-target.js"; import { workerTargetPaths } from "./worker-target.js"; import { @@ -6673,6 +6674,11 @@ function preflight7(load: LoadResult): { esmNamedImportLinkCheck(program, entry, order, diags), ); + // ECMAScript early errors the TypeScript checker does not model (plan + // §7.7 D-4): a JavaScript source the spec makes a SyntaxError fails here, + // before any lowering, exactly where a tsc parse error fails. + diags.push(...earlyErrorDiagnostics7(order, (sf) => isNodeEsmFile7(sf, program))); + return { diags, moduleOrder: order, diff --git a/packages/compiler/src/frontend/tsc-codes.ts b/packages/compiler/src/frontend/tsc-codes.ts index 1f9ce923e..79a95c2b4 100644 --- a/packages/compiler/src/frontend/tsc-codes.ts +++ b/packages/compiler/src/frontend/tsc-codes.ts @@ -36,6 +36,21 @@ export const ADOPTED_OPTIONS = [ "reactNamespace", ] as const; +/* ── The early-error boundary (read this before relaxing a code) ──────── + * + * Some of the codes below do double duty: TypeScript reports them for a + * JavaScript construct that ECMAScript ALSO makes a SyntaxError (a + * redeclared block binding, a declaration in a statement-only position). + * Relaxing such a code removes that construct's only rejection, so a + * parse-negative program would start compiling and running. + * + * frontend/early-errors.ts owns those rejections now, under their own + * preflight codes — SC0005 (duplicate lexical declaration in a block or + * switch statement list), SC0006 (lexical declaration colliding with a + * `var` in the same block), SC0007 (declaration in a single-statement + * position). A relaxation that needs one of those rules to keep firing must + * land with the matching rule in that pass, not rely on this table. */ + export const JS_RELAXED_TSC_CODES: ReadonlySet = new Set([ // implicit any (params, variables, elements, this, returns, rest, // construct signatures, index reads over shapes with no index diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index ea8295406..34914a034 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -9,7 +9,7 @@ "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 24.22.0 with bundled Undici 7.29.1. 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.", + "Process-level diagnostic codes are not surface entries: SC0001-SC0007 are preflight gates (SC0005-SC0007 are the ECMAScript early errors), 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." ], diff --git a/tests/diagnostics/js-block-redeclaration.js b/tests/diagnostics/js-block-redeclaration.js new file mode 100644 index 000000000..c6af1944b --- /dev/null +++ b/tests/diagnostics/js-block-redeclaration.js @@ -0,0 +1,18 @@ +// ECMAScript early errors the TypeScript checker does not model: a statement +// list may not declare one lexical name twice, and a lexical name may not be +// `var`-declared in the same list (Block : { StatementList }, and a switch's +// case block is one such list). Both are SC0005/SC0006, reported at +// preflight — the constructs come from Test262's negative-parse suites. +{ + class collide {} + async function collide() {} +} + +switch (1) { + case 1: + let scoped = 1; + break; + case 2: + var scoped = 2; + break; +} diff --git a/tests/diagnostics/js-single-statement-declaration.js b/tests/diagnostics/js-single-statement-declaration.js new file mode 100644 index 000000000..21a07b54a --- /dev/null +++ b/tests/diagnostics/js-single-statement-declaration.js @@ -0,0 +1,10 @@ +// ECMAScript early errors the TypeScript checker does not model: the body of +// a loop, a `with` statement, or an if clause admits only a Statement, and +// Annex B's function-declaration allowance never covers an async function, a +// generator, a class, or a lexical declaration. SC0007, reported at +// preflight — the shapes come from Test262's negative-parse suites. +while (false) function inLoop() {} +l: class inLabel {} +if (true) async function inIf() {} +if (false) il: function labelledInIf() {} +do function* inDoWhile() {} while (false); diff --git a/tests/diagnostics/js-single-statement-strict.js b/tests/diagnostics/js-single-statement-strict.js new file mode 100644 index 000000000..d13cddbcf --- /dev/null +++ b/tests/diagnostics/js-single-statement-strict.js @@ -0,0 +1,8 @@ +// The strict half of SC0007: Annex B's allowance for a plain function +// declaration in an if/else body or a labelled statement is non-strict code +// only, so a "use strict" prologue takes it away — Node agrees on both +// shapes. The sloppy fixture beside this one pins the refusals that hold in +// both modes. +"use strict"; +if (true) function inIf() {} +l: function inLabel() {} diff --git a/tests/harness/__snapshots__/js-block-redeclaration.js.txt b/tests/harness/__snapshots__/js-block-redeclaration.js.txt new file mode 100644 index 000000000..4e73a842c --- /dev/null +++ b/tests/harness/__snapshots__/js-block-redeclaration.js.txt @@ -0,0 +1,41 @@ +js-block-redeclaration.js:7:9 - error SC0001: Class declaration cannot implement overload list for 'collide'. + + 6 | { + 7 | class collide {} + | ^~~~~~~ + 8 | async function collide() {} + +js-block-redeclaration.js:8:3 - error SC0005: the declaration 'collide' is already declared in this block's statement list + + 7 | class collide {} + 8 | async function collide() {} + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ + 9 | } + +js-block-redeclaration.js:8:18 - error SC0001: Function with bodies can only merge with classes that are ambient. + + 7 | class collide {} + 8 | async function collide() {} + | ^~~~~~~ + 9 | } + +js-block-redeclaration.js:13:9 - error SC0006: the lexical declaration 'scoped' collides with a 'var' declaration of the same name in this block + + 12 | case 1: + 13 | let scoped = 1; + | ^~~~~~ + 14 | break; + +js-block-redeclaration.js:15:8 - error SC0001: Type '2' is not comparable to type '1'. + + 14 | break; + 15 | case 2: + | ^ + 16 | var scoped = 2; + +js-block-redeclaration.js:16:9 - error SC0001: Cannot initialize outer scoped variable 'scoped' in the same scope as block scoped declaration 'scoped'. + + 15 | case 2: + 16 | var scoped = 2; + | ^~~~~~ + 17 | break; \ No newline at end of file diff --git a/tests/harness/__snapshots__/js-single-statement-declaration.js.txt b/tests/harness/__snapshots__/js-single-statement-declaration.js.txt new file mode 100644 index 000000000..892b49ce3 --- /dev/null +++ b/tests/harness/__snapshots__/js-single-statement-declaration.js.txt @@ -0,0 +1,48 @@ +js-single-statement-declaration.js:6:15 - error SC0007: a function declaration can only appear at the top level, inside a block, or as the body of an if statement + + 5 | // preflight — the shapes come from Test262's negative-parse suites. + 6 | while (false) function inLoop() {} + | ^~~~~~~~~~~~~~~~~~~~ + 7 | l: class inLabel {} + +js-single-statement-declaration.js:7:1 - error SC0001: A label is not allowed here. + + 6 | while (false) function inLoop() {} + 7 | l: class inLabel {} + | ^ + 8 | if (true) async function inIf() {} + +js-single-statement-declaration.js:7:4 - error SC0007: a class declaration cannot appear in a single-statement position + + 6 | while (false) function inLoop() {} + 7 | l: class inLabel {} + | ^~~~~~~~~~~~~~~~ + 8 | if (true) async function inIf() {} + +js-single-statement-declaration.js:8:11 - error SC0007: an async function or generator declaration can only appear at the top level or inside a block + + 7 | l: class inLabel {} + 8 | if (true) async function inIf() {} + | ^~~~~~~~~~~~~~~~~~~~~~~~ + 9 | if (false) il: function labelledInIf() {} + +js-single-statement-declaration.js:9:12 - error SC0001: A label is not allowed here. + + 8 | if (true) async function inIf() {} + 9 | if (false) il: function labelledInIf() {} + | ^~ + 10 | do function* inDoWhile() {} while (false); + +js-single-statement-declaration.js:9:16 - error SC0007: a labelled function declaration cannot be the body of a loop, a 'with' statement, or an 'if' clause + + 8 | if (true) async function inIf() {} + 9 | if (false) il: function labelledInIf() {} + | ^~~~~~~~~~~~~~~~~~~~~~~~~~ + 10 | do function* inDoWhile() {} while (false); + +js-single-statement-declaration.js:10:4 - error SC0007: an async function or generator declaration can only appear at the top level or inside a block + + 9 | if (false) il: function labelledInIf() {} + 10 | do function* inDoWhile() {} while (false); + | ^~~~~~~~~~~~~~~~~~~~~~~~ + 11 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/js-single-statement-strict.js.txt b/tests/harness/__snapshots__/js-single-statement-strict.js.txt new file mode 100644 index 000000000..0fdfcfe6d --- /dev/null +++ b/tests/harness/__snapshots__/js-single-statement-strict.js.txt @@ -0,0 +1,20 @@ +js-single-statement-strict.js:7:11 - error SC0007: strict mode code may not declare a function in a single-statement position + + 6 | "use strict"; + 7 | if (true) function inIf() {} + | ^~~~~~~~~~~~~~~~~~ + 8 | l: function inLabel() {} + +js-single-statement-strict.js:8:1 - error SC0001: A label is not allowed here. + + 7 | if (true) function inIf() {} + 8 | l: function inLabel() {} + | ^ + 9 | + +js-single-statement-strict.js:8:4 - error SC0007: strict mode code may not declare a function in a single-statement position + + 7 | if (true) function inIf() {} + 8 | l: function inLabel() {} + | ^~~~~~~~~~~~~~~~~~~~~ + 9 | \ No newline at end of file diff --git a/tests/test262/support.mjs b/tests/test262/support.mjs index ecb5d45c4..46c601b7c 100644 --- a/tests/test262/support.mjs +++ b/tests/test262/support.mjs @@ -261,8 +261,29 @@ function earlyErrorDiagnostics(source, variant) { !(variant === "sloppy" && (diagnostic.code === 1214 || diagnostic.code === 1215))); } +// The compiler's OWN early errors (frontend/early-errors.ts): static +// semantics tsc does not model, reported at preflight under their own codes. +// A negative-parse test only asks for SOME SyntaxError, so one of these +// inside the test's source is a match — the same standard as the tsc +// diagnostics above, minus tsc's ability to tell us where it points. +const ownEarlyErrorCodes = new Set(["SC0005", "SC0006", "SC0007"]); + export function matchesParseNegative(outcome, source, variant = "strict") { if (outcome.status !== "compile-refusal") return false; + { + const prepared = prepare(source, false, completion, variant); + const start = prepared.indexOf(source); + const end = start + source.length; + const suffix = variant === "sloppy" ? "/main.cjs" : "/main.js"; + const own = outcome.diagnostics?.some( + (reported) => + ownEarlyErrorCodes.has(reported.code) && + reported.loc?.file?.endsWith(suffix) && + reported.loc.start >= start && + reported.loc.start < end, + ); + if (own) return true; + } const matches = (parsed) => outcome.diagnostics?.some((reported) => reported.code === "SC0001" && reported.loc?.file?.endsWith(variant === "sloppy" ? "/main.cjs" : "/main.js") && From ff93a5a4de2e123f72a7e4cf8c48daaacadb6c96 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 15:03:49 -0700 Subject: [PATCH 55/70] Refuse sloppy implicit-global writes by name, and relax undeclared JS names Node resolves a name nothing declares at run time. In sloppy mode a WRITE creates a property on the process-wide global object, which a native binary has no object for, so those programs belong on the Node island route (plan D-2/D-5). Until that routing exists, an undeclared write in a `.js` source is refused at build time under a dedicated code, SC1091, whose text names the island route instead of failing with a generic no-lowering message. Reaching that refusal at all means the checker has to stop treating an undeclared name in a JavaScript source as a compile error, so 2304, its did-you-mean spelling 2552, and shorthand position's 18004 join the JS relaxation set. The relaxation stays `isJsSourceFileName`-gated: TypeScript input keeps the compile error, where an undeclared name is a bug rather than a runtime path. The refusal is a preflight walk over every JavaScript file, so it reports once per write, before any code is emitted, at the span the checker used. `isJsWriteTarget7` is shared with the lowerer's unresolved-reference fence, which supplies the same refusal for a write that reaches lowering by a path preflight does not model. An empty identifier is skipped: it is a parse recovery artifact, never a name a program writes. The JS-read half of D-2 (the name lowers to Node's catchable `ReferenceError`) is the next commit; this one only changes what the build says about a write. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/diagnostics/codes.ts | 1 + .../compiler/src/diagnostics/diagnostic.ts | 5 + .../compiler/src/frontend/lowering/lowerer.ts | 13 +++ packages/compiler/src/frontend/program.ts | 106 ++++++++++++++++++ packages/compiler/src/frontend/tsc-codes.ts | 14 +++ packages/compiler/surface-manifest.json | 8 ++ .../sloppy-implicit-global-write.js | 6 + .../sloppy-implicit-global-write.js.txt | 8 ++ 8 files changed, 161 insertions(+) create mode 100644 tests/diagnostics/sloppy-implicit-global-write.js create mode 100644 tests/harness/__snapshots__/sloppy-implicit-global-write.js.txt diff --git a/packages/compiler/src/diagnostics/codes.ts b/packages/compiler/src/diagnostics/codes.ts index bd597cb01..3ec5e6c87 100644 --- a/packages/compiler/src/diagnostics/codes.ts +++ b/packages/compiler/src/diagnostics/codes.ts @@ -43,6 +43,7 @@ export const DIAGNOSTIC_CODES = { SC1071: "generators", SC1080: "'this' outside a class method", SC1090: "this syntax (generic construct fence)", + SC1091: "write to an undeclared name in a JavaScript source", SC1100: "operations on 'unknown' values", SC1101: "converting typed values to 'unknown'", SC1110: "comptime evaluation failure", diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index de377441f..6b5973418 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -674,6 +674,11 @@ export const UNSUPPORTED: Record = { // remains is `this` outside any method (top level, plain functions). SC1080: { feature: "'this' outside a class method", milestone: "later" }, SC1090: { feature: "this syntax", milestone: "later" }, // generic fallback; message names the construct + SC1091: { + feature: "writing an undeclared name in a JavaScript source", + milestone: "later", + hint: "a write to an undeclared name has no native lowering: sloppy mode makes it a property on Node's global object, which a native binary has none of, and strict mode makes it throw; declare the name, or keep the module in a Node island (sloppy-mode programs route there)", + }, SC1100: { feature: "operations on 'unknown' values", milestone: "later", diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 417f13c20..c1759e8a5 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -357,6 +357,7 @@ import { overridesDtsPath, npmStaticDepSf7, requireSpecOf, + isJsWriteTarget7, resolveImport, workspacePackageOfPath, type CycleEdge, @@ -15512,6 +15513,18 @@ export class Lowerer { node: ts.Node, fallback: string, ): never { + // A `.js` WRITE to a name nothing declares. Sloppy mode resolves it by + // creating a property on the global object — process-wide state a native + // binary has no object for, so those programs run in a Node island + // (plan D-5). Until the write is routed there it is refused by name + // rather than by the generic no-lowering text. + if (symbol === null && isJsWriteTarget7(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) { + this.unsupported( + "SC1091", + node, + `writing the undeclared name '${name}' in a JavaScript source`, + ); + } if (this.isBlockedBinding(symbol)) { this.pushDiag(blockedBindingUseDiag(name, locOf(node))); throw new PoisonError(); diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 0d3269b3c..2bd726de4 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -2759,6 +2759,107 @@ function isAssignmentTarget7(node: ts.Node): boolean { ); } +/** True when the identifier is a reference position that WRITES: an + * assignment (or compound-assignment) target, a `++`/`--` operand, a + * for-in/for-of head, or an element of a destructuring pattern — through + * parentheses and pattern containers, exactly the shapes a sloppy-mode + * implicit global can be introduced by. */ +export function isJsWriteTarget7(node: ts.Node): boolean { + let cur: ts.Node = node; + for (;;) { + const parent: ts.Node | undefined = cur.parent; + if (parent === undefined) return false; + if (ts.isParenthesizedExpression(parent)) { + cur = parent; + continue; + } + if (ts.isBinaryExpression(parent)) + return ( + parent.left === cur && + parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ); + if (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) + return ( + parent.operand === cur && + (parent.operator === ts.SyntaxKind.PlusPlusToken || + parent.operator === ts.SyntaxKind.MinusMinusToken) + ); + if (ts.isForInStatement(parent) || ts.isForOfStatement(parent)) + return parent.initializer === cur; + if (ts.isArrayLiteralExpression(parent) || ts.isObjectLiteralExpression(parent)) { + cur = parent; + continue; + } + if ( + (ts.isShorthandPropertyAssignment(parent) && parent.name === cur) || + (ts.isPropertyAssignment(parent) && parent.initializer === cur) || + ((ts.isSpreadElement(parent) || ts.isSpreadAssignment(parent)) && parent.expression === cur) + ) { + cur = parent; + continue; + } + return false; + } +} + +/** Nothing binds `node`: the tsc had no symbol, and a shorthand pattern's + * name has no VALUE binding either (its property symbol is the target, not a + * declaration of the name being written). */ +function undeclaredJsName7( + checker: ReturnType, + node: ts.Identifier, +): boolean { + const parent = node.parent; + if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === node) + return checker.getShorthandAssignmentValueSymbol(parent) === undefined; + return checker.getSymbolAtLocation(node) === undefined; +} + +/** A `.js` WRITE to a name nothing declares (plan D-2/D-5). Sloppy mode + * resolves such a write by creating a property on Node's global object — + * process-wide state a native binary has no object for — so the program is + * refused at build time, under SC1091, whose text names the Node-island + * route those semantics need. A READ of the same name is admitted instead: + * it throws Node's own catchable `ReferenceError` at the access + * (lowering/expressions/identifiers.ts). TypeScript input never reaches + * either path — an undeclared name is a compile error there. */ +function jsUndeclaredWriteDiags( + program: ts.Program, + files: readonly ts.SourceFile[], +): ScrDiagnostic[] { + const diags: ScrDiagnostic[] = []; + const checker = program.getTypeChecker(); + for (const sf of files) { + if (!isJsSourceFileName(sf.fileName)) continue; + const stack: ts.Node[] = [sf]; + while (stack.length > 0) { + const node = stack.pop()!; + // An empty name is a parse-recovery artifact (`@import x = require(…)` + // in a JSDoc prologue leaves one behind), never a reference a program + // writes; the syntax error that created it is already reported. + if ( + ts.isIdentifier(node) && + node.text !== "" && + isJsWriteTarget7(node) && + undeclaredJsName7(checker, node) + ) + diags.push( + unsupportedDiag( + "SC1091", + { file: sf.fileName, start: node.getStart(sf), end: node.end }, + `writing the undeclared name '${node.text}' in a JavaScript source`, + ), + ); + ts.forEachChild(node, (child) => { + stack.push(child); + return undefined; + }); + } + } + return diags; +} + /** 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. */ @@ -6583,6 +6684,11 @@ function preflight7(load: LoadResult): { }; visit(entry); + // ECMAScript's sloppy-mode WRITE to an undeclared name, which the JS + // relaxation above admits for READS (plan D-2): refused by name, with the + // island route in the text (SC1091). + for (const d of jsUndeclaredWriteDiags(program, order)) diags.push(d); + // The startup crash Node's RESOLUTION phase reports: modules resolve // their request specifiers in source order, children linking depth-first // as each resolves (ModuleJob.syncLink) — a preorder walk over the diff --git a/packages/compiler/src/frontend/tsc-codes.ts b/packages/compiler/src/frontend/tsc-codes.ts index 79a95c2b4..9bf16dc80 100644 --- a/packages/compiler/src/frontend/tsc-codes.ts +++ b/packages/compiler/src/frontend/tsc-codes.ts @@ -175,6 +175,20 @@ export const JS_RELAXED_TSC_CODES: ReadonlySet = new Set([ // ride the checked-dynamic tree; unsupported operations report a compile-time diagnostic). 18046, 2571, + // UNDECLARED NAMES in a JavaScript source: Node resolves them at run time, + // never at compile time — a READ throws the catchable + // `ReferenceError: is not defined` at the access (`typeof` of one + // answers "undefined"), and a sloppy-mode WRITE creates a global. The + // lowerer turns the read into `scr_undef_global_read` (exact Node text and + // behavior) and refuses the write with SC1091, which names the Node-island + // route the sloppy semantics need (the plan's D-2/D-5). TypeScript input + // keeps the compile error: an undeclared name there is a bug, not a + // runtime path. 2552 is 2304's did-you-mean spelling, and 18004 is the + // same unresolved name in SHORTHAND position (`{ x }`), which the checker + // reports against the property instead of the reference. + 2304, + 2552, + 18004, ]); /* Arithmetic on an `any` operand (TS2362/2363/2365 fire when the other diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 34914a034..793625daa 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -170,6 +170,14 @@ "status": "unsupported", "code": "SC1090" }, + { + "id": "diagnostic.sc1091", + "kind": "diagnostic-fence", + "name": "writing an undeclared name in a JavaScript source", + "status": "unsupported", + "code": "SC1091", + "note": "a write to an undeclared name has no native lowering: sloppy mode makes it a property on Node's global object, which a native binary has none of, and strict mode makes it throw; declare the name, or keep the module in a Node island (sloppy-mode programs route there)" + }, { "id": "diagnostic.sc1100", "kind": "diagnostic-fence", diff --git a/tests/diagnostics/sloppy-implicit-global-write.js b/tests/diagnostics/sloppy-implicit-global-write.js new file mode 100644 index 000000000..895e27e93 --- /dev/null +++ b/tests/diagnostics/sloppy-implicit-global-write.js @@ -0,0 +1,6 @@ +// A sloppy-mode implicit-global WRITE in a JavaScript source. Node resolves +// the write by creating a property on the global object — process-wide state +// a native binary has none of — so the write is refused by name (SC1091) and +// names the Node-island route the sloppy semantics need (plan D-2/D-5). +implicitGlobal = 42; +console.log(implicitGlobal); diff --git a/tests/harness/__snapshots__/sloppy-implicit-global-write.js.txt b/tests/harness/__snapshots__/sloppy-implicit-global-write.js.txt new file mode 100644 index 000000000..31d294056 --- /dev/null +++ b/tests/harness/__snapshots__/sloppy-implicit-global-write.js.txt @@ -0,0 +1,8 @@ +sloppy-implicit-global-write.js:5:1 - error SC1091: writing the undeclared name 'implicitGlobal' in a JavaScript source is not supported yet + + 4 | // names the Node-island route the sloppy semantics need (plan D-2/D-5). + 5 | implicitGlobal = 42; + | ^~~~~~~~~~~~~~ + 6 | console.log(implicitGlobal); + + hint: a write to an undeclared name has no native lowering: sloppy mode makes it a property on Node's global object, which a native binary has none of, and strict mode makes it throw; declare the name, or keep the module in a Node island (sloppy-mode programs route there) \ No newline at end of file From 48d524a3216d650de55e925c5e9327c69dee1eab Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 15:04:04 -0700 Subject: [PATCH 56/70] Lower a JavaScript read of an undeclared name to Node's ReferenceError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D-2: Node resolves a name nothing declares at run time, and the read always throws the catchable `ReferenceError: x is not defined` at the access. A `.js` read of such a name now compiles to exactly that throw instead of refusing the program, with Node's message and Node's catchability. The read's value is checked-dynamic (the checker's error-any after the relaxation in the previous commit, narrowed by the use site where one provides context), and every path through it unwinds before a use observes the value, so the dynamic slot never carries a wrong value. Shorthand position (`{ x }`) needs its own declaration test: there the checker answers the PROPERTY symbol, so the name is bound when `getShorthandAssignmentValueSymbol` has no value for it. `typeof` is the one operator that never touches its operand value, and Node answers "undefined" for an undeclared name, so `lowerDynTypeofTest` folds the test to a literal before the read is ever lowered — `typeof x === "undefined"` stays true and `typeof x` alone prints "undefined". No name that Node serves from the global object reaches this branch: the compiler models those as declared (that is how the corpus reads `process`, `console` and friends), so this is the name Node itself would report missing. The corpus program pins every shape: bare read, call, member read, arithmetic, comparison, shorthand, destructuring default, typeof test, a guarded read that keeps running after the throw, and a declared control. Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/identifiers.ts | 21 ++++++++++ .../frontend/lowering/expressions/typeof.ts | 18 +++++++++ tests/corpus/8423-js-undeclared-read.js | 40 +++++++++++++++++++ .../8423-js-undeclared-read.js.json | 1 + 4 files changed, 80 insertions(+) create mode 100644 tests/corpus/8423-js-undeclared-read.js create mode 100644 tests/harness/corpus-costs/8423-js-undeclared-read.js.json diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index c73217ef8..807f47413 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -1031,6 +1031,27 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: ); } } + // A `.js` READ of a name NOTHING declares. Node resolves such a name at + // run time: the access throws the catchable `ReferenceError: is not + // defined` (a `typeof` test lowers earlier, in expressions/typeof.ts, and + // answers "undefined" — exactly Node's rule), so the read compiles to that + // throw and never yields a value. The value is checked-dynamic (the + // checker's error-any), which every use site carries or fences — and the + // read always unwinds before any of them observes it. A WRITE to the same + // name is not a read: it reaches rejectUnresolved and its SC1091 story. + // Typed sources keep the compile error (preflight's JS relaxation is + // `isJsSourceFileName`-gated, so no TS file reaches this branch). + // In shorthand position (`{ x }`) `resolveValueSymbol` answers the + // PROPERTY symbol, so the declaration test there is the checker's + // shorthand-VALUE symbol. + const parent = expr.parent; + const undeclared = + parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === expr + ? lowerer.checker.getShorthandAssignmentValueSymbol(parent) === undefined + : lowerer.resolveValueSymbol(expr) === null; + if (undeclared && isJsSourceFile(ts.getSourceFileOfNode(expr))) { + return nsUndefRead(lowerer, expr.text, expr, contextualUndefReadType(lowerer, expr) ?? DYN); + } // Preflight guarantees no unresolved identifiers; anything else here // is a blocked declaration's binding (the SC2004 cascade) or a // binding form we don't model yet. diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts index 5f1e4a830..6a545ffcb 100644 --- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts +++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts @@ -493,6 +493,24 @@ export function lowerDynTypeofTest( // preserving both operands' evaluation order without a narrowing test. if (!ts.isStringLiteral(b)) continue; const declared = lowerer.mapTypeOf(lowerer.typeOf(a.expression)); + // `typeof undeclared === "..."` in a JavaScript source: nothing declares + // the name, so Node answers "undefined" without ever reading it — the + // test is that constant's comparison, never a read (a read throws). + { + let operand = a.expression; + while (ts.isParenthesizedExpression(operand)) operand = operand.expression; + if ( + ts.isIdentifier(operand) && + isJsSourceFile(ts.getSourceFileOfNode(operand)) && + lowerer.resolveValueSymbol(operand) === null + ) + return { + kind: "boolLit", + value: (b.text === "undefined") !== negated, + type: BOOL, + loc, + }; + } let value: IrExpr; if (declared?.kind === "dyn") { value = lowerer.lowerExpr(a.expression); diff --git a/tests/corpus/8423-js-undeclared-read.js b/tests/corpus/8423-js-undeclared-read.js new file mode 100644 index 000000000..cd9caef55 --- /dev/null +++ b/tests/corpus/8423-js-undeclared-read.js @@ -0,0 +1,40 @@ +// A JavaScript READ of a name nothing declares resolves at run time, exactly +// as Node does: the access throws a catchable ReferenceError whose message +// names the identifier, `typeof` answers "undefined" without throwing, and a +// call through the same name throws at the callee read (plan D-2 / C5). The +// undeclared name is lowered to the runtime's global-read helper, which +// throws Node's " is not defined". +'use strict'; + +const describe = (label, fn) => { + try { + console.log(`${label}: ${String(fn())}`); + } catch (e) { + console.log(`${label}: threw ${e.name}: ${e.message}`); + } +}; + +console.log("typeof:", typeof neverDeclared); + +describe("bare read", () => neverDeclared); +describe("call", () => neverDeclared(1, 2)); +describe("member read", () => neverDeclared.length); +describe("arithmetic", () => neverDeclared + 1); +describe("comparison", () => neverDeclared === undefined); +describe("shorthand property", () => { + const wrapper = { neverDeclared }; + return String(wrapper.neverDeclared); +}); +describe("destructuring default", () => { + const pick = ({ value = unresolvableReference } = {}) => value; + return pick({}); +}); +describe("typeof test", () => typeof neverDeclared === "undefined"); + +// `typeof` never throws, so a guarded read is the one safe shape. +if (typeof maybeConfigured !== "undefined") console.log("configured:", maybeConfigured); +else console.log("configured: unset"); + +// A declared binding keeps the ordinary lowered path. +let declared = 3; +describe("declared", () => declared * 2); diff --git a/tests/harness/corpus-costs/8423-js-undeclared-read.js.json b/tests/harness/corpus-costs/8423-js-undeclared-read.js.json new file mode 100644 index 000000000..23aca49c0 --- /dev/null +++ b/tests/harness/corpus-costs/8423-js-undeclared-read.js.json @@ -0,0 +1 @@ +{ "plain": 3300 } From b1b7aff25cf69f46a0435501d0d69c0992aea04b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 16:41:21 -0700 Subject: [PATCH 57/70] Renumber the new corpus programs to 8514 and 8515 Group C adds two corpus programs whose 8423 prefix another group also proposes; main's highest corpus number is 8513, so take the next two free numbers and move the matching per-program cost files with them. Co-Authored-By: Claude Opus 5.5 --- ...-protocol-shapes.js => 8514-async-iterator-protocol-shapes.js} | 0 .../{8423-js-undeclared-read.js => 8515-js-undeclared-read.js} | 0 ...shapes.js.json => 8514-async-iterator-protocol-shapes.js.json} | 0 ...js-undeclared-read.js.json => 8515-js-undeclared-read.js.json} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/corpus/{8423-async-iterator-protocol-shapes.js => 8514-async-iterator-protocol-shapes.js} (100%) rename tests/corpus/{8423-js-undeclared-read.js => 8515-js-undeclared-read.js} (100%) rename tests/harness/corpus-costs/{8423-async-iterator-protocol-shapes.js.json => 8514-async-iterator-protocol-shapes.js.json} (100%) rename tests/harness/corpus-costs/{8423-js-undeclared-read.js.json => 8515-js-undeclared-read.js.json} (100%) diff --git a/tests/corpus/8423-async-iterator-protocol-shapes.js b/tests/corpus/8514-async-iterator-protocol-shapes.js similarity index 100% rename from tests/corpus/8423-async-iterator-protocol-shapes.js rename to tests/corpus/8514-async-iterator-protocol-shapes.js diff --git a/tests/corpus/8423-js-undeclared-read.js b/tests/corpus/8515-js-undeclared-read.js similarity index 100% rename from tests/corpus/8423-js-undeclared-read.js rename to tests/corpus/8515-js-undeclared-read.js diff --git a/tests/harness/corpus-costs/8423-async-iterator-protocol-shapes.js.json b/tests/harness/corpus-costs/8514-async-iterator-protocol-shapes.js.json similarity index 100% rename from tests/harness/corpus-costs/8423-async-iterator-protocol-shapes.js.json rename to tests/harness/corpus-costs/8514-async-iterator-protocol-shapes.js.json diff --git a/tests/harness/corpus-costs/8423-js-undeclared-read.js.json b/tests/harness/corpus-costs/8515-js-undeclared-read.js.json similarity index 100% rename from tests/harness/corpus-costs/8423-js-undeclared-read.js.json rename to tests/harness/corpus-costs/8515-js-undeclared-read.js.json From f76a8202dd82be7d0ad6faa4bc44c6f3d4296858 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 17:58:14 -0700 Subject: [PATCH 58/70] Record order-parity baselines for the group C fixtures Add the TS7 preflight/order baselines for the new diagnostics and corpus fixtures so the completeness check and the parity batches cover them. Co-Authored-By: Claude Opus 5.5 --- ...514-async-iterator-protocol-shapes.js.json | 6 ++ .../corpus/8515-js-undeclared-read.js.json | 6 ++ .../js-block-redeclaration.js.json | 61 ++++++++++++++++ .../js-single-statement-declaration.js.json | 70 +++++++++++++++++++ .../js-single-statement-strict.js.json | 34 +++++++++ .../sloppy-implicit-global-write.js.json | 18 +++++ 6 files changed, 195 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8514-async-iterator-protocol-shapes.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8515-js-undeclared-read.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/js-block-redeclaration.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/js-single-statement-declaration.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/js-single-statement-strict.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/sloppy-implicit-global-write.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8514-async-iterator-protocol-shapes.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8514-async-iterator-protocol-shapes.js.json new file mode 100644 index 000000000..736bdb4c4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8514-async-iterator-protocol-shapes.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8514-async-iterator-protocol-shapes.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8515-js-undeclared-read.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8515-js-undeclared-read.js.json new file mode 100644 index 000000000..a8baf582c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8515-js-undeclared-read.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8515-js-undeclared-read.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/js-block-redeclaration.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/js-block-redeclaration.js.json new file mode 100644 index 000000000..616d4614d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/js-block-redeclaration.js.json @@ -0,0 +1,61 @@ +{ + "order": [ + "/tests/diagnostics/js-block-redeclaration.js" + ], + "diags": [ + { + "code": "SC0001", + "message": "Class declaration cannot implement overload list for 'collide'.", + "loc": { + "file": "/tests/diagnostics/js-block-redeclaration.js", + "start": 386, + "end": 393 + } + }, + { + "code": "SC0001", + "message": "Function with bodies can only merge with classes that are ambient.", + "loc": { + "file": "/tests/diagnostics/js-block-redeclaration.js", + "start": 414, + "end": 421 + } + }, + { + "code": "SC0001", + "message": "Type '2' is not comparable to type '1'.", + "loc": { + "file": "/tests/diagnostics/js-block-redeclaration.js", + "start": 491, + "end": 492 + } + }, + { + "code": "SC0001", + "message": "Cannot initialize outer scoped variable 'scoped' in the same scope as block scoped declaration 'scoped'.", + "loc": { + "file": "/tests/diagnostics/js-block-redeclaration.js", + "start": 502, + "end": 508 + } + }, + { + "code": "SC0006", + "message": "the lexical declaration 'scoped' collides with a 'var' declaration of the same name in this block", + "loc": { + "file": "/tests/diagnostics/js-block-redeclaration.js", + "start": 461, + "end": 467 + } + }, + { + "code": "SC0005", + "message": "the declaration 'collide' is already declared in this block's statement list", + "loc": { + "file": "/tests/diagnostics/js-block-redeclaration.js", + "start": 399, + "end": 426 + } + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/js-single-statement-declaration.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/js-single-statement-declaration.js.json new file mode 100644 index 000000000..4dcb13d8f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/js-single-statement-declaration.js.json @@ -0,0 +1,70 @@ +{ + "order": [ + "/tests/diagnostics/js-single-statement-declaration.js" + ], + "diags": [ + { + "code": "SC0001", + "message": "A label is not allowed here.", + "loc": { + "file": "/tests/diagnostics/js-single-statement-declaration.js", + "start": 405, + "end": 406 + } + }, + { + "code": "SC0001", + "message": "A label is not allowed here.", + "loc": { + "file": "/tests/diagnostics/js-single-statement-declaration.js", + "start": 471, + "end": 473 + } + }, + { + "code": "SC0007", + "message": "an async function or generator declaration can only appear at the top level or inside a block", + "loc": { + "file": "/tests/diagnostics/js-single-statement-declaration.js", + "start": 505, + "end": 529 + } + }, + { + "code": "SC0007", + "message": "a labelled function declaration cannot be the body of a loop, a 'with' statement, or an 'if' clause", + "loc": { + "file": "/tests/diagnostics/js-single-statement-declaration.js", + "start": 475, + "end": 501 + } + }, + { + "code": "SC0007", + "message": "an async function or generator declaration can only appear at the top level or inside a block", + "loc": { + "file": "/tests/diagnostics/js-single-statement-declaration.js", + "start": 435, + "end": 459 + } + }, + { + "code": "SC0007", + "message": "a class declaration cannot appear in a single-statement position", + "loc": { + "file": "/tests/diagnostics/js-single-statement-declaration.js", + "start": 408, + "end": 424 + } + }, + { + "code": "SC0007", + "message": "a function declaration can only appear at the top level, inside a block, or as the body of an if statement", + "loc": { + "file": "/tests/diagnostics/js-single-statement-declaration.js", + "start": 384, + "end": 404 + } + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/js-single-statement-strict.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/js-single-statement-strict.js.json new file mode 100644 index 000000000..d70f7bb28 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/js-single-statement-strict.js.json @@ -0,0 +1,34 @@ +{ + "order": [ + "/tests/diagnostics/js-single-statement-strict.js" + ], + "diags": [ + { + "code": "SC0001", + "message": "A label is not allowed here.", + "loc": { + "file": "/tests/diagnostics/js-single-statement-strict.js", + "start": 355, + "end": 356 + } + }, + { + "code": "SC0007", + "message": "strict mode code may not declare a function in a single-statement position", + "loc": { + "file": "/tests/diagnostics/js-single-statement-strict.js", + "start": 358, + "end": 379 + } + }, + { + "code": "SC0007", + "message": "strict mode code may not declare a function in a single-statement position", + "loc": { + "file": "/tests/diagnostics/js-single-statement-strict.js", + "start": 336, + "end": 354 + } + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/sloppy-implicit-global-write.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/sloppy-implicit-global-write.js.json new file mode 100644 index 000000000..edbbc475e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/sloppy-implicit-global-write.js.json @@ -0,0 +1,18 @@ +{ + "order": [ + "/tests/diagnostics/sloppy-implicit-global-write.js" + ], + "diags": [ + { + "code": "SC1091", + "message": "writing the undeclared name 'implicitGlobal' in a JavaScript source is not supported yet", + "loc": { + "file": "/tests/diagnostics/sloppy-implicit-global-write.js", + "start": 306, + "end": 320 + }, + "milestone": "later", + "hint": "a write to an undeclared name has no native lowering: sloppy mode makes it a property on Node's global object, which a native binary has none of, and strict mode makes it throw; declare the name, or keep the module in a Node island (sloppy-mode programs route there)" + } + ] +} From ad8f8daccd5e193cb54fa239d07d9167a9f7671e Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 20:56:10 -0700 Subject: [PATCH 59/70] Keep scr_lib.c's default object byte-identical for the fs.watchFile poll Adding scr_stats_of_path and scr_stats_zero to scr_lib.c moved every program's executable: scr_lib.o is linked everywhere, so its new symbols and strings shifted addresses even for programs that never watch a file. The two entry points now live in an SCR_FS_WATCH_FILE build of scr_lib.c, the matrix's variant for programs whose feature set has watchFile, and the poll unit calls the renamed scr_stats_poll_path. Programs that do not poll link the default object unchanged: compiled without the define, scr_lib.o is byte-identical to the base (verified by comparing the two objects). Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/backend/native/executable.ts | 2 ++ .../runtime-pack-common/runtime-pack-matrix.mjs | 11 +++++++++++ packages/runtime/src/scr_lib.c | 14 ++++++++------ packages/runtime/src/scr_lib.h | 6 ++++-- packages/runtime/src/scr_watch_file.c | 2 +- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 8ecac9923..8222bd9b7 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -426,6 +426,8 @@ const RUNTIME_DEFINES: readonly { { define: "SCR_NATIVE_REACTIONS", when: (opts) => opts.webStreams }, { define: "SCR_HANDLE_CLASSES", when: (opts) => opts.webStreams }, { define: "SCR_YIN", when: (opts) => opts.youngNursery }, + // scr_lib.c's path stat and zero Stats for the fs.watchFile poll. + { define: "SCR_FS_WATCH_FILE", when: (opts) => opts.watchFile }, { define: "SCR_NODE_ISLANDS", when: (opts) => opts.nodeIsland }, ]; diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 6fff2b653..41eef2b23 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -274,6 +274,17 @@ export function variantsFor(source) { defines: ["SCR_TEXT_DECODER_LEGACY"], }); } + // fs.watchFile's poll needs a path stat that reports the errno (its failed + // poll IS a reportable event). The Stats snapshot is private to scr_lib.c, + // so the two entry points live in a build of that unit only programs that + // poll link; every other program keeps the default object, byte for byte. + if (source === "scr_lib.c") { + variants.push({ + id: "fs-watch-file", + when: { watchFile: true }, + defines: ["SCR_FS_WATCH_FILE"], + }); + } // Programs that assign process.emitWarning link the replacement-aware // warning paths; every other program keeps the default object. if (source === "scr_async_dyn.c") { diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index c47339a68..f10b4f5fa 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -5584,7 +5584,7 @@ static bool scr_stats_link_target(HANDLE h, DWORD tag, double *size_out, ScrStr return true; } -static ScrStats *scr_stats_win_handle_stats(const ScrStr *path, const char *op, +static ScrStats *scr_stats_of_path(const ScrStr *path, const char *op, bool no_follow) { WCHAR *wide = scr_fs_win_wide(path); if (!wide) return scr_stats_crt_fallback(path, op); @@ -5630,7 +5630,7 @@ static ScrStats *scr_stats_win_handle_stats(const ScrStr *path, const char *op, /* Reparse points are a general interception mechanism. Node/libuv * follows non-link tags even for lstat, so reopen the resolved target. */ CloseHandle(h); - return scr_stats_win_handle_stats(path, op, false); + return scr_stats_of_path(path, op, false); } if (!scr_stats_link_target(h, tagged.ReparseTag, &link_size, NULL)) { /* A mount-point tag may name a mounted volume rather than a junction. @@ -5638,7 +5638,7 @@ static ScrStats *scr_stats_win_handle_stats(const ScrStr *path, const char *op, * ordinary reparse points and retries with the final component * followed. Do not classify from the tag alone. */ CloseHandle(h); - return scr_stats_win_handle_stats(path, op, false); + return scr_stats_of_path(path, op, false); } is_link = true; } @@ -6081,7 +6081,7 @@ ScrBytes *scr_fs_readlink_bytes(ScrStr *path) { ScrStats *scr_fs_stat(ScrStr *path) { if (!scr_fs_path_ok(path, "path")) return NULL; #ifdef _WIN32 - return scr_stats_win_handle_stats(path, "stat", false); + return scr_stats_of_path(path, "stat", false); #else int error = 0; ScrStats *s = scr_stats_posix(path->data, -1, false, &error); /* follows symlinks */ @@ -6093,7 +6093,7 @@ ScrStats *scr_fs_stat(ScrStr *path) { ScrStats *scr_fs_lstat(ScrStr *path) { if (!scr_fs_path_ok(path, "path")) return NULL; #ifdef _WIN32 - return scr_stats_win_handle_stats(path, "lstat", true); + return scr_stats_of_path(path, "lstat", true); #else int error = 0; ScrStats *s = scr_stats_posix(path->data, -1, true, &error); /* no follow */ @@ -6135,10 +6135,11 @@ ScrStats *scr_fs_fstat(double fd) { return s; } +#ifdef SCR_FS_WATCH_FILE /* fs.watchFile's poll (scr_watch_file.c calls this): stat(2) following * symlinks, libuv's uv_fs_stat — NULL with *error = errno on failure, * never throwing, because a failed poll IS a reportable event. +1. */ -ScrStats *scr_stats_of_path(ScrStr *path, int *error) { +ScrStats *scr_stats_poll_path(ScrStr *path, int *error) { if (!scr_fs_path_ok(path, "path")) { *error = EINVAL; return NULL; @@ -6161,6 +6162,7 @@ ScrStats *scr_stats_of_path(ScrStr *path, int *error) { * field 0, so isFile()/isDirectory() and the size/date reads all answer * zero/false. +1. */ ScrStats *scr_stats_zero(void) { return scr_stats_new(); } +#endif /* SCR_FS_WATCH_FILE */ /* ── Stats objects ─────────────────────────────────────────────────── * Node's Stats and BigIntStats over a snapshot. The integers and the *Ms diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index 775f04e33..69c470217 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -507,8 +507,10 @@ double scr_stats_big_birthtime(ScrStats *s); /* fstat without throwing: +1, or NULL with *error set to the errno. */ ScrStats *scr_stats_of_fd(int fd, int *error); /* stat(2) following symlinks without throwing (the fs.watchFile poll): - * +1, or NULL with *error set to the errno. */ -ScrStats *scr_stats_of_path(ScrStr *path, int *error); + * +1, or NULL with *error set to the errno. Both this and scr_stats_zero + * exist only in the SCR_FS_WATCH_FILE build of scr_lib.c, which programs + * that poll link; every other program links the default object. */ +ScrStats *scr_stats_poll_path(ScrStr *path, int *error); /* The all-zero snapshot a failed poll reports (Node's kZeroStats). +1. */ ScrStats *scr_stats_zero(void); /* Node's Stats (or BigIntStats) object for the snapshot: own fields in the diff --git a/packages/runtime/src/scr_watch_file.c b/packages/runtime/src/scr_watch_file.c index b1bee2826..325e10adb 100644 --- a/packages/runtime/src/scr_watch_file.c +++ b/packages/runtime/src/scr_watch_file.c @@ -171,7 +171,7 @@ static void scr_watch_file_poll(ScrClosure *cb) { if (p == NULL) return; p->timer = 0; /* the loop consumed this arm */ int err = 0; - ScrStats *cur = scr_stats_of_path(p->path, &err); + ScrStats *cur = scr_stats_poll_path(p->path, &err); if (cur == NULL) { if (p->state != -err) { scr_watch_file_emit(p, NULL, p->last, -err); From 9021a031338f6951330ca0fe636febaceaf338bb Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 07:50:05 -0500 Subject: [PATCH 60/70] Pass a shared view whole to a replaced stdout or stderr write A shared view written to stdout or stderr went out in 64 KiB pieces through scr_stdio_write, so a replaced process.stdout.write received the first piece alone, and a false return, which only reports backpressure, ended the loop: a view larger than 64 KiB lost the rest. With a write hook, the view is now copied whole and passed in one call, as Node passes it; the bounded pieces go to the stream itself through scr_stdio_write_raw, which never stops early. tests/corpus/shared-bytes-stdio-write.ts replaces process.stdout.write with a function that records each chunk's length and returns false, and writes a 150,000-byte shared view through it. --- packages/runtime/src/scr_bytes_io.c | 23 +++++++++++++------ tests/corpus/shared-bytes-stdio-write.ts | 13 +++++++++++ .../shared-bytes-stdio-write.ts.json | 2 +- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 2459d2e05..423b953b7 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1227,23 +1227,32 @@ ScrBytes *scr_crypto_random_bytes(double n) { /* ── process.stdout/stderr.write(buf) ──────────────────────────────────── */ /* Where another thread can write shared bytes (where SCR_BYTES_SNAPSHOT - * copies them), they are written through a bounded local buffer, read with - * scr_shared_load, so no shared raw memory reaches the system call and a - * large shared view is not copied whole first. */ + * copies them), they are read with scr_shared_load, so no shared raw memory + * reaches the system call. Written to the stream itself, they go through a + * bounded local buffer, so a large shared view is not copied whole first. + * With a write hook (the program reads process.stdout or process.stderr as + * a value, and may have replaced its write), the view is copied whole: a + * replaced write receives it in one call, as in Node, whatever it returns. */ static bool scr_stdio_write_bytes(int fd, const ScrBytes *b) { size_t n = b->len * scr_bytes_elem_size(b->elem); #if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) if (!b->shared) return scr_stdio_write(fd, b->data, n); + if (scr_stdio_write_hook) { + unsigned char *copy = scr_mem_alloc(n ? n : 1); + scr_shared_load(copy, b->data, n); + bool ok = scr_stdio_write(fd, copy, n); + scr_mem_free(copy); + return ok; + } enum { CHUNK = 64 * 1024 }; unsigned char *chunk = scr_mem_alloc(n < CHUNK ? (n ? n : 1) : CHUNK); - bool ok = true; - for (size_t off = 0; ok && off < n; off += CHUNK) { + for (size_t off = 0; off < n; off += CHUNK) { size_t k = n - off < CHUNK ? n - off : CHUNK; scr_shared_load(chunk, b->data + off, k); - ok = scr_stdio_write(fd, chunk, k); + (void)scr_stdio_write_raw(fd, chunk, k); } scr_mem_free(chunk); - return ok; + return true; #else return scr_stdio_write(fd, b->data, n); #endif diff --git a/tests/corpus/shared-bytes-stdio-write.ts b/tests/corpus/shared-bytes-stdio-write.ts index 0a138fb77..a0d4b4e92 100644 --- a/tests/corpus/shared-bytes-stdio-write.ts +++ b/tests/corpus/shared-bytes-stdio-write.ts @@ -18,6 +18,19 @@ if (isMainThread) { process.stderr.write(new Uint8Array(sab, 63, 70_000)); process.stderr.write("\n"); + + // A replaced write receives the whole view in one call, even when it + // returns false. + const out = process.stdout as unknown as Record; + const original = out["write"]; + const sizes: number[] = []; + out["write"] = (chunk: unknown): boolean => { + sizes.push(typeof chunk === "string" ? chunk.length : (chunk as Uint8Array).length); + return false; + }; + const accepted = process.stdout.write(new Uint8Array(sab, 1000, 150_000)); + out["write"] = original; + console.log("replaced", accepted, sizes.join(",")); console.log("done"); }); } else { diff --git a/tests/harness/corpus-costs/shared-bytes-stdio-write.ts.json b/tests/harness/corpus-costs/shared-bytes-stdio-write.ts.json index 46d9e66e9..56f905494 100644 --- a/tests/harness/corpus-costs/shared-bytes-stdio-write.ts.json +++ b/tests/harness/corpus-costs/shared-bytes-stdio-write.ts.json @@ -1 +1 @@ -{ "plain": 8136, "san": 68380 } +{ "plain": 7025, "san": 74291 } From dd927301fc177f6fcca738c95ad7ffdbcda18206 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 07:57:38 -0500 Subject: [PATCH 61/70] Read builtins into untyped TypeScript slots as Node's values In a TypeScript source, a builtin global used as an object literal member (`const o: any = { j: JSON }`) failed to lower, and the member silently became a closure that threw when called. Node reads the global itself, so `typeof o.j` answered "function" instead of "object", two reads were never equal, and `o.j.stringify(x)` threw "is not a function". There was no diagnostic. A builtin read into a slot the checker types as any or unknown now holds the value a JavaScript source reads: the JSON, Math and Reflect namespace objects, Buffer, and functions such as structuredClone, atob and queueMicrotask. This covers object and array literal members, arguments, initializers and return values. A member of a literal whose own slot is any takes the literal's slot. Array, setTimeout and setImmediate keep their refusal: their values lack Array's statics and the timers' util.promisify.custom getter. Object literal members of TypeScript sources no longer defer a failed lowering to a runtime closure. A builtin with no value (Intl, Proxy, parseInt, process, a typed slot) reports SC2020 at compile time. util.inspect now prints a plain object's Symbol.toStringTag as Node does: `Object [Math] {}`, `[Object [Y]]` past the depth limit, and `[Object: null prototype] [N] {}`. The tag is read through the prototype chain and getters, and it is skipped when it is empty, not a string, the name "Object", listed as an own enumerable key, or its getter throws. --- .../lowering/expressions/identifiers.ts | 4 +- .../lowering/expressions/object-literals.ts | 11 ++-- .../src/frontend/lowering/global-values.ts | 49 +++++++++++++++++ .../builtin-values-untyped-slots.ts.json | 6 +++ .../object-to-string-tag-inspect.mjs.json | 6 +++ .../builtin-values-without-lowering.ts.json | 6 +++ packages/runtime/src/scr_dyn.h | 3 ++ packages/runtime/src/scr_inspect.c | 51 ++++++++++++++++++ packages/runtime/src/scr_json.c | 1 + tests/corpus/builtin-values-untyped-slots.ts | 53 +++++++++++++++++++ tests/corpus/object-to-string-tag-inspect.mjs | 37 +++++++++++++ .../builtin-values-without-lowering.ts | 11 ++++ .../builtin-values-without-lowering.ts.txt | 53 +++++++++++++++++++ .../builtin-values-untyped-slots.ts.json | 1 + .../object-to-string-tag-inspect.mjs.json | 1 + 15 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-values-untyped-slots.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-to-string-tag-inspect.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-values-without-lowering.ts.json create mode 100644 tests/corpus/builtin-values-untyped-slots.ts create mode 100644 tests/corpus/object-to-string-tag-inspect.mjs create mode 100644 tests/diagnostics/builtin-values-without-lowering.ts create mode 100644 tests/harness/__snapshots__/builtin-values-without-lowering.ts.txt create mode 100644 tests/harness/corpus-costs/builtin-values-untyped-slots.ts.json create mode 100644 tests/harness/corpus-costs/object-to-string-tag-inspect.mjs.json diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index ebe7e2556..45c100535 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -1,5 +1,5 @@ /* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */ -import { globalMemberValue } from "../global-values.js"; +import { globalMemberValue, tsUntypedGlobalValue } from "../global-values.js"; import { predeclareForwardClass } from "../lower-stmts.js"; import { cjsMemberAliasValueOf } from "../cjs-exports-object.js"; import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js"; @@ -825,6 +825,8 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { return { kind: "strLit", value: `[builtin ${canonical}]`, type: STRING, loc }; } + const untyped = tsUntypedGlobalValue(lowerer, expr, canonical, loc); + if (untyped !== null) return untyped; // The families with a WHY: each hint states what makes the // surface genuinely non-static (or what to use instead). const globalHints: Record = { diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index a3d2a5b11..e5b464edc 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -47,6 +47,7 @@ import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs. import { builtinGlobalValue } from "./property-access.js"; import { holdsEvolvingArray } from "../lower-stmts.js"; import { builtinNamespaceValue } from "../builtin-namespaces.js"; +import { tsUntypedGlobalValue } from "../global-values.js"; /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the * contextual type when tsc has one (annotated declarations, arguments, @@ -446,8 +447,11 @@ export function lowerDynObjectLiteral( // A PURE member read a JS file cannot lower (a namespace object // in an export aggregate): the slot takes a boxed fence closure — // the diagnostics defer to the runtime-fence ledger, the object - // builds, and only USING the member stops the run. - const pureMember = ts.isIdentifier(valueExpr); + // builds, and only USING the member stops the run. A TypeScript + // source keeps the compile-time refusal: the closure would answer + // typeof "function" and undefined members where Node reads the + // value. + const pureMember = ts.isIdentifier(valueExpr) && isJsSourceFile(ts.getSourceFileOfNode(prop)); if (!(err instanceof PoisonError) || !pureMember) throw err; const fence = lowerer.deferToRuntimeFence(propDiagsBefore, prop, { kind: "closure", @@ -3540,7 +3544,8 @@ function shorthandBuiltinValue( const name = ts.symbolName(symbol); return ( (js ? builtinNamespaceValue(lowerer, name, locOf(prop.name)) : null) ?? - builtinGlobalValue(name, locOf(prop.name), js) + builtinGlobalValue(name, locOf(prop.name), js) ?? + tsUntypedGlobalValue(lowerer, prop.name as ts.Identifier, name, locOf(prop.name)) ); } diff --git a/packages/compiler/src/frontend/lowering/global-values.ts b/packages/compiler/src/frontend/lowering/global-values.ts index b5d5a7225..5b8d828e9 100644 --- a/packages/compiler/src/frontend/lowering/global-values.ts +++ b/packages/compiler/src/frontend/lowering/global-values.ts @@ -138,6 +138,55 @@ export function jsGlobalValueByName( ); } +/** Globals whose values a TypeScript read keeps refused: the Array + * constructor value has none of Array's static members (`o.Array.isArray` + * would read undefined), and the timer values lack the + * util.promisify.custom getter Node's util.inspect lists. */ +const UNTYPED_UNMODELED: ReadonlySet = new Set(["Array", "setTimeout", "setImmediate"]); + +/** The value a TypeScript source reads for the declared global `name` at + * `node` when the checker types the slot it flows into as any or unknown + * (`const o: any = { j: JSON }`, a call to a parameter typed unknown): the + * value a JavaScript source reads. Null in JavaScript sources, in typed + * slots, and for a global without a value, whose read keeps its SC2020 + * refusal. Null for the primitive constructors too: a TypeScript read of + * String, Number, Boolean or BigInt is the closure with the typed + * signature (lowerIdentifierExpr), and `{ String }` must equal it. */ +export function tsUntypedGlobalValue( + lowerer: Lowerer, + node: ts.Expression, + name: string, + loc: SrcLoc, +): IrExpr | null { + if (isJsSourceFile(ts.getSourceFileOfNode(node)) || UNTYPED_UNMODELED.has(name)) return null; + if (name === "String" || name === "Number" || name === "Boolean" || name === "BigInt") + return null; + if (!flowsIntoUntypedSlot(lowerer, node)) return null; + return jsGlobalValueByName(lowerer, name, loc); +} + +/** Whether the checker types the slot `node` flows into as any or unknown. + * A member of an object or array literal whose own slot is any has no + * contextual type, so the literal's slot decides. */ +function flowsIntoUntypedSlot(lowerer: Lowerer, node: ts.Expression): boolean { + let value: ts.Expression = node; + for (;;) { + const slot = lowerer.checker.getContextualType(value); + if (slot !== undefined) return (slot.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; + const parent = value.parent; + if ( + (ts.isPropertyAssignment(parent) && parent.initializer === value) || + ts.isShorthandPropertyAssignment(parent) + ) { + value = parent.parent as ts.ObjectLiteralExpression; + } else if (ts.isArrayLiteralExpression(parent) || ts.isParenthesizedExpression(parent)) { + value = parent; + } else { + return false; + } + } +} + /** jsGlobalValueByName for the globals other than the interned builtin * functions and constructors (builtinGlobalValue). */ export function jsSpecialGlobalValue(lowerer: Lowerer, name: string, loc: SrcLoc): IrExpr | null { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-values-untyped-slots.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-values-untyped-slots.ts.json new file mode 100644 index 000000000..1d0c4e6d9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-values-untyped-slots.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/builtin-values-untyped-slots.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-to-string-tag-inspect.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-to-string-tag-inspect.mjs.json new file mode 100644 index 000000000..3ecd83187 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-to-string-tag-inspect.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/object-to-string-tag-inspect.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-values-without-lowering.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-values-without-lowering.ts.json new file mode 100644 index 000000000..e86bb86ce --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-values-without-lowering.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/builtin-values-without-lowering.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index f4bc8db68..7cd0c2b19 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1220,6 +1220,9 @@ 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); void scr_dyn_install_to_string_tag_symbol(ScrDyn *key); +/* Symbol.toStringTag (borrowed), or NULL before the program first + * evaluates it. */ +const ScrDyn *scr_dyn_to_string_tag_symbol(void); /* Whether key is Symbol.asyncIterator. */ bool scr_dyn_is_async_iterator_symbol(const ScrDyn *key); /* Symbol.asyncIterator (+1), or NULL before the symbol unit installed it. */ diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index a015c25fe..84a370aa9 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -1708,7 +1708,48 @@ static bool insp_obj_shows(const ScrDynProp *ent) { return ent->enumerable || (g_show_hidden && !(ent->key_len && ent->key[0] == '%')); } +/* The @@toStringTag Node brackets after a plain object's prefix + * (`Object [Math] {}`), +1: a nonempty string read through the object and + * its prototypes, unless the object's own tag is listed with its keys + * (an enumerable one, or any under showHidden). NULL otherwise, and when + * the tag's getter throws, as in Node. */ +static ScrStr *insp_plain_object_tag(ScrDyn *d) { + const ScrDyn *key = scr_dyn_to_string_tag_symbol(); + if (!key) return NULL; + ScrDynProp own; + if (scr_dyn_own_symbol_prop(d, key, &own) && (own.enumerable || g_show_hidden)) return NULL; + ScrDyn *tag = scr_dyn_symbol_key_get(d, key, true); + if (!tag) { + if (scr_exc_pending()) scr_exc_clear(); + return NULL; + } + ScrStr *out = tag->kind == SCR_DYN_STR && tag->v.str->len ? scr_str_retain(tag->v.str) : NULL; + scr_dyn_release(tag); + return out; +} + +/* Node's getPrefix for a plain object, without the trailing space: "", + * "Object [tag]" ("Object" for the tag "Object"), or the null-prototype + * marker followed by " [tag]". +1. */ +static ScrStr *insp_plain_object_prefix(const ScrDyn *d, const ScrStr *tag) { + InspBuf out = {0}; + if (d->null_proto) ib_cstr(&out, "[Object: null prototype]"); + else if (tag) ib_cstr(&out, "Object"); + if (tag && !(tag->len == 6 && memcmp(tag->data, "Object", 6) == 0)) { + ib_cstr(&out, " ["); + ib_bytes(&out, tag->data, tag->len); + ib_char(&out, ']'); + } + return ib_take(&out); +} + static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char *cls) { + /* A plain object with a tag takes Node's tagged prefix in all three + * forms: `Object [Math] {}`, `[Object [Math]]` past the depth limit, + * and `Object [Math] { key: value }`. */ + ScrStr *tag = cls ? NULL : insp_plain_object_tag(d); + ScrStr *tagged = tag ? insp_plain_object_prefix(d, tag) : NULL; + if (tag) scr_str_release(tag); size_t shown = 0; for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { ScrDynProp ent = scr_dyn_obj_prop_at(d, i); @@ -1719,6 +1760,15 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char ScrDynProp ent; shown += scr_insp_symbol_entry(d, i, &ent); } + if (tagged && (shown == 0 || recurse > depth)) { + InspBuf out = {0}; + bool bracket = shown != 0 && !d->null_proto; + if (bracket) ib_char(&out, '['); + ib_bytes(&out, tagged->data, tagged->len); + ib_cstr(&out, bracket ? "]" : shown == 0 ? " {}" : ""); + scr_str_release(tagged); + return shown == 0 ? ib_take(&out) : insp_styled(ib_take(&out), INSP_SPECIAL); + } if (shown == 0) { if (cls) return scr_insp_class_tag(cls, " {}"); return d->null_proto ? scr_str_new("[Object: null prototype] {}", 27) @@ -1771,6 +1821,7 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char scr_str_release(entry); } ScrStr *base = cls ? scr_str_new(cls, strlen(cls)) + : tagged ? tagged : d->null_proto ? scr_str_new("[Object: null prototype]", 24) : scr_str_new("", 0); ScrStr *b0 = scr_str_new("{", 1); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index bee032446..638c2f1ba 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -261,6 +261,7 @@ void scr_dyn_install_to_string_tag_symbol(ScrDyn *key) { scr_to_string_tag_symbol = scr_dyn_retain(key); scr_atexit(scr_to_string_tag_symbol_cleanup); } +const ScrDyn *scr_dyn_to_string_tag_symbol(void) { return scr_to_string_tag_symbol; } void scr_dyn_install_iterator_symbol(ScrDyn *key) { if (scr_iterator_symbol) return; diff --git a/tests/corpus/builtin-values-untyped-slots.ts b/tests/corpus/builtin-values-untyped-slots.ts new file mode 100644 index 000000000..4185a524e --- /dev/null +++ b/tests/corpus/builtin-values-untyped-slots.ts @@ -0,0 +1,53 @@ +// Builtins read into any- and unknown-typed slots hold the values Node reads. +function take(x: unknown): any { + return x; +} +function give(): any { + return Math; +} + +const record: Record = { + json: JSON, + math: Math, + buffer: Buffer, +}; +const loose: any = { + JSON, + Reflect, + k: Map, + bytes: Uint8Array, + clone: structuredClone, +}; +const list: any[] = [JSON, Math, atob, btoa]; +const json: any = record.json; +const math: any = record.math; + +console.log(typeof json, typeof math, typeof loose.Reflect, typeof loose.clone); +console.log( + json === loose.JSON, + json === list[0], + math === list[1], + math === give(), +); +console.log( + take(JSON) === json, + take(Map) === loose.k, + take(Buffer) === record.buffer, +); + +console.log(json.stringify({ a: [1, "x"] }), json.parse("[2,3]").length); +console.log(math.max(3, 9, 4), math.PI.toFixed(5), list[1].sqrt(16)); +console.log(loose.Reflect.ownKeys({ b: 1, c: 2 })); +console.log(new loose.k([[1, "one"]]).get(1), new loose.bytes(3).length); +console.log((record.buffer as any).from("hi").toString("hex")); +console.log(loose.clone({ n: [1, 2] }).n[1], list[2]("aGk="), list[3]("hi")); +console.log(math.max.name, math.max.length, json.parse.name, loose.clone.name); + +console.log(Math, JSON, Reflect); +console.log({ nested: { math: record.math } }); +console.log(Object.prototype.toString.call(math), String(json)); +console.log(JSON.stringify({ math, json })); +console.log( + Object.keys(math).length, + Object.getOwnPropertyNames(json).join(","), +); diff --git a/tests/corpus/object-to-string-tag-inspect.mjs b/tests/corpus/object-to-string-tag-inspect.mjs new file mode 100644 index 000000000..c72f43e65 --- /dev/null +++ b/tests/corpus/object-to-string-tag-inspect.mjs @@ -0,0 +1,37 @@ +// util.inspect brackets a plain object's Symbol.toStringTag after its prefix. +const hidden = {}; +Object.defineProperty(hidden, Symbol.toStringTag, { value: "X" }); +const keyed = { a: 1 }; +Object.defineProperty(keyed, Symbol.toStringTag, { value: "Y" }); +const bare = Object.create(null); +Object.defineProperty(bare, Symbol.toStringTag, { value: "N" }); +const bareKeyed = Object.create(null); +bareKeyed.k = 1; +Object.defineProperty(bareKeyed, Symbol.toStringTag, { value: "N" }); +const empty = {}; +Object.defineProperty(empty, Symbol.toStringTag, { value: "" }); +const named = { z: 1 }; +Object.defineProperty(named, Symbol.toStringTag, { value: "Object" }); +const getter = { c: 1 }; +Object.defineProperty(getter, Symbol.toStringTag, { get: () => "G" }); +const throwing = { c: 1 }; +Object.defineProperty(throwing, Symbol.toStringTag, { + get() { + throw new Error("boom"); + }, +}); +const numeric = { c: 1 }; +Object.defineProperty(numeric, Symbol.toStringTag, { value: 7 }); +const inherited = Object.create(hidden); +const listed = { [Symbol.toStringTag]: "Z", b: 2 }; + +console.log(hidden, keyed, bare, bareKeyed); +console.log(empty, named, getter, throwing, numeric); +console.log(inherited, listed, [keyed], { keyed }); +console.log({ a: { b: { keyed, bareKeyed, named, hidden } } }); +console.log(Math, JSON, Reflect, { a: { b: { c: Math } } }); +console.log( + Object.prototype.toString.call(hidden), + String(keyed), + `${inherited}`, +); diff --git a/tests/diagnostics/builtin-values-without-lowering.ts b/tests/diagnostics/builtin-values-without-lowering.ts new file mode 100644 index 000000000..b8b5c90a1 --- /dev/null +++ b/tests/diagnostics/builtin-values-without-lowering.ts @@ -0,0 +1,11 @@ +// Builtins without a lowered value refuse at compile time in TypeScript, +// including as object literal members. +function take(x: unknown): unknown { + return x; +} + +const inferred = { json: JSON }; +const loose: any = { intl: Intl, proxy: Proxy }; +const timers: Record = { setTimeout }; +const list: any[] = [Array, parseInt]; +console.log(inferred, loose, timers, list, take(process)); diff --git a/tests/harness/__snapshots__/builtin-values-without-lowering.ts.txt b/tests/harness/__snapshots__/builtin-values-without-lowering.ts.txt new file mode 100644 index 000000000..97ee5fc49 --- /dev/null +++ b/tests/harness/__snapshots__/builtin-values-without-lowering.ts.txt @@ -0,0 +1,53 @@ +builtin-values-without-lowering.ts:7:7 - error SC2009: values of type '{ json: JSON; }' cannot be compiled: the record shape is supported, but its member 'json.parse' has type '{ (text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined): any; (text: string): unknown; (text: string, reviver: (this: unknown, key: string, value: unknown) => unknown): unknown; }', which does not compile + + 6 | + 7 | const inferred = { json: JSON }; + | ^~~~~~~~ + 8 | const loose: any = { intl: Intl, proxy: Proxy }; + + hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') + +builtin-values-without-lowering.ts:7:18 - error SC2009: values of type '{ json: JSON; }' cannot be compiled: the record shape is supported, but its member 'json.parse' has type '{ (text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined): any; (text: string): unknown; (text: string, reviver: (this: unknown, key: string, value: unknown) => unknown): unknown; }', which does not compile + + 6 | + 7 | const inferred = { json: JSON }; + | ^~~~~~~~~~~~~~ + 8 | const loose: any = { intl: Intl, proxy: Proxy }; + + hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') + +builtin-values-without-lowering.ts:8:28 - error SC2020: 'Intl' is part of the standard library types but has no scriptc lowering yet + + 7 | const inferred = { json: JSON }; + 8 | const loose: any = { intl: Intl, proxy: Proxy }; + | ^~~~ + 9 | const timers: Record = { setTimeout }; + + hint: native Intl supports default Unicode grapheme segmentation and the composed new Intl.NumberFormat("en-US").format(x); locale negotiation and other ICU-backed operations remain unsupported + +builtin-values-without-lowering.ts:9:43 - error SC2020: 'setTimeout' is part of the standard library types but has no scriptc lowering yet + + 8 | const loose: any = { intl: Intl, proxy: Proxy }; + 9 | const timers: Record = { setTimeout }; + | ^~~~~~~~~~ + 10 | const list: any[] = [Array, parseInt]; + + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + +builtin-values-without-lowering.ts:10:22 - error SC2020: 'Array' is part of the standard library types but has no scriptc lowering yet + + 9 | const timers: Record = { setTimeout }; + 10 | const list: any[] = [Array, parseInt]; + | ^~~~~ + 11 | console.log(inferred, loose, timers, list, take(process)); + + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + +builtin-values-without-lowering.ts:11:13 - error SC2004: uses of 'inferred' inherit the blocker on its declaration + + 10 | const list: any[] = [Array, parseInt]; + 11 | console.log(inferred, loose, timers, list, take(process)); + | ^~~~~~~~ + 12 | + + hint: the declaration of 'inferred' 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/builtin-values-untyped-slots.ts.json b/tests/harness/corpus-costs/builtin-values-untyped-slots.ts.json new file mode 100644 index 000000000..b42466fc7 --- /dev/null +++ b/tests/harness/corpus-costs/builtin-values-untyped-slots.ts.json @@ -0,0 +1 @@ +{ "plain": 4051, "san": 5400 } diff --git a/tests/harness/corpus-costs/object-to-string-tag-inspect.mjs.json b/tests/harness/corpus-costs/object-to-string-tag-inspect.mjs.json new file mode 100644 index 000000000..ed19ae7fb --- /dev/null +++ b/tests/harness/corpus-costs/object-to-string-tag-inspect.mjs.json @@ -0,0 +1 @@ +{ "plain": 2793, "san": 3480 } From 352842f020e0bc6b8b058c9278b018b846784fc9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 08:37:25 -0500 Subject: [PATCH 62/70] Scope early errors to function bodies and class strictness, and match the upstream compareArray reads - A function body or class static block treats its top-level function declarations as var-scoped: duplicates are legal in strict code, and a let/const/class of the same name is SC0006. - Strictness is the code around a node: a function declaration's own "use strict" no longer makes its declaring context strict. - Every part of a class is strict; object literal methods and accessors are strict only by their own prologue or their surroundings, and an arrow function's block body can carry "use strict". - The bare compareArray reads b.length, then a.length, and a.length again before every element pair, converting it as the upstream `i < a.length` does, so a string length compares its elements. - Tests that relied on the old handling of undeclared names now use constructs that are still refused or still compile: the installed-entry diagnostics fixture reports a syntax error (a read of an undeclared name is now Node's run-time ReferenceError), and static-objects-order's loops.js stores from its for-in head into `undefined` instead of an undeclared name (a store into an undeclared name refuses its package with SC1091). --- .../src/frontend/early-errors.test.ts | 43 ++++++++++++ .../compiler/src/frontend/early-errors.ts | 66 ++++++++++++------- .../static-objects-order/loops.js | 10 +-- tests/harness/installed-entry.test.ts | 4 +- tests/harness/static-objects-order.test.ts | 6 +- tests/harness/test262.test.ts | 17 +++++ tests/test262/harness.ts | 14 ++-- 7 files changed, 123 insertions(+), 37 deletions(-) create mode 100644 packages/compiler/src/frontend/early-errors.test.ts diff --git a/packages/compiler/src/frontend/early-errors.test.ts b/packages/compiler/src/frontend/early-errors.test.ts new file mode 100644 index 000000000..718294a48 --- /dev/null +++ b/packages/compiler/src/frontend/early-errors.test.ts @@ -0,0 +1,43 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { checkPreflight, loadProgram } from "./program-node.js"; + +/** The preflight diagnostics of a CommonJS script (sloppy unless it says + * "use strict"). */ +function diagnostics(source: string) { + const directory = mkdtempSync(join(tmpdir(), "scriptc-early-errors-")); + writeFileSync(join(directory, "main.cjs"), source); + const load = loadProgram(join(directory, "main.cjs")); + try { + return checkPreflight(load).map((diagnostic) => diagnostic.code); + } finally { + load.dispose(); + rmSync(directory, { recursive: true, force: true }); + } +} + +test.for([ + // A function body's top-level function declarations are var-scoped, so + // duplicates are legal in strict code too. + 'function outer() { "use strict"; function d() {} function d() {} }\nouter();', + "class C { static { function s() {} function s() {} } }\nnew C();", + // Object literal methods and accessors are sloppy unless they say otherwise. + "var o = { m() { if (true) function f() {} } };\no.m();", + "var o = { get g() { { function h() {} function h() {} } return 1; } };\no.g;", + // A function's own directive makes its body strict, not the code declaring it. + 'if (true) function k() { "use strict"; }', + '{ function z() { "use strict"; } function z() {} }', +])("Node accepts: %s", (source) => { + expect(diagnostics(source)).toEqual([]); +}); + +test.for([ + ["function outer() { let d; function d() {} }\nouter();", "SC0006"], + ['(() => { "use strict"; if (true) function f() {} })();', "SC0007"], + ["class C { m() { if (true) function f() {} } }\nnew C().m();", "SC0007"], + ['var o = { m() { "use strict"; { function h() {} function h() {} } } };\no.m();', "SC0005"], +])("Node refuses: %s", ([source, code]) => { + expect(diagnostics(source!)).toContain(code); +}); diff --git a/packages/compiler/src/frontend/early-errors.ts b/packages/compiler/src/frontend/early-errors.ts index 37dd71b48..e37112115 100644 --- a/packages/compiler/src/frontend/early-errors.ts +++ b/packages/compiler/src/frontend/early-errors.ts @@ -92,7 +92,13 @@ type EarlyErrorCode = (typeof EARLY_ERROR_CODES)[number]; * reach into nested blocks (a `var` anywhere below the block belongs to it) * but never into a function or class body. The duplicate rule's one carve-out * is Annex B's B.3.3.1: two plain function declarations of the same name in - * the same block are legal in non-strict code. */ + * the same block are legal in non-strict code. + * + * A function body and a class static block have the same two rules over + * their top-level declarations (FunctionStatementList and + * ClassStaticBlockStatementList), but there a function declaration is + * var-scoped: it is one of the VarDeclaredNames, so two of them are legal in + * strict code too, while `let f; function f() {}` still collides. */ function checkStatementListScope( sf: ts.SourceFile, @@ -107,10 +113,11 @@ function checkStatementListScope( } if (statements === null) return; if (statements.length < 2) return; + const functionBody = isFunctionBody(node); const lexical = new Map(); for (const stmt of statements) { - for (const [name, decl] of lexicalDeclarationsOf(stmt)) { + for (const [name, decl] of lexicalDeclarationsOf(stmt, functionBody)) { const list = lexical.get(name); if (list === undefined) lexical.set(name, [decl]); else list.push(decl); @@ -119,7 +126,11 @@ function checkStatementListScope( if (lexical.size === 0) return; const varNames = new Set(); - for (const stmt of statements) collectVarDeclaredNames(stmt, varNames); + for (const stmt of statements) { + collectVarDeclaredNames(stmt, varNames); + if (functionBody && ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) + varNames.add(stmt.name.text); + } for (const [name, decls] of lexical) { if (varNames.has(name)) { @@ -127,7 +138,9 @@ function checkStatementListScope( diags, LEXICAL_VAR_COLLISION, decls[0]!, - `the lexical declaration '${name}' collides with a 'var' declaration of the same name in this block`, + functionBody + ? `the lexical declaration '${name}' collides with a 'var' or function declaration of the same name in this function body` + : `the lexical declaration '${name}' collides with a 'var' declaration of the same name in this block`, ); continue; } @@ -146,10 +159,22 @@ function checkStatementListScope( } } +/** Whether a block is the body of a function or a class static block, whose + * top-level function declarations are var-scoped. */ +function isFunctionBody(node: ts.Node): boolean { + const parent = node.parent; + if (parent === undefined || !ts.isBlock(node)) return false; + if (ts.isClassStaticBlockDeclaration(parent)) return true; + return ts.isFunctionLike(parent) && (parent as { body?: ts.Node }).body === node; +} + /** The names a DIRECT statement of a statement list declares lexically - * (let/const/using, class, and every function declaration form), paired with - * the node to point the diagnostic at. */ -function lexicalDeclarationsOf(stmt: ts.Statement): Array<[string, ts.Node]> { + * (let/const/using, class, and every function declaration form outside a + * function body), paired with the node to point the diagnostic at. */ +function lexicalDeclarationsOf( + stmt: ts.Statement, + functionBody: boolean, +): Array<[string, ts.Node]> { const names: Array<[string, ts.Node]> = []; if (ts.isVariableStatement(stmt)) { if ((stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0) return names; @@ -157,7 +182,7 @@ function lexicalDeclarationsOf(stmt: ts.Statement): Array<[string, ts.Node]> { collectBindingNames(decl.name, names); return names; } - if (ts.isClassDeclaration(stmt) || ts.isFunctionDeclaration(stmt)) { + if (ts.isClassDeclaration(stmt) || (ts.isFunctionDeclaration(stmt) && !functionBody)) { const name = stmt.name; if (name !== undefined && ts.isIdentifier(name)) names.push([name.text, stmt]); } @@ -322,28 +347,23 @@ function labelledFunctionOf(stmt: ts.Statement): ts.FunctionDeclaration | null { /* ── strictness of the code around a node ─────────────────────────────── * * A directive prologue's "use strict" applies to the function (or script) it - * opens, arrows cannot carry one, and a class body is strict by - * construction. Module sources are strict whether they say so or not, and - * Node's classification — not the checker's forced external-module scope — - * decides which sources are modules. */ + * opens, an arrow function's block body included, and every part of a class is strict by + * construction. Object literal methods and accessors are ordinary functions: + * strict only by their own prologue or their surroundings. Module sources are + * strict whether they say so or not, and Node's classification — not the + * checker's forced external-module scope — decides which sources are modules. + * + * The walk starts at the node's parent: a function declaration's own "use + * strict" makes its body strict, not the code that declares it. */ function isStrictCode( sf: ts.SourceFile, node: ts.Node, isModuleCode: (sf: ts.SourceFile) => boolean, ): boolean { if (isModuleCode(sf)) return true; - for (let n: ts.Node | undefined = node; n !== undefined; n = n.parent) { + for (let n: ts.Node | undefined = node.parent; n !== undefined; n = n.parent) { if (ts.isSourceFile(n)) return hasUseStrictPrologue(n.statements.nodes); - if ( - ts.isClassStaticBlockDeclaration(n) || - ts.isMethodDeclaration(n) || - ts.isConstructorDeclaration(n) || - ts.isGetAccessorDeclaration(n) || - ts.isSetAccessorDeclaration(n) || - ts.isPropertyDeclaration(n) - ) - return true; - if (ts.isArrowFunction(n)) continue; + if (ts.isClassDeclaration(n) || ts.isClassExpression(n)) return true; if (ts.isFunctionLike(n)) { const body = n.body; if (body !== undefined && ts.isBlock(body) && hasUseStrictPrologue(body.statements.nodes)) diff --git a/tests/fixtures/npm/cases/static-objects-order/node_modules/static-objects-order/loops.js b/tests/fixtures/npm/cases/static-objects-order/node_modules/static-objects-order/loops.js index ef5c2565b..8b759735e 100644 --- a/tests/fixtures/npm/cases/static-objects-order/node_modules/static-objects-order/loops.js +++ b/tests/fixtures/npm/cases/static-objects-order/node_modules/static-objects-order/loops.js @@ -1,13 +1,15 @@ // Methods that store from a for-in or for-of head where sloppy mode and -// strict mode differ: into an undeclared name (sloppy mode creates a -// global) and into a property of a string (sloppy mode drops the store). +// strict mode differ: into the read-only global `undefined` and into a +// property of a string (sloppy mode drops both stores; strict mode throws). +// A store into an undeclared name is refused for the whole package (SC1091), +// so it cannot appear here. var shapes = {}; shapes.Bag = function (size) { this.size = size; }; shapes.Bag.prototype.lastKey = function () { - for (bagLastKey in { first: 1, last: 2 }); - return bagLastKey + " " + this.size; + for (undefined in { first: 1, last: 2 }); + return typeof undefined + " " + this.size; }; shapes.Cell = function (label) { this.label = label; diff --git a/tests/harness/installed-entry.test.ts b/tests/harness/installed-entry.test.ts index 0bf9928a4..2c7473941 100644 --- a/tests/harness/installed-entry.test.ts +++ b/tests/harness/installed-entry.test.ts @@ -132,7 +132,9 @@ test("explicit installed JavaScript entry keeps diagnostics in its own modules", mkdirSync(dirname(entry), { recursive: true }); writeFileSync(join(dirname(entry), "package.json"), '{"type":"module"}\n'); writeFileSync(entry, 'import { value } from "./value.js"; console.log(value);\n'); - writeFileSync(join(dirname(entry), "value.js"), "export const value = missingName;\n"); + // A syntax error: a read of an undeclared name is Node's run-time + // ReferenceError in JavaScript, not a diagnostic. + writeFileSync(join(dirname(entry), "value.js"), "export const value = ;\n"); const { coverage } = analyze(entry); expect(coverage.preflightFailed).toBe(true); expect(coverage.diagnostics.map((d) => d.code)).toEqual(["SC0001"]); diff --git a/tests/harness/static-objects-order.test.ts b/tests/harness/static-objects-order.test.ts index 208048f18..bb68a0908 100644 --- a/tests/harness/static-objects-order.test.ts +++ b/tests/harness/static-objects-order.test.ts @@ -104,9 +104,9 @@ test( expect(rewrittenIfAny(debug, join(pkg, "coerce.js")) ?? "").not.toContain( "class __ns_shapes_Box", ); - // loops.js's methods store from for-in and for-of heads into an - // undeclared name and into a property of a string, which throws in a - // class body. + // loops.js's methods store from for-in and for-of heads into the + // read-only global `undefined` and into a property of a string, which + // throws in a class body. const loops = rewrittenIfAny(debug, join(pkg, "loops.js")) ?? ""; expect(loops).not.toContain("class __ns_shapes_Bag"); expect(loops).not.toContain("class __ns_shapes_Cell"); diff --git a/tests/harness/test262.test.ts b/tests/harness/test262.test.ts index 948c016bb..72dad6531 100644 --- a/tests/harness/test262.test.ts +++ b/tests/harness/test262.test.ts @@ -204,6 +204,23 @@ assert.notSameValue(1, "1"); status: "pass", source: "assert(compareArray(1, 2)); assert(compareArray({}, {}));", }, + { + name: "bare compareArray compares the elements of a string length", + status: "pass", + source: + 'assert(!compareArray({ length: "2", 0: 1, 1: 2 }, { length: "2", 0: 1, 1: 3 })); assert(compareArray({ length: "1", 0: 1 }, { length: "1", 0: 1 }));', + }, + { + name: "bare compareArray reads the lengths in the upstream order", + status: "pass", + source: ` +var log = []; +var a = { get length() { log.push("a"); return 1; }, 0: 1 }; +var b = { get length() { log.push("b"); return 1; }, 0: 1 }; +assert(compareArray(a, b)); +assert.compareArray(log, ["b", "a", "a", "a"]); +`, + }, { name: "bare compareArray does not throw on a mismatch", status: "fail", diff --git a/tests/test262/harness.ts b/tests/test262/harness.ts index 8a0f7bd1a..40e09734a 100644 --- a/tests/test262/harness.ts +++ b/tests/test262/harness.ts @@ -70,13 +70,15 @@ function elementAt(value: unknown, index: number): unknown { /** The bare `compareArray(a, b)` the upstream `harness/assert.js` defines * alongside `assert`. It RETURNS a boolean (the throwing assertion is - * `assert.compareArray`), and it has no shape checks of its own: it reads - * `.length` off both operands, so a null operand throws TypeError there and - * here, and two operands without a length of their own compare equal. */ + * `assert.compareArray`), and it has no shape checks of its own. It makes the + * upstream reads in the upstream order: `b.length`, then `a.length`, then + * `a.length` again before every element pair, `b[i]` before `a[i]`. A null + * operand throws TypeError there and here, and two operands without a length + * of their own compare equal. The loop bound converts the length the way the + * upstream `i < a.length` does, so a length of "2" compares two elements. */ export function compareArray(actual: unknown, expected: unknown): boolean { - const length = lengthOf(actual); - if (lengthOf(expected) !== length) return false; - for (let i = 0; typeof length === "number" && i < length; i++) { + if (lengthOf(expected) !== lengthOf(actual)) return false; + for (let i = 0; i < Number(lengthOf(actual)); i++) { if (!same(elementAt(expected, i), elementAt(actual, i))) return false; } return true; From ac978fc686a517dd6532b6bb79056a553017b889 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 08:48:35 -0500 Subject: [PATCH 63/70] Don't offset a null pointer when an array has no dense storage scr_arr_alloc_start subtracted the prefix from the dense block even when the array had no storage, which computes NULL - 0. That is undefined in C, and the macOS lane's UndefinedBehaviorSanitizer reports it ("applying zero offset to null pointer" at scr_array.c:60) in scr_json.test.ts's weak-map program. Without a prefix, the function now returns the block as it is. --- packages/runtime/src/scr_array.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index 7731c98ea..d90a2ec6f 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -55,9 +55,12 @@ static size_t scr_arr_prefix(const ScrArr *a) { return (a->spare & SCR_ARR_HAS_EXT) ? SCR_ARR_EXT_PREFIX : 0; } -/* The start of the block the dense storage was allocated as. */ +/* The start of the block the dense storage was allocated as. Without a + * prefix it is the block itself, which is NULL while the array has no + * storage (offsetting a null pointer, even by 0, is undefined). */ static void *scr_arr_alloc_start(const ScrArr *a) { - return (char *)scr_arr_block(a) - scr_arr_prefix(a); + size_t prefix = scr_arr_prefix(a); + return prefix ? (char *)scr_arr_block(a) - prefix : (void *)scr_arr_block(a); } /* Bytes per dense slot: an 8-byte value or a boolean byte, and its state From 78400c316fff9a4e4172560b774f0f3b068b20d2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 09:19:10 -0500 Subject: [PATCH 64/70] Give a default export the name default in reuse's declaration tables A default export without a name of its own (`export default function (...)`, `export default class`, an `export default` expression) declared no name, so a change to it left the units of its default imports applied. Each default export now declares `default` beside its own names, and a change to it reaches the names that import it. --- .../src/frontend/lowering/unit-reuse.ts | 22 ++++++++--- packages/compiler/test/unit-reuse.test.ts | 38 +++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/unit-reuse.ts b/packages/compiler/src/frontend/lowering/unit-reuse.ts index 0a5289c77..7d89b996d 100644 --- a/packages/compiler/src/frontend/lowering/unit-reuse.ts +++ b/packages/compiler/src/frontend/lowering/unit-reuse.ts @@ -428,6 +428,22 @@ function surfaceText(sf: ts.SourceFile, node: ts.Node, js: boolean): string { * base of an expando `f.prop = value`; in JavaScript, where such statements * also define exports, every name and string it spells. */ function statementNames(statement: ts.Statement, js: boolean, hooks: UnitReuseHooks): string[] { + const names = declaredNames(statement, js, hooks); + // A default export also declares the module's `default`, the name a + // default import holds (Alias), also when it has no name of its own. + return isDefaultExport(statement) && !names.includes("default") ? [...names, "default"] : names; +} + +/** Whether `statement` is its module's default export: `export default` + * or `export =`. */ +function isDefaultExport(statement: ts.Statement): boolean { + if (ts.isExportAssignment(statement)) return true; + const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined; + return modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) === true; +} + +/** The names `statement` declares (statementNames), apart from `default`. */ +function declaredNames(statement: ts.Statement, js: boolean, hooks: UnitReuseHooks): string[] { if (ts.isVariableStatement(statement)) { const names: string[] = []; for (const declaration of statement.declarationList.declarations.nodes) { @@ -575,11 +591,7 @@ function aliasesOf(statement: ts.Statement, names: readonly string[], out: Alias if (clause !== undefined && ts.isNamedExports(clause)) specifiers(clause.elements.nodes); return; } - const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined; - if ( - ts.isExportAssignment(statement) || - modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) === true - ) { + if (isDefaultExport(statement)) { for (const name of names) add(name, "default"); return; } diff --git a/packages/compiler/test/unit-reuse.test.ts b/packages/compiler/test/unit-reuse.test.ts index da79ced88..b455e33bc 100644 --- a/packages/compiler/test/unit-reuse.test.ts +++ b/packages/compiler/test/unit-reuse.test.ts @@ -378,3 +378,41 @@ test("an import of a renamed export or of a default export lowers again when its await workspace.dispose(); } }, 300_000); + +test("an import of an anonymous default export lowers again when its signature changes", async () => { + const workspace = await Workspace.create({ + "values.ts": [ + "export default function (value: number): number {", + " return value * 2;", + "}", + "export function other(value: number): number {", + " return value + 1;", + "}", + "", + ].join("\n"), + "main.ts": [ + 'import grow, { other } from "./values.js";', + "function grown(value: number): number {", + " return grow(value);", + "}", + "function next(value: number): number {", + " return other(value);", + "}", + "console.log(grown(4), next(1));", + "", + ].join("\n"), + }); + try { + await workspace.build(); + await workspace.edit( + "values.ts", + "export default function (value: number): number {", + "export default function (value: number, by = 1): number {", + ); + const applied = await workspace.build(); + expect(has(applied, "grown")).toBe(false); + expect(has(applied, "next")).toBe(true); + } finally { + await workspace.dispose(); + } +}, 300_000); From 7aaf98b53c20e6400623bc4c78be98b3ee676b59 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 10:24:12 -0500 Subject: [PATCH 65/70] Check nullable destructuring sources through their unit arm The TypeError check for a null or undefined source now reads the unit arm the value holds as a dyn null or undefined, instead of converting the whole union to dyn. A union whose data arm has no dyn form, such as a record holding a generator, compiles again instead of failing with SC9001. --- .../src/frontend/lowering/lower-stmts.ts | 36 +++++++++++++++++-- .../destructure-nullable-union-source.js | 30 ++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 05a8cfa2d..502761002 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -2581,14 +2581,46 @@ export function lowerDestructuringDecl( (arm.kind === "array" || arm.kind === "record") ) { const tag = def.arms.indexOf(arm); + const unionId = init.type.unionId; const utmp = lowerer.declareHiddenLocal("%destrU", init.type); out.push({ kind: "varDecl", localId: utmp.id, init, loc }); const uref: IrExpr = { kind: "varRef", localId: utmp.id, type: init.type, loc }; + // The failure branch holds one of the unit arms: the check reads it + // as the dyn null or undefined it is, so the data arm needs no dyn + // form (a record holding a generator has none). + const unitValue = (): IrExpr => { + const unitOf = (t: IrType): IrExpr => + lowerer.coerceToExpected( + { kind: "unitLit", unit: t.kind === "nullT" ? "null" : "undefined", type: t, loc }, + DYN, + ); + const units = def.arms.filter(isUnitType); + const last = units[units.length - 1]!; + return units.slice(0, -1).reduceRight( + (rest, t) => ({ + kind: "ternary", + cond: { + kind: "unionIsTag", + unionId, + tag: def.arms.indexOf(t), + negated: false, + value: uref, + type: BOOL, + loc, + }, + then: unitOf(t), + else_: rest, + type: DYN, + loc, + }), + unitOf(last), + ); + }; const spelling = destrSpellingOf(src); const check: IrExpr | null = ts.isArrayBindingPattern(decl.name) ? { kind: "dynIterN", - value: lowerer.coerceToExpected(uref, DYN), + value: unitValue(), count: decl.name.elements.nodes.length, ...(dynSpell ? { notIterableMessage: dynSpell } : {}), type: DYN, @@ -2597,7 +2629,7 @@ export function lowerDestructuringDecl( : spelling !== null ? { kind: "dynDestrCheck", - value: lowerer.coerceToExpected(uref, DYN), + value: unitValue(), spelling, ...firstPatternProp(lowerer, decl.name as ts.ObjectBindingPattern), type: DYN, diff --git a/tests/corpus/destructure-nullable-union-source.js b/tests/corpus/destructure-nullable-union-source.js index 84af858bf..b1d9de7b3 100644 --- a/tests/corpus/destructure-nullable-union-source.js +++ b/tests/corpus/destructure-nullable-union-source.js @@ -56,6 +56,36 @@ show("array-function-call", () => { console.log("never", first); }); +// A source that may be null or undefined, and data arms holding a +// generator, which has no untyped form. +function* count() { + yield 1; + yield 2; +} +function pick(kind) { + if (kind === 0) return null; + if (kind === 1) return undefined; + return { counter: count(), label: "picked" }; +} +function counters(present) { + return present ? [count(), count()] : null; +} +show("object-null-arm", () => { + const { counter } = pick(0); + console.log("never", counter); +}); +show("object-undefined-arm", () => { + const { label } = pick(1); + console.log("never", label); +}); +show("array-generator-elements", () => { + const [first] = counters(false); + console.log("never", first); +}); +const { counter, label } = pick(2); +const [left, right] = counters(true); +console.log(label, [...counter].join(","), [...left, ...right].join(",")); + // Sources that hold a value bind as before. const { index } = "abc".match(/b/); const [whole] = "abc".match(/c/); From 23e35772643d8ca870b0bfc58e212342110a8ac8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 10:34:42 -0500 Subject: [PATCH 66/70] Compute an array's allocation start as an integer The previous change avoided offsetting a null pointer with a branch in scr_arr_alloc_start. The function is inlined into scr_arr_release and scr_arr_destroy, so the branch changed their control flow, and the functions that changed since the committed runtime profile was collected then held 5.2% of its counts, over runtime-profile.test.ts's 5% limit. The address is now computed as an integer, which is defined for a null block and leaves the callers' control flow as it was. --- packages/runtime/src/scr_array.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index d90a2ec6f..7e2b63d88 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -55,12 +55,12 @@ static size_t scr_arr_prefix(const ScrArr *a) { return (a->spare & SCR_ARR_HAS_EXT) ? SCR_ARR_EXT_PREFIX : 0; } -/* The start of the block the dense storage was allocated as. Without a - * prefix it is the block itself, which is NULL while the array has no - * storage (offsetting a null pointer, even by 0, is undefined). */ +/* The start of the block the dense storage was allocated as. The block is + * NULL while the array has no storage, and offsetting a null pointer, even + * by 0, is undefined: the address is computed as an integer, which keeps the + * callers' control flow (and the runtime profile's view of it) as it was. */ static void *scr_arr_alloc_start(const ScrArr *a) { - size_t prefix = scr_arr_prefix(a); - return prefix ? (char *)scr_arr_block(a) - prefix : (void *)scr_arr_block(a); + return (void *)((uintptr_t)scr_arr_block(a) - scr_arr_prefix(a)); } /* Bytes per dense slot: an 8-byte value or a boolean byte, and its state From 51c9e3153344c68f6ad7e6f353752e7de612d33e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 10:42:30 -0500 Subject: [PATCH 67/70] Skip the receiver bracket for adapters of receiver-free callbacks A signature adapter forwards the ambient receiver to its original, and its closure carries the original's RECEIVER_FREE bit. Calls through the closure's own pointer tested that bit, but the direct entry of a known adapter target bracketed every call, because the adapter itself counts as a receiver reader. The direct entry now tests the bit too. The receiver readers analysis also admits more node kinds that run no user code: optional chains, record presence and absent fields, dynamic boxing, array for-of loops, typed array writes, typed array methods and constructors over typed operands, the module binding check and region intrinsics, performance.now, Atomics operations, string and number library calls over primitives, and new Error(message, options) without an options object. --- .../backend/llvm/constant-callbacks.test.ts | 56 +++++++++ .../compiler/src/backend/llvm/expr-calls.ts | 14 ++- .../backend/llvm/receiver-brackets.test.ts | 68 +++++++++++ packages/compiler/src/ir/receiver-readers.ts | 60 +++++++++- .../corpus/receiver-free-adapter.ts.json | 6 + tests/corpus/receiver-free-adapter.ts | 112 ++++++++++++++++++ .../receiver-free-adapter.ts.json | 1 + 7 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/src/backend/llvm/receiver-brackets.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/receiver-free-adapter.ts.json create mode 100644 tests/corpus/receiver-free-adapter.ts create mode 100644 tests/harness/corpus-costs/receiver-free-adapter.ts.json diff --git a/packages/compiler/src/backend/llvm/constant-callbacks.test.ts b/packages/compiler/src/backend/llvm/constant-callbacks.test.ts index 8bdab0408..a54f3294c 100644 --- a/packages/compiler/src/backend/llvm/constant-callbacks.test.ts +++ b/packages/compiler/src/backend/llvm/constant-callbacks.test.ts @@ -1,7 +1,10 @@ import { expect, test } from "vitest"; import { BOOL, + BYTES_U8, + DYN, F64, + STRING, VOID, funcOf, type IrExpr, @@ -260,6 +263,59 @@ test("receiver elision admits only synchronous receiver-free work", () => { expect(callbackIgnoresReceiver(fn)).toBe(false); }); +test("receiver elision admits library work that runs no user code", () => { + const str: IrExpr = { kind: "strLit", value: "a", type: STRING, loc }; + const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); + const dyn = ref("d", DYN); + const ignores = (expr: IrExpr): boolean => { + const fn = fixture().functions[0]!; + fn.body.unshift({ kind: "exprStmt", expr, loc }); + return callbackIgnoresReceiver(fn); + }; + const lib = (fn: string, args: IrExpr[]): IrExpr => + ({ kind: "libCall", fn, args, type: F64, loc }) as IrExpr; + const intrinsic = (name: string, args: IrExpr[]): IrExpr => + ({ kind: "intrinsic", name, args, type: VOID, loc }) as IrExpr; + // String and number members over primitives; a callback operand may run. + expect(ignores(lib("string.lastIndexOfFrom", [str, str, num(0)]))).toBe(true); + expect(ignores(lib("num.sameValue", [num(0), num(1)]))).toBe(true); + expect(ignores(lib("string.lastIndexOf", [str, closure("predicate")]))).toBe(false); + expect(ignores(lib("perf.now", []))).toBe(true); + // `new Error(message, options)` with a primitive message and no options. + const message: IrExpr = { kind: "dynFrom", value: str, type: DYN, loc }; + expect(ignores(lib("error.newOptions", [message, message]))).toBe(true); + expect(ignores(lib("error.newOptions", [message, dyn]))).toBe(false); + // The module binding check only throws; console output may inspect. + const initialized: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc }; + expect(ignores(intrinsic("module.tdzCheck", [initialized, str]))).toBe(true); + expect(ignores(intrinsic("console.log", [dyn]))).toBe(false); + // A typed array built from nothing or a typed source; a dynamic source + // may be an iterable. + const bytes = (source: IrExpr | null): IrExpr => ({ + kind: "bytesNew", + source, + type: BYTES_U8, + loc, + }); + expect(ignores(bytes(null))).toBe(true); + expect(ignores(bytes(dyn))).toBe(false); +}); + +test("receiver elision admits array for-of loops", () => { + const fn = fixture().functions[0]!; + const numbers = { kind: "array", elem: F64 } as const; + const one: IrExpr = { kind: "numLit", value: 1, type: F64, loc }; + fn.locals.push({ id: "x", name: "x", type: F64, mutable: false }); + fn.body.unshift({ + kind: "forOf", + localId: "x", + iterable: { kind: "arrayLit", elems: [one], type: numbers, loc }, + body: [], + loc, + }); + expect(callbackIgnoresReceiver(fn)).toBe(true); +}); + function callsOf(name: string, callee: string, extra: IrStmt[] = []): IrFunction { return { name, diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index 9ad8e5f05..e62fae52a 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -1003,6 +1003,16 @@ export function emitCallExpr( } else if (needsReceiver) B.line("call void @scr_dyn_this_pop()"); return result; }; + // The direct entry of a known target brackets the call when the target + // may observe the receiver. A signature adapter that only forwards the + // receiver to its original carries the original's RECEIVER_FREE bit + // (set where the adapter closure is built), so its entry tests the bit, + // like the entry through the closure's own pointer. + const invokeKnown = (target: string): string => { + const readers = host.receiverReaders; + if (readers.ignores(known!)) return invoke(target, false); + return invoke(target, true, args, readers.forwardsOnly(known!)); + }; const lending = lent && !dynamic ? known!.captures !== undefined @@ -1025,7 +1035,7 @@ export function emitCallExpr( (e.callee.kind === "closure" || (e.callee.kind === "varRef" && host.knownCallbacks.has(e.callee.localId))) ) { - t = invoke(lending ?? direct, !host.receiverReaders.ignores(known!)); + t = invokeKnown(lending ?? direct); if (quiet) pending = false; } else { const fnp = B.tmp(), @@ -1072,7 +1082,7 @@ export function emitCallExpr( B.line(`${matches} = icmp eq ptr ${fn}, ${direct}`); B.condBr(matches, fast, fallback); B.startBlock(fast); - const specialized = invoke(lending ?? direct, !host.receiverReaders.ignores(known!)); + const specialized = invokeKnown(lending ?? direct); // `fast` may have split for exception checks; join from the end. const fastEnd = B.currentLabel(); B.br(done); diff --git a/packages/compiler/src/backend/llvm/receiver-brackets.test.ts b/packages/compiler/src/backend/llvm/receiver-brackets.test.ts new file mode 100644 index 000000000..65ceb8f85 --- /dev/null +++ b/packages/compiler/src/backend/llvm/receiver-brackets.test.ts @@ -0,0 +1,68 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../../index.js"; +import { emitLlvmModule } from "./emitter.js"; + +async function llvmFor(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-receiver-brackets-")); + 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")); + return emitLlvmModule(deserializeModule(await readFile(output, "utf8"))); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function body(ir: string, name: string): string { + const match = new RegExp(`^define [^\\n]*@${name}\\(.*?^}`, "ms").exec(ir); + if (!match) throw new Error(`no function ${name}`); + return match[0]; +} + +test("a known signature adapter's direct entry brackets the receiver only when its original reads it", async () => { + const ir = await llvmFor(` +class Node { + kids: Node[] = []; + constructor(readonly kind: number) {} +} +function walk(root: Node, cb: (node: Node, depth: number) => void | "skip" | "stop"): void { + const stack: Node[] = [root]; + while (stack.length > 0) { + const node = stack.pop()!; + const verdict = cb(node, stack.length); + if (verdict === "stop") return; + if (verdict === "skip") continue; + for (const kid of node.kids) stack.push(kid); + } +} +class Counter { + count = 0; + run(root: Node): number { + walk(root, (node) => { + if (node.kind === 3) this.count++; + }); + return this.count; + } +} +const root = new Node(0); +root.kids.push(new Node(3)); +console.log(new Counter().run(root)); +`); + const walk = body(ir, "sc_bf_walk"); + const blocks = walk.split(/^(?=[\w.]+:$)/m); + const direct = blocks.find((block) => block.startsWith("callback.direct")); + // The adapter forwards the receiver to the arrow function, which never + // reads it: the direct entry tests the closure's RECEIVER_FREE bit, and + // every receiver push is behind such a test. + expect(direct).toMatch(/ = and i32 %\S+, 16\n/); + const pushes = blocks.filter((block) => block.includes("@scr_dyn_this_push_dyn")); + expect(pushes.length).toBeGreaterThan(0); + for (const block of pushes) expect(block).toMatch(/^receiver\.push\d+:/); +}); diff --git a/packages/compiler/src/ir/receiver-readers.ts b/packages/compiler/src/ir/receiver-readers.ts index 067c474ca..84e2a49d1 100644 --- a/packages/compiler/src/ir/receiver-readers.ts +++ b/packages/compiler/src/ir/receiver-readers.ts @@ -27,6 +27,8 @@ const RECEIVER_FREE_EXPRS = new Set([ "strCmp", "nullish", "orDefault", + "optChain", + "chainRecv", "unionWrap", "unionRetype", "unionNarrow", @@ -35,9 +37,12 @@ const RECEIVER_FREE_EXPRS = new Set([ "unionDisc", "upcast", "downcast", + "dynFrom", "instanceOf", "fieldGet", + "fieldAbsent", "recordGet", + "recordHas", "recordLit", "recordClone", "arrayGet", @@ -58,6 +63,9 @@ const RECEIVER_FREE_EXPRS = new Set([ "setIntrinsic", "mapNew", "setNew", + "bytesIntrinsic", + "bytesNew", + "intrinsic", ]); const RECEIVER_FREE_STMTS = new Set([ @@ -69,6 +77,7 @@ const RECEIVER_FREE_STMTS = new Set([ "while", "doWhile", "for", + "forOf", "switch", "block", "break", @@ -78,6 +87,7 @@ const RECEIVER_FREE_STMTS = new Set([ "arraySet", "arraySetUndefined", "arraySetLength", + "bytesSet", "throw", "rethrow", "tryCatch", @@ -101,7 +111,10 @@ const TRIVIALLY_RECEIVER_FREE = new Set([ kind !== "arrIntrinsic" && kind !== "strIntrinsic" && kind !== "mapIntrinsic" && - kind !== "setIntrinsic", + kind !== "setIntrinsic" && + kind !== "bytesIntrinsic" && + kind !== "bytesNew" && + kind !== "intrinsic", ), ]); @@ -114,8 +127,44 @@ const RECEIVER_FREE_LIB_FNS = new Set([ "math.minArr", "error.new", "error.nodeThrow", + "perf.now", + "atomics.op", ]); +/** Intrinsics that only check a flag or switch the current lifetime + * region: the module binding check (which throws a fixed ReferenceError) + * and the LifetimeRegion operations. */ +const RECEIVER_FREE_INTRINSICS = new Set([ + "module.tdzCheck", + "region.create", + "region.enter", + "region.leave", + "region.release", +]); + +function primitiveOperand(e: IrExpr): boolean { + const kind = e.type.kind; + return kind === "string" || kind === "f64" || kind === "bool"; +} + +/** A dynamic value boxed from a primitive, which converts to a string + * without user code. */ +function primitiveDynOperand(e: IrExpr): boolean { + if (e.kind !== "dynFrom") return false; + const kind = e.value.type.kind; + return kind === "string" || kind === "f64" || kind === "bool" || kind === "undefinedT"; +} + +/** Whether a library call runs no user code: scalar Math, the members + * above, `new Error(message, options)` over a primitive message without an + * options object, and string and number members over primitive operands. */ +function receiverFreeLibCall(e: Extract): boolean { + if (isBorrowSafeMath(e.fn) || RECEIVER_FREE_LIB_FNS.has(e.fn)) return true; + if (e.fn === "error.newOptions") return e.args.every(primitiveDynOperand); + if (e.fn.startsWith("string.") || e.fn.startsWith("num.")) return e.args.every(primitiveOperand); + return false; +} + /** Typed container and string operations. Their runtime paths compare and * copy values but never call back into compiled code, as long as no * operand is a function or a checked value. */ @@ -128,7 +177,12 @@ function dataIntrinsicOperands(e: IrExpr): IrExpr[] | null { case "strIntrinsic": case "mapIntrinsic": case "setIntrinsic": + case "bytesIntrinsic": return [e.receiver, ...e.args]; + // A typed source is copied element by element; a dynamic one may be an + // iterable whose iterator is user code. + case "bytesNew": + return e.source === null ? [] : [e.source]; case "mapNew": case "setNew": return []; @@ -210,7 +264,9 @@ export class AmbientReceiverReaders { return targets.length > 0 && targets.every(edge); } case "libCall": - return isBorrowSafeMath(expr.fn) || RECEIVER_FREE_LIB_FNS.has(expr.fn); + return receiverFreeLibCall(expr); + case "intrinsic": + return RECEIVER_FREE_INTRINSICS.has(expr.name); case "toString": return !opaqueOperand(expr.operand); default: { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/receiver-free-adapter.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/receiver-free-adapter.ts.json new file mode 100644 index 000000000..79b9bc7f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/receiver-free-adapter.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/receiver-free-adapter.ts" + ], + "diags": [] +} diff --git a/tests/corpus/receiver-free-adapter.ts b/tests/corpus/receiver-free-adapter.ts new file mode 100644 index 000000000..92582bd49 --- /dev/null +++ b/tests/corpus/receiver-free-adapter.ts @@ -0,0 +1,112 @@ +// Callbacks that reach a walker through a signature adapter (the walker's +// callback takes more parameters and returns a verdict), some reading +// `this` and some not. A plain call leaves `this` undefined; an arrow keeps +// its enclosing `this`. +class Tree { + readonly kind: number; + readonly kids: Tree[] = []; + constructor(kind: number) { + this.kind = kind; + } +} + +function walk(root: Tree, visit: (node: Tree, depth: number) => void | "skip" | "stop"): number { + const stack: [Tree, number][] = [[root, 0]]; + let visited = 0; + while (stack.length > 0) { + const [node, depth] = stack.pop()!; + visited++; + const verdict = visit(node, depth); + if (verdict === "stop") break; + if (verdict === "skip") continue; + for (let i = node.kids.length - 1; i >= 0; i--) stack.push([node.kids[i]!, depth + 1]); + } + return visited; +} + +const root = new Tree(0); +for (let i = 1; i <= 4; i++) { + const kid = new Tree(i); + for (let j = 0; j < 3; j++) kid.kids.push(new Tree(i * 10 + j)); + root.kids.push(kid); +} + +const out: string[] = []; + +// An arrow that ignores the receiver and one that captures its class's. +class Counter { + count = 0; + label = "counter"; + run(): string { + walk(root, (node) => { + if (node.kind % 2 === 0) this.count++; + }); + const labels: string[] = []; + walk(root, (node) => { + if (node.kind === 2) return "skip"; + labels.push(`${this.label}:${node.kind}`); + return undefined; + }); + return `${this.count} ${labels.length}`; + } +} +out.push(new Counter().run()); + +// Function expressions that read `this`: a plain call binds undefined. +const receivers: string[] = []; +walk(root, function (this: unknown, node: Tree) { + if (node.kind < 2) receivers.push(this === undefined ? "undefined" : typeof this); +}); +out.push(receivers.join(",")); + +// An unbound method: its `this` is undefined when the walker calls it. +const holder = { + name: "holder", + visit(this: { name: string } | undefined, node: Tree): void { + if (node.kind === 1) out.push(this === undefined ? "unbound" : this.name); + }, +}; +walk(root, holder.visit); +// A bound method keeps its receiver. +walk(root, holder.visit.bind(holder)); + +// Receiver-free bodies with optional chains, record presence and array loops. +type Info = { name?: string; tags: string[] }; +const infos = new Map([ + [1, { name: "one", tags: ["a", "b"] }], + [3, { tags: [] }], +]); +const described: string[] = []; +walk(root, (node) => { + const info = infos.get(node.kind); + let tags = 0; + for (const tag of info?.tags ?? []) tags += tag.length; + if (info !== undefined) described.push(`${info.name?.toUpperCase() ?? "anonymous"}/${"name" in info}/${tags}`); + return node.kind === 3 ? "stop" : undefined; +}); +out.push(described.join(" ")); + +// Walkers with one callback each, which their calls enter directly: one +// reads `this`, the other ignores it. +function eachKid(node: Tree, visit: (kid: Tree, index: number) => boolean | void): number { + let calls = 0; + for (let i = 0; i < node.kids.length; i++) { + calls++; + if (visit(node.kids[i]!, i) === false) break; + } + return calls; +} +const kidReceivers: string[] = []; +eachKid(root, function (this: unknown, kid: Tree) { + kidReceivers.push(`${kid.kind}:${this === undefined ? "undefined" : typeof this}`); +}); +out.push(kidReceivers.join(",")); +function sumKinds(node: Tree, add: (kind: number, depth: number) => number | void): number { + let total = 0; + for (const kid of node.kids) total += add(kid.kind, 1) ?? 0; + return total; +} +let added = 0; +out.push(String(sumKinds(root.kids[2]!, (kind) => (added += kind))), String(added)); + +console.log(out.join("\n")); diff --git a/tests/harness/corpus-costs/receiver-free-adapter.ts.json b/tests/harness/corpus-costs/receiver-free-adapter.ts.json new file mode 100644 index 000000000..74247d8d2 --- /dev/null +++ b/tests/harness/corpus-costs/receiver-free-adapter.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 63000 } From c12f2d64f3e41aede49fe36b5d2a0b5714446956 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 11:11:35 -0500 Subject: [PATCH 68/70] Keep the runtime pack's verified artifacts in a record beside the pack Each build stages the runtime artifacts it links from the runtime pack and hashes each one with SHA-256 unless the build cache records that it was verified with the file identity it still has. A build with an empty cache therefore hashed every artifact again: on Linux x64, 40 objects and 4.3 MB for a Benchmarks Game program, about 25 ms of a 300-1100 ms build. Staging now also reads and writes runtime-pack.verified.json beside the pack. It lists each verified artifact with its digest and file identity (device, inode, size, mtime and ctime), as the cache's record does: - An artifact is trusted only when one of the two records lists it with the manifest's digest and size and the identity its file has during the read. Any change to the file means hashing it again. - A missing, unreadable or malformed record, or a malformed entry, is treated as absent. - The record is written to a new file and renamed over the old one, so a reader sees a whole record. A write merges the record as it is then. - When the pack directory cannot be written, the cache's record is the only one, as before. The runtime pack packages do not publish the file. The native CLI build removes the record its probe builds leave in the bundled pack, and verify-native-cli.mjs rejects a distribution that has one. --- .gitignore | 1 + .../src/backend/runtime-pack-native.test.ts | 189 +++++++++++++++++- .../src/backend/runtime-pack-native.ts | 143 ++++++++++--- scripts/build-native-cli.mts | 7 + scripts/verify-native-cli.mjs | 9 + 5 files changed, 321 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 04c889158..36b158611 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ output/ /packages/runtime-*/artifacts/ /packages/runtime-*/runtime-pack.json /packages/runtime-*/.runtime-pack-* +/packages/runtime-*/runtime-pack.verified.json* # libnode packs: built by build:native, never committed /packages/libnode-*/libnode.so.* /packages/libnode-*/libnode.*.dylib diff --git a/packages/compiler/src/backend/runtime-pack-native.test.ts b/packages/compiler/src/backend/runtime-pack-native.test.ts index 7a8a2e6c4..18975e1f9 100644 --- a/packages/compiler/src/backend/runtime-pack-native.test.ts +++ b/packages/compiler/src/backend/runtime-pack-native.test.ts @@ -1,8 +1,12 @@ import { createHash } from "node:crypto"; import { + chmodSync, + existsSync, mkdirSync, + readdirSync, readFileSync, renameSync, + rmSync, statSync, symlinkSync, unlinkSync, @@ -148,10 +152,12 @@ async function packFixture() { store, ); let stages = 0; - const stage = (selection: NativeRuntimeSelection) => { - const directory = join(root, `stage-${stages++}`); + const stage = (selection: NativeRuntimeSelection, with_: RuntimePackStore = store) => { + const directory = join(tmpdir(), `scriptc-runtime-pack-stage-${process.pid}-${stages++}`); + rmSync(directory, { recursive: true, force: true }); mkdirSync(directory); - return stageNativeRuntimeSelection(selection, directory, store); + dirs.push(directory); + return stageNativeRuntimeSelection(selection, directory, with_); }; return { root, object, manifestText, entries, select, stage }; } @@ -241,3 +247,180 @@ test.skipIf(process.platform === "win32")( expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object"); }, ); + +/** A store that keeps nothing: a build with an empty cache. */ +function emptyStore(): RuntimePackStore { + const entries = new Map(); + return { + read: (key) => entries.get(key) ?? null, + write: (key, text) => void entries.set(key, text), + }; +} + +const packRecord = (root: string) => join(root, "runtime-pack.verified.json"); +const hashes = () => vi.mocked(createHash).mock.calls.length; + +test("a build with an empty cache trusts the record beside the pack", async () => { + const f = await packFixture(); + const selection = f.select(); + let start = hashes(); + f.stage(selection); + const firstStage = hashes() - start; + const record = JSON.parse(readFileSync(packRecord(f.root), "utf8")); + expect(record.schema).toBe(1); + expect(record.artifacts.map((artifact: { path: string }) => artifact.path)).toEqual([ + "artifacts/base.o", + ]); + start = hashes(); + const staged = f.stage(selection, emptyStore()); + // Only the store key is hashed. + expect(hashes() - start).toBe(firstStage - 1); + expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object"); +}); + +test.each(["replaced", "rewritten in place", "resized"])( + "the record beside the pack does not trust an artifact that was %s", + 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 + (change === "resized" ? 1 : 0))); + utimesSync(changed, before.atime, before.mtime); + if (changed !== f.object) renameSync(changed, f.object); + expect(() => f.stage(selection, emptyStore())).toThrow( + "runtime pack artifact hash mismatch: artifacts/base.o", + ); + }, +); + +test("an artifact whose mtime changed is hashed again and recorded again", async () => { + const f = await packFixture(); + const selection = f.select(); + f.stage(selection); + const before = statSync(f.object); + utimesSync(f.object, before.atime, new Date(before.mtimeMs + 5000)); + const start = hashes(); + f.stage(selection, emptyStore()); + // The store key and the artifact. + expect(hashes() - start).toBe(2); + const [entry] = JSON.parse(readFileSync(packRecord(f.root), "utf8")).artifacts; + expect(entry.mtime).toBe(statSync(f.object).mtimeMs); + const again = hashes(); + f.stage(selection, emptyStore()); + expect(hashes() - again).toBe(1); +}); + +test.each([ + ["truncated", () => "{"], + ["null", () => "null"], + ["another schema", (text: string) => text.replace('"schema":1', '"schema":2')], + [ + "an entry with a string ctime", + (text: string) => text.replace(/"ctime":([\d.]+)/, '"ctime":"$1"'), + ], + ["an entry without an inode", (text: string) => text.replace(/"ino":[\d.]+,/, "")], + [ + "another digest", + (text: string) => text.replace(/"sha256":"[0-9a-f]+"/, `"sha256":"${"0".repeat(64)}"`), + ], +])("a record beside the pack that is %s is not trusted", async (_name, rewrite) => { + const f = await packFixture(); + const selection = f.select(); + f.stage(selection); + const text = readFileSync(packRecord(f.root), "utf8"); + const rewritten = rewrite(text); + expect(rewritten).not.toBe(text); + writeFileSync(packRecord(f.root), rewritten); + const start = hashes(); + const staged = f.stage(selection, emptyStore()); + expect(hashes() - start).toBe(2); + expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object"); + // The record is written again, whole. + const [entry] = JSON.parse(readFileSync(packRecord(f.root), "utf8")).artifacts; + expect(entry.ino).toBe(statSync(f.object).ino); +}); + +test("an unreadable record beside the pack is not trusted, and staging still succeeds", async () => { + const f = await packFixture(); + const selection = f.select(); + // A directory where the record would be: it can be neither read nor replaced. + mkdirSync(packRecord(f.root)); + const start = hashes(); + f.stage(selection, emptyStore()); + f.stage(selection, emptyStore()); + expect(hashes() - start).toBe(4); + expect(readdirSync(f.root).filter((name) => name.endsWith(".tmp"))).toEqual([]); +}); + +test.skipIf(process.platform === "win32")( + "a pack directory that cannot be written keeps the record in the cache only", + async (context) => { + const f = await packFixture(); + const selection = f.select(); + chmodSync(f.root, 0o555); + try { + // A process that may write any directory cannot run this case. + try { + writeFileSync(join(f.root, "probe"), ""); + rmSync(join(f.root, "probe")); + context.skip(); + } catch { + /* The directory is read-only for this process. */ + } + const store = emptyStore(); + let start = hashes(); + f.stage(selection, store); + expect(hashes() - start).toBe(2); + expect(existsSync(packRecord(f.root))).toBe(false); + expect(readdirSync(f.root).filter((name) => name.endsWith(".tmp"))).toEqual([]); + // The cache's record still answers the next build with that cache. + start = hashes(); + f.stage(selection, store); + expect(hashes() - start).toBe(1); + } finally { + chmodSync(f.root, 0o755); + } + }, +); + +test("writing the record beside the pack keeps the entries another build wrote", async () => { + const f = await packFixture(); + const selection = f.select(); + const other = { + path: "artifacts/other.o", + sha256: "1".repeat(64), + dev: 1, + ino: 2, + size: 3, + mtime: 4, + ctime: 5, + }; + writeFileSync(packRecord(f.root), JSON.stringify({ schema: 1, artifacts: [other] })); + f.stage(selection, emptyStore()); + const record = JSON.parse(readFileSync(packRecord(f.root), "utf8")); + expect(record.artifacts.map((artifact: { path: string }) => artifact.path).sort()).toEqual([ + "artifacts/base.o", + "artifacts/other.o", + ]); + expect( + record.artifacts.find((artifact: { path: string }) => artifact.path === other.path), + ).toEqual(other); + expect(readdirSync(f.root).filter((name) => name.endsWith(".tmp"))).toEqual([]); +}); + +test("a cache that already trusts the artifacts writes the record beside the pack", async () => { + const f = await packFixture(); + const selection = f.select(); + f.stage(selection); + rmSync(packRecord(f.root)); + const start = hashes(); + f.stage(selection); + // The cache's record answers; only the store key is hashed. + expect(hashes() - start).toBe(1); + expect(existsSync(packRecord(f.root))).toBe(true); + const empty = hashes(); + f.stage(selection, emptyStore()); + expect(hashes() - empty).toBe(1); +}); diff --git a/packages/compiler/src/backend/runtime-pack-native.ts b/packages/compiler/src/backend/runtime-pack-native.ts index 0fff34455..bd827d963 100644 --- a/packages/compiler/src/backend/runtime-pack-native.ts +++ b/packages/compiler/src/backend/runtime-pack-native.ts @@ -2,9 +2,11 @@ * 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. */ + * identity is unchanged since it was hashed is not hashed again. The + * identities are also kept in a record beside the pack when its directory + * can be written, which builds with an empty store read too. */ import type { NativeOptimization } from "./optimization.js"; -import { createHash } from "node:crypto"; +import { createHash, randomBytes } from "node:crypto"; import { closeSync, constants, @@ -13,6 +15,8 @@ import { mkdirSync, openSync, readFileSync, + renameSync, + rmSync, statSync, writeSync, type Stats, @@ -239,21 +243,53 @@ function readVerified( 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); + for (const artifact of storedArtifacts(text)) verified.set(artifact.path, artifact); } catch { /* A malformed entry verifies everything again. */ } return verified; } +/** The well-formed entries of a stored verification; none when the text is + * not a stored verification. */ +function storedArtifacts(text: string): VerifiedArtifact[] { + const stored = JSON.parse(text) as Partial | null; + if (stored === null || stored.schema !== 1 || !Array.isArray(stored.artifacts)) return []; + const number = (value: unknown): boolean => typeof value === "number" && Number.isFinite(value); + return stored.artifacts.filter( + (artifact): artifact is VerifiedArtifact => + artifact !== null && + typeof artifact === "object" && + typeof artifact.path === "string" && + typeof artifact.sha256 === "string" && + number(artifact.dev) && + number(artifact.ino) && + number(artifact.size) && + number(artifact.mtime) && + number(artifact.ctime), + ); +} + +/** `known` records `artifact`'s digest and size for a file with `identity`. */ +function verifiedAs( + known: VerifiedArtifact | undefined, + artifact: { sha256: string; size: number }, + identity: FileIdentity, +): boolean { + return ( + known !== undefined && + known.sha256 === artifact.sha256 && + known.size === artifact.size && + sameIdentity(known, identity) + ); +} + function stageArtifact( root: string, stage: string, artifact: RuntimePackArtifact, - verified: Map, - newlyVerified: VerifiedArtifact[], + records: readonly Map[], + verified: VerifiedArtifact[], ): string { const source = join(root, artifact.path); const destination = join(stage, artifact.path); @@ -276,26 +312,17 @@ function stageArtifact( } // 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; + unchanged && records.some((record) => verifiedAs(record.get(artifact.path), artifact, before)); if ( bytes.length !== artifact.size || (!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({ + if (unchanged) + verified.push({ path: artifact.path, sha256: artifact.sha256, dev: before.dev, @@ -353,10 +380,12 @@ export function stageNativeRuntimeSelection( const packagePath = join(root, "package.json"); const manifestPath = join(root, "runtime-pack.json"); const key = storeKey(JSON.stringify({ schema: 1, verified: root })); - const verified = readVerified(store, key); - const newlyVerified: VerifiedArtifact[] = []; + const cacheRecord = readVerified(store, key); + const packRecordPath = join(root, PACK_VERIFIED_FILE); + const packRecord = readPackVerified(packRecordPath); + const verified: VerifiedArtifact[] = []; const stage = (artifact: RuntimePackArtifact): string => - stageArtifact(root, stageRoot, artifact, verified, newlyVerified); + stageArtifact(root, stageRoot, artifact, [cacheRecord, packRecord], verified); const runtimeObjects = selected.runtime.map(stage); const archives = selected.archives.map(stage); for (const license of licenses) { @@ -372,14 +401,78 @@ 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()] }; + // Each record learns the artifacts verified here that it does not list + // with their current identity. + const missing = (record: Map): VerifiedArtifact[] => + verified.filter((artifact) => !verifiedAs(record.get(artifact.path), artifact, artifact)); + const cacheMissing = missing(cacheRecord); + if (store !== undefined && cacheMissing.length > 0) { + for (const artifact of cacheMissing) cacheRecord.set(artifact.path, artifact); + const entry: StoredVerification = { schema: 1, artifacts: [...cacheRecord.values()] }; try { store.write(key, JSON.stringify(entry)); } catch { /* A failed write only means hashing again next time. */ } } + const packMissing = missing(packRecord); + if (packMissing.length > 0) writePackVerified(packRecordPath, packMissing); return { runtimeObjects, archives, systemLibraries: selected.systemLibraries }; } + +/** The verification record kept beside the runtime pack, so a build with an + * empty cache does not hash the pack's artifacts again. Like the cache's + * record, it trusts an artifact only while its file keeps the identity + * (device, inode, size, mtime, ctime) the record lists. */ +export const PACK_VERIFIED_FILE = "runtime-pack.verified.json"; + +/** The pack record's well-formed entries; none when it is missing, + * unreadable or malformed. */ +function readPackVerified(path: string): Map { + const verified = new Map(); + try { + for (const artifact of storedArtifacts(readFileSync(path, "utf8"))) + verified.set(artifact.path, artifact); + } catch { + /* The artifacts are hashed again. */ + } + return verified; +} + +/** Adds `artifacts` to the pack record: the record as it is now, with these + * entries replacing the ones for the same paths, written to a new file and + * renamed over the record, so a reader sees a whole record. Of two builds + * writing at once, the last rename wins; the entries the other added are + * hashed again by a later build. When the pack directory cannot be written, + * the cache's record is the only one. */ +function writePackVerified(path: string, artifacts: readonly VerifiedArtifact[]): void { + const temporary = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`; + try { + const record = readPackVerified(path); + for (const artifact of artifacts) record.set(artifact.path, artifact); + const entry: StoredVerification = { schema: 1, artifacts: [...record.values()] }; + const bytes = Buffer.from(JSON.stringify(entry)); + const fd = openSync( + temporary, + constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, + 0o644, + ); + try { + let offset = 0; + while (offset < bytes.length) { + const written = writeSync(fd, bytes, offset, bytes.length - offset); + if (written === 0) throw new Error("the pack record was not written"); + offset += written; + } + } finally { + closeSync(fd); + } + renameSync(temporary, path); + } catch { + try { + rmSync(temporary, { force: true }); + } catch { + /* Cleanup is best effort. */ + } + } +} diff --git a/scripts/build-native-cli.mts b/scripts/build-native-cli.mts index 6c17b829b..88f86f4fd 100644 --- a/scripts/build-native-cli.mts +++ b/scripts/build-native-cli.mts @@ -12,6 +12,7 @@ import { nativeCodegenTarget, } from "../packages/compiler/src/backend/targets.js"; import { tsgoExecutable } from "../packages/compiler/src/frontend/ts7/rpc-api.js"; +import { PACK_VERIFIED_FILE } from "../packages/compiler/src/backend/runtime-pack-native.js"; import { prebuiltLibDiagnosticsText } from "../packages/compiler/src/frontend/ts7/lib-diagnostics.js"; import { stageNativeCliAssets } from "./native-cli-assets.mjs"; import type { NativeToolchainManifest } from "../packages/compiler/src/native/toolchain.js"; @@ -237,6 +238,12 @@ writeFileSync(executable + ".json", JSON.stringify(manifest, null, 2) + "\n"); // for programs without a tsconfig, recorded by the built compiler itself. A // compiler this host cannot run ships without them. const prebuiltKeys = recordPrebuiltLibDiagnostics(executable); +// The probes' builds record the bundled runtime pack's verified artifacts +// beside it, by this host's file identities; the distribution ships without +// that record. +rmSync(join(lib, target.runtimePackPackage.replace("@scriptc/", ""), PACK_VERIFIED_FILE), { + force: true, +}); if (prebuiltKeys === null) { console.warn( "warning: the built compiler did not run here; it ships without prebuilt lib diagnostics", diff --git a/scripts/verify-native-cli.mjs b/scripts/verify-native-cli.mjs index fc94927be..4fa312121 100644 --- a/scripts/verify-native-cli.mjs +++ b/scripts/verify-native-cli.mjs @@ -99,6 +99,15 @@ for (const directory of [distribution, join(distribution, "lib/runtime-sources/v if (readdirSync(directory).some((name) => ["node_modules", ".cache", "seed"].includes(name))) throw new Error(`development files present in ${directory}`); } +// A build's record of the runtime pack's verified artifacts (runtime-pack-native.ts) +// names that host's file identities. +for (const name of readdirSync(join(distribution, "lib"))) { + if ( + name.startsWith("runtime-") && + existsSync(join(distribution, "lib", name, "runtime-pack.verified.json")) + ) + throw new Error(`runtime pack verification record present in lib/${name}`); +} if (process.argv.includes("--run")) { if (target.startsWith("linux-")) { for (const executable of [ From f9b024dbab8d96c531505609a3458618994f597e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 13:36:41 -0500 Subject: [PATCH 69/70] Count ArrayBuffer storage in process.memoryUsage() process.memoryUsage() reported external and arrayBuffers as 0. arrayBuffers is now the storage of the ArrayBuffers, typed arrays, Buffers, DataViews and SharedArrayBuffers the program holds, and external the same storage without SharedArrayBuffers, as Node splits them. A view over existing storage adds nothing. v8.getHeapStatistics().external_memory reads the same figure as external. Each ScrBytes owner records the bytes it counts in the struct's padding, so ScrBytes stays 64 bytes and the offsets compiled code reads hold; a static assertion pins them. Owners are counted in a per-instance counter, because their reference counts are not atomic and they are released on their instance's thread. Shared storage moves its owner's count to a process-wide atomic counter, since any thread may release it. --- .../builtin-apis/process-memory.mdx | 5 +- docs/content/limitations/builtin-apis/v8.mdx | 4 +- .../src/frontend/lowering/builtins/v8.ts | 12 +++-- ...s-memory-usage-array-buffers-typed.ts.json | 6 +++ ...rocess-memory-usage-array-buffers.cjs.json | 6 +++ packages/runtime/src/scr_bytes.c | 53 +++++++++++++++---- packages/runtime/src/scr_bytes.h | 43 +++++++++++++++ packages/runtime/src/scr_file_handle.c | 2 +- packages/runtime/src/scr_lib.c | 10 ++-- packages/runtime/src/scr_shared.c | 5 ++ ...rocess-memory-usage-array-buffers-typed.ts | 29 ++++++++++ .../process-memory-usage-array-buffers.cjs | 41 ++++++++++++++ tests/corpus/v8-heap-statistics.ts | 7 +++ ...s-memory-usage-array-buffers-typed.ts.json | 1 + ...rocess-memory-usage-array-buffers.cjs.json | 1 + 15 files changed, 202 insertions(+), 23 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-memory-usage-array-buffers-typed.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-memory-usage-array-buffers.cjs.json create mode 100644 tests/corpus/process-memory-usage-array-buffers-typed.ts create mode 100644 tests/corpus/process-memory-usage-array-buffers.cjs create mode 100644 tests/harness/corpus-costs/process-memory-usage-array-buffers-typed.ts.json create mode 100644 tests/harness/corpus-costs/process-memory-usage-array-buffers.cjs.json diff --git a/docs/content/limitations/builtin-apis/process-memory.mdx b/docs/content/limitations/builtin-apis/process-memory.mdx index 6a3121e2f..6865fd7ff 100644 --- a/docs/content/limitations/builtin-apis/process-memory.mdx +++ b/docs/content/limitations/builtin-apis/process-memory.mdx @@ -1,2 +1,5 @@ - **Process memory:** `process.memoryUsage()` reads `rss` from the operating system and `heapTotal` and `heapUsed` from the runtime's allocator, so the heap figures start near zero and grow as the program allocates. - `external` and `arrayBuffers` are `0`, because Buffer and typed-array storage is part of the same heap. + `arrayBuffers` is the storage of the ArrayBuffers, typed arrays, Buffers, DataViews and SharedArrayBuffers the program holds, and `external` is the same storage without SharedArrayBuffers, as in Node. + Node also counts its own startup allocations in both, so a program that holds no such storage reports `0` where Node reports a few megabytes. + In a program with worker threads, `external` and the non-shared part of `arrayBuffers` count the calling thread's storage. + The storage comes from the same allocator, so it also counts in `heapTotal` and `heapUsed`. diff --git a/docs/content/limitations/builtin-apis/v8.mdx b/docs/content/limitations/builtin-apis/v8.mdx index 066fb000d..daafa3d6c 100644 --- a/docs/content/limitations/builtin-apis/v8.mdx +++ b/docs/content/limitations/builtin-apis/v8.mdx @@ -1,5 +1,5 @@ - **V8 heap statistics:** `node:v8` can be imported. - `v8.getHeapStatistics()` returns Node's fields in Node's order, computed from the runtime allocator like `process.memoryUsage()`: `total_heap_size` and `total_physical_size` are the bytes the heap holds, `used_heap_size` the bytes allocated, `heap_size_limit` the machine's memory (the heap has no fixed ceiling), and `total_available_size` the limit minus the used bytes. - Fields that describe V8 itself (`total_heap_size_executable`, `malloced_memory`, `peak_malloced_memory`, the global handle sizes, `external_memory`, and `does_zap_garbage`) are `0`, and `number_of_native_contexts` is `1`. + `v8.getHeapStatistics()` returns Node's fields in Node's order, computed from the runtime allocator like `process.memoryUsage()`: `total_heap_size` and `total_physical_size` are the bytes the heap holds, `used_heap_size` the bytes allocated, `external_memory` the same figure as `process.memoryUsage().external`, `heap_size_limit` the machine's memory (the heap has no fixed ceiling), and `total_available_size` the limit minus the used bytes. + Fields that describe V8 itself (`total_heap_size_executable`, `malloced_memory`, `peak_malloced_memory`, the global handle sizes, and `does_zap_garbage`) are `0`, and `number_of_native_contexts` is `1`. The values are approximate and are not V8 heap figures. The other `node:v8` members observe the V8 engine, such as heap snapshots, profiles, flags, and serialization, and throw `SC2020` where they are used. diff --git a/packages/compiler/src/frontend/lowering/builtins/v8.ts b/packages/compiler/src/frontend/lowering/builtins/v8.ts index 989a61a3f..f95a6e6c2 100644 --- a/packages/compiler/src/frontend/lowering/builtins/v8.ts +++ b/packages/compiler/src/frontend/lowering/builtins/v8.ts @@ -30,11 +30,11 @@ const HEAP_STATISTICS_FIELDS = [ ]; /** `v8.getHeapStatistics()`: one memoryUsage sample (the runtime heap's - * total and used bytes), with the machine's memory as the heap limit (the - * runtime heap has no fixed ceiling) and the available size the limit - * minus used bytes. V8-only figures (executable code space, malloced - * memory, global handles, external memory, zapping) read 0, and one - * native context exists. The record is the call site's interned HeapInfo + * total and used bytes, and external memory, memoryUsage's `external`), + * with the machine's memory as the heap limit (the runtime heap has no + * fixed ceiling) and the available size the limit minus used bytes. + * V8-only figures (executable code space, malloced memory, global handles, + * zapping) read 0, and one native context exists. The record is the call site's interned HeapInfo * shape, built by a shared helper per shape. */ export function lowerV8HeapStatisticsCall( lowerer: Lowerer, @@ -73,6 +73,8 @@ export function lowerV8HeapStatisticsCall( return sample(1); case "used_heap_size": return sample(2); + case "external_memory": + return sample(3); case "heap_size_limit": return limit(); case "total_available_size": diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-memory-usage-array-buffers-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-memory-usage-array-buffers-typed.ts.json new file mode 100644 index 000000000..0a1424dad --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-memory-usage-array-buffers-typed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/process-memory-usage-array-buffers-typed.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-memory-usage-array-buffers.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-memory-usage-array-buffers.cjs.json new file mode 100644 index 000000000..93d0a37fa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-memory-usage-array-buffers.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/process-memory-usage-array-buffers.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 6bb56195a..03cb552b4 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -173,15 +173,37 @@ double scr_bytes_to_u8_clamp(double value) { /* ── lifecycle ─────────────────────────────────────────────────────────── */ -/* Adopt private malloc storage without clearing or copying its contents. The - * caller transfers ownership; views keep it alive through the usual owner. */ -ScrBytes *scr_bytes_take_data(uint8_t *data, size_t len) { +/* The storage of this instance's owners, which are created and released on + * its thread (their reference counts are not atomic), and of the + * SharedArrayBuffer storage of the process, which any thread may release. */ +static SCR_TL size_t scr_array_buffers_local; +static size_t scr_array_buffers_shared; + +double scr_array_buffers_held(bool shared) { + return shared ? (double)__atomic_load_n(&scr_array_buffers_shared, __ATOMIC_RELAXED) + : (double)scr_array_buffers_local; +} + +void scr_array_buffers_sub(bool shared, size_t n) { + if (shared) __atomic_fetch_sub(&scr_array_buffers_shared, n, __ATOMIC_RELAXED); + else scr_array_buffers_local -= n; +} + +void scr_array_buffers_share(size_t n) { + scr_array_buffers_local -= n; + __atomic_fetch_add(&scr_array_buffers_shared, n, __ATOMIC_RELAXED); +} + +/* An owner of `len` elements of `elem` over malloc storage `data`, counting + * `held` bytes of it in scr_array_buffers_held. */ +__attribute__((always_inline)) static inline ScrBytes * +scr_bytes_own(uint8_t *data, size_t len, ScrBytesElem elem, size_t held) { if (!data && len) scr_trap("scriptc: NULL owned byte storage\n"); ScrBytes *b = malloc(sizeof(ScrBytes)); if (!b) scr_bytes_oom(); b->rc = 1; b->len = len; - b->elem = SCR_BYTES_U8; + b->elem = elem; b->data = data ? data : malloc(1); if (!b->data) scr_bytes_oom(); b->backing = NULL; @@ -192,21 +214,27 @@ ScrBytes *scr_bytes_take_data(uint8_t *data, size_t len) { b->shared = NULL; b->properties = NULL; b->properties_traced = false; + scr_bytes_set_held(b, held); + scr_array_buffers_local += held; #ifdef SCR_RC_AUDIT scr_live_bytes++; #endif return b; } +/* Adopt private malloc storage without clearing or copying its contents. The + * caller transfers ownership; views keep it alive through the usual owner. */ +ScrBytes *scr_bytes_take_data(uint8_t *data, size_t len) { + return scr_bytes_own(data, len, SCR_BYTES_U8, len); +} + /* Only complete, non-observable initialization may use private storage. */ static ScrBytes *scr_bytes_alloc_private(ScrBytesElem elem, size_t len) { size_t width = scr_bytes_elem_size(elem); if (len > SIZE_MAX / width) scr_bytes_oom(); uint8_t *data = malloc(len ? len * width : 1); if (!data) scr_bytes_oom(); - ScrBytes *b = scr_bytes_take_data(data, len); - b->elem = elem; - return b; + return scr_bytes_own(data, len, elem, len * width); } static ScrBytes *scr_bytes_alloc(ScrBytesElem elem, size_t len) { @@ -214,9 +242,7 @@ static ScrBytes *scr_bytes_alloc(ScrBytesElem elem, size_t len) { if (len > SIZE_MAX / width) scr_bytes_oom(); uint8_t *data = calloc(len ? len : 1, width); if (!data) scr_bytes_oom(); - ScrBytes *b = scr_bytes_take_data(data, len); - b->elem = elem; - return b; + return scr_bytes_own(data, len, elem, len * width); } ScrBytes *scr_bytes_from_external(void *data, size_t length) { @@ -229,6 +255,7 @@ ScrBytes *scr_bytes_from_external(void *data, size_t length) { b->external = true; } b->len = length; + scr_bytes_set_held(b, 0); return b; } @@ -312,6 +339,7 @@ void scr_bytes_release(ScrBytes *b) { } else if (b->shared) { scr_shared_release(b->shared); } else if (!b->external) { + scr_array_buffers_local -= scr_bytes_held(b); free(b->data); } #ifdef SCR_RC_AUDIT @@ -745,6 +773,7 @@ ScrBytes *scr_bytes_subarray(ScrBytes *b, double start, double end) { v->shared = owner->shared; v->properties = NULL; v->properties_traced = false; + scr_bytes_set_held(v, 0); #ifdef SCR_RC_AUDIT scr_live_bytes++; #endif @@ -1018,6 +1047,7 @@ ScrBytes *scr_dataview_new(ScrBytes *src, double byte_off, bool has_len, double v->shared = owner->shared; v->properties = NULL; v->properties_traced = false; + scr_bytes_set_held(v, 0); #ifdef SCR_RC_AUDIT scr_live_bytes++; #endif @@ -2819,7 +2849,7 @@ static ScrBytes *scr_bytes_conversion_storage_in(ScrBytesSink *sink, size_t capa } uint8_t *data = malloc(capacity ? capacity : 1); if (!data) scr_bytes_oom(); - return scr_bytes_take_data(data, 0); + return scr_bytes_own(data, 0, SCR_BYTES_U8, capacity); } static ScrBytes *scr_bytes_conversion_storage(size_t capacity) { @@ -3169,6 +3199,7 @@ static ScrBytes *scr_pool_take(size_t len) { v->shared = NULL; v->properties = NULL; v->properties_traced = false; + scr_bytes_set_held(v, 0); #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 4b9174bc0..e3846737d 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -55,6 +55,11 @@ typedef struct ScrBytes { size_t rc; size_t len; /* ELEMENT count, fixed at construction */ ScrBytesElem elem; +#if SIZE_MAX > UINT32_MAX + /* Bits 0-31 of the storage an owner counts (scr_bytes_held), in the + * padding after elem, so the offsets compiled code uses hold. */ + uint32_t held_lo; +#endif uint8_t *data; /* len * elem_size bytes; owned unless backing is set */ /* NULL for owners. A view (DataView, subarray, Buffer-slice) sets this * to the retained OWNER it aliases (chain depth is always exactly 1: @@ -69,12 +74,41 @@ typedef struct ScrBytes { * value that held this view (see scr_dyn_trace_v). In padding, so the * offsets compiled code uses hold. */ bool properties_traced; +#if SIZE_MAX > UINT32_MAX + uint16_t held_hi; /* bits 32-47 of the counted storage, in padding */ +#endif 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; +#if SIZE_MAX <= UINT32_MAX + uint32_t held_lo; /* the counted storage, runtime-only */ +#endif } ScrBytes; +#if SIZE_MAX > UINT32_MAX +_Static_assert(offsetof(ScrBytes, data) == 24 && offsetof(ScrBytes, shared) == 48 && sizeof(ScrBytes) == 64, + "the counted storage sits in padding: compiled code reads data at 24 and shared at 48"); +#endif + +/* The bytes of storage an owner counts in process.memoryUsage()'s + * arrayBuffers and external (scr_array_buffers_held): 0 for views, foreign + * memory and owners whose storage became shared. On 64-bit targets its 48 + * bits hold any size the address space can allocate. */ +static inline size_t scr_bytes_held(const ScrBytes *b) { +#if SIZE_MAX > UINT32_MAX + return (size_t)b->held_lo | (size_t)b->held_hi << 32; +#else + return b->held_lo; +#endif +} + +static inline void scr_bytes_set_held(ScrBytes *b, size_t n) { + b->held_lo = (uint32_t)n; +#if SIZE_MAX > UINT32_MAX + b->held_hi = (uint16_t)(n >> 32); +#endif +} /* Copies out of and into shared storage, made of relaxed atomic accesses * (aligned 8-byte words, single bytes at unaligned edges). Lock-free @@ -204,6 +238,15 @@ ScrBytes *scr_array_buffer_view_dv(ScrDyn *, ScrDyn *, ScrDyn *); * empty span. */ ScrBytes *scr_bytes_from_data(const uint8_t *data, size_t len); /* +1 */ ScrBytes *scr_bytes_take_data(uint8_t *data, size_t len); /* +1, consumes malloc storage */ +/* Bytes of storage the runtime holds: the typed array, Buffer and DataView + * owners of the calling instance (shared false), and the SharedArrayBuffer + * storage of the process (shared true). process.memoryUsage()'s + * arrayBuffers is their sum and external counts the first, as Node's do. + * scr_array_buffers_sub uncounts released storage; scr_array_buffers_share + * moves an owner's bytes to shared storage. */ +double scr_array_buffers_held(bool shared); +void scr_array_buffers_sub(bool shared, size_t n); +void scr_array_buffers_share(size_t n); /* Same-element-kind copy, preserving the raw representation. Cross-kind * construction uses scr_bytes_convert. Borrows src. Never throws. */ diff --git a/packages/runtime/src/scr_file_handle.c b/packages/runtime/src/scr_file_handle.c index ed0580165..d5a3bb7ca 100644 --- a/packages/runtime/src/scr_file_handle.c +++ b/packages/runtime/src/scr_file_handle.c @@ -422,7 +422,7 @@ ScrBytes *scr_file_handle_read_file_bytes(ScrFileHandle *h) { static void scr_file_handle_write_all(ScrFileHandle *h, const void *data, size_t length) { if (!scr_file_handle_require_open(h)) return; - ScrBytes bytes = {SIZE_MAX, length, SCR_BYTES_U8, (uint8_t *)data, NULL}; + ScrBytes bytes = {.rc = SIZE_MAX, .len = length, .elem = SCR_BYTES_U8, .data = (uint8_t *)data}; size_t at = 0; while (at < length) { double count = scr_fs_write_sync((double)h->fd, &bytes, (double)at, diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 9ef71a223..4ed421f7a 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -2659,8 +2659,12 @@ void scr_process_memory_usage_take(void) { scr_memory_usage_sample[0] = rss; scr_memory_usage_sample[1] = total; scr_memory_usage_sample[2] = used; - scr_memory_usage_sample[3] = 0; - scr_memory_usage_sample[4] = 0; + /* external is the storage of the ArrayBuffers and typed arrays this + * instance holds, and arrayBuffers adds the SharedArrayBuffer storage of + * the process, as Node counts them. */ + double local = scr_array_buffers_held(false); + scr_memory_usage_sample[3] = local; + scr_memory_usage_sample[4] = local + scr_array_buffers_held(true); } double scr_process_memory_usage_field(double k) { @@ -4447,7 +4451,7 @@ static double scr_fs_vector_sync(double fd, ScrArr *buffers, double position, scr_bytes_release(buf); } } - ScrBytes window = {SIZE_MAX, length, SCR_BYTES_U8, data, NULL}; + ScrBytes window = {.rc = SIZE_MAX, .len = length, .elem = SCR_BYTES_U8, .data = data}; position = isfinite(position) && position >= 0 && position <= 9007199254740991.0 ? trunc(position) : -1; double transferred = writing diff --git a/packages/runtime/src/scr_shared.c b/packages/runtime/src/scr_shared.c index 6505a71ab..0cff859d3 100644 --- a/packages/runtime/src/scr_shared.c +++ b/packages/runtime/src/scr_shared.c @@ -24,6 +24,7 @@ struct ScrSharedBytes { atomic_size_t references; size_t length; uint8_t *data; + size_t held; /* the owner's count, moved to the shared bytes (scr_array_buffers_share) */ }; void scr_shared_load(void *dst, const void *src, size_t n) { @@ -56,6 +57,7 @@ ScrSharedBytes *scr_shared_retain(ScrSharedBytes *storage) { void scr_shared_release(ScrSharedBytes *storage) { if (!storage || atomic_fetch_sub_explicit(&storage->references, 1, memory_order_acq_rel) != 1) return; + scr_array_buffers_sub(true, storage->held); free(storage->data); free(storage); } @@ -67,6 +69,9 @@ void scr_bytes_make_shared(ScrBytes *bytes) { atomic_init(&storage->references, 1); storage->length = bytes->len * scr_bytes_elem_size(bytes->elem); storage->data = bytes->data; + storage->held = scr_bytes_held(bytes); + scr_array_buffers_share(storage->held); + scr_bytes_set_held(bytes, 0); bytes->shared = storage; } diff --git a/tests/corpus/process-memory-usage-array-buffers-typed.ts b/tests/corpus/process-memory-usage-array-buffers-typed.ts new file mode 100644 index 000000000..6ccac3e43 --- /dev/null +++ b/tests/corpus/process-memory-usage-array-buffers-typed.ts @@ -0,0 +1,29 @@ +// process.memoryUsage() in typed code counts the storage of typed arrays +// and Buffers created there, and a subarray view adds nothing. +const size = 2 * 1024 * 1024; +function grew(before: NodeJS.MemoryUsage, after: NodeJS.MemoryUsage): string { + return `${after.arrayBuffers - before.arrayBuffers >= size} ${after.external - before.external >= size}`; +} + +let before = process.memoryUsage(); +const bytes = new Uint8Array(size); +console.log("Uint8Array", bytes.length, grew(before, process.memoryUsage())); + +before = process.memoryUsage(); +const ints = new Int32Array(size / 4); +ints[0] = 7; +console.log("Int32Array", ints.length, ints[0], grew(before, process.memoryUsage())); + +before = process.memoryUsage(); +const buf = Buffer.alloc(size); +buf[1] = 3; +console.log("Buffer.alloc", buf.length, grew(before, process.memoryUsage())); + +before = process.memoryUsage(); +const copy = Uint8Array.from(bytes); +console.log("Uint8Array.from", copy.length, grew(before, process.memoryUsage())); + +before = process.memoryUsage(); +const view = bytes.subarray(16); +const after = process.memoryUsage(); +console.log("subarray", view.length, after.arrayBuffers - before.arrayBuffers < size); diff --git a/tests/corpus/process-memory-usage-array-buffers.cjs b/tests/corpus/process-memory-usage-array-buffers.cjs new file mode 100644 index 000000000..2c96b7f6b --- /dev/null +++ b/tests/corpus/process-memory-usage-array-buffers.cjs @@ -0,0 +1,41 @@ +// process.memoryUsage() counts the storage of ArrayBuffers, typed arrays, +// Buffers, DataViews and SharedArrayBuffers in arrayBuffers, and external +// includes arrayBuffers. A view over existing storage adds nothing. +const size = 4 * 1024 * 1024; +const grew = (before, after) => + [after.arrayBuffers - before.arrayBuffers >= size, after.external - before.external >= size]; +const sample = () => process.memoryUsage(); + +let before = sample(); +console.log(typeof before.arrayBuffers, typeof before.external, before.external >= before.arrayBuffers); +const ab = new ArrayBuffer(size); +console.log("ArrayBuffer", ab.byteLength, ...grew(before, sample())); + +before = sample(); +const f64 = new Float64Array(size / 8); +console.log("Float64Array", f64.byteLength, ...grew(before, sample())); + +before = sample(); +const buf = Buffer.alloc(size, 1); +console.log("Buffer.alloc", buf.length, ...grew(before, sample())); + +before = sample(); +const unsafe = Buffer.allocUnsafeSlow(size); +console.log("Buffer.allocUnsafeSlow", unsafe.length, ...grew(before, sample())); + +before = sample(); +const text = Buffer.from("x".repeat(size)); +console.log("Buffer.from(string)", text.length, ...grew(before, sample())); + +before = sample(); +const sab = new SharedArrayBuffer(size); +const shared = new Int32Array(sab); +console.log("SharedArrayBuffer", sab.byteLength, shared.length, ...grew(before, sample())); + +before = sample(); +const views = [new DataView(ab), buf.subarray(1), new Uint8Array(f64.buffer, 8, 16)]; +const after = sample(); +console.log("views", views.length, after.arrayBuffers - before.arrayBuffers < size); + +const all = sample(); +console.log(all.arrayBuffers >= 6 * size, all.external >= all.arrayBuffers); diff --git a/tests/corpus/v8-heap-statistics.ts b/tests/corpus/v8-heap-statistics.ts index 9586f60a2..64109d7a3 100644 --- a/tests/corpus/v8-heap-statistics.ts +++ b/tests/corpus/v8-heap-statistics.ts @@ -14,3 +14,10 @@ console.log(stats.used_heap_size <= stats.total_heap_size); console.log(stats.heap_size_limit > 0, stats.total_available_size <= stats.heap_size_limit); console.log(stats.does_zap_garbage === 0 || stats.does_zap_garbage === 1); console.log(stats.number_of_native_contexts >= 1, warm.length); + +// external_memory is process.memoryUsage().external: it grows by the +// storage of a new ArrayBuffer. +const before = v8.getHeapStatistics().external_memory; +const storage = new Uint8Array(4 * 1024 * 1024); +const after = v8.getHeapStatistics(); +console.log(after.external_memory - before >= storage.byteLength, after.external_memory <= process.memoryUsage().external); diff --git a/tests/harness/corpus-costs/process-memory-usage-array-buffers-typed.ts.json b/tests/harness/corpus-costs/process-memory-usage-array-buffers-typed.ts.json new file mode 100644 index 000000000..ca7c3fc00 --- /dev/null +++ b/tests/harness/corpus-costs/process-memory-usage-array-buffers-typed.ts.json @@ -0,0 +1 @@ +{ "plain": 5463, "san": 4978 } diff --git a/tests/harness/corpus-costs/process-memory-usage-array-buffers.cjs.json b/tests/harness/corpus-costs/process-memory-usage-array-buffers.cjs.json new file mode 100644 index 000000000..c718d14d3 --- /dev/null +++ b/tests/harness/corpus-costs/process-memory-usage-array-buffers.cjs.json @@ -0,0 +1 @@ +{ "plain": 4864, "san": 3953 } From d67f378a5048b1afa4c03bc1fa94d9a8f52eb0c7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 13:44:32 -0500 Subject: [PATCH 70/70] Release the web view of an abandoned fetch body while its transfer runs A Response body's stream gets a web ReadableStream view when a member the fetch stream lacks is read (for example `body.tee`). The view's source holds the stream, and the stream released the view only when that source was its last holder. While the transfer runs it holds the stream too, so a dropped body with a view kept a reference count of 2: the unobserved-response discard never ran, the paused socket was never resumed, and a compressed response dropped before HTTP EOF kept the process alive. The stream now releases the view when only the view's source and the live transfer hold it, and then takes the unobserved-response discard, as a body without a view does. The static-abandon-view fixture reads `body.tee` and `body.locked` of a 4 MiB incompressible gzip response from the new /gzip-incompressible route and drops it. It hung until the test's timeout before this change and now exits as Node does. --- packages/runtime/src/scr_fetch.c | 10 ++++++---- tests/fixtures/fetch/servers.mjs | 18 +++++++++++++++++- .../fetch/static-abandon-view/main.mjs | 10 ++++++++++ tests/harness/fetch.test.ts | 13 +++++++++---- 4 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 tests/fixtures/fetch/static-abandon-view/main.mjs diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index c64144ab7..eda6f3210 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -934,10 +934,12 @@ static void sf_stream_release(SfStream *s) { if (!s) return; if (--s->rc > 0) { #ifdef SCR_HANDLE_CLASSES - /* Only the web view's source still holds the stream: the view no - * longer needs to be found from it, and the view, which reads it, - * keeps it while anything else holds the view. */ - if (s->rc == 1 && s->view_source_live && s->web_view) { + /* Only the web view's source still holds the stream, with the transfer + * that fills it while the response runs: the view no longer needs to be + * found from it, and the view, which reads it, keeps it while anything + * else holds the view. A view nothing else holds releases its source, + * and the stream then takes the unobserved-response discard below. */ + if (s->rc == (s->response_owner ? 2u : 1u) && s->view_source_live && s->web_view) { ScrDyn *view = s->web_view, *lock = s->web_view_lock; s->web_view = s->web_view_lock = NULL; scr_dyn_release(lock); diff --git a/tests/fixtures/fetch/servers.mjs b/tests/fixtures/fetch/servers.mjs index d3f04b153..17034b03e 100644 --- a/tests/fixtures/fetch/servers.mjs +++ b/tests/fixtures/fetch/servers.mjs @@ -22,12 +22,13 @@ * /early-hints * /switching-protocols /invalid-utf8 /slow /drip * /chunked /backpressure /backpressure-state /gzip /gzip-concat - * /gzip-truncated /gzip-pressure /deflate + * /gzip-truncated /gzip-pressure /gzip-incompressible /deflate * /status-meta /no-content /reset-content /reset-content-large * /truncated-response /upload-failure /sse, * 404 for the rest; * the proxy relays absolute-URI requests and CONNECT tunnels, counting one * per proxied request either way. */ +import { createCipheriv } from "node:crypto"; import { readFileSync } from "node:fs"; import { createServer, request } from "node:http"; import { createServer as createHttpsServer } from "node:https"; @@ -39,6 +40,7 @@ export async function startFetchServers() { const fragmentRedirects = new Set(); const backpressureStates = new Map(); let gzipPressurePayload; + let gzipIncompressiblePayload; const server = createServer((req, res) => { const url = req.url ?? "/"; if (url === "/upload-failure") { @@ -292,6 +294,20 @@ export async function startFetchServers() { "content-length": String(gzipPressurePayload.length), }); res.end(gzipPressurePayload); + } else if (url === "/gzip-incompressible") { + // 4 MiB of fixed pseudo-random bytes: the representation spans many + // socket reads, so an unread body pauses the socket before HTTP EOF. + gzipIncompressiblePayload ??= gzipSync( + createCipheriv("aes-128-ctr", Buffer.alloc(16), Buffer.alloc(16)).update( + Buffer.alloc(4 * 1024 * 1024), + ), + ); + res.writeHead(200, { + "content-type": "application/octet-stream", + "content-encoding": "gzip", + "content-length": String(gzipIncompressiblePayload.length), + }); + res.end(gzipIncompressiblePayload); } else if (url === "/deflate") { // zlib-wrapped deflate (Node servers' `deflate` spelling). const payload = deflateSync(Buffer.from("deflated wörld", "utf8")); diff --git a/tests/fixtures/fetch/static-abandon-view/main.mjs b/tests/fixtures/fetch/static-abandon-view/main.mjs new file mode 100644 index 000000000..862d5769c --- /dev/null +++ b/tests/fixtures/fetch/static-abandon-view/main.mjs @@ -0,0 +1,10 @@ +// A compressed body whose web-stream members were read, then dropped unread +// with its Response before HTTP EOF, must settle the native transfer: the +// body's ReadableStream view does not keep it alive. Otherwise its paused +// socket keeps the process alive while the server waits to send the rest. +async function inspect() { + const response = await fetch(`${process.argv[2]}/gzip-incompressible`); + return [typeof response.body.tee, response.body.locked]; +} +console.log(...(await inspect())); +console.log("abandoned inspected compressed response"); diff --git a/tests/harness/fetch.test.ts b/tests/harness/fetch.test.ts index 1df7965f6..f6a4f1392 100644 --- a/tests/harness/fetch.test.ts +++ b/tests/harness/fetch.test.ts @@ -194,10 +194,15 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { 120_000, ); - test.for(["llvm"] as const)( - "static fetch / %s backend settles an abandoned compressed response", - async () => { - const entry = join(fixturesRoot, "static-abandon/main.mts"); + // static-abandon-view reads web-stream members of the body first, so the + // body has a ReadableStream view when it is dropped. + test.for([ + ["static-abandon/main.mts", "llvm"], + ["static-abandon-view/main.mjs", "llvm"], + ] as const)( + "static fetch / %s / %s backend settles an abandoned compressed response", + async ([fixture]) => { + const entry = join(fixturesRoot, fixture); const binary = await build(entry); const [nodeRes, nativeRes] = await Promise.all([ runBinary("node", [entry, baseUrl], undefined, 3_000),